← board

Text-assembler codegen helpers (EmitAsm386 / EmitAsmX64 …)

Scope split (this is now the shared-core + x86-64 + inline-asm-unify ticket)

The shared asmtext.inc front-end (parser, %/@data/@glob/.label markers, hole binding, rel8/rel32 resolution) and EmitAsmX64 are done here. Each remaining target is its own incremental ticket, all built on this core, recommended order (cheapest-first — targets with a typed encoder layer already done cost least):

This ticket retains: further EmitAsmX64 block conversions + scope item 6 (retarget the user asm … end path in asmenc.inc onto the shared engine).

Done so far (2026-06-14)

Self-host landmine hit (fixed)

PXX evaluates and/or fully (no short-circuit), and indexing an EMPTY AnsiString derefs a nil data pointer. So (Length(s) > 0) and (s[i] = ..) still touches s[i] and crashes on an empty string (FPC short-circuits, hence the divergence: FPC-built compiler fine, PXX-built segfaults). All conditional char reads in asmtext.inc go through AsmTextCharAt (range-checked, returns #0). Also: a var AnsiString parameter reassigned in the callee is a frozen-inline landmine — AsmTextSizeKeyword returns the size instead.

Remaining: @data/@glob reloc holes; single-line overload; EmitAsmX64 for more codegen blocks; then EmitAsm386 and the inline-asm unification.

Motivation

Codegen currently emits raw bytes — EmitB($19); EmitB($D3); { sbb ebx,edx } — with the mnemonic in a comment. ~970 EmitB lines in ir_codegen386.inc alone. It is unreadable and error-prone: this exact pattern produced the session's 19 DB vs 19 D3 ModRM bug (sbb ebx,ebx instead of sbb ebx,edx), which the assembler-computes-ModRM approach cannot make.

Goal: write emit blocks as assembly text, parsed and encoded by a per-target text-assembler, with runtime values bound inline. Readable, fewer bugs (encode ModRM/SIB/REX once, correctly), and it doubles as the backend for built-in asm … end inline assembly (today a separate, rudimentary x86-64-only parser in asmenc.inc) — one engine, two consumers, every mnemonic added helps both.

Target shape (depends on array of const)

One interleaved array of const: each string is one instruction (one instruction per line — house rule), and the integers right after a string are that line's hole values, in order.

EmitAsm386([
  'push ebp',
  'mov ebp, esp',
  'sub esp, %',            frameSize,
  'mov eax, [ebp+%]',      Syms[si].Offset,
  'mov edi, @data',        INTBUF_OFFSET + INTBUF_SIZE,
'.loop:',
  'mov eax, esi',
  'div ecx',
  'dec edi',
  'jnz .loop',
  'int 0x80'
]);

Provide a single-line overload so trivial cases skip the brackets: EmitAsm386('ret'); / EmitAsm386('mov eax, %', v);.

Binding / marker rules

Why this and not alternatives (settled in design)

Scope

  1. x86 text-assembler first — covers i386 and x86-64 from a shared ModRM/SIB/(REX) encoder core (the two hottest backends; also the encoding layer where the ModRM bug lived). Mnemonic table grown on demand — start with exactly the instructions the converted blocks use.
  2. Operand parsing: registers (8/16/32/64), [base+disp], immediates, the % / @data / @glob / label markers above.
  3. Emit through the existing byte sink + fixup tables (EmitB, EmitDataRef, EmitGlobRef, Code[]/Patch32) — no new relocation or ELF machinery; the assembler is a front-end over what's already there.
  4. EmitAsm386 + EmitAsmX64 entry points (+ single-line overloads). Later EmitAsmA64 / EmitAsmArm32 / EmitAsmRv32 / EmitAsmXtensa as those ISAs get an assembler (separate, incremental).
  5. Incremental adoption — mix freely. Convert fixed / label-heavy / lightly bound blocks; leave heavily-dynamic blocks ([ebp+Syms[..].Offset] with many holes) on EmitB/typed encoders. No big-bang rewrite.
  6. Unify inline asm: once the x86 assembler is solid, retarget the user asm … end path (asmenc.inc) onto it so inline asm and codegen share one engine (own follow-up slice; the encoder is the shared asset).

Acceptance

Notes / landmines

Log