Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Semantic checking and encoding

Semantic checking and evaluation

The semantic checker evaluates expression nodes in dependency order, then validates operands and stores each MOV, ADD, SUB, OR, AND, ADC, CMP, or INT immediate in Statement.value. For example:

mov eax, (10+4)*3;

Produces:

B8 2A 00 00 00

MOV and arithmetic/bitwise instructions require eax; PUSH/POP require rax. Every literal and intermediate expression result must fit the signed 32-bit range -2147483648..2147483647. The final MOV immediate must also be nonnegative, giving an accepted range of 0..2147483647. These are the current language restrictions. The lexer’s larger literal range does not bypass semantic checking.

Examples rejected by the semantic checker:

InputDiagnostic
mov ebx, 7;only register eax is supported
mov eax, 1-2;mov immediate must be nonnegative in this language
mov eax, 2147483647+1;expression outside signed 32-bit range

Intermediate results are checked too: mov eax, (2147483647+1)-1; is rejected even though its final mathematical result would fit. Evaluation computes values for later encoding; it does not execute instructions or modify CPU registers.

Encoding and current limitations

The encoder emits five bytes per MOV: opcode B8, followed by the evaluated immediate as four bytes in little-endian order. For example, 42 becomes 2A 00 00 00. RET emits one byte, C3. The byte buffer grows dynamically as instructions are appended.

The current language supports MOV, ADD, SUB, OR, AND, ADC, CMP, INC, and DEC on eax, PUSH/POP on rax, INT with an immediate vector, operand-free RET, short, near, or absolute indirect JMP, and near JZ/JNZ/JB/JL to a label. Far jumps, short conditional jumps, other condition codes, other instructions and registers, labels in expressions, memory operands, directives beyond DB/DW/DD/DQ and their aliases, and object or executable file formats beyond COFF are not implemented. MOV immediates must be in 0..2147483647; ADD/SUB/OR/ADC accept signed 32-bit expression results, subject to the expression restrictions below. INT requires a final value in 0..255. Blocks provide grouping, not scope or control flow. Empty input or empty blocks print an empty hex line and Decoding successful., write an empty binary, and generate a header with code_size = 0 and a placeholder array element so the declaration remains valid C.

ADD and SUB immediate expressions

add eax, <expression>; adds the evaluated immediate to EAX at runtime; sub eax, <expression>; subtracts it. Only lowercase eax is accepted. The parser requires a comma and a final semicolon, just as for MOV.

InstructionOpcodeImmediateTotal size
add eax,7;0507 00 00 005 bytes
sub eax,7;2D07 00 00 005 bytes

The destination EAX is implicit in these opcodes. The immediate is four bytes in little-endian order. Layout reserves five bytes for either instruction, and the decoder prints the immediate as a signed value.

ADD/SUB accept expression results in -2147483648..2147483647; MOV retains its nonnegative restriction. Every literal and intermediate result must still fit the existing signed 32-bit expression limits. Unary minus can be written as -1 or -(1+1). Runtime arithmetic wraps to 32 bits and updates arithmetic flags, including carry; it does not use the assembler’s expression-overflow checks.

examples/add.asm loads 35 and adds 3+4, returning 42:

mov eax,35;
add eax,3+4;
ret;

Its bytes are B8 23 00 00 00 05 07 00 00 00 C3. The permanent encode.add test checks these bytes and the listing in tests/add_expected.txt. Run it after building:

ctest --test-dir build/windows-debug -C Debug -R encode.add --output-on-failure

The SUB counterpart mov eax,49; sub eax,3+4; ret; produces B8 31 00 00 00 2D 07 00 00 00 C3. Both examples returned 42 in manual WSL2 execution checks. SUB’s bytes and listing were also checked manually; SUB does not yet have a permanent CTest entry. These runtime checks did not measure flags.

Bitwise OR

or eax, <expression>; combines the current EAX value with the evaluated immediate, setting each result bit if that bit is set in either operand. It requires eax, a comma, an expression, and a semicolon. The expression uses the same signed 32-bit limits as ADD/SUB; a negative result supplies its 32-bit two’s-complement bit pattern. This adds a runtime instruction, not a new expression operator.

The encoding is 0D followed by four immediate bytes in little-endian order, for a total of five bytes. The decoder reads the immediate and prints or eax, <value> with a signed decimal value.

mov eax,40;
or eax,2;
ret;

The program produces B8 28 00 00 00 0D 02 00 00 00 C3. The binary patterns 00101000 (40) and 00000010 (2) combine to 00101010 (42). Exact bytes and the decoded listing were checked manually, and WSL2 execution returned result = 42. These checks are not yet registered as a permanent CTest test, and flags were not directly tested.

Bitwise AND

and eax, <expression>; combines the current EAX value with the evaluated immediate, clearing each result bit unless it is set in both operands. It requires eax, a comma, an expression, and a semicolon. The expression uses the same signed 32-bit limits as ADD/SUB/OR; a negative result supplies its 32-bit two’s-complement bit pattern.

The encoding is 25 followed by four immediate bytes in little-endian order, for a total of five bytes. The decoder reads the immediate and prints and eax, <value> with a signed decimal value.

mov eax,42;
and eax,-1;
ret;

This produces B8 2A 00 00 00 25 FF FF FF FF C3. Unary negative expressions are supported, so -1 is equivalent to 0-1.

Adding with carry

adc eax, <expression>; adds the evaluated immediate and the current carry flag (CF) to EAX at runtime:

EAX = EAX + immediate + CF

It requires lowercase eax and accepts signed 32-bit expression results under the same expression limits as ADD/SUB. Its encoding is 15 followed by four immediate bytes in little-endian order, for five bytes total. For example, adc eax,2+3; emits 15 05 00 00 00 and decodes as adc eax, 5.

The CPU performs 32-bit arithmetic and updates arithmetic flags, including CF. ADC consumes the incoming carry and produces a new carry, which lets additions propagate carry between parts of a larger number. MOV does not change CF, so loading EAX alone does not establish a known carry value.

This example explicitly sets carry before ADC:

mov eax,0;
sub eax,1;
mov eax,10;
adc eax,2+3;
ret;

Subtracting one from zero sets CF; the following MOV preserves it. ADC therefore computes 10 + 5 + 1 and returns 16. Change the first instruction to mov eax,1; and SUB clears CF, so the program returns 15 instead.

Both versions passed exact-byte and decoded-listing checks, and WSL2 execution returned 16 and 15 respectively. The existing 15 Windows CTest tests also passed. The ADC checks are currently temporary manual checks under build/adc-check, not permanent CTest entries. They verify incoming carry behavior, but do not directly measure the outgoing flags.

Software interrupt encoding

int <expression>; takes one immediate operand, with no register or comma. The internal statement kind is ST_INT_IMM8. The expression is evaluated during assembly and must produce a value in 0..255; values outside that range report interrupt vector must be in range 0..255. Existing expression limits still apply to literals and intermediate results.

The encoding is opcode CD followed by a single unsigned vector byte. Layout reserves two bytes, and the decoder displays the vector in hexadecimal:

SourceBytesDecoded instruction
int 0;CD 00int 0x00
int 0x10;CD 10int 0x10
int 8+8;CD 10int 0x10
int 255;CD FFint 0xFF

To check encoding, save int 0x10; as examples/interrupt.asm and run from the repository root after rebuilding:

cmake "-DKASM=build/windows-debug/Debug/kasm.exe" "-DSOURCE=examples/interrupt.asm" "-DEXPECTED_HEX=CD 10" -P tests/encode_file.cmake

This test assembles and decodes the bytes; it does not execute the interrupt. INT support alone does not add a real-mode target, boot-image generation, or BIOS services to the existing x86-64 Windows/WSL2 runner. BIOS-style use of int 0x10 needs the appropriate execution environment and CPU mode.

Manual tests checked the four valid cases above, each followed by RET to verify the next decoded offset is 2. Negative (0-1) and oversized (256) vectors were rejected. All 15 existing CTest tests passed. The temporary INT checks live under build/int-check; they are not permanent CTest entries, and no interrupts were executed.

Saving and restoring RAX

push rax; emits 50, and pop rax; emits 58. Both occupy one instruction byte, but in the x86-64 runner they transfer an eight-byte register value to or from the stack. Their source operand must be rax, not eax; other operands report push/pop require register rax. They have no immediate expression.

A balanced sequence can save a value while another instruction changes EAX:

mov eax,42;
push rax;
mov eax,99;
pop rax;
ret;

The expected result is 42. Restore the stack before ret so it reads the caller’s return address. The assembler does not verify stack balance.