Extending the assembler: a checklist
Use this order when adding an instruction. ADD and SUB are useful examples of the full path from source text to bytes and back.
-
Choose the exact syntax and encoding. Match the complete operand form and CPU mode in the instruction reference, not just the mnemonic. Write down the opcode, any prefixes or ModR/M bytes, immediate width, total size, and effects on registers and flags. Calculate a small expected byte sequence by hand before implementing it.
add eax,7;uses05 07 00 00 00;sub eax,7;uses2D 07 00 00 00. Each instruction occupies five bytes. -
Add a statement kind in include/program.h. Give it a consistent name, such as
ST_ADD_RIMorST_SUB_RIM. Existing fields are sufficient for these forms:operandstores the register token,expressionstores the expression root,valuestores its evaluated immediate, andoffsetstores the instruction’s position in the image. -
Parse the operands in src/program.c. Recognize the mnemonic and assign its statement kind. For register/immediate instructions, follow MOV’s pattern: register, comma, expression, semicolon. Save the register token in
s.operandand the result ofparse_expression()ins.expression. INC/DEC’s one-operand parser is not enough for ADD/SUB. Ordinary instruction names are already identifier tokens; change the lexer only if the new syntax introduces something it cannot tokenize. -
Validate and evaluate in src/semantic.c. Include the new kind in the register check and, when it has an immediate expression, in the code that assigns
s->valuefrom the evaluated expression. Decide the accepted range explicitly. ADD/SUB currently use signed 32-bit expression results; MOV additionally requires a nonnegative immediate. Forgetting the value assignment can silently encode zero instead of the requested value. -
Reserve the full size in src/layout.c. Add the kind to
instruction_size(). Count every emitted byte, including prefixes, operands, and embedded address slots. ADD/SUB reserve five bytes. An incorrect size shifts later labels and breaks jumps even if the instruction’s own bytes look correct. -
Emit bytes in src/encode.c. Select the correct opcode and write the operand in its required format.
little_endian(bytes, value, 4)writes four operand bytes, not a four-byte instruction. ADD/SUB each write one opcode byte followed by four immediate bytes.FFis an opcode group, not a prefix to put before every instruction. For branches, resolve the target and calculate a displacement from the instruction’s end; for address slots, record relocation patches instead. Propagate allocation/write failures. -
Recognize the bytes in src/decode.c. Check enough bytes remain before reading operands, print the instruction and its reconstructed operands, and advance by the complete encoded size. Match the chosen signed or unsigned interpretation of immediates. The CLI currently decodes after writing its files, so forgetting this step can make a correctly encoded program exit with
unknown or truncated encoding. -
Add an example and a permanent test. Put a small source file under
examples/and register it in CMakeLists.txt. Useadd_example_test(name examples/name.asm "EXPECTED HEX")for byte checks. Followencode.addwhen also checking the listing: passEXPECTED_DECODE_FILEto the reusable script and commit that text fixture undertests/. Hand-calculated expectations should be independent of the encoder. Files underbuild/are temporary and do not become regression tests automatically. -
Rebuild, test, and inspect runtime behavior. Run the build and CTest commands in Building and testing. Cover a normal value, zero, supported negative expressions, range boundaries, invalid registers, and malformed operands as appropriate. Test layout with a label or jump after the new instruction. For execution, regenerate
program.hbeside the runner, then recompile the runner before running it. Check the result against a value calculated by hand; test flags explicitly when their behavior matters. Run Windows and WSL2 checks before a release. -
Update documentation and the development milestone. Record the syntax, supported operands, byte format, limits, and test coverage in this reference, and add a chapter to the dev journal describing why the feature was added. Check the version in CMake and the version history agree. A new feature does not require an immediate release.
For example, mov eax,49; sub eax,3+4; ret; follows this path: the
parser stores the expression, semantic checking computes 7, layout
reserves five bytes for SUB, the encoder emits 2D 07 00 00 00, the
decoder prints sub eax, 7, and execution returns 42. The expression is
evaluated during assembly; the subtraction from EAX happens at runtime.