nx_shader_ir_before_front_t217.nx source
↩ module page · 591 lines · 35138 B
1// nx_shader_ir.nx -- THE SHADER IR: ONE source of truth, N dialect backends.
2//
3// WHY THIS EXISTS (measured 2026-08-23). The estate's shaders are hand-written GLSL string
4// literals inside nx_game_page_emit. Drawing characters on the WebGPU tier therefore demanded a
5// SECOND HAND COPY in WGSL -- ~600 lines (dual-quaternion skinning off a joint texture, instance
6// uniform arrays, the voxel occlusion march, the procedural anatomy/eye/hair/skin paint) of the
7// most actively-edited shader in the estate: three separate feature edits landed in it on the day
8// this was written. Two artifacts that must agree, kept in agreement by discipline, is the exact
9// shape the banked law forbids. This module is the construction that makes disagreement
10// IMPOSSIBLE: neither dialect is authored, both are EMITTED from this representation.
11//
12// HOUSE SHAPE. This mirrors the NishiLang compiler itself -- lex_source -> parse_module ->
13// opt_run -> {nx_compile_x86, nx_compile_wat}: one front-end representation, many backends.
14// nx_glsl.nx and nx_wgsl.nx are backends over this IR, not transpilers bolted to a call site.
15//
16// THE SOURCE OF TRUTH IS NISHILANG (operator, 2026-08-23: "we dont want python or glsl or
17// javascript or all that it should be nishi lang and nishi ecosystem from the first byte up each
18// rung gaining all these capabilities"). GLSL IS A TARGET, NOT AN INPUT. This is NOT a GLSL->WGSL
19// transpiler: translating would enshrine GLSL as the canonical language and make a foreign dialect
20// the artifact we maintain. A shader is authored ONCE in NishiLang; GLSL and WGSL are both OUTPUTS,
21// exactly as x86_64 and WAT are outputs of the same NishiLang front-end. The ~600 lines of GLSL in
22// MVS/MFS are therefore a MIGRATION TARGET, not the source: each rung expresses more of them in
23// NishiLang and PROVES the emitted GLSL equivalent to the hand-written GLSL that ships today.
24//
25// LADDER (each rung widens the covered subset; rung 1 is what this lane ships):
26// rung 1 the fullscreen-triangle vertex stage -- authored in NishiLang, emitting BOTH the GLSL
27// VSH and the WGSL vs that are hand-written and SHIPPING today, proven against them.
28// rung 2 lower the real NishiLang AST (nx_tokenizer/nx_parse) into this IR, so a shader is an
29// ordinary NishiLang function rather than builder calls -- the full 'target, not
30// translator' shape, sharing the front-end with nx_compile_x86 and the WAT lane.
31// rung 3 widen the subset to the MFS paint set; rung 4 the MVS skinning set; then the character
32// pass becomes a compile target and the WebGPU abstention flips from owed to shipped.
33//
34// THE SOURCE OF TRUTH IS NISHILANG (operator, 2026-08-23: "we dont want python or glsl or
35// javascript or all that it should be nishi lang and nishi ecosystem from the first byte up each
36// rung gaining all these capabilities"). GLSL IS A TARGET, NOT AN INPUT. This is NOT a GLSL->WGSL
37// transpiler: translating would enshrine GLSL as the canonical language and make a foreign dialect
38// the artifact we maintain. A shader is authored ONCE in NishiLang; GLSL and WGSL are both OUTPUTS,
39// exactly as x86_64 and WAT are outputs of the same NishiLang front-end. The ~600 lines of GLSL in
40// MVS/MFS are therefore a MIGRATION TARGET, not the source: each rung expresses more of them in
41// NishiLang and PROVES the emitted GLSL equivalent to the hand-written GLSL that ships today.
42//
43// LADDER (each rung widens the covered subset; rung 1 is what this lane ships):
44// rung 1 the fullscreen-triangle vertex stage -- authored in NishiLang, emitting BOTH the GLSL
45// VSH and the WGSL vs that are hand-written and SHIPPING today, proven against them.
46// rung 2 lower the real NishiLang AST (nx_tokenizer/nx_parse) into this IR, so a shader is an
47// ordinary NishiLang function rather than builder calls -- the full "target, not
48// translator" shape, sharing the front-end with nx_compile_x86 and the WAT lane.
49// rung 3 widen the subset to the MFS paint set; rung 4 the MVS skinning set; then the pass
50// itself becomes a compile target and the WebGPU character abstention flips to shipped.
51//
52// SCOPE IS DECLARED AND ENFORCED, NEVER SILENT. The node kinds below cover what MVS/MFS actually
53// use. A backend meeting anything outside the subset must call sir_refuse and return -1.
54// A NAMED REFUSAL IS A CONTRACT; A SILENT MISTRANSLATION IS THE WORST POSSIBLE FAILURE IN A
55// SHADER BACKEND -- it compiles, it runs, and it draws the wrong thing.
56//
57// NO FLOATING POINT IS USED OR NEEDED: the NishiLang toolchain carries no f32, and a shader
58// emitter never computes with literals -- it TRANSCRIBES them. Numeric literals live in the
59// string pool in canonical form ("0.0", never "0."), which is also what WGSL requires.
60
61import "nx_syscalls.nx"
62
63// ---- fixed-width node arena -------------------------------------------------------------
64// Flat i64 slots rather than structs: the walker stays trivial and the encoding is auditable
65// by eye. Slot 7 is the intrusive NEXT link, so every list is a chain of node ids.
66const SIR_SLOTS: i64 = 8
67// S12c-6 (2026-09-05): the node ARENA. Measured need: the cast vertex stage is 1,394 nodes and its fragment stage is 2.35x
68// its canonical-token count, i.e. ~3,300 nodes -- under 4,096 by a margin no future slice could keep. sir_node OVERFLOW IS
69// SILENT (returns 0, sets M_OVER, and every builder ignores a 0 child), so the arena is sized at 4x the measured vertex+fragment
70// sum and every verb that builds a real module PRINTS M_OVER beside its node count; the flag, not the size, is the guard.
71const SIR_MAXNODE: i64 = 16384
72const SIR_POOLCAP: i64 = 65536
73
74const SIR_KIND: i64 = 0
75const SIR_TY: i64 = 1
76const SIR_A: i64 = 2
77const SIR_B: i64 = 3
78const SIR_C: i64 = 4
79const SIR_NAME: i64 = 5
80const SIR_D: i64 = 6
81const SIR_NEXT: i64 = 7
82
83// module header words
84const M_NODES: i64 = 0
85const M_NCOUNT: i64 = 1
86const M_POOL: i64 = 2
87const M_POOLUSED: i64 = 3
88const M_DECLH: i64 = 4
89const M_DECLT: i64 = 5
90const M_FUNCH: i64 = 6
91const M_FUNCT: i64 = 7
92const M_REFUSE: i64 = 8
93const M_OVER: i64 = 9
94// S2 (2026-09-04): THE EMISSION CONTEXT. The K_FUNC whose body a backend is emitting right now (0 between
95// functions). An E_IDENT carries only a name, and what that name resolves to -- a parameter or local of
96// THIS function, or a module-scope declaration -- decides how a dialect spells it (WGSL reads a uniform as
97// `u.<name>`). Both backends set it; the resolver reads it. Word 10 of the 16-word header sir_new maps.
98const M_CURFN: i64 = 10
99// S5 (2026-09-04): THE FRAGMENT OUTPUT BEING EMITTED. The K_FRAGOUT declaration whose value the entry
100// point currently returns (0 everywhere else -- between functions, inside a helper, outside a fragment
101// stage). GLSL has no value-returning fragment entry: `void main()` ASSIGNS a declared `out` variable,
102// while WGSL RETURNS one. Both backends walk ONE S_RETURN node, so the shape that differs has to be
103// readable from somewhere; this word is that somewhere, and it is set ONLY around the entry body so a
104// plain helper in the same module still emits an ordinary `return`. Word 11 of the 16 sir_new maps.
105const M_FRAGOUT: i64 = 11
106// S7 (2026-09-05): THE INTERSTAGE-STRUCT MODE. 0 = no varyings in play; 1 = inside the vertex entry that
107// RETURNS struct VIO (varying writes and the position assignment land in vo.<name>); 2 = inside the fragment
108// entry that TAKES vin:VIO (varying and position reads come from vin.<name>). Set and cleared by the WGSL
109// entry emitter; wg_expr / wg_stmts read it. GLSL never sets it. Word 12 of the 16 sir_new maps.
110const M_VIOMODE: i64 = 12
111// S12a (2026-09-05): DOES THIS MODULE READ THE INSTANCE INDEX? Set by sir_builtin the moment an instance_index node is
112// built, read by the WGSL vertex-entry emitter, which binds @builtin(instance_index) ii:u32 ONLY then -- so every module
113// that never asks for it (the fullscreen triangle, every S0..S9 fixture) keeps a byte-identical signature. GLSL never
114// reads it: gl_InstanceID needs no declaration. Word 13 of the 16 sir_new maps.
115const M_USESII: i64 = 13
116// S12c-5 (2026-09-05): THE WGSL FRAGMENT OUTPUT VARIABLE. The hand cast fragment ASSIGNS its output (`fc=...;`) and exits
117// early with a void `return;` -- valid GLSL (fc is an `out` variable) and the shape the token ruler must see. WGSL RETURNS
118// its output, so the WGSL backend opens such an entry with `var fc:vec4f;`, lowers every void return inside it to
119// `return fc;` and appends one after the body when the last statement is not a return. This word holds the K_FRAGOUT
120// declaration ONLY while the WGSL backend is inside a fragment entry whose body assigns it (0 everywhere else) -- the
121// WGSL twin of M_FRAGOUT, which the GLSL backend uses for the opposite lowering (value return -> assignment). Word 14.
122const M_WGFRAGOUT: i64 = 14
123// S12 step 2 (2026-09-06): THE BIND GROUP a module's WGSL resources are spelled in (@group(N)). 0 for every module until
124// now; the cast FRAGMENT module is emitted at 1, because its struct U and its textures differ from the vertex module's and
125// WebGPU binds ONE resource per (group,binding) across both stages of a pipeline -- two structs named U at group 0 binding 0
126// would read one buffer through two layouts, and vertex uJT (2d f32) against fragment uVm (3d u32) at binding 1 is a
127// validation refusal. GLSL never reads this word. Word 15 of the 16 sir_new maps (the last free slot).
128const M_BINDGROUP: i64 = 15
129// Source clip depth is explicit: backends translate it to their device convention.
130// Existing modules retain their native convention until they declare otherwise.
131const M_CLIP_DEPTH: i64 = 16
132const SIR_MODULE_WORDS: i64 = 17
133const SIR_CLIP_NATIVE: i64 = 0
134const SIR_CLIP_NEGATIVE_ONE_TO_ONE: i64 = 1
135// S12c-5: an E_LIT flag (SIR_A) -- the unsigned literal keeps its `u` suffix in GLSL too (see sir_lit_u).
136const SIR_LIT_USUFFIX: i64 = 1
137
138// ---- node kinds (the DECLARED SUBSET) ----------------------------------------------------
139const K_UNIFORM: i64 = 1
140const K_ATTRIB: i64 = 2
141const K_VARY: i64 = 3
142const K_FUNC: i64 = 4
143const K_PARAM: i64 = 5
144// RUNG GE43 -- COMPUTE (gameengine board, 2026-09-02). A storage buffer is the one declaration a
145// rasterizer KERNEL cannot do without: the depth buffer, the tile bins and the framebuffer all live
146// in read_write storage and are resolved with atomics. GLSL ES 3.00 (WebGL2) has NEITHER storage
147// buffers nor atomics nor a compute stage, so the GLSL backend REFUSES this kind BY NAME and the
148// WGSL door is the only third-party-browser path for it -- exactly the split GE42-GE45 declare:
149// ONE kernel authored here, one door per host (WebGPU as a submission pipe; dxg on NishiOS).
150// SIR_TY = element type SIR_A = binding index SIR_B = 1 read_write / 0 read SIR_C = 1 atomic<>
151const K_STORAGE: i64 = 6
152// S3 (2026-09-04): a TEXTURE is its own declaration kind. It is a BOUND RESOURCE in both dialects -- a
153// sampler uniform (`uniform highp usampler3D`) in GLSL, a `@group/@binding var` OUTSIDE struct U in WGSL --
154// and it was never a uniform: a texture-typed K_UNIFORM passed the WGSL type check and landed INSIDE
155// struct U (WGSL that does not compile, no refusal) until S1 made that a named refusal in both backends.
156// SIR_TY = the texture type (T_TEX3U is the covered one; every other texture type refuses BY NAME)
157// No binding slot is stored: GLSL has no bindings and the WGSL backend DERIVES one from declaration order.
158const K_TEXTURE: i64 = 7
159// S5 (2026-09-04): THE FRAGMENT OUTPUT is its own declaration kind, for the same reason K_TEXTURE is.
160// It was not modelled at all, and the cost was silent: a fragment stage emitted `void main(){ return
161// vec4(...); }` -- a value returned from a void function, against no declared output -- which is invalid
162// GLSL ES 3.00 on BOTH counts and which no gate could see, because nothing pinned the GLSL fragment
163// bytes. The WGSL side had the mirror defect wearing the opposite face: it spelled the whole signature
164// tail as the LITERAL ")->@location(0) vec4f{", so the location and the type were unreadable constants
165// rather than data -- exactly the defect S4 removed from the parameter half of the same line.
166// ONE declaration now feeds both: GLSL emits `out <ty> <name>;` and lowers the entry's return to
167// `<name>=<expr>;`, WGSL emits no declaration and DERIVES `->@location(<loc>) <ty>`.
168// SIR_TY = the output type SIR_A = the @location index
169const K_FRAGOUT: i64 = 8
170// R-D (2026-09-04, GE44, B1 of the hand-shader diff): A STRUCT is a module-scope declaration whose members are
171// K_PARAM nodes chained under it EXACTLY as a function's parameters are (sir_member IS sir_param), so member
172// spelling shares the parameter machinery and cannot drift from it. A struct TYPE is named by its declaration:
173// sir_struct_ty(st) = SIR_MAXNODE + st, unique by construction and above every T_ scalar, so a type speller that
174// meets one looks the declaration up (sir_struct_of) instead of refusing. Member ACCESS needs no new expression
175// kind -- both dialects spell it base.member, the bytes E_SWZ already emits -- and CONSTRUCTION is an E_CALL
176// named after the struct (both dialects spell Hit(a,b)). SIR_A = member chain head, SIR_C = member tail.
177const K_STRUCT: i64 = 9
178const S_RETURN: i64 = 10
179const S_ASSIGN: i64 = 11
180const S_VAR: i64 = 12
181const S_IF: i64 = 13
182const S_DISCARD: i64 = 14
183// RUNG 2b-GENERAL: structured LOOP + BREAK. A back edge cannot be emitted as a goto -- neither
184// GLSL ES 3.00 nor WGSL has one -- so the recovered form is an infinite loop whose exit is an
185// explicit break. That is exactly the Relooper's Loop block, whose Inner block "will appear
186// inside the loop, i.e., when execution reaches the end of that block, flow will return to the
187// beginning" and which "will contain a conditional break defining where it is exited"
188// (Zakai, Emscripten, OOPSLA 2011 -- READ from the mirrored PDF banked as graphics.refs
189// key relooper-zakai11, not recalled from memory).
190const S_LOOP: i64 = 15
191const S_BREAK: i64 = 16
192// GE43: an EXPRESSION STATEMENT -- an atomic read-modify-write whose old value is discarded is a
193// statement in both dialects (`atomicMin(&d[i],z);`), and neither S_ASSIGN nor S_VAR can carry it
194// without inventing a dead local. SIR_A = the expression.
195const S_EXPR: i64 = 17
196const E_LIT: i64 = 20
197const E_IDENT: i64 = 21
198const E_BIN: i64 = 22
199const E_CALL: i64 = 23
200const E_SWZ: i64 = 24
201const E_INDEX: i64 = 25
202const E_TEXLOAD: i64 = 26
203const E_TEXSAMPLE: i64 = 27
204const E_CTOR: i64 = 28
205// rung-1 additions: a numeric CAST (GLSL float(x) / WGSL f32(x)) and a stage BUILTIN
206// (GLSL gl_VertexID / WGSL @builtin(vertex_index)). Both are pure dialect-SPELLING differences
207// over one IR node -- the thesis of this module in miniature.
208const E_CAST: i64 = 29
209const E_BUILTIN: i64 = 30
210// GE43: an ATOMIC read-modify-write. SIR_NAME = the op spelling (atomicMin / atomicMax / atomicAdd,
211// identical in WGSL and in the dxg-lane HLSL family so the IR carries the WGSL spelling), SIR_A =
212// the TARGET lvalue (an E_INDEX into a K_STORAGE), SIR_B = the value. WGSL takes the ADDRESS of the
213// target (`&d[i]`); that `&` is dialect knowledge the backend owns, the author never writes it.
214const E_ATOMIC: i64 = 31
215// R-A (2026-09-04, the GE44 world-shader contract): a SELECT. SIR_A = the condition, SIR_B = the value when
216// true, SIR_C = the value when false. GLSL spells it as the ternary (cond?then:else), WGSL as select(f,t,cond)
217// -- note WGSL's argument order puts the FALSE value first; that ordering is dialect knowledge the backend
218// owns. The condition is a scalar bool by construction of this IR (there is no vector-bool type), so the
219// ternary's scalar-condition rule holds wherever this node appears.
220const E_SELECT: i64 = 32
221// R-G0 (2026-09-04): unary negation. SIR_A = the operand; both dialects spell (-x). The world shader writes
222// -vec3f(...), -u.yp.x and -1.0 as expressions; an E_BIN(0 - x) would be semantically equal but would spell a
223// subtraction the hand shader never wrote, so negation is its own node with one arm per dialect.
224const E_NEG: i64 = 33
225// K_PRIVATE (GE55, 2026-09-05): MODULE-SCOPE PRIVATE STATE -- `var<private> name:T;` in WGSL, a bare global `T name;`
226// in GLSL. Written by one function and read bare by another, never qualified as u.<name>, invisible to the uniform
227// layout. It exists so a shader can derive every float it needs from RAW uniform words in ONE function (upk) while
228// every other function body stays byte-identical: the sim hands the GPU integers, the kernel does the math. Numbered
229// after the last expression kind because node kinds share one space and nothing range-tests them (measured).
230const K_PRIVATE: i64 = 34
231// S12c-4 (2026-09-05): LOGICAL NOT. SIR_A = the operand (a bool); both dialects spell (!x). The cast vertex stage gates a
232// garment instance on `!(gbm==4||gbm==5||gbm==8||gbm==9)` and `!wr9`; the world shader never needed it (its exits are
233// spelled as the inverted comparison, see sir_for_until), so it lands here as its own node kind rather than a rewrite the
234// hand text does not contain -- the token ruler would name that rewrite as a divergence. Numbered after K_PRIVATE for the
235// same reason K_PRIVATE is numbered after E_NEG: one kind space, nothing range-tests it.
236const E_NOT: i64 = 35
237
238// ---- types -------------------------------------------------------------------------------
239const T_VOID: i64 = 0
240const T_F32: i64 = 1
241const T_I32: i64 = 2
242const T_U32: i64 = 3
243const T_BOOL: i64 = 4
244const T_V2F: i64 = 5
245const T_V3F: i64 = 6
246const T_V4F: i64 = 7
247const T_V3I: i64 = 8
248const T_TEX3U: i64 = 9
249const T_TEX2F: i64 = 10
250// GE43: vec3<u32> -- the type of @builtin(global_invocation_id); GLSL ES 3.10 would spell it uvec3.
251const T_V3U: i64 = 11
252// S8 (2026-09-05): vec2<i32> -- the INTEGER texel coordinate of a 2D fetch (the cast reads its joint table and
253// garment table with texelFetch(uJT, ivec2(...), 0)); WGSL spells it vec2i, GLSL ivec2.
254const T_V2I: i64 = 12
255// S9 (2026-09-05): mat3x3<f32> -- the cast's TBN / joint-rotation matrix. WGSL spells it mat3x3f, GLSL mat3; the
256// constructor takes three column vectors and mat*vec is plain E_BIN '*' in both dialects, so the type NAME is the only
257// dialect fact and it lives in wg_ty/gl_ty like every other type.
258const T_M3F: i64 = 13
259const T_MAX: i64 = 13
260
261func sir_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
262
263func sir_new() -> *i64 {
264 let m: *i64 = sys_mmap(SIR_MODULE_WORDS*8) as *i64
265 m[M_NODES] = sys_mmap(SIR_MAXNODE*SIR_SLOTS*8) as i64
266 m[M_NCOUNT] = 1
267 m[M_POOL] = sys_mmap(SIR_POOLCAP) as i64
268 m[M_POOLUSED] = 1
269 m[M_DECLH] = 0
270 m[M_DECLT] = 0
271 m[M_FUNCH] = 0
272 m[M_FUNCT] = 0
273 m[M_REFUSE] = 0
274 m[M_OVER] = 0
275 m[M_CURFN] = 0
276 m[M_FRAGOUT] = 0
277 return m
278}
279
280func sir_get(m: *i64, id: i64, k: i64) -> i64 {
281 let base: *i64 = m[M_NODES] as *i64
282 return base[id*SIR_SLOTS + k]
283}
284
285func sir_set(m: *i64, id: i64, k: i64, v: i64) -> i64 {
286 let base: *i64 = m[M_NODES] as *i64
287 base[id*SIR_SLOTS + k] = v
288 return 0
289}
290
291// intern a string; returns its pool offset (0 means none). Offsets start at 1 so 0 is a
292// usable NULL for every name slot.
293func sir_str(m: *i64, s: *u8) -> i64 {
294 let pool: *u8 = m[M_POOL] as *u8
295 let n: i64 = sir_slen(s)
296 var u: i64 = m[M_POOLUSED]
297 if u + n + 1 >= SIR_POOLCAP { m[M_OVER] = 1; return 0 }
298 let at: i64 = u
299 var i: i64 = 0
300 while i < n { pool[u] = s[i]; u = u + 1; i = i + 1 }
301 pool[u] = 0 as u8
302 u = u + 1
303 m[M_POOLUSED] = u
304 return at
305}
306
307func sir_cstr(m: *i64, off: i64) -> *u8 {
308 return ((m[M_POOL] + off) as *u8)
309}
310
311func sir_node(m: *i64, kind: i64, ty: i64, a: i64, b: i64, c: i64, nameoff: i64) -> i64 {
312 var id: i64 = m[M_NCOUNT]
313 if id >= SIR_MAXNODE { m[M_OVER] = 1; return 0 }
314 m[M_NCOUNT] = id + 1
315 sir_set(m, id, SIR_KIND, kind)
316 sir_set(m, id, SIR_TY, ty)
317 sir_set(m, id, SIR_A, a)
318 sir_set(m, id, SIR_B, b)
319 sir_set(m, id, SIR_C, c)
320 sir_set(m, id, SIR_NAME, nameoff)
321 sir_set(m, id, SIR_D, 0)
322 sir_set(m, id, SIR_NEXT, 0)
323 return id
324}
325
326// append onto an intrusive chain whose head/tail live in two module words
327func sir_chain(m: *i64, hw: i64, tw: i64, id: i64) -> i64 {
328 if id == 0 { return 0 }
329 if m[hw] == 0 { m[hw] = id; m[tw] = id; return id }
330 sir_set(m, m[tw], SIR_NEXT, id)
331 m[tw] = id
332 return id
333}
334
335// ---- declaration builders ----------------------------------------------------------------
336// arraylen 0 = scalar. location -1 = unlocated. flat 1 = no interpolation.
337func sir_uniform(m: *i64, name: *u8, ty: i64, arraylen: i64) -> i64 {
338 let id: i64 = sir_node(m, K_UNIFORM, ty, arraylen, 0, 0, sir_str(m, name))
339 return sir_chain(m, M_DECLH, M_DECLT, id)
340}
341
342// module-scope private state (K_PRIVATE): arraylen 0 = scalar, N = array<ty,N> / ty name[N]
343func sir_private(m: *i64, name: *u8, ty: i64, arraylen: i64) -> i64 {
344 let id: i64 = sir_node(m, K_PRIVATE, ty, arraylen, 0, 0, sir_str(m, name))
345 return sir_chain(m, M_DECLH, M_DECLT, id)
346}
347
348func sir_attrib(m: *i64, name: *u8, ty: i64, location: i64) -> i64 {
349 let id: i64 = sir_node(m, K_ATTRIB, ty, location, 0, 0, sir_str(m, name))
350 return sir_chain(m, M_DECLH, M_DECLT, id)
351}
352
353func sir_vary(m: *i64, name: *u8, ty: i64, location: i64, flat: i64) -> i64 {
354 let id: i64 = sir_node(m, K_VARY, ty, location, flat, 0, sir_str(m, name))
355 return sir_chain(m, M_DECLH, M_DECLT, id)
356}
357
358// GE43: a storage buffer `name: array<elemty>` at @group(0)@binding(binding). readwrite 1 = var<storage,read_write>,
359// 0 = var<storage,read>. atomic 1 wraps the element as atomic<elemty> so E_ATOMIC may target it.
360func sir_storage(m: *i64, name: *u8, elemty: i64, binding: i64, readwrite: i64, atomic: i64) -> i64 {
361 let id: i64 = sir_node(m, K_STORAGE, elemty, binding, readwrite, atomic, sir_str(m, name))
362 return sir_chain(m, M_DECLH, M_DECLT, id)
363}
364
365// S3: a texture declaration `name: <ty>` (see K_TEXTURE). Read with sir_texload / sir_texsample.
366func sir_texture(m: *i64, name: *u8, ty: i64) -> i64 {
367 let id: i64 = sir_node(m, K_TEXTURE, ty, 0, 0, 0, sir_str(m, name))
368 return sir_chain(m, M_DECLH, M_DECLT, id)
369}
370
371// S5: the fragment stage's colour output (see K_FRAGOUT). Declared ONCE; each backend owns how its
372// dialect carries it. A shader that only discards declares none, and neither backend requires one.
373func sir_fragout(m: *i64, name: *u8, ty: i64, location: i64) -> i64 {
374 let id: i64 = sir_node(m, K_FRAGOUT, ty, location, 0, 0, sir_str(m, name))
375 return sir_chain(m, M_DECLH, M_DECLT, id)
376}
377
378// R-D (2026-09-04): struct builders. sir_member IS sir_param -- the same K_PARAM node under the same A/C chain
379// -- so there is exactly one member/parameter shape in the IR. sir_struct_ty derives the type id from the
380// declaration (never picked, never colliding with a T_ scalar); sir_struct_of is the ONE inverse both backends
381// spell a struct type through. sir_member_of delegates to E_SWZ because no dialect distinguishes a member
382// read from a swizzle in its bytes -- a second kind would be two arms that could disagree about `.`.
383func sir_struct(m: *i64, name: *u8) -> i64 {
384 let id: i64 = sir_node(m, K_STRUCT, T_VOID, 0, 0, 0, sir_str(m, name))
385 return sir_chain(m, M_DECLH, M_DECLT, id)
386}
387func sir_member(m: *i64, st: i64, name: *u8, ty: i64) -> i64 { return sir_param(m, st, name, ty) }
388func sir_struct_ty(st: i64) -> i64 { return SIR_MAXNODE + st }
389// the K_STRUCT declaration a type id names, or 0 for every scalar, vector and resource type
390func sir_struct_of(m: *i64, ty: i64) -> i64 {
391 if ty <= SIR_MAXNODE { return 0 }
392 let st: i64 = ty - SIR_MAXNODE
393 if st >= SIR_MAXNODE { return 0 }
394 if sir_get(m, st, SIR_KIND) != K_STRUCT { return 0 }
395 return st
396}
397func sir_member_of(m: *i64, base: i64, name: *u8, ty: i64) -> i64 { return sir_swz(m, base, name, ty) }
398
399// a function: A = param chain head, B = body chain head, C = param tail (build scratch)
400func sir_func(m: *i64, name: *u8, rettype: i64) -> i64 {
401 let id: i64 = sir_node(m, K_FUNC, rettype, 0, 0, 0, sir_str(m, name))
402 return sir_chain(m, M_FUNCH, M_FUNCT, id)
403}
404
405// S4 (2026-09-04): HOW A PARAMETER IS BOUND. SIR_A of a K_PARAM names its binding: P_PLAIN (an ordinary
406// function parameter -- the only kind before S4, and sir_node's zero default) or P_BUILTIN_POSITION (the
407// fragment stage's position input: `@builtin(position) name:vec4f` in WGSL, `gl_FragCoord` in GLSL). The IR
408// says WHAT the input is; each backend owns how its dialect spells it, and an entry-point parameter with a
409// binding a backend cannot derive REFUSES by name rather than being dropped from the signature.
410const P_PLAIN: i64 = 0
411const P_BUILTIN_POSITION: i64 = 1
412
413func sir_param(m: *i64, fn: i64, name: *u8, ty: i64) -> i64 {
414 let id: i64 = sir_node(m, K_PARAM, ty, P_PLAIN, 0, 0, sir_str(m, name))
415 if sir_get(m, fn, SIR_A) == 0 { sir_set(m, fn, SIR_A, id); sir_set(m, fn, SIR_C, id); return id }
416 sir_set(m, sir_get(m, fn, SIR_C), SIR_NEXT, id)
417 sir_set(m, fn, SIR_C, id)
418 return id
419}
420
421// S4: a parameter bound to the fragment stage's @builtin(position). Typed vec4f by the builtin itself.
422func sir_param_position(m: *i64, fn: i64, name: *u8) -> i64 {
423 let id: i64 = sir_param(m, fn, name, T_V4F)
424 sir_set(m, id, SIR_A, P_BUILTIN_POSITION)
425 return id
426}
427
428// append a statement to a function body (D holds the body tail while building)
429func sir_stmt(m: *i64, fn: i64, st: i64) -> i64 {
430 if st == 0 { return 0 }
431 if sir_get(m, fn, SIR_B) == 0 { sir_set(m, fn, SIR_B, st); sir_set(m, fn, SIR_D, st); return st }
432 sir_set(m, sir_get(m, fn, SIR_D), SIR_NEXT, st)
433 sir_set(m, fn, SIR_D, st)
434 return st
435}
436
437// ---- statement builders ------------------------------------------------------------------
438func sir_return(m: *i64, e: i64) -> i64 { return sir_node(m, S_RETURN, T_VOID, e, 0, 0, 0) }
439// Fragment depth is window depth [0,1] in both emitted dialects. S_RETURN.B
440// carries it alongside the color expression; ordinary returns leave B zero.
441func sir_return_depth(m: *i64, color: i64, depth: i64) -> i64 {
442 return sir_node(m, S_RETURN, T_VOID, color, depth, 0, 0)
443}
444
445func sir_assign(m: *i64, lhs: i64, rhs: i64) -> i64 { return sir_node(m, S_ASSIGN, T_VOID, lhs, rhs, 0, 0) }
446func sir_var(m: *i64, name: *u8, ty: i64, init: i64) -> i64 { return sir_node(m, S_VAR, ty, init, 0, 0, sir_str(m, name)) }
447func sir_if(m: *i64, cond: i64, thenh: i64, elseh: i64) -> i64 { return sir_node(m, S_IF, T_VOID, cond, thenh, elseh, 0) }
448func sir_discard(m: *i64) -> i64 { return sir_node(m, S_DISCARD, T_VOID, 0, 0, 0, 0) }
449// SIR_A of an S_LOOP is the body chain. The loop is UNCONDITIONAL by construction; every exit is
450// an explicit S_BREAK inside the body, so no dialect needs a loop-condition slot and the two
451// backends cannot disagree about where the test lives.
452func sir_loop(m: *i64, body: i64) -> i64 { return sir_node(m, S_LOOP, T_VOID, body, 0, 0, 0) }
453func sir_break(m: *i64) -> i64 { return sir_node(m, S_BREAK, T_VOID, 0, 0, 0, 0) }
454// GE43: an expression evaluated for its effect (an atomic whose old value is not kept)
455func sir_expr(m: *i64, e: i64) -> i64 { return sir_node(m, S_EXPR, T_VOID, e, 0, 0, 0) }
456// R-B (2026-09-04, GE44): A COUNTED LOOP IS BUILDER SUGAR, NOT A NODE KIND. The whole-population diff of the
457// two hand world shaders named the counted for-loop as a missing IR kind; it lands as a composition of the
458// incumbent S_LOOP/S_BREAK shape -- init; loop { if until { break } body; step } -- appended to fn in that
459// order. Both dialects therefore lower it through arms they already share, so they cannot disagree about
460// where the test lives (the argument sir_loop makes for itself) and the node trace is dialect-independent by
461// construction -- no second visitation order to get wrong. `until` is the EXIT test, spelled by the caller:
462// the IR carries no unary not, and inverting a comparison on the author's behalf would silently change its
463// NaN semantics. Returns the S_LOOP node; init and the loop are already appended to fn.
464func sir_for_until(m: *i64, fn: i64, init: i64, until: i64, step: i64, body: i64) -> i64 {
465 sir_stmt(m, fn, init)
466 let ex: i64 = sir_if(m, until, sir_break(m), 0)
467 let lp: i64 = sir_loop(m, sir_seq(m, sir_seq(m, ex, body), step))
468 sir_stmt(m, fn, lp)
469 return lp
470}
471
472// chain two statements (for if-branch bodies)
473func sir_seq(m: *i64, a: i64, b: i64) -> i64 {
474 if a == 0 { return b }
475 var t: i64 = a
476 while sir_get(m, t, SIR_NEXT) != 0 { t = sir_get(m, t, SIR_NEXT) }
477 sir_set(m, t, SIR_NEXT, b)
478 return a
479}
480
481// ---- expression builders -----------------------------------------------------------------
482func sir_lit(m: *i64, text: *u8, ty: i64) -> i64 { return sir_node(m, E_LIT, ty, 0, 0, 0, sir_str(m, text)) }
483// S12c-5: an UNSIGNED literal spelled with its u suffix in BOTH dialects (`0u`). A plain sir_lit(.., T_U32) keeps GLSL bare
484// (the fullscreen triangle's shift amount ships that way); a uint COMPARE in GLSL ES 3.00 needs the suffix, so the cast
485// fragment builds its block ids with this.
486func sir_lit_u(m: *i64, text: *u8) -> i64 {
487 let id: i64 = sir_lit(m, text, T_U32)
488 sir_set(m, id, SIR_A, SIR_LIT_USUFFIX)
489 return id
490}
491// S12c-5: a CONSTANT local (`const float K=1.2;` in GLSL, `const K:f32=1.2;` in WGSL) -- SIR_D = SIR_VAR_CONST on a plain
492// sir_var node, one flavour beside sir_let, so both backends spell it from the same word.
493const SIR_VAR_CONST: i64 = 2
494func sir_const(m: *i64, name: *u8, ty: i64, init: i64) -> i64 {
495 let id: i64 = sir_var(m, name, ty, init)
496 sir_set(m, id, SIR_D, SIR_VAR_CONST)
497 return id
498}
499func sir_ident(m: *i64, name: *u8, ty: i64) -> i64 { return sir_node(m, E_IDENT, ty, 0, 0, 0, sir_str(m, name)) }
500func sir_bin(m: *i64, op: *u8, l: i64, r: i64, ty: i64) -> i64 { return sir_node(m, E_BIN, ty, l, r, 0, sir_str(m, op)) }
501func sir_swz(m: *i64, base: i64, sel: *u8, ty: i64) -> i64 { return sir_node(m, E_SWZ, ty, base, 0, 0, sir_str(m, sel)) }
502func sir_index(m: *i64, base: i64, idx: i64, ty: i64) -> i64 { return sir_node(m, E_INDEX, ty, base, idx, 0, 0) }
503// S8 (2026-09-05): the load's result type FOLLOWS THE TEXTURE -- texture_2d<f32> returns vec4f (the cast uses the
504// fetched joint-table row WHOLE); texture_3d<u32> keeps the T_U32 the vox fixture swizzles .x from, so every
505// existing 3D read is byte-identical. `tex` is the texture's E_IDENT and its SIR_TY names the texture type.
506func sir_texload(m: *i64, tex: i64, coord: i64, lod: i64) -> i64 {
507 var rt: i64 = T_U32
508 if sir_get(m, tex, SIR_TY) == T_TEX2F { rt = T_V4F }
509 return sir_node(m, E_TEXLOAD, rt, tex, coord, lod, 0)
510}
511func sir_texsample(m: *i64, tex: i64, coord: i64) -> i64 { return sir_node(m, E_TEXSAMPLE, T_V4F, tex, coord, 0, 0) }
512
513// call/ctor argument chains are built with sir_arg
514func sir_call(m: *i64, name: *u8, ty: i64) -> i64 { return sir_node(m, E_CALL, ty, 0, 0, 0, sir_str(m, name)) }
515func sir_ctor(m: *i64, ty: i64) -> i64 { return sir_node(m, E_CTOR, ty, 0, 0, 0, 0) }
516func sir_cast(m: *i64, e: i64, ty: i64) -> i64 { return sir_node(m, E_CAST, ty, e, 0, 0, 0) }
517func sir_builtin(m: *i64, which: *u8, ty: i64) -> i64 {
518 // S12a: an instance_index read marks the module so the WGSL vertex entry binds the builtin (M_USESII).
519 if which[0] == (105 as u8) { m[M_USESII] = 1 }
520 return sir_node(m, E_BUILTIN, ty, 0, 0, 0, sir_str(m, which))
521}
522// GE43: op is the WGSL spelling (atomicMin/atomicMax/atomicAdd); target is an E_INDEX into a storage buffer
523func sir_atomic(m: *i64, op: *u8, target: i64, val: i64, ty: i64) -> i64 { return sir_node(m, E_ATOMIC, ty, target, val, 0, sir_str(m, op)) }
524// R-A: select -- (cond, then, else) in IR order; each backend spells its own argument order.
525func sir_select(m: *i64, cond: i64, tval: i64, fval: i64, ty: i64) -> i64 { return sir_node(m, E_SELECT, ty, cond, tval, fval, 0) }
526// R-G0: unary negation of e, typed ty.
527func sir_neg(m: *i64, e: i64, ty: i64) -> i64 { return sir_node(m, E_NEG, ty, e, 0, 0, 0) }
528// S12c-4: logical not of a bool e (see E_NOT).
529func sir_not(m: *i64, e: i64) -> i64 { return sir_node(m, E_NOT, T_BOOL, e, 0, 0, 0) }
530// R-C: an IMMUTABLE local. Same node as sir_var with SIR_D = 1; WGSL spells it `let`, GLSL has no such
531// spelling for a non-constant initialiser and emits the plain local -- one IR node, two shapes.
532const SIR_VAR_LET: i64 = 1
533func sir_let(m: *i64, name: *u8, ty: i64, init: i64) -> i64 {
534 let id: i64 = sir_var(m, name, ty, init)
535 sir_set(m, id, SIR_D, SIR_VAR_LET)
536 return id
537}
538
539func sir_arg(m: *i64, callid: i64, e: i64) -> i64 {
540 if e == 0 { return 0 }
541 // List linkage belongs to an argument occurrence, never to its reusable expression.
542 // Child expressions remain shared; only the intrusive sibling link needs ownership.
543 let arg:i64=sir_node(m,sir_get(m,e,SIR_KIND),sir_get(m,e,SIR_TY),
544 sir_get(m,e,SIR_A),sir_get(m,e,SIR_B),sir_get(m,e,SIR_C),sir_get(m,e,SIR_NAME))
545 if arg==0 { return 0 }
546 sir_set(m,arg,SIR_D,sir_get(m,e,SIR_D))
547 if sir_get(m, callid, SIR_A) == 0 { sir_set(m, callid, SIR_A, arg); sir_set(m, callid, SIR_C, arg); return e }
548 sir_set(m, sir_get(m, callid, SIR_C), SIR_NEXT, arg)
549 sir_set(m, callid, SIR_C, arg)
550 return e
551}
552
553// ---- refusal -----------------------------------------------------------------------------
554// A backend that meets a construct outside its declared subset records the construct BY NAME
555// and returns -1. Never emit a guess.
556func sir_refuse(m: *i64, what: *u8) -> i64 {
557 if m[M_REFUSE] == 0 { m[M_REFUSE] = sir_str(m, what) }
558 return 0 - 1
559}
560func sir_refused(m: *i64) -> i64 { return m[M_REFUSE] }
561
562// ---- shared emit buffer ------------------------------------------------------------------
563func eb_put(out: *u8, pos: i64, cap: i64, s: *u8) -> i64 {
564 var p: i64 = pos
565 var i: i64 = 0
566 let n: i64 = sir_slen(s)
567 while i < n { if p + 1 < cap { out[p] = s[i]; p = p + 1 } i = i + 1 }
568 return p
569}
570
571func eb_num(out: *u8, pos: i64, cap: i64, v: i64) -> i64 {
572 var p: i64 = pos
573 var x: i64 = v
574 if x < 0 { p = eb_put(out, p, cap, "-" as *u8); x = 0 - x }
575 let tmp: *u8 = sys_mmap(32)
576 var k: i64 = 0
577 if x == 0 { tmp[0] = 48 as u8; k = 1 }
578 while x > 0 { tmp[k] = ((48 + x - (x/10)*10) as u8); x = x/10; k = k + 1 }
579 while k > 0 { k = k - 1; if p + 1 < cap { out[p] = tmp[k]; p = p + 1 } }
580 return p
581}
582
583// ---- backend trace ------------------------------------------------------------------------
584// EVERY backend appends each node id as it emits it. Two backends that consumed the SAME ir
585// COMPLETELY produce IDENTICAL traces -- that identity is the equivalence oracle available on a
586// box with no GPU, and it is not vacuous: a backend that silently skips a statement produces a
587// SHORTER trace, and one that visits in a different order produces a DIFFERENT one.
588func tr_add(trace: *i64, tcap: i64, tn: *i64, id: i64) -> i64 {
589 if tn[0] < tcap { trace[tn[0]] = id; tn[0] = tn[0] + 1 }
590 return 0
591}