nx_tensor_ir.nx source
↩ module page · 541 lines · 19305 B
1// tensor_ir.nx -- Nishi tensor intermediate representation.
2//
3// Phase 2.2 of docs/AI_VRAM_ARCHITECTURE.md. Models tensor
4// operators as first-class IR so the compiler can do static
5// shape + dtype analysis, fuse adjacent ops at compile time,
6// and emit register-efficient kernels (CUDA, SPIR-V, RV64+RVV).
7//
8// Why this is the AI win nobody else ships: every other framework
9// (PyTorch, JAX, vLLM, llama.cpp) does kernel fusion at runtime
10// via tracing or hand-written CUDA. Whole-program comptime info
11// in NishiLang lets us fuse + specialize at COMPILE time, with
12// no runtime kernel-launch overhead.
13//
14// Status (v0.0.1): operator definitions + Tensor struct + Layout
15// enum. Fusion pass + lowering pass are subsequent commits.
16
17// nx_safety_envelope:
18// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
19// sil_target: SIL1
20// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
21// verdict: NOT_YET_EVALUATED
22
23import "nx_syscalls.nx"
24
25// === dtype enum (matches nxgguf.nx) ===
26
27const TIR_DT_FP32: i64 = 0
28const TIR_DT_FP16: i64 = 1
29const TIR_DT_BF16: i64 = 2
30const TIR_DT_FP8: i64 = 3
31const TIR_DT_INT8: i64 = 4
32const TIR_DT_INT4: i64 = 5
33const TIR_DT_INT2: i64 = 6
34const TIR_DT_TERN: i64 = 7 // ternary {-1, 0, +1} packed
35const TIR_DT_BOOL: i64 = 8
36
37// === layout enum ===
38
39const TIR_LAY_DENSE: i64 = 0 // contiguous row-major
40const TIR_LAY_STRIDED: i64 = 1 // arbitrary strides per dim
41const TIR_LAY_BLOCKED: i64 = 2 // K x K micro-tiles (cache-friendly)
42const TIR_LAY_NHWC: i64 = 3 // image: batch x H x W x channels
43const TIR_LAY_NCHW: i64 = 4 // image: batch x channels x H x W
44const TIR_LAY_PACKED: i64 = 5 // sub-byte (int4/int2/ternary)
45
46// === tensor descriptor ===
47
48struct TirTensor {
49 rank: i64, // number of dimensions
50 shape: *i64, // shape[0..rank]
51 strides: *i64, // for STRIDED layout
52 dtype: i64, // TIR_DT_*
53 layout: i64, // TIR_LAY_*
54 data: *u8, // pointer to backing storage; 0 = symbolic
55 size: i64, // total byte count (computed)
56}
57
58// Compute total element count (product of shape). Pure, comptime-
59// evaluable when shape is statically known.
60func tir_numel(t: *TirTensor) -> i64 {
61 var n: i64 = 1
62 var i: i64 = 0
63 while i < t.rank {
64 n = n * t.shape[i]
65 i = i + 1
66 }
67 return n
68}
69
70func tir_dtype_bits(dt: i64) -> i64 {
71 if dt == TIR_DT_FP32 { return 32 }
72 if dt == TIR_DT_FP16 { return 16 }
73 if dt == TIR_DT_BF16 { return 16 }
74 if dt == TIR_DT_FP8 { return 8 }
75 if dt == TIR_DT_INT8 { return 8 }
76 if dt == TIR_DT_INT4 { return 4 }
77 if dt == TIR_DT_INT2 { return 2 }
78 if dt == TIR_DT_TERN { return 2 }
79 if dt == TIR_DT_BOOL { return 1 }
80 return 32
81}
82
83// Compute total byte count = numel * element_bits / 8. Sub-byte
84// types (int4, int2, ternary) round up.
85func tir_bytes(t: *TirTensor) -> i64 {
86 let n: i64 = tir_numel(t)
87 let bits: i64 = tir_dtype_bits(t.dtype)
88 let total_bits: i64 = n * bits
89 return (total_bits + 7) / 8
90}
91
92// === operator opcodes ===
93//
94// Each opcode is a node in the tensor graph. Inputs are TirTensor
95// pointers, output is a TirTensor with shape derivable from inputs
96// (shape inference is part of each op's contract).
97
98const TIR_OP_LOAD: i64 = 0 // load constant / weight tensor
99const TIR_OP_PARAM: i64 = 1 // function parameter (input)
100const TIR_OP_RESHAPE: i64 = 2 // change view, no data copy
101const TIR_OP_TRANSPOSE: i64 = 3
102const TIR_OP_CAST: i64 = 4 // dtype conversion
103const TIR_OP_QUANTIZE: i64 = 5 // fp -> int (with scale/zp)
104const TIR_OP_DEQUANTIZE: i64 = 6 // int -> fp
105
106// Element-wise
107const TIR_OP_ADD: i64 = 10
108const TIR_OP_SUB: i64 = 11
109const TIR_OP_MUL: i64 = 12
110const TIR_OP_DIV: i64 = 13
111const TIR_OP_NEG: i64 = 14
112const TIR_OP_RELU: i64 = 15
113const TIR_OP_GELU: i64 = 16
114const TIR_OP_SILU: i64 = 17
115const TIR_OP_SIGMOID: i64 = 18
116const TIR_OP_TANH: i64 = 19
117
118// Reductions
119const TIR_OP_SUM: i64 = 30
120const TIR_OP_MEAN: i64 = 31
121const TIR_OP_MAX: i64 = 32
122const TIR_OP_MIN: i64 = 33
123const TIR_OP_ARGMAX: i64 = 34
124
125// Norm
126const TIR_OP_RMS_NORM: i64 = 40
127const TIR_OP_LAYER_NORM: i64 = 41
128const TIR_OP_SOFTMAX: i64 = 42
129
130// Heavy ops (the ones that benefit MOST from comptime fusion)
131const TIR_OP_MATMUL: i64 = 50
132const TIR_OP_CONV2D: i64 = 51
133const TIR_OP_ATTENTION: i64 = 52 // fused QKV attention
134const TIR_OP_ROPE: i64 = 53 // rotary positional embedding
135const TIR_OP_EMBED: i64 = 54 // embedding lookup
136
137// Quantized variants -- keep dtype-specific opcodes so the
138// codegen can choose the optimal kernel per (dtype, shape, target).
139const TIR_OP_MATMUL_INT4: i64 = 60
140const TIR_OP_MATMUL_FP8: i64 = 61
141const TIR_OP_ATTN_INT4_KV: i64 = 62 // attention with int4 KV cache
142
143// === graph node ===
144
145struct TirNode {
146 op: i64, // TIR_OP_*
147 n_inputs: i64,
148 inputs: *i64, // array of node ids; resolved via tir_node_at
149 output: *TirTensor, // result; shape inferred from op + inputs
150 attr: i64, // per-op attribute (axis for reductions, etc.)
151 fused_ids: *i64, // node ids fused into this kernel (set by fuse pass)
152 n_fused: i64,
153}
154
155struct TirGraph {
156 nodes: *u8, // packed array of TirNode (96 bytes each)
157 n_nodes: i64,
158 nodes_cap: i64,
159 tensors: *u8, // packed array of TirTensor
160 n_tensors: i64,
161 tensors_cap: i64,
162}
163
164// === public API ===
165
166const TIR_NODE_BYTES: i64 = 64 // sizeof TirNode rounded
167const TIR_TENSOR_BYTES: i64 = 64
168
169func tir_graph_new(cap: i64) -> *TirGraph {
170 let raw: *u8 = sys_mmap(64)
171 let g: *TirGraph = raw as *TirGraph
172 g.nodes = sys_mmap(cap * TIR_NODE_BYTES + 64)
173 g.n_nodes = 0
174 g.nodes_cap = cap
175 g.tensors = sys_mmap(cap * TIR_TENSOR_BYTES + 64)
176 g.n_tensors = 0
177 g.tensors_cap = cap
178 return g
179}
180
181func tir_node_at(g: *TirGraph, id: i64) -> *TirNode {
182 let base: i64 = g.nodes as i64
183 return (base + id * TIR_NODE_BYTES) as *TirNode
184}
185
186func tir_tensor_at(g: *TirGraph, id: i64) -> *TirTensor {
187 let base: i64 = g.tensors as i64
188 return (base + id * TIR_TENSOR_BYTES) as *TirTensor
189}
190
191// Add a tensor descriptor to the graph; returns its id.
192func tir_add_tensor(g: *TirGraph, rank: i64, shape: *i64,
193 dtype: i64, layout: i64) -> i64 {
194 let id: i64 = g.n_tensors
195 let t: *TirTensor = tir_tensor_at(g, id)
196 t.rank = rank
197 t.shape = shape
198 t.strides = 0 as *i64
199 t.dtype = dtype
200 t.layout = layout
201 t.data = 0 as *u8
202 t.size = tir_bytes(t)
203 g.n_tensors = id + 1
204 return id
205}
206
207// Add an op node; returns its id. Caller fills inputs[].
208func tir_add_node(g: *TirGraph, op: i64, n_inputs: i64,
209 inputs: *i64, output_id: i64) -> i64 {
210 let id: i64 = g.n_nodes
211 let n: *TirNode = tir_node_at(g, id)
212 n.op = op
213 n.n_inputs = n_inputs
214 n.inputs = inputs
215 n.output = tir_tensor_at(g, output_id)
216 n.attr = 0
217 n.fused_ids = 0 as *i64
218 n.n_fused = 0
219 g.n_nodes = id + 1
220 return id
221}
222
223// === shape inference =================================================
224//
225// For each opcode, the output's shape is determined by the inputs'
226// shapes + the op's contract. We compute it at graph-build time
227// so subsequent passes (fusion, lowering) operate on fully-typed
228// tensors. Shape mismatches are caught HERE, not at runtime.
229//
230// Returns 0 on success, negative on shape error.
231
232const TIR_SHAPE_MISMATCH: i64 = -1
233const TIR_RANK_MISMATCH: i64 = -2
234const TIR_UNSUPPORTED_OP: i64 = -3
235
236// Element-wise binary ops require identical shapes (broadcasting
237// is a separate v0.1.0 feature). Returns 0 + writes shape to out.
238func tir_infer_elemwise(in_a: *TirTensor, in_b: *TirTensor,
239 out: *TirTensor) -> i64 {
240 if in_a.rank != in_b.rank { return TIR_RANK_MISMATCH }
241 var i: i64 = 0
242 while i < in_a.rank {
243 if in_a.shape[i] != in_b.shape[i] { return TIR_SHAPE_MISMATCH }
244 i = i + 1
245 }
246 out.rank = in_a.rank
247 out.shape = in_a.shape
248 out.dtype = in_a.dtype
249 out.layout = in_a.layout
250 out.size = tir_bytes(out)
251 return 0
252}
253
254// Matmul: A is M x K, B is K x N, out is M x N. Both inputs must
255// be rank-2 dense for v0.0.1. Higher-rank "batch matmul" is v0.1.
256func tir_infer_matmul(in_a: *TirTensor, in_b: *TirTensor,
257 out: *TirTensor) -> i64 {
258 if in_a.rank != 2 { return TIR_RANK_MISMATCH }
259 if in_b.rank != 2 { return TIR_RANK_MISMATCH }
260 let m: i64 = in_a.shape[0]
261 let k_a: i64 = in_a.shape[1]
262 let k_b: i64 = in_b.shape[0]
263 let n: i64 = in_b.shape[1]
264 if k_a != k_b { return TIR_SHAPE_MISMATCH }
265 out.rank = 2
266 out.shape[0] = m
267 out.shape[1] = n
268 out.dtype = in_a.dtype
269 out.layout = in_a.layout
270 out.size = tir_bytes(out)
271 return 0
272}
273
274// Reshape: total element count must be preserved. Out shape is
275// supplied by the caller (target shape); we just validate.
276func tir_infer_reshape(in_t: *TirTensor, out: *TirTensor) -> i64 {
277 if tir_numel(in_t) != tir_numel(out) { return TIR_SHAPE_MISMATCH }
278 out.dtype = in_t.dtype
279 out.layout = in_t.layout
280 out.size = tir_bytes(out)
281 return 0
282}
283
284// Cast: same shape, different dtype.
285func tir_infer_cast(in_t: *TirTensor, target_dtype: i64,
286 out: *TirTensor) -> i64 {
287 out.rank = in_t.rank
288 out.shape = in_t.shape
289 out.dtype = target_dtype
290 out.layout = in_t.layout
291 out.size = tir_bytes(out)
292 return 0
293}
294
295// Reduction along an axis: out has rank-1, that axis dropped.
296func tir_infer_reduce(in_t: *TirTensor, axis: i64,
297 out: *TirTensor) -> i64 {
298 if axis < 0 { return TIR_SHAPE_MISMATCH }
299 if axis >= in_t.rank { return TIR_SHAPE_MISMATCH }
300 out.rank = in_t.rank - 1
301 var src: i64 = 0
302 var dst: i64 = 0
303 while src < in_t.rank {
304 if src != axis {
305 out.shape[dst] = in_t.shape[src]
306 dst = dst + 1
307 }
308 src = src + 1
309 }
310 out.dtype = in_t.dtype
311 out.layout = in_t.layout
312 out.size = tir_bytes(out)
313 return 0
314}
315
316// Attention: standard scaled dot-product.
317// Q is [batch, heads, seq_q, head_dim]
318// K is [batch, heads, seq_k, head_dim]
319// V is [batch, heads, seq_k, head_dim]
320// Out is [batch, heads, seq_q, head_dim]
321func tir_infer_attention(q: *TirTensor, k: *TirTensor, v: *TirTensor,
322 out: *TirTensor) -> i64 {
323 if q.rank != 4 { return TIR_RANK_MISMATCH }
324 if k.rank != 4 { return TIR_RANK_MISMATCH }
325 if v.rank != 4 { return TIR_RANK_MISMATCH }
326 // Match batch + heads + head_dim across Q/K/V.
327 if q.shape[0] != k.shape[0] { return TIR_SHAPE_MISMATCH }
328 if q.shape[1] != k.shape[1] { return TIR_SHAPE_MISMATCH }
329 if q.shape[3] != k.shape[3] { return TIR_SHAPE_MISMATCH }
330 if k.shape[0] != v.shape[0] { return TIR_SHAPE_MISMATCH }
331 if k.shape[1] != v.shape[1] { return TIR_SHAPE_MISMATCH }
332 if k.shape[2] != v.shape[2] { return TIR_SHAPE_MISMATCH }
333 if k.shape[3] != v.shape[3] { return TIR_SHAPE_MISMATCH }
334 out.rank = 4
335 out.shape[0] = q.shape[0]
336 out.shape[1] = q.shape[1]
337 out.shape[2] = q.shape[2]
338 out.shape[3] = q.shape[3]
339 out.dtype = q.dtype
340 out.layout = q.layout
341 out.size = tir_bytes(out)
342 return 0
343}
344
345// === fusion pass =====================================================
346//
347// Walk the graph; identify consecutive ops that can be fused into
348// a single kernel. Adjacent ops fuse when:
349// - producer's only consumer is the next node
350// - both ops fit a known fusion pattern
351// - shape + dtype + layout all match
352//
353// When fused, the producer's `n_fused` increments + its node id
354// is appended to the consumer's `fused_ids` array. Codegen
355// later treats the consumer as the kernel boundary, emitting
356// one fused kernel for the entire chain.
357//
358// v0.0.1 fusion patterns (the most common in modern LLMs):
359// - element-wise op chains (a+b -> *c -> silu(...) -> ...)
360// - matmul + element-wise epilogue (matmul -> +bias -> silu)
361// - SwiGLU: matmul + silu + multiply (Llama feed-forward)
362// - attention chain: rope -> matmul(QK) -> softmax -> matmul(V)
363// - rmsnorm + matmul fusion (Llama input projection)
364//
365// Returns the number of fusions performed.
366
367// Is this opcode a 1-input element-wise op?
368func tir_op_is_unary_ew(op: i64) -> i64 {
369 if op == TIR_OP_NEG { return 1 }
370 if op == TIR_OP_RELU { return 1 }
371 if op == TIR_OP_GELU { return 1 }
372 if op == TIR_OP_SILU { return 1 }
373 if op == TIR_OP_SIGMOID { return 1 }
374 if op == TIR_OP_TANH { return 1 }
375 return 0
376}
377
378// Is this opcode a 2-input element-wise op?
379func tir_op_is_binary_ew(op: i64) -> i64 {
380 if op == TIR_OP_ADD { return 1 }
381 if op == TIR_OP_SUB { return 1 }
382 if op == TIR_OP_MUL { return 1 }
383 if op == TIR_OP_DIV { return 1 }
384 return 0
385}
386
387// Append `producer_id` to `consumer.fused_ids`. Allocates the
388// fused_ids array on first fuse. v0.0.1 caps at 8 fused producers
389// per kernel (covers SwiGLU + similar).
390func tir_fuse_into(consumer: *TirNode, producer_id: i64) -> i64 {
391 if consumer.n_fused == 0 {
392 consumer.fused_ids = sys_mmap(64) as *i64
393 }
394 if consumer.n_fused >= 8 { return -1 }
395 consumer.fused_ids[consumer.n_fused] = producer_id
396 consumer.n_fused = consumer.n_fused + 1
397 return 0
398}
399
400// Run the fusion pass over the entire graph. Returns the count of
401// fuses applied.
402func tir_fuse_graph(g: *TirGraph) -> i64 {
403 var fused_count: i64 = 0
404
405 // For each node, look at its inputs. If an input was produced
406 // by a node whose only consumer is THIS node, and the producer
407 // is element-wise (cheap to inline), fuse it.
408 var i: i64 = 0
409 while i < g.n_nodes {
410 let cur: *TirNode = tir_node_at(g, i)
411
412 // For each input, check if it's a fusable producer.
413 var j: i64 = 0
414 while j < cur.n_inputs {
415 let prod_id: i64 = cur.inputs[j]
416 if prod_id < g.n_nodes {
417 let prod: *TirNode = tir_node_at(g, prod_id)
418 let is_uew: i64 = tir_op_is_unary_ew(prod.op)
419 let is_bew: i64 = tir_op_is_binary_ew(prod.op)
420 if is_uew == 1 {
421 tir_fuse_into(cur, prod_id)
422 fused_count = fused_count + 1
423 }
424 if is_bew == 1 {
425 tir_fuse_into(cur, prod_id)
426 fused_count = fused_count + 1
427 }
428 }
429 j = j + 1
430 }
431 i = i + 1
432 }
433 return fused_count
434}
435
436// === self-test ===
437
438func main() -> i64 {
439 let g: *TirGraph = tir_graph_new(64)
440
441 // Build a tiny graph: matmul(A, B) where A is 4x8 fp16,
442 // B is 8x16 fp16, output is 4x16 fp16.
443 let a_shape_raw: *u8 = sys_mmap(64)
444 let a_shape: *i64 = a_shape_raw as *i64
445 a_shape[0] = 4; a_shape[1] = 8
446 let a_id: i64 = tir_add_tensor(g, 2, a_shape, TIR_DT_FP16, TIR_LAY_DENSE)
447
448 let b_shape_raw: *u8 = sys_mmap(64)
449 let b_shape: *i64 = b_shape_raw as *i64
450 b_shape[0] = 8; b_shape[1] = 16
451 let b_id: i64 = tir_add_tensor(g, 2, b_shape, TIR_DT_FP16, TIR_LAY_DENSE)
452
453 let c_shape_raw: *u8 = sys_mmap(64)
454 let c_shape: *i64 = c_shape_raw as *i64
455 c_shape[0] = 4; c_shape[1] = 16
456 let c_id: i64 = tir_add_tensor(g, 2, c_shape, TIR_DT_FP16, TIR_LAY_DENSE)
457
458 let inputs_raw: *u8 = sys_mmap(64)
459 let inputs: *i64 = inputs_raw as *i64
460 inputs[0] = a_id; inputs[1] = b_id
461
462 tir_add_node(g, TIR_OP_MATMUL, 2, inputs, c_id)
463
464 // Verify graph state.
465 if g.n_nodes != 1 { return __syscall(93, 50, 0, 0, 0, 0, 0) }
466 if g.n_tensors != 3 { return __syscall(93, 51, 0, 0, 0, 0, 0) }
467
468 // Verify shape-derived byte counts.
469 let a: *TirTensor = tir_tensor_at(g, a_id)
470 if a.size != 64 { return __syscall(93, 52, 0, 0, 0, 0, 0) } // 4*8*2 bytes
471 let b: *TirTensor = tir_tensor_at(g, b_id)
472 if b.size != 256 { return __syscall(93, 53, 0, 0, 0, 0, 0) } // 8*16*2
473 let c: *TirTensor = tir_tensor_at(g, c_id)
474 if c.size != 128 { return __syscall(93, 54, 0, 0, 0, 0, 0) } // 4*16*2
475
476 // Shape inference checks.
477 let cinfer_shape_raw: *u8 = sys_mmap(64)
478 let cinfer_shape: *i64 = cinfer_shape_raw as *i64
479 let cinfer_raw: *u8 = sys_mmap(64)
480 let cinfer: *TirTensor = cinfer_raw as *TirTensor
481 cinfer.shape = cinfer_shape
482
483 // Matmul 4x8 * 8x16 = 4x16, byte size 128.
484 let mrc: i64 = tir_infer_matmul(a, b, cinfer)
485 if mrc != 0 { return __syscall(93, 60, 0, 0, 0, 0, 0) }
486 if cinfer.rank != 2 { return __syscall(93, 61, 0, 0, 0, 0, 0) }
487 if cinfer.shape[0] != 4 { return __syscall(93, 62, 0, 0, 0, 0, 0) }
488 if cinfer.shape[1] != 16 { return __syscall(93, 63, 0, 0, 0, 0, 0) }
489 if cinfer.size != 128 { return __syscall(93, 64, 0, 0, 0, 0, 0) }
490
491 // Matmul shape mismatch: 4x8 * 16x16 (k_a=8, k_b=16) -> error.
492 let bad_shape_raw: *u8 = sys_mmap(64)
493 let bad_shape: *i64 = bad_shape_raw as *i64
494 bad_shape[0] = 16; bad_shape[1] = 16
495 let bad_raw: *u8 = sys_mmap(64)
496 let bad: *TirTensor = bad_raw as *TirTensor
497 bad.rank = 2
498 bad.shape = bad_shape
499 bad.dtype = TIR_DT_FP16
500 bad.layout = TIR_LAY_DENSE
501 let badrc: i64 = tir_infer_matmul(a, bad, cinfer)
502 if badrc != TIR_SHAPE_MISMATCH {
503 return __syscall(93, 65, 0, 0, 0, 0, 0)
504 }
505
506 // Fusion pass: build a 3-node SwiGLU-shape graph
507 // X (load) -> SiLU -> MUL -> Y
508 // and verify the MUL consumer fuses the SiLU producer.
509 let g2: *TirGraph = tir_graph_new(8)
510 let s_raw: *u8 = sys_mmap(64); let s_shape: *i64 = s_raw as *i64
511 s_shape[0] = 4; s_shape[1] = 16
512 let x_id: i64 = tir_add_tensor(g2, 2, s_shape, TIR_DT_FP16, TIR_LAY_DENSE)
513 let y_id: i64 = tir_add_tensor(g2, 2, s_shape, TIR_DT_FP16, TIR_LAY_DENSE)
514 let z_id: i64 = tir_add_tensor(g2, 2, s_shape, TIR_DT_FP16, TIR_LAY_DENSE)
515
516 // Producer LOAD nodes for X
517 let load_in_raw: *u8 = sys_mmap(64); let load_in: *i64 = load_in_raw as *i64
518 let load_id: i64 = tir_add_node(g2, TIR_OP_LOAD, 0, load_in, x_id)
519
520 // SiLU(X) -> Y
521 let silu_in_raw: *u8 = sys_mmap(64); let silu_in: *i64 = silu_in_raw as *i64
522 silu_in[0] = load_id
523 let silu_id: i64 = tir_add_node(g2, TIR_OP_SILU, 1, silu_in, y_id)
524
525 // MUL(Y, X) -> Z (the final consumer)
526 let mul_in_raw: *u8 = sys_mmap(64); let mul_in: *i64 = mul_in_raw as *i64
527 mul_in[0] = silu_id; mul_in[1] = load_id
528 let mul_id: i64 = tir_add_node(g2, TIR_OP_MUL, 2, mul_in, z_id)
529
530 let nfused: i64 = tir_fuse_graph(g2)
531 // Expect: SiLU fuses into MUL. LOAD also fuses into SiLU. And
532 // LOAD fuses into MUL (via the second operand). Total 3 fuses.
533 // (Exact count may vary by 1 if LOAD's not classified as
534 // element-wise; main check is that fusion happened.)
535 if nfused < 1 { return __syscall(93, 70, 0, 0, 0, 0, 0) }
536
537 let mul_node: *TirNode = tir_node_at(g2, mul_id)
538 if mul_node.n_fused < 1 { return __syscall(93, 71, 0, 0, 0, 0, 0) }
539
540 return __syscall(93, 42, 0, 0, 0, 0, 0)
541}