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
- networking/tls_sp_c32.c — the only remaining non-crtl failure in the
build. Its asm arms are guarded
#if ALLOW_ASM && defined(__GNUC__) && defined(__x86_64__), and pxx announces__GNUC__(which is also why lua takes its computed-goto interpreter here). There IS a portable#elsearm in the file; we do not reach it. Its failure is also the link failure —undefined reference to curve_P256_compute_pubkey_and_premasteris this one object missing, not a separate defect. - Not
networking/udhcp/dhcpc.c, which reported the same error until 2026-09-02. That one was the HOST's<asm/swab.h>(__asm__("bswapl %0" : "=r" (val) : "0" (val))), reached through<linux/filter.h>, and is fixed by shadowing that one header with a file that defines no__arch_swab*so<linux/swab.h>takes its own portable branch. Worth knowing before this ticket is picked up: the error names the file it was reached FROM, not the file the asm is in, and that cost one wrong diagnosis already.
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:
compiler/asmenc.incis a real inline assembler with a per-target parse body for x86-64, i386, aarch64, arm32, xtensa and riscv32.- Its operands already resolve by name to a local, param, global or
register — locals and params become
[rbp+disp32](asmenc.inc:6), globals get a deferredAsmGlobFixentry patched with the realEmitGlobRefat codegen. - Pascal's
asmlowers toIR_ASMcarrying a token span;ir_codegen.inc:6402blits the pre-encoded bytes 1:1, fixing up global operands.
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:
grep -n 'RegAlloc\|AllocReg\|register allocator' compiler/*.increturns nothing. pxx's x86-64 codegen keeps nothing live in registers across statements — which is exactly whyAsmParseBodycan discard clobbers (asmenc.inc:2048 says so, and the empty grep is a second source that fails differently). Somemory,ccanddxare all free, provided the pinned scratch pool avoidsrdx.- Tied operands are only hard when you allocate. Under the ticket's own
pinning scheme, operand N IS a fixed register, so
"0"(a)means "loadainto that same register first" — satisfied by construction. Tied is the cheapest constraint here, not the dearest.
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:
adc,sbb,cmcare absent fromAsmDispatch(822-1260). All four blocks are carry chains; none of them can encode today."m" (bb[j])is not a name.AsmParseOperandresolves operands byFindSym; an indexed lvalue has no symbol. Its address has to be materialised into a pinned register and substituted as[rN].
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
- Three of the four x86-64-reachable arms of
tls_sp_c32.ccarried verbatim intotest/casm_gnu_operands.cprint exactly whatgcc -O0prints for the same source. Carry inputs are chosen so a dropped carry cannot pass. --emit-objon the realnetworking/tls_sp_c32.csucceeds and the object definescurve_P256_compute_pubkey_and_premaster.tools/casm_att_diff.py: 40 AT&T instructions, each disassembling exactly as gas assembles the same text, with a swapped-operand positive control.
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.
A real link, verified here
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.
networking_tls_sp_c32.ois in the object list, so the file this ticket is about was compiled by pxx and linked, not skipped.- The linked binary defines
curve_P256_compute_pubkey_and_premaster— the exact symbol whose absence took the 400-object link down. PASS x86_64 byte-identical to the gcc oracle over 31 cases.
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
- 2026-09-02 — resolved; this names the commit that carried the resolve, which is not always the one that carried the change — commit 8a9c0f7c7.