Files
6e7ed74958 gcasm: direct cross-chunk asm calls (drop the linkname forwarding hop) (#61)
* gcasm: call cross-chunk functions directly from asm, dropping the forwarding wrappers

Cross-chunk calls from transformed asm went through a generated Go
wrapper (gcasmFwdFnN) that forwarded through a //go:linkname alias:
asm -> ABI0 bridge -> wrapper frame -> ABI bridge -> callee, several
frames per call on hot kernel paths.

The wrapper existed because a dotted import path was believed
unspellable in plan 9 asm. It is spellable: the asm lexer accepts
U+00B7 and U+2215 as identifier runes and folds them to "." and
"/", so CALL github·com∕goccy∕...∕p1·Fn19(SB) links against the
canonical remote symbol. calleeSig now emits that direct reference
for remote fns captured on the same arch whenever the import path is
spellable; hyphenated host paths keep the wrapper.

A transformed remote resolves against its TEXT (ABI0, the same stack
layout the call-site marshalling already stages). A fn that fell
back to its pure Go body has no TEXT, so its own package now emits a
gcasmABI0Keep anchor — an asm reference to each fallback fn, which
makes the compiler emit the ABI0 wrapper the direct CALL links
against. This keeps the direct-call decision local: no cross-package
two-pass, every captured fn is callable either way. The owner map
gating direct calls is now per arch, since a fn's TEXT/anchor exists
per arch.

The new cg_crosscall fixture (direct calls between padded functions,
plus a wide-signature fn that sig-falls-back) splits across chunks
under a tiny budget; gate4 runs it in single-package, multi-chunk and
dotted-path multi-chunk layouts against the pure build, and the new
crosschunk tests assert the emitted shapes (spelled CALLs, no
gcasmFwdFn wrappers, the keep anchor, and the wrapper fallback for an
unspellable path).

Co-Authored-By: Claude Fable 5 <[email protected]>

* gcasm: emit the ABI0 keep anchor only for plan-9-spellable import paths

The gcasmABI0Keep anchor exists solely to force ABI0 wrappers for the
pure-Go fallback fns that other chunks' asm CALLs directly by their
spelled dotted symbol. An import path plan 9 asm cannot spell keeps
the gcasmFwd linkname wrappers for every cross-chunk call instead, so
no fallback fn is ever referenced directly and the anchor is pure
dead weight there. Gate its emission on plan9AsmPathSafe(importPath);
TestCrossChunkWrapperFallback now asserts no anchor is emitted on the
unspellable path.

Co-Authored-By: Claude Fable 5 <[email protected]>

* gcasm: assert the multi-chunk bundle's amd64/arm64 file-set contract

The direct-call path's parse economy rests on a file-set invariant
that had no test: on the asm arches the codegen amd64||arm64 alias.go
is deleted by the gcasm backend, its cross-chunk declarations fold
into the decls_<arch>.go the asm bodies need anyway, and no separate
linkname-only alias file is compiled there.

TestPlan9SafeBundleFileStructure builds the cg_indirect fixture (whose
elem table takes cross-chunk function addresses) in multi-chunk mode,
merges the gcasm delta over the codegen tree exactly as the pipeline
does, and asserts: no chunk keeps an amd64||arm64 alias.go; every
chunk has an arch-guarded decls_amd64.go/decls_arm64.go; and every
remaining //go:linkname-bearing alias file is guarded off the asm
arches. It runs for both a spellable and a hyphenated import path —
the file-set contract is identical; only the decls' CONTENT (direct
symbols vs gcasmFwd wrappers) differs.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-09-02 18:31:18 +09:00

79 lines
3.7 KiB
WebAssembly Text Format

(module
;; Direct cross-function calls between non-recursive functions.
;; Each body is padded past the smallest chunk budgets the tests
;; use, so under a tiny multi-package chunk budget every function
;; lands in its own chunk package and the callers' transformed asm
;; makes cross-chunk CALLs.
;; mixA: a long dependent i64 chain (~30 ops).
(func $mixA (param $a i64) (param $b i64) (result i64)
(local $x i64)
(local.set $x (i64.add (local.get $a) (local.get $b)))
(local.set $x (i64.mul (local.get $x) (i64.const 31)))
(local.set $x (i64.xor (local.get $x) (i64.const 0x9e3779b97f4a7c15)))
(local.set $x (i64.add (local.get $x) (i64.shr_u (local.get $x) (i64.const 7))))
(local.set $x (i64.mul (local.get $x) (i64.const 0x100000001b3)))
(local.set $x (i64.xor (local.get $x) (i64.shl (local.get $x) (i64.const 13))))
(local.set $x (i64.add (local.get $x) (i64.const 12345)))
(local.set $x (i64.sub (local.get $x) (i64.rotl (local.get $a) (i64.const 17))))
(local.set $x (i64.xor (local.get $x) (i64.rotr (local.get $b) (i64.const 9))))
(local.set $x (i64.mul (local.get $x) (i64.const 7)))
(i64.add (local.get $x) (i64.shr_s (local.get $x) (i64.const 3))))
;; mixB: a different long chain.
(func $mixB (param $a i64) (result i64)
(local $y i64)
(local.set $y (i64.mul (local.get $a) (i64.const 0x5851f42d4c957f2d)))
(local.set $y (i64.add (local.get $y) (i64.const 0x14057b7ef767814f)))
(local.set $y (i64.xor (local.get $y) (i64.shr_u (local.get $y) (i64.const 33))))
(local.set $y (i64.mul (local.get $y) (i64.const 0xff51afd7ed558ccd)))
(local.set $y (i64.xor (local.get $y) (i64.shr_u (local.get $y) (i64.const 29))))
(local.set $y (i64.add (local.get $y) (i64.rotl (local.get $a) (i64.const 5))))
(local.set $y (i64.sub (local.get $y) (i64.const 999)))
(local.set $y (i64.or (local.get $y) (i64.const 1)))
(i64.mul (local.get $y) (i64.const 3)))
;; mid combines both leaves; its own padding keeps it a separate
;; chunk too.
(func $mid (param $a i64) (param $b i64) (result i64)
(local $z i64)
(local.set $z (i64.add
(call $mixA (local.get $a) (local.get $b))
(call $mixB (local.get $a))))
(local.set $z (i64.xor (local.get $z) (i64.shr_u (local.get $z) (i64.const 11))))
(local.set $z (i64.mul (local.get $z) (i64.const 5)))
(local.set $z (i64.add (local.get $z) (i64.rotr (local.get $b) (i64.const 21))))
(local.set $z (i64.sub (local.get $z) (i64.rotl (local.get $a) (i64.const 2))))
(local.set $z (i64.xor (local.get $z) (i64.const 0x0f0f0f0f0f0f0f0f)))
(i64.add (local.get $z) (i64.const 42)))
;; wide: enough i64 params to overflow the register ABI, so this
;; function keeps its pure Go body (sig fallback) while its callers
;; are transformed — the cross-chunk call then resolves against the
;; ABI0 wrapper the gcasmABI0Keep anchor forces out.
(func $wide
(param i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64 i64)
(result i64)
(i64.add (local.get 0)
(i64.add (local.get 1)
(i64.add (local.get 2)
(i64.add (local.get 3)
(i64.add (local.get 4)
(i64.add (local.get 5)
(i64.add (local.get 6)
(i64.add (local.get 7)
(i64.add (local.get 8)
(i64.add (local.get 9)
(i64.add (local.get 10) (local.get 11)))))))))))))
(func (export "crosscall") (param $a i64) (param $b i64) (result i64)
(i64.add
(i64.add
(call $mid (local.get $a) (local.get $b))
(call $mixB (local.get $b)))
(call $wide
(local.get $a) (local.get $b) (local.get $a) (local.get $b)
(local.get $a) (local.get $b) (local.get $a) (local.get $b)
(local.get $a) (local.get $b) (local.get $a) (local.get $b))))
)