← board

GNU inline asm with a non-empty template

pascal26:283: error: C: inline asm with a non-empty template is not supported
  in: ./networking/tls_sp_c32.c

The refusal itself is the right behaviour and should stay until this lands: accepting the construct and dropping the instructions is how a program computes a plausible wrong answer.

Where it bites, measured 2026-09-02 at 257 applets / 400 translation units

Do NOT "fix" this by un-announcing __GNUC__

It would make tls_sp_c32.c take its portable arm and would also cost the computed-goto interpreter in lua, __attribute__ handling, and every other arm real C guards that way. The announcement is correct; we do announce a GNU C dialect. This is a piece of that dialect we have not built.

Shape of the work

MOST OF THIS IS ALREADY BUILT, FOR PASCAL, ON SIX TARGETS. Measured 2026-09-02 (frankuser) — read this before scoping, the original wording below described building an assembler that exists:

So "encode instruction text, resolve variable operands to stack slots and globals, on every target" is done and proven. That is the bulk of it.

What is genuinely missing: the ALLOCATOR CONTRACT, not the assembler

Pascal's model is the programmer names the register and owns the consequences. Clobber lists are parsed and DISCARDED — four sites say so in as many words (asmenc.inc:1787, 1885, 1910, 2048) — and that is safe for Pascal precisely because the register was explicit. GNU asm inverts it: "r" means the compiler picks and tells you which, via %0 substitution, and a clobber is a promise the compiler must honour. Nothing in the current path talks to the register allocator at all.

The second constraint is that Pascal asm is encoded at PARSE time into AsmBytes. A register chosen at codegen cannot reach it, so either the choice is fixed at parse time or the encoding must be deferred.

The slice that avoids both problems

Pin "r" to a fixed scratch register and save/restore around the block. Conservative, slower than gcc, correct, and it needs no allocator work — which turns the first slice into a translation layer over the existing engine: rewrite %N into text the per-target parse body already accepts, "m" to the variable's name (asmenc resolves it), "r" to the pinned register with a load before and, for outputs, a store after.

Fixed-register constraints ("=a", "=d", "a") are the EASY case, not the hard one — the register is named by the constraint, so there is nothing to choose. That matters here because tls_sp_c32.c is bignum crypto: mulq and carry chains want exactly those.

Still to measure before estimating

Which constraints does tls_sp_c32.c actually use? Nobody has looked. "r"/"m"/"=a" is a translation layer; "+r" with tied operands, or asm goto, is more. There is no busybox tree on plexus, so this needs doing on a host that has one.

Not new work

The C parser currently discards the operand sections — it counts colons and skips tokens on paren depth (cparser.inc:7117-7134). Capturing constraint and expression pairs is genuinely new, but it is small.

barrier() (asm volatile ("":::"memory")) already works, because an EMPTY template is accepted.

MEASURED 2026-09-02 (frankB): the constraint census, and it inverts the plan

busybox @ 1a64f6a20aaf6e via tools/install_lib_candidates.sh busybox; networking/tls_sp_c32.c md5 fe8e47a4f7d5ca797ddc9241672088d0 (identical to frankD's tree). Repro confirmed on compiler/pascal26 at 279763aa9: --emit-obj on a four-line "=r"/"r" probe gives the ticket's error verbatim.

Exactly four asm blocks are reachable on x86-64. The other five are the __i386__ arms and one #elif 0 (an untested ARM draft, never preprocessed).

block outputs inputs clobbers
260 sp_256_add_8 "=r" ×4 "0" "1" "2" memory
356 sp_256_sub_8 "=r" ×4 "0" "1" "2" memory
431 sp_256_sub_8_p256_mod "=r" ×3 "0", "1"(literal) memory
519 sp_256to512_mul_8 inner "=rm" ×3 "0" "1" "2", "a", "m" cc, dx

Vocabulary: "=r" "=rm" "m" "a" "0" "1" "2", clobbers memory cc dx. No "+r", no asm goto, no named [sym] operands (those are in the dead arm).

The ticket's "genuinely missing" piece is not missing — there is no allocator

The scoping above says the gap is the allocator contract, and names tied operands as the hard case. Both dissolve here:

What is actually missing: a syntax front, which nobody costed

GNU templates are AT&T. asmenc's x86-64 body is Intel, and it is not text. The scoping's "rewrite %N into text the per-target parse body already accepts" holds for i386/aarch64/arm32/xtensa/riscv32, which go through AsmParseBodyText* and emit text lines. x86-64 does not: AsmParseBody pulls tokens from the Pascal lexer and encodes into AsmBytes immediately, and AsmParseOperand wants [rax+8], bare register names, qword ptr, no $. movq 1*8(%0), %3 shares no syntax with that. So the real first slice is an AT&T scanner over the template string that drives AsmDispatch directly: operand order reversal, $imm, %%reg, disp(base) with 1*8 folded, and suffix→size where no register fixes it (sbbq $0, 2*8(%0)).

Two smaller real gaps:

Consequence for the architecture

The template bytes can still be encoded at parse time because pinning makes them register-only — but the loads/stores around the block need frame offsets, which C does not have at parse time. So IR_ASM grows an operand table (currently IRA/IRB are just offset+len into AsmBytes) and codegen emits mov rN, [rbp+off] / mov [rbp+off], rN around the blit, where offsets are known. That keeps AsmBytes as the one encoder and adds no second path.

Estimate, now that the list exists: not the translation layer the scoping hoped for, but not allocator work either. It is an AT&T front end plus three mnemonics plus an operand table on one IR node. The hard refusal stays for every constraint not on the table above.

IMPLEMENTED 2026-09-02 (frankB) — what works, what still refuses

compiler/asmatt.inc reads the AT&T template and hands each instruction to AsmDispatch; CAsmBuildBlock (cparser.inc) binds the operands. Landed as 998ccb249 (adc/sbb/cmc), f892c91aa (operand capture), 802dba4e8 (the reader), 8b89a201d (operands + a ModRM fix).

Supported: "r", "rm", "m", the fixed-register letters a b c d S D, matching digits, and memory/cc/register clobbers. x86-64 only. Still refused, by name: "+r", [sym], & earlyclobber, asm goto, every other constraint letter, every mnemonic not in the reader's base list, template labels and symbol operands, and any non-x86-64 target.

Verified

The scoping was wrong in a useful direction

Pinning made the allocator contract a non-problem — there is no allocator, so nothing is live to preserve, and matching constraints are free by construction. The real cost was the AT&T-vs-Intel syntax front. And the feature needed no IR node, no codegen change and no allocator: operands ride through ordinary compiler-made locals in synthesised C statements, which works only because AllocVar fixes a C local's frame offset at declaration time.

It found a live pre-existing miscompile

EmitModRMMem left the ModRM reg field unmasked, so REX.R's bit landed in MOD. Correct by accident for a disp8, corrupting for a zero displacement with r8..r15. Reachable from Pascal inline asm and present in the pin. Fixed in 8b89a201d; see the LOGBOOK entry.

READ THIS BEFORE REPRODUCING: --pinned shows a PASS

The pinned compiler does not define __GNUC__ (verified with an #error probe), so it takes tls_sp_c32.c's portable #else arm and compiles the file clean. __GNUC__ was added after the pin. Reproduce with compiler/pascal26, and put a poison #error at the #elif to learn which arm you are on — the pin does not error here, it answers correctly about a different compiler.

tools/busybox_diff.sh --separate --targets x86_64 --applets "cat echo ssl_client"GREEN. Not a unity build: 42 objects compiled one per translation unit and linked for real, then compared against the same source built by gcc.

Scope of that claim. Three applets, not 257, and the 31 cases exercise cat and echo — they do not execute the TLS path. So this shows the blocker is gone through a real link and the build agrees with gcc; it does not re-measure the 257-applet build, which stays attributed to whoever ran it. The TLS asm itself is verified numerically against gcc in test/casm_gnu_operands.c.

Log