Skip to main content

ternlang_core/codegen/
betbc.rs

1use crate::ast::*;
2use crate::vm::bet::pack_trits;
3use crate::trit::Trit;
4
5pub struct BytecodeEmitter {
6    code: Vec<u8>,
7    symbols: std::collections::HashMap<String, u8>,
8    func_addrs: std::collections::HashMap<String, u16>,
9    function_patches: std::collections::HashMap<String, Vec<usize>>,
10    break_patches: Vec<usize>,
11    continue_patches: Vec<usize>,
12    next_reg: usize,
13    pub struct_layouts: std::collections::HashMap<String, Vec<String>>,
14    agent_type_ids: std::collections::HashMap<String, u16>,
15    agent_handlers: Vec<(u16, u16)>,
16    /// Snapshots of the local symbol table for each function, keyed by function name.
17    /// Captured just before scope is restored so callers can map reg→varname after execution.
18    function_symbols: std::collections::HashMap<String, std::collections::HashMap<String, u8>>,
19    /// True while emitting a function decorated with @sparseskip.
20    /// When set, matmul() emits 0x38 TSparseMatmul instead of 0x10 TCALL.
21    in_sparseskip: bool,
22}
23
24impl BytecodeEmitter {
25    pub fn new() -> Self {
26        Self {
27            code: Vec::new(),
28            symbols: std::collections::HashMap::new(),
29            func_addrs: std::collections::HashMap::new(),
30            function_patches: std::collections::HashMap::new(),
31            break_patches: Vec::new(),
32            continue_patches: Vec::new(),
33            next_reg: 0,
34            struct_layouts: std::collections::HashMap::new(),
35            agent_type_ids: std::collections::HashMap::new(),
36            agent_handlers: Vec::new(),
37            function_symbols: std::collections::HashMap::new(),
38            in_sparseskip: false,
39        }
40    }
41
42    /// Returns the top-level variable-name → register-number map.
43    pub fn get_symbols(&self) -> &std::collections::HashMap<String, u8> {
44        &self.symbols
45    }
46
47    /// Returns the local symbol snapshot for a specific function (e.g. "main").
48    /// Used by `ternlang-cli --emit-symbols` to correlate VM register dumps with source variable names.
49    pub fn get_function_symbols(&self, name: &str) -> Option<&std::collections::HashMap<String, u8>> {
50        self.function_symbols.get(name)
51    }
52
53    pub fn register_agents(&self, vm: &mut crate::vm::BetVm) {
54        for &(type_id, addr) in &self.agent_handlers {
55            vm.register_agent_type(type_id, addr as usize);
56        }
57    }
58
59    /// Emit a single agent definition incrementally (used by the WASM fallback loop
60    /// when the full `parse_program` path is unavailable).
61    pub fn emit_agent_def(&mut self, agent: &crate::AgentDef) {
62        let type_id = self.agent_type_ids.len() as u16;
63        self.agent_type_ids.insert(agent.name.clone(), type_id);
64        let mut handler_addr: Option<u16> = None;
65        for method in &agent.methods {
66            let addr = self.code.len() as u16;
67            if handler_addr.is_none() {
68                handler_addr = Some(addr);
69            }
70            self.emit_function(method);
71            self.func_addrs.insert(format!("{}::{}", agent.name, method.name), addr);
72        }
73        if let Some(addr) = handler_addr {
74            self.agent_handlers.push((type_id, addr));
75        }
76    }
77
78    pub fn emit_header_jump(&mut self) -> usize {
79        let patch_pos = self.code.len() + 1;
80        self.code.push(0x0b); // TJMP
81        self.code.extend_from_slice(&[0u8, 0u8]);
82        patch_pos
83    }
84
85    pub fn patch_header_jump(&mut self, patch_pos: usize) {
86        let addr = self.code.len() as u16;
87        self.patch_u16(patch_pos, addr);
88    }
89
90    pub fn emit_program(&mut self, program: &Program) {
91        let parent_next_reg = self.next_reg;
92        for s in &program.structs {
93            let names: Vec<String> = s.fields.iter().map(|(n, _)| n.clone()).collect();
94            self.struct_layouts.insert(s.name.clone(), names);
95        }
96        for (idx, agent) in program.agents.iter().enumerate() {
97            self.agent_type_ids.insert(agent.name.clone(), idx as u16);
98        }
99
100        // PASS 1: Addresses
101        let real_code = std::mem::take(&mut self.code);
102        let real_func_addrs = std::mem::take(&mut self.func_addrs);
103        let real_agent_handlers = std::mem::take(&mut self.agent_handlers);
104        let base_addr = real_code.len() as u16;
105
106        for agent in &program.agents {
107            let type_id = self.agent_type_ids[&agent.name];
108            let mut handler_addr = None;
109            for method in &agent.methods {
110                let addr = base_addr + self.code.len() as u16;
111                if handler_addr.is_none() { handler_addr = Some(addr); }
112                self.emit_function(method);
113                // Restore correct absolute address overwritten by emit_function (TCALL-BUG fix):
114                self.func_addrs.insert(format!("{}::{}", agent.name, method.name), addr);
115            }
116            if let Some(addr) = handler_addr { self.agent_handlers.push((type_id, addr)); }
117        }
118        for func in &program.functions {
119            let addr = base_addr + self.code.len() as u16;
120            self.func_addrs.insert(func.name.clone(), addr);
121            // Ensure any global symbols or previous definitions are visible
122            self.emit_function(func);
123            // emit_function overwrites func_addrs[name] with a temp-buffer offset that
124            // omits base_addr. Restore the correct absolute address so that forward
125            // references resolved later in PASS 1 get the right TCALL target.
126            self.func_addrs.insert(func.name.clone(), addr);
127        }
128
129        let final_func_addrs = std::mem::replace(&mut self.func_addrs, real_func_addrs);
130        let final_agent_handlers = std::mem::replace(&mut self.agent_handlers, real_agent_handlers);
131        self.code = real_code;
132        self.func_addrs = final_func_addrs;
133        self.agent_handlers = final_agent_handlers;
134        self.next_reg = parent_next_reg;
135
136        // PASS 2: Real
137        for agent in &program.agents {
138            for method in &agent.methods { self.emit_function(method); }
139        }
140        for func in &program.functions { self.emit_function(func); }
141    }
142
143    pub fn emit_function(&mut self, func: &Function) {
144        let func_addr = self.code.len() as u16;
145        self.func_addrs.insert(func.name.clone(), func_addr);
146        if let Some(patches) = self.function_patches.remove(&func.name) {
147            for p in patches {
148                self.code[p..p + 2].copy_from_slice(&func_addr.to_le_bytes());
149            }
150        }
151        let parent_symbols = self.symbols.clone();
152        let parent_next_reg = self.next_reg;
153        let parent_in_sparseskip = self.in_sparseskip;
154        self.next_reg = 0;
155        self.in_sparseskip = func.directive.as_deref() == Some("sparseskip");
156
157        for (name, ty) in func.params.iter().rev() {
158            if let Type::Named(s_name) = ty {
159                if let Some(fields) = self.struct_layouts.get(s_name).cloned() {
160                    // Structs are passed as a bundle: [field1, field2, ..., root_dummy]
161                    // We must pop root dummy first, then fields.
162                    
163                    // Pop root dummy
164                    let root_reg = self.alloc_reg();
165                    self.symbols.insert(name.clone(), root_reg);
166                    self.code.push(0x08); self.code.push(root_reg);
167
168                    // Pop fields in reverse order of how they were pushed
169                    for f_name in fields.iter().rev() {
170                        let f_reg = self.alloc_reg();
171                        let key = format!("{}.{}", name, f_name);
172                        self.symbols.insert(key, f_reg);
173                        self.code.push(0x08); self.code.push(f_reg);
174                    }
175                    continue;
176                }
177            }
178            let reg = self.alloc_reg();
179            self.symbols.insert(name.clone(), reg);
180            self.code.push(0x08); self.code.push(reg);
181        }
182        for stmt in &func.body { self.emit_stmt(stmt); }
183        // Snapshot local symbols before scope is restored — used by --emit-symbols
184        self.function_symbols.insert(func.name.clone(), self.symbols.clone());
185        self.symbols = parent_symbols;
186        self.next_reg = parent_next_reg;
187        self.in_sparseskip = parent_in_sparseskip;
188        self.code.push(0x11); // TRET
189    }
190
191    pub fn emit_stmt(&mut self, stmt: &Stmt) {
192        match stmt {
193            Stmt::Let { name, ty, value } => {
194                let mut handled = false;
195                match ty {
196                    Type::TritTensor { dims } => {
197                        // Auto-alloc for any zero-initializer (TritLiteral(0) or IntLiteral(0))
198                        let is_zero_init = matches!(value, Expr::TritLiteral(0) | Expr::IntLiteral(0));
199                        if !dims.is_empty() && !dims.contains(&0) && is_zero_init {
200                            let rows = dims[0];
201                            let cols = if dims.len() > 1 { dims[1] } else { 1 };
202                            self.code.push(0x0f); // TALLOC (trit)
203                            self.code.extend_from_slice(&(rows as u32).to_le_bytes());
204                            self.code.extend_from_slice(&(cols as u32).to_le_bytes());
205                            handled = true;
206                        }
207                    }
208                    Type::PackedTritTensor { dims } => {
209                        let is_zero_init = matches!(value, Expr::TritLiteral(0) | Expr::IntLiteral(0));
210                        if !dims.is_empty() && !dims.contains(&0) && is_zero_init {
211                            let rows = dims[0];
212                            let cols = if dims.len() > 1 { dims[1] } else { 1 };
213                            self.code.push(0x56); // TALLOC_PACKED
214                            self.code.extend_from_slice(&(rows as u32).to_le_bytes());
215                            self.code.extend_from_slice(&(cols as u32).to_le_bytes());
216                            handled = true;
217                        }
218                    }
219                    Type::IntTensor { dims } => {
220                        let is_zero_init = matches!(value, Expr::TritLiteral(0) | Expr::IntLiteral(0));
221                        if !dims.is_empty() && !dims.contains(&0) && is_zero_init {
222                            let rows = dims[0];
223                            let cols = if dims.len() > 1 { dims[1] } else { 1 };
224                            self.code.push(0x3c); // TALLOC_Int
225                            self.code.extend_from_slice(&(rows as u32).to_le_bytes());
226                            self.code.extend_from_slice(&(cols as u32).to_le_bytes());
227                            handled = true;
228                        }
229                    }
230                    Type::FloatTensor { dims } => {
231                        let is_zero_init = matches!(value, Expr::TritLiteral(0) | Expr::IntLiteral(0));
232                        if !dims.is_empty() && !dims.contains(&0) && is_zero_init {
233                            let rows = dims[0];
234                            let cols = if dims.len() > 1 { dims[1] } else { 1 };
235                            self.code.push(0x3d); // TALLOC_Float
236                            self.code.extend_from_slice(&(rows as u32).to_le_bytes());
237                            self.code.extend_from_slice(&(cols as u32).to_le_bytes());
238                            handled = true;
239                        }
240                    }
241                    Type::Named(_) => {
242                        if let Expr::StructLiteral { fields, .. } = value {
243                            // Flatten struct fields into mangled registers
244                            for (f_name, f_val) in fields {
245                                self.emit_expr(f_val);
246                                let reg = self.alloc_reg();
247                                let key = format!("{}.{}", name, f_name);
248                                self.symbols.insert(key, reg);
249                                self.code.push(0x08); self.code.push(reg);
250                            }
251                            // Now we let the normal path emit the root variable's dummy value
252                        }
253                    }
254                    _ => {}
255                }
256                if !handled {
257                    self.emit_expr(value);
258                }
259                let reg = self.alloc_reg();
260                self.symbols.insert(name.clone(), reg);
261                self.code.push(0x08); self.code.push(reg); // TSTORE
262            }
263            Stmt::Set { name, value } => {
264                self.emit_expr(value);
265                if let Some(&reg) = self.symbols.get(name) {
266                    self.code.push(0x08); self.code.push(reg);
267                }
268            }
269            Stmt::FieldSet { object, field, value } => {
270                let key = format!("{}.{}", object, field);
271                self.emit_expr(value);
272                if let Some(&reg) = self.symbols.get(&key) {
273                    self.code.push(0x08); self.code.push(reg);
274                }
275            }
276            Stmt::IndexSet { object, row, col, value } => {
277                if let Some(&reg) = self.symbols.get(object) {
278                    self.code.push(0x09); self.code.push(reg);
279                    self.emit_expr(row);
280                    self.emit_expr(col);
281                    self.emit_expr(value);
282                    self.code.push(0x23);
283                }
284            }
285            Stmt::IfTernary { condition, on_pos, on_zero, on_neg } => {
286                let pre_reg = self.next_reg;
287                self.emit_expr(condition);
288                let cond_reg = self.alloc_reg();
289                self.code.push(0x08); self.code.push(cond_reg); // Tstore
290                
291                // Load condition for checks
292                self.code.push(0x09); self.code.push(cond_reg); // Tload
293                
294                // Check POS
295                let pos_patch = self.code.len() + 1;
296                self.code.push(0x05); self.code.extend_from_slice(&[0, 0]); // TJMP_POS
297                
298                // Check ZERO
299                let zero_patch = self.code.len() + 1;
300                self.code.push(0x06); self.code.extend_from_slice(&[0, 0]); // TJMP_ZERO
301                
302                // NEG arm: pop the condition and execute
303                self.code.push(0x0c); // TPOP
304                self.emit_stmt(on_neg);
305                let exit_patch = self.code.len() + 1;
306                self.code.push(0x0b); self.code.extend_from_slice(&[0, 0]); // TJMP to end
307                
308                // POS arm
309                let pos_addr = self.code.len() as u16;
310                self.patch_u16(pos_patch, pos_addr);
311                self.code.push(0x0c); // TPOP
312                self.emit_stmt(on_pos);
313                let exit_pos = self.code.len() + 1;
314                self.code.push(0x0b); self.code.extend_from_slice(&[0, 0]);
315                
316                // ZERO arm
317                let zero_addr = self.code.len() as u16;
318                self.patch_u16(zero_patch, zero_addr);
319                self.code.push(0x0c); // TPOP
320                self.emit_stmt(on_zero);
321                
322                let end = self.code.len() as u16;
323                self.patch_u16(exit_patch, end);
324                self.patch_u16(exit_pos, end);
325                self.next_reg = pre_reg;
326            }
327            Stmt::Match { condition, arms } => {
328                let pre_reg = self.next_reg;
329                self.emit_expr(condition);
330                let cond_reg = self.alloc_reg();
331                self.code.push(0x08); self.code.push(cond_reg); // Tstore
332
333                let mut end_patches = Vec::new();
334                let mut next_arm_patch = None;
335
336                for (pattern, stmt) in arms {
337                    if let Some(p) = next_arm_patch {
338                        let addr = self.code.len() as u16;
339                        self.patch_u16(p, addr);
340                    }
341
342                    // Load condition for this arm
343                    self.code.push(0x09); self.code.push(cond_reg); // Tload
344
345                    let match_patch;
346                    match pattern {
347                        Pattern::Trit(1) | Pattern::Int(1) => {
348                            self.code.push(0x05); // TjmpPos (peeks)
349                            match_patch = self.code.len();
350                            self.code.extend_from_slice(&[0, 0]);
351                        }
352                        Pattern::Trit(0) | Pattern::Int(0) => {
353                            self.code.push(0x06); // TjmpZero (peeks)
354                            match_patch = self.code.len();
355                            self.code.extend_from_slice(&[0, 0]);
356                        }
357                        Pattern::Trit(-1) | Pattern::Int(-1) => {
358                            self.code.push(0x07); // TjmpNeg (peeks)
359                            match_patch = self.code.len();
360                            self.code.extend_from_slice(&[0, 0]);
361                        }
362                        Pattern::Int(v) => {
363                            self.code.push(0x25); // TjmpEqInt (peeks)
364                            self.code.extend_from_slice(&v.to_le_bytes());
365                            match_patch = self.code.len();
366                            self.code.extend_from_slice(&[0, 0]);
367                        }
368                        Pattern::Trit(v) => {
369                            self.code.push(0x25); // TjmpEqInt (peeks)
370                            self.code.extend_from_slice(&(*v as i64).to_le_bytes());
371                            match_patch = self.code.len();
372                            self.code.extend_from_slice(&[0, 0]);
373                        }
374                        Pattern::Float(v) => {
375                            self.code.push(0x2a); // TjmpEqFloat (peeks)
376                            self.code.extend_from_slice(&v.to_le_bytes());
377                            match_patch = self.code.len();
378                            self.code.extend_from_slice(&[0, 0]);
379                        }
380                        Pattern::Wildcard => {
381                            // Wildcard always matches — unconditional jump to body.
382                            // Do NOT pop here: the body's shared TPOP below will clean
383                            // the TLOAD value from the stack, keeping it balanced.
384                            match_patch = self.code.len() + 1;
385                            self.code.push(0x0b); self.code.extend_from_slice(&[0, 0]); // TJMP placeholder
386                        }
387                    }
388
389                    // Mismatch: the conditional test above PEEKS (doesn't pop), so if it
390                    // didn't jump the TLOAD result is still on the stack. Pop it before
391                    // jumping to the next arm to keep the stack balanced.
392                    // (Wildcard never reaches here — it always jumps above.)
393                    self.code.push(0x0c); // TPOP — discard unmatched arm's cond value
394                    let skip_patch = self.code.len() + 1;
395                    self.code.push(0x0b); self.code.extend_from_slice(&[0, 0]);
396                    next_arm_patch = Some(skip_patch);
397
398                    // Match found: execute body
399                    let body_addr = self.code.len() as u16;
400                    self.patch_u16(match_patch, body_addr);
401                    
402                    // Body: first pop the condition we were peeking at
403                    self.code.push(0x0c); // Tpop
404                    self.emit_stmt(stmt);
405                    
406                    // After body, jump to end of match
407                    let end_patch = self.code.len() + 1;
408                    self.code.push(0x0b); self.code.extend_from_slice(&[0, 0]);
409                    end_patches.push(end_patch);
410                }
411
412                if let Some(p) = next_arm_patch {
413                    let addr = self.code.len() as u16;
414                    self.patch_u16(p, addr);
415                }
416                
417                if !arms.is_empty() {
418                    // Each arm's mismatch path now does its own TPOP (see per-arm fix above),
419                    // so the stack is already clean when we reach the fallback.
420                    // VM-MATCH-001: non-exhaustive match — no arm was taken.
421                    // Push a Tend (hold/undefined) placeholder so the stack is balanced
422                    // even if the caller expects a return value from this match expression.
423                    self.code.push(0x01); self.code.extend(pack_trits(&[Trit::Tend]));
424                }
425
426                let end_addr = self.code.len() as u16;
427                for p in end_patches { self.patch_u16(p, end_addr); }
428                self.next_reg = pre_reg;
429            }
430            Stmt::ForIn { var, iter, body } => {
431                // Save next_reg so loop-internal registers are freed after the loop ends.
432                // Without this, each for-in permanently consumes 4 registers, exhausting
433                // the register file after 6-7 loops in a single function.
434                let pre_loop_reg = self.next_reg;
435
436                self.emit_expr(iter);
437                let it_reg = self.alloc_reg();
438                self.code.push(0x08); self.code.push(it_reg);
439                self.code.push(0x09); self.code.push(it_reg);
440                self.code.push(0x24); // TSHAPE: pushes rows then cols (cols on top)
441                self.code.push(0x0c); // pop cols — iterate over rows, not cols
442                let r_reg = self.alloc_reg();
443                self.code.push(0x08); self.code.push(r_reg); // store rows as loop bound
444                let i_reg = self.alloc_reg();
445                self.code.push(0x17); self.code.extend_from_slice(&0i64.to_le_bytes());
446                self.code.push(0x08); self.code.push(i_reg);
447
448                // Use a register for the loop comparison so we avoid TDUP accumulation.
449                // Previously: TDUP + TjmpNeg/TjmpZero (peek) left 2 values on the stack per
450                // iteration, causing a stack leak that corrupted subsequent operations.
451                let cmp_reg = self.alloc_reg();
452
453                let top = self.code.len() as u16;
454                let pre_break = self.break_patches.len();
455                let pre_cont = self.continue_patches.len();
456
457                // Compute i < r → cmp_reg (stack neutral: push then immediately store)
458                self.code.push(0x09); self.code.push(i_reg);
459                self.code.push(0x09); self.code.push(r_reg);
460                self.code.push(0x14);                        // Tless → [cmp]
461                self.code.push(0x08); self.code.push(cmp_reg); // TSTORE cmp → []
462
463                // Load and test for NEG (i >= r → Reject → exit)
464                self.code.push(0x09); self.code.push(cmp_reg); // [cmp]
465                let neg = self.code.len() + 1;
466                self.code.push(0x07); self.code.extend_from_slice(&[0, 0]); // TjmpNeg → peeks
467                self.code.push(0x0c); // TPOP — clean up after failed neg check
468
469                // Load and test for ZERO (i == r → Tend → exit)
470                self.code.push(0x09); self.code.push(cmp_reg); // [cmp]
471                let zero = self.code.len() + 1;
472                self.code.push(0x06); self.code.extend_from_slice(&[0, 0]); // TjmpZero → peeks
473                self.code.push(0x0c); // TPOP — clean up after failed zero check, body runs clean
474
475                // Body: load element tensor[it, i, 0] → v_reg
476                self.code.push(0x09); self.code.push(it_reg);
477                self.code.push(0x09); self.code.push(i_reg);
478                self.code.push(0x17); self.code.extend_from_slice(&0i64.to_le_bytes());
479                self.code.push(0x22);
480                let v_reg = self.alloc_reg();
481                self.symbols.insert(var.clone(), v_reg);
482                self.code.push(0x08); self.code.push(v_reg);
483                self.emit_stmt(body);
484
485                let cont_addr = self.code.len() as u16;
486                let cs: Vec<usize> = self.continue_patches.drain(pre_cont..).collect();
487                for p in cs { self.patch_u16(p, cont_addr); }
488
489                self.code.push(0x09); self.code.push(i_reg);
490                self.code.push(0x17); self.code.extend_from_slice(&1i64.to_le_bytes());
491                self.code.push(0x18);
492                self.code.push(0x08); self.code.push(i_reg);
493                let back = self.code.len() + 1;
494                self.code.push(0x0b); self.code.extend_from_slice(&[0, 0]);
495                self.patch_u16(back, top);
496
497                // Exit paths for neg/zero: the TjmpNeg/TjmpZero PEEK so the cmp value
498                // is still on the stack when they jump. Add a TPOP cleanup then TJMP end.
499                let neg_exit_addr = self.code.len() as u16;
500                self.patch_u16(neg, neg_exit_addr);
501                self.code.push(0x0c); // TPOP — clean peeked cmp
502                let neg_to_end = self.code.len() + 1;
503                self.code.push(0x0b); self.code.extend_from_slice(&[0, 0]);
504
505                let zero_exit_addr = self.code.len() as u16;
506                self.patch_u16(zero, zero_exit_addr);
507                self.code.push(0x0c); // TPOP — clean peeked cmp
508                let zero_to_end = self.code.len() + 1;
509                self.code.push(0x0b); self.code.extend_from_slice(&[0, 0]);
510
511                let end = self.code.len() as u16;
512                self.patch_u16(neg_to_end, end);
513                self.patch_u16(zero_to_end, end);
514                let bs: Vec<usize> = self.break_patches.drain(pre_break..).collect();
515                for p in bs { self.patch_u16(p, end); }
516
517                // Free loop registers: loop variable is out of scope, and the 4
518                // internal registers (it, r, i, v) are no longer needed.
519                self.symbols.remove(var);
520                self.next_reg = pre_loop_reg;
521            }
522            Stmt::WhileTernary { condition, on_pos, on_zero, on_neg } => {
523                let pre_reg = self.next_reg;
524                let cond_reg = self.alloc_reg();
525                let top = self.code.len() as u16;
526                let pre_break = self.break_patches.len();
527                let pre_cont = self.continue_patches.len();
528
529                self.emit_expr(condition);
530                self.code.push(0x08); self.code.push(cond_reg); // Tstore
531                
532                // Load condition for checks
533                self.code.push(0x09); self.code.push(cond_reg); // Tload
534                
535                // Check POS
536                let pos_patch = self.code.len() + 1;
537                self.code.push(0x05); self.code.extend_from_slice(&[0, 0]); // TJMP_POS
538                
539                // Check ZERO
540                let zero_patch = self.code.len() + 1;
541                self.code.push(0x06); self.code.extend_from_slice(&[0, 0]); // TJMP_ZERO
542                
543                // NEG ARM: pop and execute and EXIT (don't loop back)
544                self.code.push(0x0c); // TPOP
545                self.emit_stmt(on_neg);
546                let exit_neg = self.code.len() + 1;
547                self.code.push(0x0b); self.code.extend_from_slice(&[0, 0]); // TJMP to end
548
549                // POS ARM: pop and execute and LOOP BACK
550                let pos_addr = self.code.len() as u16;
551                self.patch_u16(pos_patch, pos_addr);
552                self.code.push(0x0c); // TPOP
553                self.emit_stmt(on_pos);
554                let back_pos = self.code.len() + 1;
555                self.code.push(0x0b); self.code.extend_from_slice(&[0, 0]);
556                self.patch_u16(back_pos, top);
557
558                // ZERO ARM: pop and execute and EXIT (don't loop back)
559                let zero_addr = self.code.len() as u16;
560                self.patch_u16(zero_patch, zero_addr);
561                self.code.push(0x0c); // TPOP
562                self.emit_stmt(on_zero);
563                
564                let end = self.code.len() as u16;
565                self.patch_u16(exit_neg, end);
566
567                let cs: Vec<usize> = self.continue_patches.drain(pre_cont..).collect();
568                for p in cs { self.patch_u16(p, top); }
569                let bs: Vec<usize> = self.break_patches.drain(pre_break..).collect();
570                for p in bs { self.patch_u16(p, end); }
571                self.next_reg = pre_reg;
572            }
573            Stmt::Loop { body } => {
574                let top = self.code.len() as u16;
575                let pre_break = self.break_patches.len();
576                let pre_cont = self.continue_patches.len();
577                self.emit_stmt(body);
578                let back = self.code.len() + 1;
579                self.code.push(0x0b); self.code.extend_from_slice(&[0, 0]);
580                self.patch_u16(back, top);
581                let end = self.code.len() as u16;
582                let cs: Vec<usize> = self.continue_patches.drain(pre_cont..).collect();
583                for p in cs { self.patch_u16(p, top); }
584                let bs: Vec<usize> = self.break_patches.drain(pre_break..).collect();
585                for p in bs { self.patch_u16(p, end); }
586            }
587            Stmt::Break => {
588                let p = self.code.len() + 1;
589                self.code.push(0x0b); self.code.extend_from_slice(&[0, 0]);
590                self.break_patches.push(p);
591            }
592            Stmt::Continue => {
593                let p = self.code.len() + 1;
594                self.code.push(0x0b); self.code.extend_from_slice(&[0, 0]);
595                self.continue_patches.push(p);
596            }
597            Stmt::Send { target, message } => {
598                self.emit_expr(target);
599                self.emit_expr(message);
600                self.code.push(0x31); // TSEND
601            }
602            Stmt::Return(e) => { self.emit_expr(e); self.code.push(0x11); }
603            Stmt::Block(ss) => { for s in ss { self.emit_stmt(s); } }
604            Stmt::Expr(e) => { self.emit_expr(e); self.code.push(0x0c); }
605            Stmt::Decorated { directive: _, stmt } => { self.emit_stmt(stmt); }
606            _ => {}
607        }
608    }
609
610    fn emit_expr(&mut self, expr: &Expr) {
611        match expr {
612            Expr::TritLiteral(v) => {
613                self.code.push(0x01);
614                self.code.extend(pack_trits(&[Trit::from(*v)]));
615            }
616            Expr::IntLiteral(v) => {
617                self.code.push(0x17);
618                self.code.extend_from_slice(&v.to_le_bytes());
619            }
620            Expr::FloatLiteral(val) => {
621                self.code.push(0x19);
622                self.code.extend_from_slice(&val.to_le_bytes());
623            }
624            Expr::StringLiteral(val) => {
625                self.code.push(0x21); // TPUSH_STRING
626                let bytes = val.as_bytes();
627                self.code.extend_from_slice(&(bytes.len() as u16).to_le_bytes());
628                self.code.extend_from_slice(bytes);
629            }
630            Expr::Ident(name) => {
631                // COMP-BOOL-001: `true`/`false` are not keywords in the lexer — they arrive
632                // as Token::Ident. Handle them here so they produce a value instead of
633                // causing a stack underflow when no symbol matches.
634                match name.as_str() {
635                    "true" => {
636                        self.code.push(0x17); // TpushInt
637                        self.code.extend_from_slice(&1i64.to_le_bytes());
638                    }
639                    "false" => {
640                        self.code.push(0x17); // TpushInt
641                        self.code.extend_from_slice(&0i64.to_le_bytes());
642                    }
643                    // COMP-TRIT-001: trit aliases that arrive as Ident if lexer misses them
644                    "affirm" => { self.code.push(0x01); self.code.extend(pack_trits(&[Trit::Affirm])); }
645                    "hold" | "tend" => { self.code.push(0x01); self.code.extend(pack_trits(&[Trit::Tend])); }
646                    "reject" => { self.code.push(0x01); self.code.extend(pack_trits(&[Trit::Reject])); }
647                    _ => {
648                        if let Some(&r) = self.symbols.get(name) {
649                            self.code.push(0x09); self.code.push(r);
650                        }
651                    }
652                }
653            }
654            Expr::BinaryOp { op, lhs, rhs } => {
655                self.emit_expr(lhs); self.emit_expr(rhs);
656                match op {
657                    BinOp::Add => self.code.push(0x02),
658                    BinOp::Mul => self.code.push(0x03),
659                    BinOp::Div => self.code.push(0x1e),
660                    BinOp::Mod => self.code.push(0x1f),
661                    BinOp::Sub => { self.code.push(0x04); self.code.push(0x02); }
662                    BinOp::Equal => self.code.push(0x16),
663                    BinOp::NotEqual => { self.code.push(0x16); self.code.push(0x04); }
664                    BinOp::And => self.code.push(0x28), // TAND = min(a,b)
665                    BinOp::Or  => self.code.push(0x29), // TOR  = max(a,b)
666                    BinOp::Less => self.code.push(0x14),
667                    BinOp::Greater => self.code.push(0x15),
668                    BinOp::LessEqual => self.code.push(0x26),
669                    BinOp::GreaterEqual => self.code.push(0x27),
670                }
671            }
672            Expr::UnaryOp { op, expr } => {
673                self.emit_expr(expr);
674                match op { UnOp::Neg => self.code.push(0x04) }
675            }
676            Expr::Call { callee, args } => {
677                match callee.as_str() {
678                    // `print` is an alias for `println` — same TPRINT opcode (0x20)
679                    "println" | "print" => {
680                        if args.is_empty() {
681                            // print newline only (not implemented, but let's push dummy)
682                        } else {
683                            for a in args {
684                                self.emit_expr(a);
685                                self.code.push(0x20); // TPRINT
686                            }
687                        }
688                        self.code.push(0x01); self.code.extend(pack_trits(&[Trit::Tend])); // return hold()
689                    }
690                    "opent" => {
691                        if args.len() == 2 {
692                            for a in args { self.emit_expr(a); }
693                            self.code.push(0x33); // TOPENT (pushes Int handle)
694                        } else {
695                            // error but push dummy
696                            self.code.push(0x01); self.code.extend(pack_trits(&[Trit::Tend]));
697                        }
698                    }
699                    "readt" => {
700                        if args.len() == 1 {
701                            self.emit_expr(&args[0]);
702                            self.code.push(0x34); // TREADT (pushes Trit)
703                        } else {
704                            self.code.push(0x01); self.code.extend(pack_trits(&[Trit::Tend]));
705                        }
706                    }
707                    "writet" => {
708                        if args.len() == 2 {
709                            for a in args { self.emit_expr(a); }
710                            self.code.push(0x35); // TWRITET
711                        }
712                        self.code.push(0x01); self.code.extend(pack_trits(&[Trit::Tend])); // push void/hold result
713                    }
714                    "consensus" => {
715
716                        for a in args { self.emit_expr(a); }
717                        if args.len() == 2 { self.code.push(0x0e); }
718                    }
719                    "length" => {
720                        if args.len() == 1 {
721                            self.emit_expr(&args[0]);
722                            self.code.push(0x24); // TSHAPE
723                            self.code.push(0x0c); // TPOP (cols)
724                        }
725                    }
726                    // VM-BUILTIN-001: `invert(t)` = ternary negation (Tneg, opcode 0x04)
727                    "invert" => {
728                        if args.len() == 1 {
729                            self.emit_expr(&args[0]);
730                            self.code.push(0x04); // Tneg
731                        }
732                    }
733                    // VM-BUILTIN-002: `len(arr)` is an alias for `length(arr)`
734                    "len" => {
735                        if args.len() == 1 {
736                            self.emit_expr(&args[0]);
737                            self.code.push(0x24); // TSHAPE
738                            self.code.push(0x0c); // TPOP (cols — TSHAPE pushes rows then cols)
739                        }
740                    }
741                    // VM-BUILTIN-001: `abs(n)` — inline: dup, push 0, less-than, branch on negative
742                    "abs" => {
743                        if args.len() == 1 {
744                            self.emit_expr(&args[0]);          // stack: [x]
745                            self.code.push(0x0a);              // TDUP   → [x, x]
746                            self.code.push(0x17);              // TpushInt 0
747                            self.code.extend_from_slice(&0i64.to_le_bytes()); // → [x, x, 0]
748                            self.code.push(0x14);              // Tless: (x < 0) → Affirm; [x, cmp]
749                            // TjmpPos (peek) to negate branch
750                            let neg_patch = self.code.len() + 1;
751                            self.code.push(0x05); self.code.extend_from_slice(&[0, 0]);
752                            // not negative: pop cmp, jump to end
753                            self.code.push(0x0c);              // TPOP → [x]
754                            let end_patch = self.code.len() + 1;
755                            self.code.push(0x0b); self.code.extend_from_slice(&[0, 0]);
756                            // negate branch: pop cmp, negate x
757                            let neg_addr = self.code.len() as u16;
758                            self.patch_u16(neg_patch, neg_addr);
759                            self.code.push(0x0c);              // TPOP → [x]
760                            self.code.push(0x04);              // Tneg → [-x] (positive when x<0)
761                            let end_addr = self.code.len() as u16;
762                            self.patch_u16(end_patch, end_addr);
763                        }
764                    }
765                    // VM-BUILTIN-001: `min(a, b)` — inline with temp registers
766                    "min" => {
767                        if args.len() == 2 {
768                            let a_reg = self.alloc_reg();
769                            let b_reg = self.alloc_reg();
770                            self.emit_expr(&args[0]);
771                            self.code.push(0x08); self.code.push(a_reg); // TSTORE a
772                            self.emit_expr(&args[1]);
773                            self.code.push(0x08); self.code.push(b_reg); // TSTORE b
774                            self.code.push(0x09); self.code.push(a_reg); // TLOAD a
775                            self.code.push(0x09); self.code.push(b_reg); // TLOAD b
776                            self.code.push(0x14);                         // Tless: a < b → Affirm
777                            // TjmpPos → a is smaller, return a
778                            let a_smaller_patch = self.code.len() + 1;
779                            self.code.push(0x05); self.code.extend_from_slice(&[0, 0]);
780                            // a >= b: return b
781                            self.code.push(0x0c);              // TPOP cmp
782                            self.code.push(0x09); self.code.push(b_reg); // TLOAD b
783                            let end_patch = self.code.len() + 1;
784                            self.code.push(0x0b); self.code.extend_from_slice(&[0, 0]);
785                            // a < b: return a
786                            let a_smaller_addr = self.code.len() as u16;
787                            self.patch_u16(a_smaller_patch, a_smaller_addr);
788                            self.code.push(0x0c);              // TPOP cmp
789                            self.code.push(0x09); self.code.push(a_reg); // TLOAD a
790                            let end_addr = self.code.len() as u16;
791                            self.patch_u16(end_patch, end_addr);
792                        }
793                    }
794                    // VM-BUILTIN-001: `max(a, b)` — inline with temp registers
795                    "max" => {
796                        if args.len() == 2 {
797                            let a_reg = self.alloc_reg();
798                            let b_reg = self.alloc_reg();
799                            self.emit_expr(&args[0]);
800                            self.code.push(0x08); self.code.push(a_reg); // TSTORE a
801                            self.emit_expr(&args[1]);
802                            self.code.push(0x08); self.code.push(b_reg); // TSTORE b
803                            self.code.push(0x09); self.code.push(b_reg); // TLOAD b
804                            self.code.push(0x09); self.code.push(a_reg); // TLOAD a
805                            self.code.push(0x14);                         // Tless: b < a → Affirm
806                            // TjmpPos → a is larger, return a
807                            let a_larger_patch = self.code.len() + 1;
808                            self.code.push(0x05); self.code.extend_from_slice(&[0, 0]);
809                            // b >= a: return b
810                            self.code.push(0x0c);              // TPOP cmp
811                            self.code.push(0x09); self.code.push(b_reg); // TLOAD b
812                            let end_patch = self.code.len() + 1;
813                            self.code.push(0x0b); self.code.extend_from_slice(&[0, 0]);
814                            // b < a: return a
815                            let a_larger_addr = self.code.len() as u16;
816                            self.patch_u16(a_larger_patch, a_larger_addr);
817                            self.code.push(0x0c);              // TPOP cmp
818                            self.code.push(0x09); self.code.push(a_reg); // TLOAD a
819                            let end_addr = self.code.len() as u16;
820                            self.patch_u16(end_patch, end_addr);
821                        }
822                    }
823                    // `pow(base, exp)` — integer power via loop: result = 1; while exp>0 { result*=base; exp-=1; }
824                    "pow" => {
825                        if args.len() == 2 {
826                            let b_reg = self.alloc_reg(); // base
827                            let e_reg = self.alloc_reg(); // exponent
828                            let r_reg = self.alloc_reg(); // result
829                            // store base
830                            self.emit_expr(&args[0]);
831                            self.code.push(0x08); self.code.push(b_reg);
832                            // store exp
833                            self.emit_expr(&args[1]);
834                            self.code.push(0x08); self.code.push(e_reg);
835                            // result = 1
836                            self.code.push(0x17); self.code.extend_from_slice(&1i64.to_le_bytes());
837                            self.code.push(0x08); self.code.push(r_reg);
838                            // loop_start: check e > 0
839                            let loop_start = self.code.len() as u16;
840                            self.code.push(0x09); self.code.push(e_reg);  // TLOAD e
841                            self.code.push(0x17); self.code.extend_from_slice(&0i64.to_le_bytes()); // push 0
842                            self.code.push(0x15);  // Tgreater: e > 0 → Affirm
843                            // TjmpPos → jump to loop body
844                            let body_patch = self.code.len() + 1;
845                            self.code.push(0x05); self.code.extend_from_slice(&[0, 0]);
846                            // e <= 0: pop cmp, jump to end
847                            self.code.push(0x0c);
848                            let end_patch = self.code.len() + 1;
849                            self.code.push(0x0b); self.code.extend_from_slice(&[0, 0]);
850                            // loop body:
851                            let body_addr = self.code.len() as u16;
852                            self.patch_u16(body_patch, body_addr);
853                            self.code.push(0x0c);  // TPOP cmp
854                            // r = r * b
855                            self.code.push(0x09); self.code.push(r_reg);
856                            self.code.push(0x09); self.code.push(b_reg);
857                            self.code.push(0x03);  // Tmul (handles int*int)
858                            self.code.push(0x08); self.code.push(r_reg);
859                            // e = e - 1
860                            self.code.push(0x09); self.code.push(e_reg);
861                            self.code.push(0x17); self.code.extend_from_slice(&(-1i64).to_le_bytes());
862                            self.code.push(0x18);  // TaddInt
863                            self.code.push(0x08); self.code.push(e_reg);
864                            // jump back to loop_start
865                            self.code.push(0x0b); self.code.extend_from_slice(&loop_start.to_le_bytes());
866                            // end:
867                            let end_addr = self.code.len() as u16;
868                            self.patch_u16(end_patch, end_addr);
869                            // push result
870                            self.code.push(0x09); self.code.push(r_reg);
871                        }
872                    }
873                    // `push(arr, val)` / `pop(arr)` — tensor mutation not yet implemented.
874                    // Emit argument expressions for side-effects then push a Tend stub so
875                    // callers get a value without falling through to an unresolved TCALL 0x0000
876                    // (which causes infinite recursion via jump-to-program-start).
877                    "push" => {
878                        for a in args { self.emit_expr(a); self.code.push(0x0c); } // eval + discard
879                        self.code.push(0x01); self.code.extend(pack_trits(&[Trit::Tend])); // stub result
880                    }
881                    "pop" => {
882                        for a in args { self.emit_expr(a); self.code.push(0x0c); } // eval + discard
883                        self.code.push(0x01); self.code.extend(pack_trits(&[Trit::Tend])); // stub result
884                    }
885                    "pack" => {
886                        for a in args { self.emit_expr(a); }
887                        if args.len() == 5 { self.code.push(0x50); }
888                    }
889                    "unpack" => {
890                        if args.len() == 1 { self.emit_expr(&args[0]); self.code.push(0x51); }
891                    }
892                    "v_add" => {
893                        if args.len() == 2 { for a in args { self.emit_expr(a); } self.code.push(0x52); }
894                    }
895                    "v_neg" => {
896                        if args.len() == 1 { self.emit_expr(&args[0]); self.code.push(0x53); }
897                    }
898                    "v_con" => {
899                        if args.len() == 2 { for a in args { self.emit_expr(a); } self.code.push(0x54); }
900                    }
901                    "bind" => {
902                        if args.len() == 2 {
903                            if let Expr::Ident(name) = &args[0] {
904                                if let Some(&reg) = self.symbols.get(name) {
905                                    self.emit_expr(&args[1]);
906                                    self.code.push(0x42); // TBIND
907                                    self.code.push(reg);
908                                }
909                            }
910                        }
911                        self.code.push(0x01); self.code.extend(pack_trits(&[Trit::Tend]));
912                    }
913                    "mul" => {
914                        for a in args { self.emit_expr(a); }
915                        if args.len() == 2 { self.code.push(0x03); }
916                    }
917                    "truth" => { self.code.push(0x01); self.code.extend(pack_trits(&[Trit::Affirm])); }
918                    "hold" => { self.code.push(0x01); self.code.extend(pack_trits(&[Trit::Tend])); }
919                    "conflict" => { self.code.push(0x01); self.code.extend(pack_trits(&[Trit::Reject])); }
920                    // @sparseskip enforcement: when inside a sparseskip function, matmul()
921                    // emits TSparseMatmul (0x38) with wildcard dims (0) so the VM reads
922                    // actual tensor dims at runtime and skips zero-weight positions.
923                    "matmul" => {
924                        if args.len() == 2 {
925                            for a in args { self.emit_expr(a); }
926                            if self.in_sparseskip {
927                                self.code.push(0x38); // TSparseMatmul
928                                self.code.push(0x00); // a_rows  — wildcard, VM reads from tensor
929                                self.code.push(0x00); // a_cols  — wildcard
930                                self.code.push(0x00); // b_cols  — wildcard
931                            } else {
932                                self.code.push(0x10); // TCALL
933                                if let Some(&addr) = self.func_addrs.get("matmul") {
934                                    self.code.extend_from_slice(&addr.to_le_bytes());
935                                } else {
936                                    let patch = self.code.len();
937                                    self.code.extend_from_slice(&[0, 0]);
938                                    self.function_patches.entry("matmul".to_string()).or_default().push(patch);
939                                }
940                            }
941                        } else {
942                            self.code.push(0x01); self.code.extend(pack_trits(&[Trit::Tend]));
943                        }
944                    }
945                    // trace::explain(label, value) — XAI decision trace.
946                    // Emits both args, then 0x60 TEXPLAIN which pops them and logs to trace_log.
947                    // Return value is hold() (pushed by the 0x01 stub below).
948                    "explain" => {
949                        if args.len() == 2 {
950                            for a in args { self.emit_expr(a); }
951                            self.code.push(0x60); // TEXPLAIN
952                        }
953                        self.code.push(0x01); self.code.extend(pack_trits(&[Trit::Tend]));
954                    }
955                    _ => {
956                        for a in args {
957                            // If argument is a struct, we need to push all its flattened fields + root dummy
958                            let mut pushed_as_struct = false;
959                            if let Expr::Ident(name) = a {
960                                // We don't have the variable type here, but we can try to find if it's a struct
961                                // by looking for any mangled keys starting with "name.".
962                                // To get the correct field order, we'd need the struct name.
963                                // Let's try to find which struct layout matches the existing mangled keys.
964                                let mut fields_found = Vec::new();
965                                for (_s_name, s_fields) in &self.struct_layouts {
966                                    let mut all_present = true;
967                                    let mut current_regs = Vec::new();
968                                    for f in s_fields {
969                                        let key = format!("{}.{}", name, f);
970                                        if let Some(&r) = self.symbols.get(&key) {
971                                            current_regs.push(r);
972                                        } else {
973                                            all_present = false;
974                                            break;
975                                        }
976                                    }
977                                    if all_present && !s_fields.is_empty() {
978                                        fields_found = current_regs;
979                                        break;
980                                    }
981                                }
982
983                                if !fields_found.is_empty() {
984                                    for reg in fields_found {
985                                        self.code.push(0x09); self.code.push(reg); // TLOAD field
986                                    }
987                                    // Push root dummy
988                                    if let Some(&reg) = self.symbols.get(name) {
989                                        self.code.push(0x09); self.code.push(reg); // TLOAD root
990                                    }
991                                    pushed_as_struct = true;
992                                }
993                            }
994                            
995                            if !pushed_as_struct {
996                                self.emit_expr(a);
997                            }
998                        }
999                        self.code.push(0x10); // TCALL
1000                        if let Some(&addr) = self.func_addrs.get(callee) {
1001                            self.code.extend_from_slice(&addr.to_le_bytes());
1002                        } else {
1003                            let patch = self.code.len();
1004                            self.code.extend_from_slice(&[0, 0]);
1005                            self.function_patches.entry(callee.to_string()).or_default().push(patch);
1006                        }
1007                    }
1008                }
1009            }
1010            Expr::Spawn { agent_name, .. } => {
1011                if let Some(&type_id) = self.agent_type_ids.get(agent_name) {
1012                    self.code.push(0x30); // TSPAWN
1013                    self.code.extend_from_slice(&type_id.to_le_bytes());
1014                } else {
1015                    self.code.push(0x01); self.code.extend(pack_trits(&[Trit::Tend]));
1016                }
1017            }
1018            Expr::Await { target } => {
1019                self.emit_expr(target);
1020                self.code.push(0x32); // TAWAIT
1021            }
1022            Expr::Slice { object, start, end, stride } => {
1023                self.emit_expr(object);
1024                self.emit_expr(start);
1025                // Compute length = end - start
1026                self.emit_expr(end);
1027                self.emit_expr(start);
1028                self.code.push(0x04); self.code.push(0x02); // TSUB (end - start)
1029                self.emit_expr(stride);
1030                self.code.push(0x55); // TVIEW
1031            }
1032            Expr::TritTensorLiteral(vs) => {
1033                let rows = vs.len();
1034                let cols = 1;
1035                self.code.push(0x0f);
1036                self.code.extend_from_slice(&(rows as u32).to_le_bytes());
1037                self.code.extend_from_slice(&(cols as u32).to_le_bytes());
1038                let tr = self.next_reg; self.next_reg += 1;
1039                self.code.push(0x08); self.code.push(tr.try_into().unwrap());
1040                for (idx, &v) in vs.iter().enumerate() {
1041                    self.code.push(0x09); self.code.push(tr.try_into().unwrap());
1042                    self.code.push(0x17); self.code.extend_from_slice(&(idx as i64).to_le_bytes());
1043                    self.code.push(0x17); self.code.extend_from_slice(&0i64.to_le_bytes());
1044                    self.code.push(0x01); self.code.extend(pack_trits(&[Trit::from(v)]));
1045                    self.code.push(0x23);
1046                }
1047                self.code.push(0x09); self.code.push(tr.try_into().unwrap());
1048            }
1049            Expr::StructLiteral { fields, .. } => {
1050                for (_, f_val) in fields {
1051                    self.emit_expr(f_val);
1052                }
1053                self.code.push(0x40); // TSTRUCT
1054                self.code.push(fields.len() as u8);
1055                // We pop in reverse order of pushing
1056                for (f_name, _) in fields.iter().rev() {
1057                    self.code.push(f_name.len() as u8);
1058                    self.code.extend_from_slice(f_name.as_bytes());
1059                }
1060            }
1061            Expr::Propagate { expr } => {
1062                self.emit_expr(expr);
1063                self.code.push(0x0a); // TDUP
1064                let patch = self.code.len() + 1;
1065                self.code.push(0x07); self.code.extend_from_slice(&[0, 0]); // TJMP_NEG
1066                let skip = self.code.len() + 1;
1067                self.code.push(0x0b); self.code.extend_from_slice(&[0, 0]); // TJMP
1068                let early_ret = self.code.len() as u16;
1069                self.patch_u16(patch, early_ret);
1070                self.code.push(0x11); // TRET
1071                let next = self.code.len() as u16;
1072                self.patch_u16(skip, next);
1073            }
1074            Expr::Index { object, row, col } => {
1075                self.emit_expr(object); self.emit_expr(row); self.emit_expr(col);
1076                self.code.push(0x22);
1077            }
1078            Expr::FieldAccess { object, field } => {
1079                if let Expr::Ident(obj_name) = object.as_ref() {
1080                    let key = format!("{}.{}", obj_name, field);
1081                    if let Some(&r) = self.symbols.get(&key) {
1082                        // Fast path: field has its own named register (locally-created struct)
1083                        self.code.push(0x09); self.code.push(r); // TLOAD
1084                    } else if let Some(&r) = self.symbols.get(obj_name) {
1085                        // Fallback: struct stored as Value::Struct in one register (function return)
1086                        // Fixes BET-001 stack underflow on struct-returning functions
1087                        self.code.push(0x09); self.code.push(r); // TLOAD struct
1088                        self.code.push(0x41); // Tfield
1089                        self.code.push(field.len() as u8);
1090                        self.code.extend_from_slice(field.as_bytes());
1091                    }
1092                }
1093            }
1094            Expr::Cast { expr, ty } => {
1095                self.emit_expr(expr);
1096                match ty {
1097                    crate::ast::Type::Int => { self.code.push(0x57); } // TtoInt
1098                    crate::ast::Type::Float => { self.code.push(0x58); } // TtoFloat
1099                    _ => {} // Trit/String/etc: pass through unchanged
1100                }
1101            }
1102            Expr::NodeId => {
1103                // Emit TNODEID (0x36): defers binding to runtime so that
1104                // `--node-addr` / vm.set_node_id() is actually respected.
1105                // Previously this emitted a hardcoded "127.0.0.1:7373" string
1106                // literal at compile time, which meant distributed modules always
1107                // announced the wrong address when deployed with a custom node addr.
1108                self.code.push(0x36); // TNODEID — pushes Value::String(vm.node_id)
1109            }
1110        }
1111    }
1112
1113    pub fn emit_entry_call(&mut self, name: &str) {
1114        if let Some(&addr) = self.func_addrs.get(name) {
1115            self.code.push(0x10); self.code.extend_from_slice(&addr.to_le_bytes());
1116        }
1117    }
1118
1119    /// Allocate the next register, returning its index as `u8` (the bytecode register width).
1120    /// Emits a stderr diagnostic if the function requires more than 255 registers — programs
1121    /// that hit this have much bigger structural problems anyway.
1122    fn alloc_reg(&mut self) -> u8 {
1123        let r = self.next_reg;
1124        self.next_reg += 1;
1125        if r > 255 {
1126            eprintln!(
1127                "[CODEGEN] Warning: register #{r} exceeds u8 range — \
1128                 this function has too many local variables (max 255). \
1129                 Split the function or reduce scope depth."
1130            );
1131        }
1132        r as u8
1133    }
1134
1135    pub fn get_agent_handlers(&self) -> Vec<(u16, usize)> {
1136        self.agent_handlers.iter().map(|&(id, addr)| (id, addr as usize)).collect()
1137    }
1138
1139    pub fn finalize(&mut self) -> Vec<u8> { std::mem::take(&mut self.code) }
1140
1141    fn patch_u16(&mut self, pos: usize, val: u16) {
1142        let b = val.to_le_bytes();
1143        self.code[pos] = b[0]; self.code[pos + 1] = b[1];
1144    }
1145}