diff --git a/docs/native-const-arg-arc-port.adoc b/docs/native-const-arg-arc-port.adoc new file mode 100644 index 0000000..70662d4 --- /dev/null +++ b/docs/native-const-arg-arc-port.adoc @@ -0,0 +1,99 @@ += Porting shape-aware const-string argument ARC to the native backend +:toc: macro +:toclevels: 2 + +== Status + +IMPLEMENTED 2026-06-11 — the port landed atomically in the native +backend along the lines below, with one major design improvement: a +single unified hoist region (EmitArgHoist) carries open-array literal +blocks, record-call argument buffers AND const-string pin slots, which +eliminated the sret-buffer/argument-slot interleaving bug as a side +effect. At unknown-signature sites (interface dispatch) every +string-typed value argument is protected by shape — pin/consume are +correct on top of a value-param callee pair too, so no +TInterfaceTypeDesc const-flag extension was needed (the var flags it +already carries mark positions to skip). Validated: full suite both +backends, FIXPOINT_OK, NATIVE_FIXPOINT_OK, natively-compiled +TestRunner (two pre-existing generic-template round-trip failures +remain — see bugs.txt). + +The original analysis below is kept for the record. + +The QBE backend gained shape-aware +const-string argument handling (commits 91f44ec, 17c14b3): literals and +named consts borrow (no ARC ops), owned returns are consumed (one +post-call release), plain locals/params borrow behind aliasing +exclusions, everything else pins. Combined with the const conversion of +the hot TStringList signatures this removed most caller/callee ARC pairs +on the compiler's hottest paths (self-compile 0.78 s -> 0.60 s). + +The native backend currently uses the OPPOSITE convention: the callee +retains const string params at entry and releases at exit +(blaise.codegen.native.x86_64.pas, the entry/exit passes in +EmitFunctionDef). That convention was chosen deliberately during the +native TestRunner bring-up: it protects rc=0 transients at EVERY call +site with zero per-site machinery, because the protection lives in the +callee. + +== Why the port must land atomically + +Flipping the native callee to skip the const-string pair makes EVERY +call path responsible for caller-side protection. Any path that passes +a const-string argument without protection reintroduces the exact heap +corruption fixed during the TestRunner bring-up (rc=0 concat transient +freed mid-call). The native backend has more call-emission paths than +the QBE backend: + +. The ~10 standard push-loop sites already funnelled through + BeginCallArgs/PushCallArg/EndCallArgs (open-array hoist machinery) — + straightforward to extend. +. EmitCall's two inline strategies (≤6 push/pop and >6 slot-block) — + extend like the open-array literal hoist. +. EmitInterfaceCall — args are pushed via EmitMethodArgPush(nil, Arg): + the TMethodParam is nil, so const-ness is invisible at the call site + today. The information EXISTS (TInterfaceTypeDesc carries method + param signatures, incl. var-param flags) but is not wired through. + Without this wiring, interface method implementors whose params are + const would be called unprotected. +. EmitCallIndirect / EmitMethodPtrCall — TProceduralTypeDesc signatures + available; same wiring needed. +. EmitIntfSretCall — no call frame (the sret buffer must sit exactly at + %rsp at call time); needs its own pin-slot region or a loud + unsupported-shape error. + +== Mechanism (per-site pin slots) + +Extend TOALCallFrame: + + PinDepths: TList; { per-arg slot depth in the pre-region; -1 = none } + PinModes: TList; { 1 = pin (AddRef+Release), 2 = consume (Release) } + +BeginCallArgs phase A: after hoisting open-array literal blocks, reserve +one zeroed slot (pushq $0, Total += 8) per const-string argument whose +NativeConstArgMode is pin or consume. PushCallArg: after the value is +pushed, copy it into its slot at (Total - PinDepth + Pushed)(%rsp); for +pin mode also AddRef it. EndCallArgs: after the callq, before the +region addq — release every reserved slot (release of a still-zero slot +is a nil no-op). The release loop must preserve the call's return +registers: %rax, %rdx (reg-class record returns) and %xmm0/%xmm1 +(SSE-class record returns) around the _StringRelease calls. + +NativeConstArgMode mirrors the QBE classification. For borrowed locals +the address-taken walker (CollectAddressTaken + the nested-capture +union) must be shared — it is pure AST logic; move it out of +TCodeGenQBE into a common unit rather than duplicating it. + +== Validation requirements + +The full QBE-side lesson applies: a codegen-rule change can pass the +fixpoint while a second-generation binary breaks elsewhere. Required: +two self-hosted generations, gen-2 `--backend native` emission of the +full compiler, the native-runner suites (127/134 currently green), the +e2e suite on both backends, and both fixpoints. + +== Expected gain + +Lower than the QBE side's, and it accrues to native-COMPILED programs, +not to compiler build times (the compiler runs as a QBE-built binary). +Worth doing for output-code quality once the wiring above is in place.