Records how the landed implementation differs from the plan: a single unified hoist region (EmitArgHoist) for open-array literals, record-call sret buffers and string pin slots, and shape-based protection at unknown-signature sites instead of a TInterfaceTypeDesc const-flag extension.
100 lines
4.8 KiB
Plaintext
100 lines
4.8 KiB
Plaintext
= 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<Integer>; { per-arg slot depth in the pre-region; -1 = none }
|
|
PinModes: TList<Integer>; { 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.
|