A baby step towards in-network compute: implementing XCalc
This tutorial implements XCalc, a network calculator on the X-Switch ISA. The Parser recognizes a custom L2 header (Ethertype 0x1234) carrying an opcode and two operands; the MAP looks the opcode up in a table of jump addresses, performs the arithmetic or logical operation, writes the result into the packet, swaps the MAC addresses and returns it to the sender — demonstrating computed branches, loops and in-network compute.
Welcome back, network enthusiasts! Today we decided to leave the world of IPv4 switching and to use XISA to implement a classical example, popularized by a P4 language tutorial, a network calculator, that we will appropriately call XCalc.
The idea is that devices with programmable data planes can do a lot more than just forward packets. For example, they can be used to perform computations using packet fields as data. Back in 2016/2017, this concept was seen as clever, but mostly academic and not very practical. Fast-forward to 2025 and it's gaining serious traction. Today, there's growing interest in using switches to accelerate machine learning, by performing simple, but critical arithmetic operations while switching the packets carrying training data.
This is where X2 switches outshine many competitors, due to their ability to perform just-in-time parsing and arbitrarily complex calculations, that resemble a traditional CPU way more than a switch.
Let's start small. In this example we'll concentrate on the essentials: a simple, table-based switch() statement and some basic arithmetic and logical operations. Think of this as laying the foundation. Once we've got that down, we'll move on to the more advanced stuff.
For this simple example we'll use the same packet format as described in the standard P4 language tutorial. It will be a simple L2-based protocol utilizing Ethertype 0x1234. The header will have the following format:

Figure 1. XCalc header (version 1)[1]
where:
Supported opcodes
The switch receives the packet and verifies that it is correctly formed. It continues to perform the specified operation, writes the result into the Result field and then sends the packet back to the same port it came from, while also swapping the Ethernet source and destination MAC addresses.
Later on, we can add more operations and extend the functionality in other ways.
Register planning is a critical part of writing a program in any assembly language. Strategically placing the data in the registers can significantly reduce the need for extra data movement or the need to temporarily save the register contents in memory and, hence, speed up the program.
XISA provides fourteen 128-bit-wide general-purpose registers, named R0..R13 in its Match-Action Processor (MAP). Each register can be subdivided into four independent 32-bit registers each. For example, register R0 can be subdivided into registers R0.0, R0.1, R0.2 and R0.3.
As we discussed in our previous articles, the programmable parser can load the most often needed headers or their parts into the MAP registers to speed up the processing. Also, it preloads certain registers with standard information, such as Port Metadata (R1), Standard Metadata (R7.0), Header Present (R11) and Header Offsets (R12..R13), so it's best if we leave these untouched.
Figure 2 illustrates the layout of the headers we are going to use for this exercise:

Figure 2. Register layout for the program headers
Parsing a custom header, such as the XCALC Header we defined above, is no different than parsing a standard header, such as Ethernet. All we need to do is:
In our case the parse graph can be very simple, as described in Figure 3.

However, we've already future-proofed the header with the signature and the version, potentially allowing for more versions of the XCalc header in the future that can be encapsulated using the same Ethertype (0x1234) if the first three bytes (two-byte signature and one-byte version fields) remain the same. Therefore, let's create a slightly more complex parse graph to allow us to easily add more versions of this protocol in the future. This will also demonstrate that state transitions, header extraction and header recording (that is the process of marking the header "present" and storing its offset) can be completely independent.
Figure 4 shows how the expanded Parse Graph might look:

While this graph has one extra transition it allows us to expand the program more easily and to also perform the signature and verification in the Parser which is different, compared to our previous example where we were performing IPv4 version and header length checks in the MAP.
Which method is better? This depends somewhat on the overall structure of the code; the important point is that XISA provides the programmer with the flexibility about where to perform the check.
We discussed the general algorithm for allocating the Header IDs and Header Offset IDs in one of our previous articles. As a reminder, Header IDs need to be allocated for all distinct headers that the program intends to process, whereas Header Offset IDs need to be allocated for the distinct layers or positions a certain header can occupy in the packet.
In our case, the table can look like this:
Table 1: Allocating Header IDs and Header Offset IDs
Note how the Layer ID is the same for both XCalcV1, XCalcV2, and potentially for other XCalc headers. It is clear from the Parse graph that only one of them can be present in a given packet yet, in all the cases it will follow the Ethernet header.
We'll skip writing the standard preamble, since it was discussed in the previous articles. Instead, we'll concentrate on coding the states that parse the Ethernet header and the XCalc header(s).
Parsing the Ethernet
The goal of this state is to preload the Ethernet Destination and Source MAC addresses into the R2 MAP register as per Figure 2 and perform the transition based on the Ethertype.
Figure 5. Parsing Ethernet
parse_ethernet: EXTMAP MAPR2, 0, 0, 96 EXTNXTP R0, 96, 16 { parse_ethernet 0: 0x001234: parse_xcalc } STHC 14, 0, 0, 1 HALTHere is a detailed breakdown of these steps:
Note: The order of these instructions is very important, as it is critical to access the data in the same order it appears in the byte stream.
Parsing the common portion of the XCalc header(s)
Now, we need to extract the next three bytes that should represent the signature and the version fields of the XCalc header (see Figure 1). Not only do we need to extract these three bytes, but we also need to place them into the most significant bits of register R3 as depicted in Figure 2.
To do this we'll use the following technique:
Here is the full code of that state:
Figure 6. Parsing XCalc
parse_xcalc: EXTNXTP R0, 0, 24 MOVMAP MAPR3, 104, R0, 0, 24 { parse_xcalc 0: 0x583201: parse_xcalc_v1 /* 'X2\x01' */ 1: 0x783201: parse_xcalc_v1 /* 'x2\x01' */ 2: 0x583201: parse_xcalc_v2 /* 'X2\x01' */ 3: 0x783201: parse_xcalc_v2 /* 'x2\x01' */ } BRNXTP 1 HALTHere is a breakdown of these steps:
Parsing the XCalcV1 header
Assuming that the signature of the header is correct (X2 or x2) and the version is equal to 1, we are now at the point where we need to extract the XCalcV1 header. Since the first 3 bytes have already been extracted and placed into the top 3 bytes of MAP register R3, all we need to do is to extract 13 more bytes (since the total length of the header is 16 bytes) and then advance the cursor and record the header presence and offset.
Figure 7. Parsing XCalc_V1
parse_xcalc_v1: EXTMAP MAPR3, 0, 24, 104 STHC 16, 1, 1, 0 HALTHere is a breakdown of these steps:
The processing code for the XCalcV1 packets that we are going to write includes the following steps:
Let's have a closer look at those steps.
Let's review the input the MAP receives from the Parser.
Table 2: MAP input from Parser [[Source captions this "MAP Input to Parser"; corrected to match the sentence above and the other tutorials.]]
The X2 match tables are very flexible and this is reflected in XISA. The details of the tables, such as their size, placement, etc., are abstracted from the table lookup instructions using the concept of Table IDs, an integer assigned to each table during its provisioning.
For this reason, the Tables are defined separately from the assembly code, using two JSON files. The first file, xdefs-tables.json specifies the Tables used by the given program on a high level.[2]
Figure 8. xdefs-tables.json OPCODE table definition
{ "table": [ { "name": "OPCODE", "id": "OPCODE_TABLE_ID", "type": "Hash", "size": 256, "key_size": 8, "key": "OpcodeKey", "value": "OpcodeValue" }The second file, xdefs.json specifies the layout of the Table entry (both Key and Value) as well as defines the Table ID.
Figure 9. xdefs.json defining the layout of the OPCODE table, key and value
{ "struct": [ { "name": "OpcodeKey", "description": "Opcode Table Key (Opcode character)", "fields": [ { "type": "BitField", "name": "opcode", "size": 8, "value": 0 } ] }, { "name": "OpcodeValue", "description": "Opcode Table Value (jump label)", "fields": [ { "type": "BitField", "name": "valid", "size": 1, "value": 0 }, { "type": "BitField", "name": "padding", "size": 23, "value": 0 }, { "type": "BitField", "name": "jump_address", "size": 32, "value": 0 } ] }, ],
"enumerator" : [ {"name": "opcode_table", "values": [ {"name":"id", "value": 5} ] } ]}As can be seen from the definition above, the OPCODE table Key is the 8-bit opcode (that will come directly from the XCalcV1 header) and the Value is the 32-bit jump address that will be programmed to point to the code that executes the operation, specified by the opcode.
Let's see how this works in the code.
The code that checks the presence of the XCalcV1 header (Figure 10), initiates the lookup in the OPCODE table and performs the jump according to the received value.
Figure 10. Using the OPCODE table for jump address selection
ingress: BRBTSTCLR R11.3, 1, not_xcalcv1_packet LKP.LF0.R R0.0, R0.0, -1, -1, R3.0, R3.0, 5, 1, 1, 4 SYNC.N 1, unknown_opcode BR R0.0
do_add: ADD R3.3, R3.1, 0, 32, R3.2, 0, 32 BRI calc_donedo_sub: SUB R3.3, R3.1, 0, 32, R3.2, 0, 32 BRI calc_donedo_and: AND R3.3, R3.1, 0, R3.2, 0, 32 BRI calc_donedo_or: OR R3.3, R3.1, 0, R3.2, 0, 32 BRI calc_donedo_xor: XOR R3.3, R3.1, 0, R3.2, 0, 32 BRI calc_done
calc_done:Here is a breakdown of these steps:
Now that the required arithmetic or logical operations have been performed, we need to update the packet and send it out, specifically:
Here is how this can be implemented:
Figure 11. Updating and sending the packet
calc_done: STH R2, 0, 0, 6 SHRI R2, R2, 0, 96, 48 STH R2, 0, 6, 6
STH.SYNC R3.3, 1, 12, 4
AQMEG.LF1.NOMIRR R0.0, R1.3 MOVI.CD R2.3, 0 SYNC 2 BRBTSTSET R0.0, 0, aqm_drop SENDOUTI.H R1, R2, 0, 0
not_xcalcv1_packet:unknown_opcode:aqm_drop: SYNCALL 255 DROP.H 0Here is a breakdown of these steps:
This concludes our basic project.
Now that we have the basic infrastructure in place, it becomes easy to add more operations to our calculator. Here are some we would suggest as an exercise to the interested readers.
Here are examples of additional useful operations that can be implemented using a single instruction available in XISA followed by a branch to calc_done:
Here are examples of additional useful operations that can be implemented using a simple sequence of instructions, for example:
In addition, it is possible to implement arbitrarily complex calculations such as multiplication for which there is no dedicated XISA instruction.
We will implement the multiplication operation using the classic algorithm as shown below:
Figure 12. Multiplication program algorithm
uint32_t multiply(uint32_t a, uint32_t b) { uint64_t x = a; uint64_t result = 0;
while (b != 0) { if (b & 1) { result += x; } x <<= 1; b >>= 1; } return (uint32_t)result;}For our case, we will allocate additional registers. Since these are 128-bit registers, we'll use only the lower 64 bits (R5.2 – R5.3 and R4.2 – R4.3).
Now let's look at the code as written in Figure 13.
Figure 13. Multiplication code
do_mul: MOV.CD R5.3, R3.1 MOVI.CD R4.3, 0
mul_loop: CMPI R3.2, 0, 0, 32 BRIEQ mul_done BRBTSTCLR R3.2, 0, mul_shifts ADD.F R4.3, R4.3, 0, 32, R5.3, 0, 32 BRINC no_carry ADDI R4.2, R4.2, 0, 32, 1no_carry: ADD R4.2, R4.2, 0, 32, R5.2, 0, 32
mul_shifts: SHLI.CD R5, R5, 0, 64, 1 SHRI.CD R3.2, R3.2, 0, 32, 1 BRI mul_loop
mul_done: MOV R3.3, R4.3 BRI calc_doneLet's explore each step in detail:
Note: Unlike the previous line that clears and shifts the entire 128-bit register, this is a 32-bit instruction that only affects R3.2, and no other word registers within R3.
As we can see, XISA allows the data plane programmer to implement algorithms that go far beyond packet switching. The instruction set is rich and comparable to the ISA of many modern CPUs, which allows us to perform computations, execute jumps to the addresses computed at run-time and implement loops.
For more information, contact us.
We, at Xsight Labs, believe this transparency will drive innovation and facilitate the development of future networking technologies.
Stay tuned for further insights into the capabilities of the X-Switch ISA and the exciting possibilities it unlocks.
That completes the series. The hub sets out what the open instruction set is and what it gives you, and the glossary covers the terminology.
XISA (the X-Switch Instruction Set Architecture) is Xsight Labs' open instruction set for programming packet processing on the X-Switch family, published under the Mozilla Public License version 2. Programs run across two stages: a Programmable Parser that identifies packet headers, and a Match-Action Processor (MAP) that performs lookups, edits and forwarding.