Files
743b14bbfa Fuse goto-form countdown loops with per-iteration scale chains (#52)
* asmgen: emit the branch-based EH ops in direct-asm bodies

Functions containing exception handling fell back to the listing
transform because the direct emitters rejected every exc op. The
branch-based EH state is plain Module fields, so the whole family
lowers to inline MOVs against the module pointer, mirroring the
inlined-global path on both architectures:

- OpExcPending / OpExcTag / OpExcVal: loads into the value's slot
  (OpExcVal narrows the uint64 operand slot by loading the low
  bytes, or the raw bits for floats, at the slot's base address.
- OpExcRaise: store the tag, widen each operand into its slot
  (zero-extend for 32-bit values, raw bits for floats), set the
  pending flag.
- OpExcRearm / OpExcClear: flag stores.
- OpCatchArg: narrow the dispatch's int64-typed snapshot.

The exception-state fields move into the Module struct's
fixed-offset leading region (right after the outline pack) so the
offsets stay trivially modeled instead of requiring the whole
struct tail: codegen computes them (moduleExcOffsets), they ride
Result.DirectAsmExc -> gcasm.Config -> asmgen FuncOptions.Exc, and
the generated compile-time layout pins assert them — including,
now, for memory-less modules. The bundle passes Config to
emitDirectAsmBody directly: offsets previously rode the SIMD memory
probe, which does not exist for modules without SIMD memory ops.

Two adjacent defects fixed along the way:
- The pure-Go emitter produced math.Float64frombits(int64) — a
  compile error — when a catch handler read an f64 operand, because
  narrowExcSlot assumed a uint64-typed slot while OpCatchArg feeds
  it the dispatch's int64 snapshot.
- i64_reinterpret_f64 / f64_reinterpret_i64 were missing from the
  always-inline helper set, forcing a per-function fallback under
  ForbidCalls; amd64 already carried (dead) inline coverage and
  arm64 gains the two-instruction FMOV lowering.

New gates: an EH fixture whose seven functions all emit direct-asm
on both architectures, verified differentially against the normal
backend plus pinned values across all four operand widths, executed
on amd64 and natively on arm64; a unit test pins the offset model.

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

* helpers: fix memoryInit64 disabling shared-image CoW for threads builds

memoryInit64 saturated dataEnd to MaxUint32 on EVERY install - a
leftover from before memory64 images were shareable. A threads build
makes every data segment passive (__wasm_init_memory installs them
via memory.init at start-up), so the very first install saturated the
extent and the sharing layer refused the image: every wasm64-threads
engine silently lost copy-on-write model sharing and fell back to
private loads.

Raise dataEnd to the real extent like the 32-bit memoryInit, and
saturate only when a segment genuinely lands beyond the uint32
bookkeeping range. Also adopt the 32-bit variant's compare-then-write
path so an image-backed instance re-running the start section over a
copy-on-write map does not fault identical pages private.

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

* codegen: carry the source SSA value on SIMD call marks

The fusion pass will use it to report each fused window's member and
root values back to direct-asm retention, so the asm emitter can
splice the same windows straight from the retained SSA instead of
re-deriving the match.

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

* codegen: record SSA provenance for fused-signature parameters

Each fused-tree parameter (scalar/float/pair) now carries where it
came from: a compile-time constant, or a member SSA value's argument
position. Chased compound expressions and named-local sum bases get
no provenance — a window carrying one will keep per-op splices in
direct-asm bodies rather than guess. The chase-rollback path
truncates the provenance array in lockstep with the parameters.

Pure bookkeeping: fusion decisions and emitted output are unchanged;
the direct-asm window retention that consumes this lands next.

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

* codegen: track member SSA values per fused-tree node

nodeVals parallels the builder's node list with the SSA value each
member call was built from (nil for synthesized scalar-chain nodes),
remapped through scheduleNodes and rolled back by restoreChase in
lockstep. Together with the parameter provenance this gives window
retention everything it needs to name a window's members, roots and
arguments in the retained SSA.

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

* codegen: report committed fused windows to direct-asm retention

At both fusion commit points (single-tree and window), a retained
function now gets a DirectAsmWindow descriptor: the interned tree,
the member and root SSA values, and the fused signature's parameter
sources. Recording is strict — any parameter without provenance or
root without a member value skips the window (FuseDebug logs why),
leaving the direct body on per-op splices.

Verified against the llama bundle: the mechanism records windows
with full provenance end to end (23 windows in one retained
function); the asm-bundle consumer lands next.

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

* asmgen: plumb fused-window descriptors to the direct-asm emitters

Canonical FusedWindow/FusedParamSrc types live in asmgen; transpile
converts the codegen retention's descriptors, gcasm carries them on
DirectAsmFn, and emitDirectAsmBody hands them to FuncOptions. No
consumer yet — emission still takes the per-op path for every
window member.

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

* asmgen: define the optional FusedSplicer interface

Per-window splice entry point the emitters will call: operands in
fused-signature order as immediates or frame slots, root result
slots for the epilogue, false to keep per-op emission. Type-asserted
from the existing Splicer so per-op-only implementations are
unaffected.

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

* asmgen: plan fused windows against the retained SSA

planFusedWindows validates each codegen-claimed region — members in
one block and unclaimed, non-root members consumed only inside the
window — and resolves the fused signature's parameter sources to
frame-slot/immediate operands plus root slot targets. Any mismatch
is a quiet per-op fallback. Emission consumption lands next.

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

* asmgen: emit validated fused windows at their last member

The whole window body emits at the LAST member — every interleaved
scalar input is materialized by then — and earlier members emit
nothing. Plan validation makes the deferral observation-free: values
between the members must be pure scalar glue (no memory ops, no
effects, no reads of a root), non-root members must have no
consumers outside the window, and planning only runs when the
splicer implements FusedSplicer. A splicer decline at emission is a
per-function fallback to the listing transform, never a half-emitted
body.

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

* gcasm: emit fused-window bodies inside direct-asm functions

SpliceFused stages the fused ABIInternal signature exactly as gc's
call site would — m in R0 from the emitter's R24 cache, scalars and
pair halves in R1.., float32 leaves in F0.. — drops in the same
prolog+core+epilogue the capture path splices, lands each root's
result register pair in the root value's slot, and re-primes the
m cache and hoisted module state the fused body clobbers.

Two defects the SIMD differential gate caught on the way in:
- The fused pool registers (v16..v30) overlap the direct-asm text
  passes' cache and home ranges, and the passes' line model does
  not understand window interiors: window-bearing bodies skip the
  four passes (a marker comment keys it; integrating the passes
  around windows is tracked separately).
- Unpacked parameters are FP-resident and their RSP slots are never
  written: operand resolution now emits FP-relative references for
  them, exactly like the per-op operand resolvers, instead of
  reading an unwritten slot.

WASM2GO_FUSEDWIN_MAX (env) caps windows per function for bisection.

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

* asmgen: allow interleaved plain loads across load-only fused windows

The codegen fusion proved exactly this hoist when it claimed a
region past scalar loads (f16 scale reads sit between the gather
members in every hot attention window); a window that writes no
memory can be deferred past them without changing any observation.
Storing windows still reject interleaved loads. Plan rejects now
report their reason under WASM2GO_FUSEDWIN_DEBUG.

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

* asmgen: pin window operands and disable splice coalesce around windows

Two llama-scale correctness fixes the NLL parity gate demanded:
window operand sources keep dedicated slots (their reads happen at
the window's deferred emission point, past the reuse model's
lifetimes), and the splice coalesce pass is off for window-bearing
bodies — a coalesced v128 loop carry lives in a home register and
never materializes the slot the window stages from, and the fused
pool clobbers the home ranges regardless.

With these the full go-llama suite (native NLL parity included)
passes on a bundle emitting the hottest attention function through
five fused windows. The configuration is slower than the gc path
for now: skipping the text passes and the coalesce costs the
function's per-op remainder more than the windows gain, so the
feature stays opt-in while pass integration around windows lands.

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

* gcasm: run the text passes around fused windows instead of skipping

The register-rewriting passes (slot forwarding, spare-register cache)
operate per outside-window segment with state flushed at each window
boundary; the read-counting dead-store pass runs whole-body so a
store whose only reader sits inside a window survives; windows are
bracketed by begin/end marker comments and passed through verbatim.

Full go-llama gates (NLL parity included) pass on the hottest
attention function emitted with five fused windows. Perf remains
behind the gc path for the whole function (the direct body's per-op
remainder still lacks gc-grade register allocation), so the feature
stays opt-in.

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

* gcasm: convert f16 scales with FCVT instead of the f32 table lookup

The guest code converts f16 scale factors through ggml's f32 table
(load16_u -> <<2 -> + table base -> f32 load); the arm64 host has
hardware half-to-single conversion, so the fused scalar-chain
splicer now emits one H load plus FCVT where it recognized the
lookup, dropping the dependent second load. The conversion is
bit-exact (IEEE f16->f32 is exact, which is precisely what the
table encodes).

Method per the asm-first rule: the sequence was first patched by
hand into a built bundle and measured (~6% faster tg on the q8_0
routing bench, NLL parity green) before being wired into
generation; the generated form reproduces the measurement (80 sites
across the dot kernels and the attention windows vs 40 in the hand
patch). A companion hand experiment (eliding the mov feeding scvtf)
measured no additional win and is intentionally not implemented.

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

* codegen: rewrite f16 table gathers to vector loads in fused windows

The guest converts f16 values (attention KV, activations) through
ggml's f32 lookup table one element at a time: scalar load16_u
reads feed shifted indices into a load32_zero + 3x load32_lane
chain over the table — eight dependent loads per four elements.
The sources are consecutive halfwords, so the whole group equals
f16x4_cvt(v128_load16x4_u(base)): one D-register load plus FCVTL,
the exact shape the native kernel compiles to.

The SSA-level gather pass cannot claim this form (the shift amount
rides a single-assignment local, constant only through definition
chasing), so the normalization runs on the built fused tree, where
the chase machinery lives: each lane's index expression is chased
to a scalar load16_u, lane adjacency (+0,+2,+4,+6) is proven
structurally with the delta carried by one constant arm
(argOffsetBy), the table is verified byte-for-byte as the IEEE
conversion, and the chain is rewritten with dead nodes and retired
parameters compacted. Supporting pieces: a function-wide
single-assignment definition map consulted by the chase (gated to
this rewrite so ordinary fusion keeps byte-identical shapes),
canonical base+const folding under the same gate, a wider
shift-amount conversion unwrap, and start-section placement
recovery for passive data segments so the table verification works
on shared-memory (threads) links, whose segments are all passive.

Method per the asm-first rule: the ldr d + fcvtl form was first
patched by hand into a built bundle and measured (~3.5% on the
routing bench, NLL parity green) before this generation landed;
the generated form measures 9-14% faster than its baseline —
beyond the hand patch, because retiring the gather also retires
the per-element scalar load glue the hand patch left running.
Full suite, lint, and the go-llama gates (native NLL parity
included) are green; three of the four hot attention windows
rewrite, the fourth keeps the literal gather pending a lane-order
generalization.

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

* codegen: recognize word-unpack and offset-node f16 gather variants

The f16 gather rewrite only matched groups whose four table indices
chase to separate load16_u reads at literal base+0/+2/+4/+6. The
attention kernel's hottest gather takes two other shapes:

- Word-unpack: the guest loads four f16 values as one 64-bit word
  and derives each table index by shift and mask - lane 0 as
  (w & 0xffff) << 2 and lanes 1..3 as (w >>u 16k-2) & 0x3fffc plus
  the table base, the strength-reduced form clang emits. The mask
  after the pre-scaled shift selects exactly halfword k times 4, so
  the group equals the same f16x4_cvt(v128_load16x4_u(base)).
- Offset nodes: lane addresses that chase to base+const add NODES
  rather than scalar/sum forms; argOffsetBy now peels constant add
  arms on either side, carrying the delta arithmetically.

Chase additions, all gated on the gather pass so ordinary fusion
keeps byte-identical shapes: identity-mask elimination (load16_u
masked with low-16-ones), and bare *(*uint16) derefs assigned
straight to wider locals.

Measured on the routing benchmark (single thread, interleaved A/B,
two rounds): decode 14.4/14.6 -> 16.6/16.8 tok/s (+15%). Full
transpiler suite, golangci-lint, and the dependent go-llama suite
(token-parity and NLL gates included) all pass; rewritten-group
count rises from 55 to 97 with no rejected groups left in the
attention kernel.

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

* gcasm: carry host pointers in fused-loop splices (arm64)

Fused loops addressed every memory operand as mBase+scalar+const,
re-materializing the effective address in scratch per access. The
q8_0 SDOT decode kernel spends its time in exactly these loads, so
the splicer now carries a host pointer (mBase + bump-carried address
scalar) in a free argument register, recomputed at the top of each
unroll copy, and the load emitters use immediate-offset forms off it
with a running bias that steps the pointer when an offset leaves the
signed-9-bit window. The coalesced range checks fold to one compare
off the guest scalar with the same reassociated bound as before. The
f16-scale peephole reads its half-word through the carried pointer
the same way. Straight-line window splices are untouched, as is
amd64; WASM2GO_NO_HOSTPTR disables the fold for A/B runs.

Proven by hand-editing the generated kernel to the native shape
first (per the asm-first rule): the hand patch measured +5.5% on
single-thread decode, and this deterministic form reproduces it —
routing benchmark decode 16.8 -> 17.8 tok/s (+5-6%, two interleaved
rounds, all routes exact). A prefetch variant measured negative and
a bare address-glue fold measured neutral; neither is included.
Full transpiler suite, golangci-lint, and the dependent go-llama
suite (token-parity and NLL gates) all pass.

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

* codegen: batch vec_dot caller row loops through a companion (opt-in)

The mul_mat drivers call the verified vec_dot once per output row
through the indirect table; per-call machinery (frame, module-state
reloads, dispatch, reduction epilogue) measured ~20% of single-
thread decode on the reference workload. With the new -vec-dot-rows
flag (requires -vec-dot-pair-entry) the translator now:

- emits a row-looped companion of the verified vec_dot: its emitted
  body is cloned (print/reparse round-trip), returns rewrite to a
  continue label, parameters become fresh per-row copies so mutable
  locals and wasm zero-init semantics hold, and the whole loop runs
  in one call frame. The boundary is packed through the per-module
  outline scratch (m + nine scalars exceeds the amd64 register
  ABI), and the companion rides the synthetic-signature channel so
  the asm bundle transforms it like an outlined function — the
  fused-loop splices apply inside the row loop.
- rewrites matching driver row loops (indirect call + mBase reload
  + counter step + guarded backedge with pointer bumps) into a
  GUARDED one-call fast path; the original loop remains as the
  guard-miss branch, so semantics are preserved for every runtime
  table target, and modules without the flag are untouched.

The traits scan learns passive data segments (threads links emit
only those) via the start-section placements, gated on the same
flag; the nrows patch that switches the guest to paired two-row
calls is suppressed under it, since batching needs the drivers to
keep stepping one row per call.

Measured on the routing benchmark (single thread, interleaved A/B,
two rounds, all routes exact): decode 17.2 -> 18.6 tok/s (+8%).
Full transpiler suite, golangci-lint, and the dependent go-llama
suite (token-parity and NLL gates) all pass; unit tests cover the
loop matcher, the return rewriter and the shift-count reduction.

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

* gcasm: feature-gate synthetic bodies like wasm functions

The feature-gated twin split (SDOT/AVX2 body + baseline twin +
dispatch stub) excluded synthetic-signature functions, which was a
no-op while outlined extraction bodies never contained gated
instructions. The batched-rows companion clones a kernel whose body
selects those forms, so its plain symbol would fault on a baseline
CPU. Synthetic bodies now take the same split; the twin
declarations reuse the original name's (packed) signature.

Verified: the companion emits dotprod/generic twins plus the
dispatch stub; full transpiler suite, golangci-lint, the go-llama
suite and the routing benchmark (routes exact, throughput unchanged
within noise) all pass.

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

* codegen/gcasm: fuse goto-form countdown loops with per-iteration scale chains

Decode-critical mul_mat hosts inline their kernel loops as goto-form
countdowns (label + backedge/exit jumps), which the loop upgrade
previously skipped; and their per-iteration f16 scale loads reached the
fused window as opaque scalar arguments, freezing the entry-time value
for the whole loop. This lands the fixes that together let those loops
fuse correctly:

- Upgrade goto-form loops by synthesizing a ForStmt (label preserved as
  the live entry target, exit jump kept); on by default with
  WASM2GO_GOTO_FUSE_MAX as a bisection cap.
- Chase computed (non base+const) memory addresses into scalar node
  chains under the loop-trial wide chase, so LUT indexes like idx<<2
  with the table constant in the memarg offset are recomputed per
  iteration instead of frozen.
- Narrow (wideload >> C) & 0xffff into a 16-bit load at addr + C/8
  (little-endian), matching clang packing adjacent f16 scale reads
  into one wide load.
- Recurse identWrites through nested statements so loop-variant writes
  inside the backedge branch are seen by the intervener consumption
  check; unabsorbed chains now refuse the upgrade instead of
  miscomputing.
- Countdown-to-threshold exits (SUBS/CSEL floor form), reassociation of
  exactly-associative vector chains, cross-chain raw SDOT rewrite with
  dual alternating accumulators, whole-loop bounds-check hoisting for
  store-free loops, and const-pattern shuffles without ABI slots.
- Deterministic carried-pointer register assignment (map order leaked
  into emitted bytes).

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

* codegen: unit-test the halfword-narrowing chase; fix its shift semantics

The extract rewrite resolved shift amounts through matchShiftConst,
whose default mod-32 reduction encodes i32 shift semantics — a bare
64-bit lane offset (>> 32, >> 48) would wrap to the wrong byte
offset, and an out-of-range extract could pass the width check.
Resolve the amount locally with no implicit reduction (the % 64
spelling of memory64 emission reduces identically), and unwrap
ParenExpr so the match does not depend on the AST provenance.

Adds unit coverage for the narrowing chase (lane offsets, wrapped
masks, operand order, rejections), the recursive identWrites, and
the carried-pointer register assignment order.

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

* codegen: bound the shuffle-pattern word split explicitly

The four u32 lane words of a const shuffle pattern derive from the
parsed 64-bit halves; mask each split explicitly so the truncation
is a stated bound rather than an implicit narrowing (flagged by
code scanning as an unchecked 64-to-32 conversion).

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

* codegen: gofmt the shuffle-pattern split

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

* Remove diagnostic tracing and environment kill switches

The fused-loop work carried per-site trace logging (chase failures,
candidate dumps, plan-reject reasons) and WASM2GO_* environment
switches used while the upgrade was being isolated. The features are
validated and unconditional now, so the switches and the tracing are
dead weight: drop them, along with the now-unused counter, reject
reason field and statement renderer. Transpiled output is unchanged
(byte-identical bundle before and after).

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

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-24 12:20:49 +09:00

934 lines
28 KiB
Go

package codegen
// v128 scalarization.
//
// The emitter's natural lowering of a v128 value is a [2]uint64 — and Go's
// ABI stack-assigns arrays, unconditionally. Every SIMD op then reads its
// operands from stack slots and writes its result back to one, so vector
// code runs at the speed of the stack, not of the register file. A wasm
// JIT never pays this: its v128 values live in vector registers.
//
// This pass rewrites one emitted function body so each v128 value is TWO
// uint64 locals (v7 → v7, v7__h). Scalars ride Go's register ABI, so the
// values stay in registers, spills disappear, and the gcasm splice
// receives its operands in GPRs instead of memory. Helper calls switch to
// their scalar-pair forms (simd_<op> → simd_p_<op>), which take and
// return the halves directly.
//
// The pass is driven entirely by structure: the emitter marks every
// SIMD call and v128 literal it produces (simdCalls / simdConsts, typed
// from the SSA), and variables become PAIR CANDIDATES only by provable
// data flow from those marks — a declaration with the [2]uint64 type, or
// an assignment whose RHS is already known to carry a v128. A variable
// with any use the pass does not recognize is dropped from the candidate
// set, and expressions that stay in array form are bridged exactly where
// they meet pair form: indexed (arr[0], arr[1]) going in, packed
// (simd_p_pack) coming out. Correctness never depends on a variable
// being scalarized.
import (
"fmt"
"go/ast"
"go/token"
"strings"
)
// pairName returns the identifier of the high half of a scalarized
// variable; the low half keeps the original name.
func pairName(name string) string { return name + "__h" }
type simdScalarizer struct {
em *ssaEmitter
// identCount is the per-name count of SEMANTIC uses in the whole
// function body: identifiers inside declarations and blank-use
// assignments do not count. Window fusion uses it to prove a
// variable is consumed entirely inside a window (LHS write plus
// in-window reads account for every occurrence), making it
// internal — no root slot, no assignment, only its declaration's
// blank use keeping it legal.
identCount map[string]int
// readCount counts semantic READ occurrences (assignment left-hand
// identifiers excluded); carryLoopReads counts, per name, the
// reads that sit inside a for-statement whose body also WRITES the
// name (each read attributed once, innermost qualifying loop).
// A name every read of which lives inside such self-carrying loops
// cannot observe a fused loop's eliminated per-iteration state
// from outside: the pair drives clone-tolerant carry escape
// checks in the loop fuser.
readCount map[string]int
carryLoopReads map[string]int
// constBind maps single-assignment locals to their constant int32
// value — the emitter CSEs multi-use constants (memarg offsets
// above all) into locals, and window fusion resolves them back to
// immediates so they cost no argument register. Sound because SSA
// lowering guarantees def-before-use on every path and the one
// static assignment always stores the same literal.
constBind map[string]int32
// defBind maps every SINGLE-ASSIGNMENT local to its defining
// statement, function-wide. The fused chase consults it when an
// identifier is not a window intervener: the definition is then
// recomputed inside the region (pure shapes only — the chase
// grammar admits nothing effectful) WITHOUT any drop bookkeeping,
// since the original definition may have other consumers.
defBind map[string]*ast.AssignStmt
// pairs is the candidate set: variables carried as two uint64s.
pairs map[string]bool
// arrays are v128-typed variables that must STAY arrays (function
// parameters, and any candidate that lost eligibility).
arrays map[string]bool
// tmp numbers hoisted nested-call temporaries. The temporaries are
// declared at function top and ASSIGNED at their hoist site: a
// `:=` there would be jumped over by the goto-emitter's labels,
// which Go forbids.
tmp int
hoistPairs []string // pair temps (__svN)
// stmtDepth counts rewriteStmts nesting: 1 at the function's
// top-level list, deeper inside blocks/loops. The goto-form loop
// upgrade fires at depth 1 only — labels inside structured
// regions belong to shapes the ForStmt path already owns.
stmtDepth int
hoistArrs []string // array temps (__saN)
}
// scalarizeSimd rewrites body in place. Functions with v128 PARAMETERS
// are skipped entirely: those are the fallback functions (v128 in a
// wasm signature routes the whole function to the pure fallback), their
// parameters arrive boxed as `any`, and nothing about them is
// performance-relevant.
func (em *ssaEmitter) scalarizeSimd(body *ast.BlockStmt, paramsV128 map[string]bool) {
if len(em.simdCalls) == 0 && len(em.simdConsts) == 0 {
return
}
if len(paramsV128) > 0 {
return
}
sc := &simdScalarizer{em: em, pairs: map[string]bool{}, arrays: map[string]bool{}}
sc.identCount = countSemanticIdents(body)
sc.readCount, sc.carryLoopReads = countCarryLoopReads(body)
sc.constBind = collectConstBindings(body, em.mem64)
sc.defBind = collectDefBindings(body)
sc.collect(body)
sc.audit(body)
body.List = sc.rewriteStmts(body.List)
// Hoisted temporaries are declared once at function top (see the
// tmp field for why), with a blank use so a temp on a path the
// compiler proves unreachable does not trip "declared and not used".
var decls []ast.Stmt
if len(sc.hoistPairs) > 0 {
var blanks, refs []ast.Expr
var idents []*ast.Ident
for _, n := range sc.hoistPairs {
idents = append(idents, newID(n), newID(pairName(n)))
blanks = append(blanks, newID("_"), newID("_"))
refs = append(refs, newID(n), newID(pairName(n)))
}
decls = append(decls, &ast.DeclStmt{Decl: &ast.GenDecl{
Tok: token.VAR,
Specs: []ast.Spec{&ast.ValueSpec{Names: idents, Type: newID("uint64")}},
}}, &ast.AssignStmt{Tok: token.ASSIGN, Lhs: blanks, Rhs: refs})
}
for _, n := range sc.hoistArrs {
decls = append(decls, &ast.DeclStmt{Decl: &ast.GenDecl{
Tok: token.VAR,
Specs: []ast.Spec{&ast.ValueSpec{
Names: []*ast.Ident{newID(n)},
Type: &ast.ArrayType{Len: intLit(2), Elt: newID("uint64")},
}},
}}, &ast.AssignStmt{Tok: token.ASSIGN, Lhs: []ast.Expr{newID("_")}, Rhs: []ast.Expr{newID(n)}})
}
if len(decls) > 0 {
body.List = append(decls, body.List...)
}
}
// countSemanticIdents counts identifier uses that carry meaning:
// declaration name lists and blank-use assignments (`_ = v`, emitted
// solely to keep declared-but-conditionally-unused locals legal) are
// excluded on both sides.
func countSemanticIdents(body *ast.BlockStmt) map[string]int {
counts := map[string]int{}
var walk func(n ast.Node)
walk = func(n ast.Node) {
switch x := n.(type) {
case *ast.ValueSpec:
for _, v := range x.Values {
walk(v)
}
return
case *ast.AssignStmt:
allBlank := true
for _, l := range x.Lhs {
if id, ok := l.(*ast.Ident); !ok || id.Name != "_" {
allBlank = false
}
}
if allBlank {
return
}
case *ast.Ident:
if x.Name != "_" {
counts[x.Name]++
}
return
}
if n != nil {
ast.Inspect(n, func(c ast.Node) bool {
if c == n {
return true
}
walk(c)
return false
})
}
}
walk(body)
return counts
}
// countCarryLoopReads returns (readCount, carryLoopReads): semantic
// read occurrences per name, and the subset that sits inside a
// for-statement whose body also writes the name. Assignment LHS
// identifiers are definitions, not reads (destinations that are
// index/star expressions read their operands); declaration lists and
// blank-use assignments are excluded like countSemanticIdents.
func countCarryLoopReads(body *ast.BlockStmt) (map[string]int, map[string]int) {
reads := map[string]int{}
inCarry := map[string]int{}
type forFrame struct {
writes map[string]bool
defSeen map[string]bool
}
var frames []*forFrame
read := func(name string) {
if name == "_" {
return
}
reads[name]++
for i := len(frames) - 1; i >= 0; i-- {
if frames[i].writes[name] && frames[i].defSeen[name] {
inCarry[name]++
return
}
}
}
var walkExpr func(n ast.Node)
walkExpr = func(n ast.Node) {
if n == nil {
return
}
if id, ok := n.(*ast.Ident); ok {
read(id.Name)
return
}
ast.Inspect(n, func(c ast.Node) bool {
if c == n {
return true
}
walkExpr(c)
return false
})
}
var walkStmt func(st ast.Stmt, direct *forFrame)
walkList := func(list []ast.Stmt, direct *forFrame) {
for _, s := range list {
walkStmt(s, direct)
}
}
walkStmt = func(st ast.Stmt, direct *forFrame) {
switch x := st.(type) {
case nil:
case *ast.BlockStmt:
walkList(x.List, nil)
case *ast.AssignStmt:
allBlank := true
for _, l := range x.Lhs {
if id, ok := l.(*ast.Ident); !ok || id.Name != "_" {
allBlank = false
}
}
if allBlank {
return
}
for _, r := range x.Rhs {
walkExpr(r)
}
for _, l := range x.Lhs {
if id, ok := l.(*ast.Ident); ok {
// A definition counts toward defSeen only when it
// executes unconditionally at the for-body's own
// level: reads attributed to a frame then have a
// same-iteration definition preceding them.
if direct != nil {
direct.defSeen[id.Name] = true
}
} else {
walkExpr(l)
}
}
case *ast.IfStmt:
walkExpr(x.Cond)
walkList(x.Body.List, nil)
if x.Else != nil {
walkStmt(x.Else, nil)
}
case *ast.ForStmt:
fr := &forFrame{writes: identWrites(x.Body.List), defSeen: map[string]bool{}}
frames = append(frames, fr)
if x.Cond != nil {
walkExpr(x.Cond)
}
walkList(x.Body.List, fr)
frames = frames[:len(frames)-1]
case *ast.LabeledStmt:
walkStmt(x.Stmt, direct)
case *ast.ExprStmt:
walkExpr(x.X)
case *ast.DeclStmt:
case *ast.ReturnStmt:
for _, r := range x.Results {
walkExpr(r)
}
case *ast.BranchStmt:
case *ast.SwitchStmt:
if x.Tag != nil {
walkExpr(x.Tag)
}
for _, c := range x.Body.List {
if cc, ok := c.(*ast.CaseClause); ok {
for _, e := range cc.List {
walkExpr(e)
}
walkList(cc.Body, nil)
}
}
default:
walkExpr(st)
}
}
walkList(body.List, nil)
return reads, inCarry
}
// collectConstBindings finds SSA value locals assigned exactly once,
// from an int32-representable literal. Only the emitter's single-def
// value locals (v<n>, its own naming contract) qualify: wasm locals
// (l<n>) are MUTABLE and default to zero, so a read can legitimately
// precede their one assignment.
// collectDefBindings maps single-assignment locals to their defining
// assignment (nil'd out on a second write, IncDec included).
func collectDefBindings(body *ast.BlockStmt) map[string]*ast.AssignStmt {
defs := map[string]*ast.AssignStmt{}
dead := map[string]bool{}
ast.Inspect(body, func(n ast.Node) bool {
switch st := n.(type) {
case *ast.IncDecStmt:
if id, ok := st.X.(*ast.Ident); ok {
dead[id.Name] = true
delete(defs, id.Name)
}
case *ast.AssignStmt:
if len(st.Lhs) != 1 || len(st.Rhs) != 1 {
for _, l := range st.Lhs {
if id, ok := l.(*ast.Ident); ok {
dead[id.Name] = true
delete(defs, id.Name)
}
}
return true
}
id, ok := st.Lhs[0].(*ast.Ident)
if !ok || id.Name == "_" {
return true
}
if dead[id.Name] {
return true
}
if _, seen := defs[id.Name]; seen {
dead[id.Name] = true
delete(defs, id.Name)
return true
}
defs[id.Name] = st
}
return true
})
return defs
}
func collectConstBindings(body *ast.BlockStmt, wide bool) map[string]int32 {
assigns := map[string]int{}
val := map[string]int32{}
isConst := map[string]bool{}
// On a memory64 function the emitter spells its constant locals
// int64(...)/uint64(...); bind those too when the value fits the
// int32 descriptor width. Every consumer folds by VALUE (memarg
// offsets, coalesced windows, shift amounts), so a fitting wide
// literal is exactly as safe as a narrow one. wasm32 functions
// keep the narrow-only matcher, byte-identical output.
constOf := intConstValue
if wide {
constOf = func(e ast.Expr) (int32, bool) {
if c, ok := e.(*ast.CallExpr); ok && len(c.Args) == 1 {
if id, ok := c.Fun.(*ast.Ident); ok && (id.Name == "int64" || id.Name == "uint64") {
return intConstValue(c.Args[0])
}
}
return intConstValue(e)
}
}
ast.Inspect(body, func(n ast.Node) bool {
switch st := n.(type) {
case *ast.IncDecStmt:
if id, ok := st.X.(*ast.Ident); ok {
assigns[id.Name]++
}
case *ast.AssignStmt:
for i, l := range st.Lhs {
id, ok := l.(*ast.Ident)
if !ok || id.Name == "_" {
continue
}
assigns[id.Name]++
if len(st.Lhs) == len(st.Rhs) && st.Tok == token.ASSIGN {
if c, ok := constOf(st.Rhs[i]); ok {
val[id.Name] = c
isConst[id.Name] = true
}
}
}
}
return true
})
out := map[string]int32{}
for name, c := range val {
if assigns[name] == 1 && isConst[name] && isSSAValueLocal(name) {
out[name] = c
}
}
return out
}
// isSSAValueLocal matches the emitter's own v<n> value-local names.
func isSSAValueLocal(name string) bool {
if len(name) < 2 || name[0] != 'v' {
return false
}
for _, r := range name[1:] {
if r < '0' || r > '9' {
return false
}
}
return true
}
// isV128ArrayType matches the emitted `[2]uint64` type node.
func isV128ArrayType(e ast.Expr) bool {
at, ok := e.(*ast.ArrayType)
if !ok || at.Len == nil {
return false
}
l, ok := at.Len.(*ast.BasicLit)
if !ok || l.Value != "2" {
return false
}
id, ok := at.Elt.(*ast.Ident)
return ok && id.Name == "uint64"
}
// carriesV128 reports whether an expression yields a v128 under the
// pass's knowledge: a marked call with v128 result, a marked literal, or
// a variable already known to be v128 (pair or array form).
func (sc *simdScalarizer) carriesV128(e ast.Expr) bool {
switch x := e.(type) {
case *ast.CallExpr:
m, ok := sc.em.simdCalls[x]
return ok && m.resV128
case *ast.CompositeLit:
return sc.em.simdConsts[x]
case *ast.Ident:
return sc.pairs[x.Name] || sc.arrays[x.Name]
}
return false
}
// collect grows the candidate set to a fixed point: declarations with
// the v128 array type, and assignment targets fed from v128 sources.
func (sc *simdScalarizer) collect(body *ast.BlockStmt) {
for {
changed := false
ast.Inspect(body, func(n ast.Node) bool {
switch st := n.(type) {
case *ast.DeclStmt:
gd, ok := st.Decl.(*ast.GenDecl)
if !ok || gd.Tok != token.VAR {
return true
}
for _, spec := range gd.Specs {
vs, ok := spec.(*ast.ValueSpec)
if !ok || vs.Type == nil || !isV128ArrayType(vs.Type) {
continue
}
for _, name := range vs.Names {
if !sc.pairs[name.Name] && !sc.arrays[name.Name] {
sc.pairs[name.Name] = true
changed = true
}
}
}
case *ast.AssignStmt:
if len(st.Lhs) != 1 || len(st.Rhs) != 1 {
return true
}
lhs, ok := st.Lhs[0].(*ast.Ident)
if !ok || lhs.Name == "_" {
return true
}
if sc.pairs[lhs.Name] || sc.arrays[lhs.Name] {
return true
}
if sc.carriesV128(st.Rhs[0]) {
sc.pairs[lhs.Name] = true
changed = true
}
}
return true
})
if !changed {
return
}
}
}
// audit demotes candidates with any use the rewriter does not handle:
// everything except appearing alone on either side of an assignment, as
// a v128 argument of a marked call, in a blank-use, or in its own
// declaration. Demotion moves the variable to array form, which every
// bridge site handles.
func (sc *simdScalarizer) audit(body *ast.BlockStmt) {
ok := map[ast.Node]bool{}
ast.Inspect(body, func(n ast.Node) bool {
switch st := n.(type) {
case *ast.AssignStmt:
if len(st.Lhs) == 1 && len(st.Rhs) == 1 {
if id, isID := st.Lhs[0].(*ast.Ident); isID {
ok[id] = true
}
if id, isID := st.Rhs[0].(*ast.Ident); isID {
ok[id] = true
}
}
// Blank uses `_ = v` arrive here too (Lhs `_`, Rhs ident).
case *ast.CallExpr:
m, marked := sc.em.simdCalls[st]
if !marked {
return true
}
for i, a := range st.Args {
if i < len(m.args) && m.args[i] {
if id, isID := a.(*ast.Ident); isID {
ok[id] = true
}
}
}
case *ast.ValueSpec:
for _, name := range st.Names {
ok[name] = true
}
}
return true
})
ast.Inspect(body, func(n ast.Node) bool {
id, isID := n.(*ast.Ident)
if !isID || !sc.pairs[id.Name] {
return true
}
if !ok[id] {
delete(sc.pairs, id.Name)
sc.arrays[id.Name] = true
}
return true
})
}
// pairExprs returns the two half-expressions of a v128-carrying
// expression, hoisting nested calls into prelude statements.
func (sc *simdScalarizer) pairExprs(e ast.Expr, prelude *[]ast.Stmt) (lo, hi ast.Expr) {
switch x := e.(type) {
case *ast.Ident:
if sc.pairs[x.Name] {
return newID(x.Name), newID(pairName(x.Name))
}
// Array form: bridge by indexing.
return &ast.IndexExpr{X: newID(x.Name), Index: intLit(0)},
&ast.IndexExpr{X: newID(x.Name), Index: intLit(1)}
case *ast.CompositeLit:
if sc.em.simdConsts[x] {
return x.Elts[0], x.Elts[1]
}
case *ast.CallExpr:
if m, marked := sc.em.simdCalls[x]; marked && m.resV128 && sc.pairable(m) {
sc.tmp++
l := fmt.Sprintf("__sv%d", sc.tmp)
h := pairName(l)
sc.hoistPairs = append(sc.hoistPairs, l)
*prelude = append(*prelude, &ast.AssignStmt{
Tok: token.ASSIGN,
Lhs: []ast.Expr{newID(l), newID(h)},
Rhs: []ast.Expr{sc.rewriteCall(x, prelude)},
})
return newID(l), newID(h)
}
}
// Unrecognized v128 source: hoist to an array temp and index it.
sc.tmp++
t := fmt.Sprintf("__sa%d", sc.tmp)
sc.hoistArrs = append(sc.hoistArrs, t)
*prelude = append(*prelude, &ast.AssignStmt{
Tok: token.ASSIGN,
Lhs: []ast.Expr{newID(t)},
Rhs: []ast.Expr{sc.rewriteExpr(e, prelude)},
})
return &ast.IndexExpr{X: newID(t), Index: intLit(0)},
&ast.IndexExpr{X: newID(t), Index: intLit(1)}
}
// pairable reports whether a marked call's op has a pair-form splice.
// The scalarizer must not emit a pair call the backend cannot take
// over: two results cannot be marshalled if the CALL survives.
// A memory64 helper ("simd_m64_v128_load") shares its wasm32 twin's
// table entry — the splice bodies are identical, only the
// effective-address glue widens (the backend strips the m64_ marker
// the same way; see gcasm's simdSpliceOp).
func (sc *simdScalarizer) pairable(m simdCallMark) bool {
op := m.name[len("simd_"):]
op = strings.TrimPrefix(op, "m64_")
return simdPairOps[op]
}
// rewriteCall converts a marked SIMD call to its scalar-pair form:
// simd_p_<op> with every v128 argument expanded into two halves.
// Callers must have checked pairable().
func (sc *simdScalarizer) rewriteCall(call *ast.CallExpr, prelude *[]ast.Stmt) *ast.CallExpr {
// A nested tree of fusable calls becomes ONE fused region call:
// its intermediates then never exist as Go values at all (see
// simd_fuse.go). Falls through to per-call rewriting when the
// tree is trivial or over the signature caps.
if sc.em.t != nil {
if fused, ok := sc.tryFuse(call, prelude); ok {
return fused
}
}
m := sc.em.simdCalls[call]
pname := "simd_p_" + m.name[len("simd_"):]
sc.em.useHelper(pname)
var args []ast.Expr
for i, a := range call.Args {
if i < len(m.args) && m.args[i] {
lo, hi := sc.pairExprs(a, prelude)
args = append(args, lo, hi)
continue
}
args = append(args, sc.rewriteExpr(a, prelude))
}
return &ast.CallExpr{Fun: sc.em.helperRef(pname), Args: args}
}
// rewriteArrayCall keeps a non-pairable SIMD call in array form,
// bridging any pair-carried argument back into a composite literal.
func (sc *simdScalarizer) rewriteArrayCall(call *ast.CallExpr, prelude *[]ast.Stmt) *ast.CallExpr {
m := sc.em.simdCalls[call]
for i, a := range call.Args {
if i < len(m.args) && m.args[i] {
if id, ok := a.(*ast.Ident); ok && sc.pairs[id.Name] {
call.Args[i] = &ast.CompositeLit{
Type: &ast.ArrayType{Len: intLit(2), Elt: newID("uint64")},
Elts: []ast.Expr{newID(id.Name), newID(pairName(id.Name))},
}
continue
}
call.Args[i] = sc.rewriteExpr(a, prelude)
continue
}
call.Args[i] = sc.rewriteExpr(a, prelude)
}
return call
}
// packCall wraps a pair-producing rewritten call back into array form
// via simd_p_pack, for sites that must keep the array (returns, array
// variables).
func (sc *simdScalarizer) packCall(inner *ast.CallExpr) *ast.CallExpr {
sc.em.useHelper("simd_p_pack")
return &ast.CallExpr{Fun: sc.em.helperRef("simd_p_pack"), Args: []ast.Expr{inner}}
}
// rewriteExpr rewrites marked SIMD calls nested inside an arbitrary
// (scalar-valued) expression tree. v128-RESULT calls in scalar context
// do not occur (the emitter types them), but scalar-result SIMD calls
// (extract_lane, bitmask, ...) appear anywhere an int can.
func (sc *simdScalarizer) rewriteExpr(e ast.Expr, prelude *[]ast.Stmt) ast.Expr {
switch x := e.(type) {
case *ast.CallExpr:
if m, marked := sc.em.simdCalls[x]; marked {
if !sc.pairable(m) {
return sc.rewriteArrayCall(x, prelude)
}
nc := sc.rewriteCall(x, prelude)
if m.resV128 {
return sc.packCall(nc)
}
return nc
}
for i, a := range x.Args {
x.Args[i] = sc.rewriteExpr(a, prelude)
}
case *ast.BinaryExpr:
x.X = sc.rewriteExpr(x.X, prelude)
x.Y = sc.rewriteExpr(x.Y, prelude)
case *ast.UnaryExpr:
x.X = sc.rewriteExpr(x.X, prelude)
case *ast.ParenExpr:
x.X = sc.rewriteExpr(x.X, prelude)
case *ast.IndexExpr:
x.X = sc.rewriteExpr(x.X, prelude)
x.Index = sc.rewriteExpr(x.Index, prelude)
case *ast.StarExpr:
x.X = sc.rewriteExpr(x.X, prelude)
case *ast.SelectorExpr:
x.X = sc.rewriteExpr(x.X, prelude)
case *ast.FuncLit:
x.Body.List = sc.rewriteStmts(x.Body.List)
}
return e
}
// rewriteStmts rewrites a statement list, expanding pair assignments
// and splitting declarations.
func (sc *simdScalarizer) rewriteStmts(list []ast.Stmt) []ast.Stmt {
sc.stmtDepth++
defer func() { sc.stmtDepth-- }()
var out []ast.Stmt
for i := 0; i < len(list); i++ {
var prelude []ast.Stmt
// GOTO-form countdown loops (the label/goto emission of
// irregular functions) upgrade exactly like ForStmt countdown
// loops; the label survives as the entry point. Top-level
// lists only: a label nested inside a structured region is
// part of a shape the ForStmt path already owns.
if _, isLbl := list[i].(*ast.LabeledStmt); isLbl && sc.stmtDepth == 1 {
if repl, consumed, ok := sc.tryFuseGotoLoop(list, i, &prelude); ok {
out = append(out, repl...)
i += consumed - 1
continue
}
}
// A run of pair assignments sharing values can fuse into one
// multi-root region call (see tryFuseWindow); otherwise the
// statement rewrites alone.
if repl, consumed, ok := sc.tryFuseWindow(list, i, &prelude); ok {
out = append(out, prelude...)
out = append(out, repl)
i += consumed - 1
continue
}
st := sc.rewriteStmt(list[i], &prelude)
out = append(out, prelude...)
if st != nil {
out = append(out, st)
}
}
return out
}
func (sc *simdScalarizer) rewriteStmt(st ast.Stmt, prelude *[]ast.Stmt) ast.Stmt {
switch s := st.(type) {
case *ast.DeclStmt:
gd, ok := s.Decl.(*ast.GenDecl)
if !ok || gd.Tok != token.VAR {
return st
}
for _, spec := range gd.Specs {
vs, ok := spec.(*ast.ValueSpec)
if !ok || vs.Type == nil || !isV128ArrayType(vs.Type) {
continue
}
// Split each scalarized name into two uint64s; names that
// stayed arrays keep the spec.
var keep []*ast.Ident
var split []*ast.Ident
for _, name := range vs.Names {
if sc.pairs[name.Name] {
split = append(split, name)
} else {
keep = append(keep, name)
}
}
if len(split) == 0 {
continue
}
var names []*ast.Ident
for _, name := range split {
names = append(names, newID(name.Name), newID(pairName(name.Name)))
}
*prelude = append(*prelude, &ast.DeclStmt{Decl: &ast.GenDecl{
Tok: token.VAR,
Specs: []ast.Spec{&ast.ValueSpec{Names: names, Type: newID("uint64")}},
}})
if len(keep) == 0 {
// The whole spec was scalarized; drop the statement by
// clearing it to an empty declaration.
vs.Names = nil
} else {
vs.Names = keep
}
}
// Drop a declaration whose every spec emptied.
empty := true
for _, spec := range gd.Specs {
if vs, ok := spec.(*ast.ValueSpec); !ok || len(vs.Names) > 0 {
empty = false
}
}
if empty {
return nil
}
return st
case *ast.AssignStmt:
return sc.rewriteAssign(s, prelude)
case *ast.ReturnStmt:
for i, r := range s.Results {
if sc.carriesV128(r) {
if call, ok := r.(*ast.CallExpr); ok {
if m := sc.em.simdCalls[call]; m.resV128 {
if sc.pairable(m) {
s.Results[i] = sc.packCall(sc.rewriteCall(call, prelude))
} else {
s.Results[i] = sc.rewriteArrayCall(call, prelude)
}
continue
}
}
if id, ok := r.(*ast.Ident); ok && sc.pairs[id.Name] {
s.Results[i] = &ast.CompositeLit{
Type: &ast.ArrayType{Len: intLit(2), Elt: newID("uint64")},
Elts: []ast.Expr{newID(id.Name), newID(pairName(id.Name))},
}
continue
}
}
s.Results[i] = sc.rewriteExpr(r, prelude)
}
return st
case *ast.LabeledStmt:
s.Stmt = sc.rewriteStmt(s.Stmt, prelude)
return st
case *ast.BlockStmt:
s.List = sc.rewriteStmts(s.List)
return st
case *ast.IfStmt:
s.Cond = sc.rewriteExpr(s.Cond, prelude)
s.Body.List = sc.rewriteStmts(s.Body.List)
if s.Else != nil {
s.Else = sc.rewriteStmt(s.Else, prelude)
}
return st
case *ast.ForStmt:
if repl, ok := sc.tryFuseLoop(s, prelude); ok {
return repl
}
if s.Cond != nil {
// A condition with a SIMD call would need per-iteration
// hoisting; the emitter never produces one, so any marked
// call here keeps its array form (rewriteExpr handles the
// scalar-result ones inline).
s.Cond = sc.rewriteExpr(s.Cond, prelude)
}
s.Body.List = sc.rewriteStmts(s.Body.List)
return st
case *ast.SwitchStmt:
if s.Tag != nil {
s.Tag = sc.rewriteExpr(s.Tag, prelude)
}
for _, c := range s.Body.List {
if cc, ok := c.(*ast.CaseClause); ok {
cc.Body = sc.rewriteStmts(cc.Body)
}
}
return st
case *ast.ExprStmt:
s.X = sc.rewriteExpr(s.X, prelude)
return st
}
return st
}
// rewriteAssign handles the four shapes an emitted assignment takes
// once v128 values are involved.
func (sc *simdScalarizer) rewriteAssign(s *ast.AssignStmt, prelude *[]ast.Stmt) ast.Stmt {
if len(s.Lhs) != 1 || len(s.Rhs) != 1 {
for i := range s.Lhs {
if _, isID := s.Lhs[i].(*ast.Ident); !isID {
s.Lhs[i] = sc.rewriteExpr(s.Lhs[i], prelude)
}
}
for i := range s.Rhs {
s.Rhs[i] = sc.rewriteExpr(s.Rhs[i], prelude)
}
return s
}
lhs, lhsIsID := s.Lhs[0].(*ast.Ident)
rhs := s.Rhs[0]
if !lhsIsID {
// A memory store: the destination expression can nest
// scalar-result SIMD calls (an extract_lane inside the address
// arithmetic).
s.Lhs[0] = sc.rewriteExpr(s.Lhs[0], prelude)
}
// Pair-form destination.
if lhsIsID && sc.pairs[lhs.Name] {
lo := newID(lhs.Name)
hi := newID(pairName(lhs.Name))
if call, ok := rhs.(*ast.CallExpr); ok {
if m := sc.em.simdCalls[call]; m.resV128 && sc.pairable(m) {
return &ast.AssignStmt{Tok: s.Tok, Lhs: []ast.Expr{lo, hi},
Rhs: []ast.Expr{sc.rewriteCall(call, prelude)}}
}
}
rlo, rhi := sc.pairExprs(rhs, prelude)
return &ast.AssignStmt{Tok: s.Tok, Lhs: []ast.Expr{lo, hi}, Rhs: []ast.Expr{rlo, rhi}}
}
// Blank use of a pair variable.
if lhsIsID && lhs.Name == "_" {
if id, ok := rhs.(*ast.Ident); ok && sc.pairs[id.Name] {
return &ast.AssignStmt{Tok: s.Tok,
Lhs: []ast.Expr{newID("_"), newID("_")},
Rhs: []ast.Expr{newID(id.Name), newID(pairName(id.Name))}}
}
}
// Array-form (or scalar) destination fed from pair sources.
if sc.carriesV128(rhs) {
if call, ok := rhs.(*ast.CallExpr); ok {
if m := sc.em.simdCalls[call]; m.resV128 {
if sc.pairable(m) {
s.Rhs[0] = sc.packCall(sc.rewriteCall(call, prelude))
} else {
s.Rhs[0] = sc.rewriteArrayCall(call, prelude)
}
return s
}
}
if id, ok := rhs.(*ast.Ident); ok && sc.pairs[id.Name] {
s.Rhs[0] = &ast.CompositeLit{
Type: &ast.ArrayType{Len: intLit(2), Elt: newID("uint64")},
Elts: []ast.Expr{newID(id.Name), newID(pairName(id.Name))},
}
return s
}
}
s.Rhs[0] = sc.rewriteExpr(rhs, prelude)
return s
}