Skip to main content

truthlinked_axiom_macro/
lib.rs

1//! Procedural macro helpers for TruthLinked Axiom cell development.
2//!
3//! The macros in this crate provide a Rust-native authoring layer for Axiom
4//! cells while keeping expansion explicit and deterministic. Generated code must
5//! remain stable across compiler versions because cell artifacts are deployed and
6//! audited on chain.
7
8use proc_macro::TokenStream;
9use proc_macro2::TokenStream as TS2;
10use quote::quote;
11use syn::{
12    parse_macro_input, spanned::Spanned, Attribute, BinOp, Expr, ExprCall, ExprMacro, Item, ItemFn,
13    ItemMod, ItemStatic, Pat, Stmt, Type, UnOp,
14};
15
16#[proc_macro_attribute]
17pub fn axiom_cell(_attr: TokenStream, item: TokenStream) -> TokenStream {
18    let module = parse_macro_input!(item as ItemMod);
19    match expand_cell(module) {
20        Ok(ts) => ts.into(),
21        Err(e) => e.to_compile_error().into(),
22    }
23}
24
25fn expand_cell(module: ItemMod) -> syn::Result<TS2> {
26    let mod_name = &module.ident;
27    let bytecode_fn = syn::Ident::new(&format!("{}_bytecode", mod_name), mod_name.span());
28    let selectors_fn = syn::Ident::new(&format!("{}_selectors", mod_name), mod_name.span());
29
30    let items = match &module.content {
31        Some((_, items)) => items,
32        None => {
33            return Err(syn::Error::new(
34                module.span(),
35                "#[axiom_cell] requires an inline module body",
36            ))
37        }
38    };
39
40    let mut storage_slots: Vec<StorageSlot> = vec![];
41    let mut selectors: Vec<SelectorFn> = vec![];
42    let mut helpers: Vec<HelperFn> = vec![];
43
44    for item in items {
45        match item {
46            Item::Static(s) if has_attr(&s.attrs, "storage") => {
47                storage_slots.push(parse_storage(s)?)
48            }
49            Item::Fn(f) if has_attr(&f.attrs, "selector") => selectors.push(parse_selector(f)?),
50            Item::Fn(f) if !has_attr(&f.attrs, "selector") && !has_attr(&f.attrs, "storage") => {
51                helpers.push(parse_helper(f)?);
52            }
53            _ => {}
54        }
55    }
56
57    let mut all_ir_code: Vec<TS2> = vec![];
58    let mut selector_entries: Vec<TS2> = vec![];
59
60    // Single monotonic counters - no magic-number offsets, no collision risk.
61    // Each selector/helper draws a contiguous block at expansion time.
62    let mut next_vreg: u32 = 1; // 0 = zero reg, reserved
63    let mut next_label: u32 = 0;
64
65    for (_sel_idx, sel) in selectors.iter().enumerate() {
66        let sel_name = &sel.name;
67        selector_entries.push(quote! {
68            (#sel_name, truthlinked_axiom_sdk::abi::selector_of(#sel_name))
69        });
70
71        let handler_label = next_label;
72        next_label += 1;
73
74        // Allocate dispatch vregs
75        let v_cd = next_vreg;
76        next_vreg += 1;
77        let v_sel = next_vreg;
78        next_vreg += 1;
79        let v_mask = next_vreg;
80        next_vreg += 1;
81        let v_mcd = next_vreg;
82        next_vreg += 1;
83        let v_cond = next_vreg;
84        next_vreg += 1;
85
86        let n_args = sel.args.len() as u32;
87        let arg_base = next_vreg;
88        next_vreg += n_args;
89        let off_base = next_vreg;
90        next_vreg += n_args;
91        let body_vreg_start = next_vreg;
92        next_vreg += 10_000; // 10k vregs per selector body - regalloc packs to physical regs
93        let label_start = next_label;
94        next_label += 10_000;
95
96        all_ir_code.push(quote! {
97            {
98                // Load calldata word 0 into v_cd
99                ir.push(Ir::LoadImm8(#v_cd, 0));
100                ir.push(Ir::GetCalldata(#v_cd, #v_cd));
101                // Load selector constant
102                let sel_bytes = truthlinked_axiom_sdk::abi::selector_of(#sel_name);
103                let mut sel32 = vec![0u8; 32];
104                sel32[..4].copy_from_slice(&sel_bytes);
105                ir.push(Ir::LoadConst(#v_sel, sel32));
106                // Mask to low 4 bytes
107                let mut mask32 = vec![0u8; 32];
108                mask32[0]=0xFF; mask32[1]=0xFF; mask32[2]=0xFF; mask32[3]=0xFF;
109                ir.push(Ir::LoadConst(#v_mask, mask32));
110                ir.push(Ir::And(#v_mcd, #v_cd, #v_mask));
111                ir.push(Ir::Eq(#v_cond, #v_mcd, #v_sel));
112                ir.push(Ir::JumpIf(#v_cond, #handler_label));
113            }
114        });
115
116        // Load args from calldata
117        let mut arg_loads: Vec<TS2> = vec![];
118        for i in 0..n_args {
119            let vreg = arg_base + i;
120            let off_vreg = off_base + i;
121            let offset = (4 + i * 32) as u64;
122            arg_loads.push(quote! {
123                ir.push(Ir::LoadImm64(#off_vreg, #offset));
124                ir.push(Ir::GetCalldata(#vreg, #off_vreg));
125            });
126        }
127
128        // Build a name→vreg map for args
129        let arg_names: Vec<String> = sel.args.iter().map(|(n, _)| n.to_string()).collect();
130        let arg_vregs: Vec<u32> = (0..n_args).map(|i| arg_base + i).collect();
131
132        // Lower body with a runtime vreg/label counter
133        let body_code = lower_body_ir_full(
134            &sel.body,
135            &storage_slots,
136            &helpers,
137            &arg_names,
138            &arg_vregs,
139            body_vreg_start,
140            label_start,
141        );
142
143        let vreg_ceiling = body_vreg_start + 10_000;
144        let label_ceiling = label_start + 10_000;
145        all_ir_code.push(quote! {
146            ir.push(Ir::Label(#handler_label));
147            #(#arg_loads)*
148            {
149                let mut _vreg: u32 = #body_vreg_start;
150                let mut _label: u32 = #label_start + 1;
151                let mut _break_stack: Vec<u32> = Vec::new();
152                let mut _continue_stack: Vec<u32> = Vec::new();
153                #body_code
154                assert!(_vreg <= #vreg_ceiling,
155                    "axiom_cell: selector '{}' used {} vregs, exceeds 500-slot limit (overflow into adjacent block)",
156                    #sel_name, _vreg - #body_vreg_start);
157                assert!(_label <= #label_ceiling,
158                    "axiom_cell: selector '{}' used {} labels, exceeds 500-slot limit",
159                    #sel_name, _label - #label_start);
160            }
161        });
162    }
163
164    all_ir_code.push(quote! { ir.push(Ir::Trap(0x0001)); });
165
166    // Emit helper subroutines after the dispatch table.
167    // Each helper gets a unique label: fn_<name>_<idx>.
168    // Call convention: args in consecutive vregs starting at _fn_arg_base,
169    // return value in _fn_ret_vreg. Both are passed by the caller.
170    // First pass: allocate labels and vregs for all helpers so call sites can reference them
171    for helper in helpers.iter_mut() {
172        let fn_label_id = next_label;
173        next_label += 1;
174        let n_args = helper.args.len() as u32;
175        let h_arg_base = next_vreg;
176        next_vreg += n_args;
177        let ret_vreg = next_vreg;
178        next_vreg += 1;
179        next_vreg += 10_000; // body locals
180        next_label += 10_000;
181        helper.label_id = fn_label_id;
182        helper.arg_vregs = (0..n_args).map(|i| h_arg_base + i).collect();
183        helper.ret_vreg = ret_vreg;
184    }
185
186    // Second pass: emit subroutine bodies
187    for helper in helpers.iter() {
188        let fn_label_id = helper.label_id;
189        let _fn_name = &helper.name;
190        let _n_args = helper.args.len() as u32;
191        let ret_vreg = helper.ret_vreg;
192        let body_vreg_start = helper.ret_vreg + 1;
193        let h_label_start = helper.label_id + 1;
194
195        let arg_names: Vec<String> = helper.args.iter().map(|(n, _)| n.to_string()).collect();
196        let arg_vregs = &helper.arg_vregs;
197
198        let body_code = lower_body_ir_full(
199            &helper.body,
200            &storage_slots,
201            &helpers,
202            &arg_names,
203            arg_vregs,
204            body_vreg_start,
205            h_label_start,
206        );
207
208        let h_vreg_ceiling = body_vreg_start + 10_000u32;
209        let h_label_ceiling = h_label_start + 10_000u32;
210        let fn_name_str = &helper.name;
211
212        all_ir_code.push(quote! {
213            ir.push(Ir::Label(#fn_label_id));
214            {
215                let mut _vreg: u32 = #body_vreg_start;
216                let mut _label: u32 = #h_label_start + 1;
217                let mut _break_stack: Vec<u32> = Vec::new();
218                let mut _continue_stack: Vec<u32> = Vec::new();
219                let _fn_ret_vreg: u32 = #ret_vreg;
220                #body_code
221                assert!(_vreg <= #h_vreg_ceiling,
222                    "axiom_cell: helper fn '{}' used {} vregs, exceeds 500-slot limit",
223                    #fn_name_str, _vreg - #body_vreg_start);
224                assert!(_label <= #h_label_ceiling,
225                    "axiom_cell: helper fn '{}' used {} labels, exceeds 500-slot limit",
226                    #fn_name_str, _label - #h_label_start);
227            }
228            ir.push(Ir::Return);
229        });
230    }
231
232    let clean_items: Vec<TS2> = items
233        .iter()
234        .filter_map(|item| match item {
235            Item::Static(s) if has_attr(&s.attrs, "storage") => None,
236            Item::Fn(f) if has_attr(&f.attrs, "selector") => None,
237            other => Some(quote! { #other }),
238        })
239        .collect();
240
241    let vis = &module.vis;
242    let mod_name2 = mod_name;
243
244    Ok(quote! {
245        #vis mod #mod_name2 {
246            #(#clean_items)*
247        }
248
249        pub fn #bytecode_fn() -> Vec<u8> {
250            use truthlinked_axiom_sdk::ir::Ir;
251            use truthlinked_axiom_sdk::regalloc;
252            use truthlinked_axiom_sdk::codegen;
253
254            let mut ir: Vec<Ir> = Vec::new();
255            #(#all_ir_code)*
256
257            let alloc = regalloc::allocate(&ir);
258            codegen::emit(&ir, &alloc)
259        }
260
261        pub fn #selectors_fn() -> Vec<(&'static str, [u8; 4])> {
262            vec![ #(#selector_entries),* ]
263        }
264    })
265}
266
267// ── Lowering ─────────────────────────────────────────────────────────────────
268
269struct StorageSlot {
270    _name: syn::Ident,
271    _ty: Box<Type>,
272}
273struct SelectorFn {
274    name: String,
275    body: Vec<Stmt>,
276    args: Vec<(syn::Ident, Box<Type>)>,
277}
278struct HelperFn {
279    name: String,
280    body: Vec<Stmt>,
281    args: Vec<(syn::Ident, Box<Type>)>,
282    label_id: u32,
283    arg_vregs: Vec<u32>,
284    ret_vreg: u32,
285}
286
287fn has_attr(attrs: &[Attribute], name: &str) -> bool {
288    attrs.iter().any(|a| a.path().is_ident(name))
289}
290
291fn get_attr_str(attrs: &[Attribute], name: &str) -> Option<String> {
292    for attr in attrs {
293        if attr.path().is_ident(name) {
294            if let Ok(list) = attr.meta.require_list() {
295                if let Ok(lit) = syn::parse2::<syn::LitStr>(list.tokens.clone()) {
296                    return Some(lit.value());
297                }
298            }
299        }
300    }
301    None
302}
303
304fn parse_storage(s: &ItemStatic) -> syn::Result<StorageSlot> {
305    Ok(StorageSlot {
306        _name: s.ident.clone(),
307        _ty: s.ty.clone(),
308    })
309}
310
311fn parse_selector(f: &ItemFn) -> syn::Result<SelectorFn> {
312    let name = get_attr_str(&f.attrs, "selector").ok_or_else(|| {
313        syn::Error::new(f.span(), "#[selector(\"name\")] requires a string literal")
314    })?;
315    let args = f
316        .sig
317        .inputs
318        .iter()
319        .filter_map(|arg| {
320            if let syn::FnArg::Typed(pt) = arg {
321                if let Pat::Ident(pi) = pt.pat.as_ref() {
322                    return Some((pi.ident.clone(), pt.ty.clone()));
323                }
324            }
325            None
326        })
327        .collect();
328    Ok(SelectorFn {
329        name,
330        body: f.block.stmts.clone(),
331        args,
332    })
333}
334
335fn parse_helper(f: &ItemFn) -> syn::Result<HelperFn> {
336    let name = f.sig.ident.to_string();
337    let args = f
338        .sig
339        .inputs
340        .iter()
341        .filter_map(|arg| {
342            if let syn::FnArg::Typed(pt) = arg {
343                if let Pat::Ident(pi) = pt.pat.as_ref() {
344                    return Some((pi.ident.clone(), pt.ty.clone()));
345                }
346            }
347            None
348        })
349        .collect();
350    Ok(HelperFn {
351        name,
352        body: f.block.stmts.clone(),
353        args,
354        label_id: 0,
355        arg_vregs: vec![],
356        ret_vreg: 0,
357    })
358}
359
360/// Lower a full selector body. Returns code that emits IR into `ir: Vec<Ir>`.
361/// Uses `_vreg` and `_label` as runtime counters (declared by caller).
362/// `arg_names`/`arg_vregs` map argument names to their pre-loaded vregs.
363fn lower_body_ir_full(
364    stmts: &[Stmt],
365    slots: &[StorageSlot],
366    helpers: &[HelperFn],
367    arg_names: &[String],
368    arg_vregs: &[u32],
369    _vreg_start: u32,
370    _label_start: u32,
371) -> TS2 {
372    // We build a name→vreg lookup as a runtime HashMap embedded in the emitted code.
373    // Each `let x = ...` allocates a new vreg via `_vreg += 1`.
374    let arg_inserts: Vec<TS2> = arg_names
375        .iter()
376        .zip(arg_vregs.iter())
377        .map(|(name, vreg)| {
378            quote! { _vars.insert(#name.to_string(), #vreg); }
379        })
380        .collect();
381
382    let stmt_code: Vec<TS2> = stmts
383        .iter()
384        .map(|s| lower_stmt(s, slots, helpers))
385        .collect();
386
387    quote! {
388        let mut _vars: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
389        #(#arg_inserts)*
390        #(#stmt_code)*
391    }
392}
393
394/// Lower a single statement to IR emission code.
395fn lower_stmt(stmt: &Stmt, slots: &[StorageSlot], helpers: &[HelperFn]) -> TS2 {
396    match stmt {
397        // return Ok(()) → Halt
398        Stmt::Expr(Expr::Return(r), _) => {
399            if let Some(expr) = &r.expr {
400                if is_ok_unit(expr) {
401                    return quote! { ir.push(Ir::Halt); };
402                }
403                if let Some(code) = is_err_code(expr) {
404                    return quote! { ir.push(Ir::Trap(#code)); };
405                }
406            }
407            quote! { ir.push(Ir::Halt); }
408        }
409        // Macro calls
410        Stmt::Expr(Expr::Macro(m), _) => lower_macro(m, helpers),
411        // let x = expr
412        Stmt::Local(local) => lower_local(local, slots, helpers),
413        // bare expression
414        Stmt::Expr(expr, _) => lower_expr_stmt(expr, slots, helpers),
415        _ => quote! {},
416    }
417}
418
419fn lower_macro(m: &ExprMacro, helpers: &[HelperFn]) -> TS2 {
420    let path = m
421        .mac
422        .path
423        .segments
424        .last()
425        .map(|s| s.ident.to_string())
426        .unwrap_or_default();
427    match path.as_str() {
428        "require_owner" => quote! { ir.push(Ir::RequireOwner); },
429        "require" => {
430            if let Ok(expr) = syn::parse2::<Expr>(m.mac.tokens.clone()) {
431                let eval = lower_expr_to_vreg(&expr, quote! { _cond_r }, helpers);
432                quote! {
433                    // Allocate vreg first, then evaluate expr into it
434                    let _cond_r = { _vreg += 1; _vreg };
435                    #eval
436                    ir.push(Ir::RequireNonZero(_cond_r));
437                }
438            } else {
439                quote! { ir.push(Ir::RequireNonZero(3)); }
440            }
441        }
442        _ => quote! {},
443    }
444}
445
446/// Lower `let x = expr` - allocates a new vreg for x, evaluates expr into it.
447fn lower_local(local: &syn::Local, _slots: &[StorageSlot], helpers: &[HelperFn]) -> TS2 {
448    let var_name = match &local.pat {
449        Pat::Ident(pi) => pi.ident.to_string(),
450        Pat::Type(pt) => match pt.pat.as_ref() {
451            Pat::Ident(pi) => pi.ident.to_string(),
452            _ => return quote! {},
453        },
454        _ => return quote! {},
455    };
456
457    if let Some(init) = &local.init {
458        let expr = &init.expr;
459        let eval = lower_expr_to_vreg(expr, quote! { _dst }, helpers);
460        quote! {
461            let _dst = { _vreg += 1; _vreg };
462            _vars.insert(#var_name.to_string(), _dst);
463            #eval
464        }
465    } else {
466        quote! {
467            let _dst = { _vreg += 1; _vreg };
468            _vars.insert(#var_name.to_string(), _dst);
469        }
470    }
471}
472
473fn lower_expr_stmt(expr: &Expr, slots: &[StorageSlot], helpers: &[HelperFn]) -> TS2 {
474    match expr {
475        // storage::set(&KEY, val)
476        Expr::Call(call) if is_path_call(call, &["storage", "set"]) => {
477            if let Some(key_name) = extract_ref_ident(&call.args) {
478                let key_str = key_name.to_string();
479                // second arg is the value variable name
480                let val_vreg = if let Some(Expr::Path(p)) = call.args.iter().nth(1) {
481                    if let Some(ident) = p.path.get_ident() {
482                        let name = ident.to_string();
483                        quote! { *_vars.get(#name).expect("undefined variable") }
484                    } else {
485                        quote! { 0u32 }
486                    }
487                } else {
488                    quote! { 0u32 }
489                };
490                return quote! {
491                    let _kv = { _vreg += 1; _vreg };
492                    let key = truthlinked_axiom_sdk::hashing::namespace(#key_str);
493                    ir.push(Ir::LoadConst(_kv, key.to_vec()));
494                    ir.push(Ir::SStore(_kv, #val_vreg));
495                };
496            }
497            quote! {}
498        }
499        // if/else as statement
500        Expr::If(ei) => lower_if_expr(ei, slots, helpers, None),
501        // while cond { body }
502        Expr::While(w) => {
503            let cond_eval = lower_expr_to_vreg(&w.cond, quote! { _while_cond }, helpers);
504            let body: Vec<TS2> = w
505                .body
506                .stmts
507                .iter()
508                .map(|s| lower_stmt(s, slots, helpers))
509                .collect();
510            quote! {{
511                let _while_cond = { _vreg += 1; _vreg };
512                let _loop_start = { _label += 1; _label };
513                let _loop_end   = { _label += 1; _label };
514                // push loop_end onto break stack so break/continue can find it
515                _break_stack.push(_loop_end);
516                _continue_stack.push(_loop_start);
517                ir.push(Ir::Label(_loop_start));
518                #cond_eval
519                ir.push(Ir::JumpIfNot(_while_cond, _loop_end));
520                #(#body)*
521                ir.push(Ir::Jump(_loop_start));
522                ir.push(Ir::Label(_loop_end));
523                _break_stack.pop();
524                _continue_stack.pop();
525            }}
526        }
527        // loop { body } - infinite loop, broken by break
528        Expr::Loop(l) => {
529            let body: Vec<TS2> = l
530                .body
531                .stmts
532                .iter()
533                .map(|s| lower_stmt(s, slots, helpers))
534                .collect();
535            quote! {{
536                let _loop_start = { _label += 1; _label };
537                let _loop_end   = { _label += 1; _label };
538                _break_stack.push(_loop_end);
539                _continue_stack.push(_loop_start);
540                ir.push(Ir::Label(_loop_start));
541                #(#body)*
542                ir.push(Ir::Jump(_loop_start));
543                ir.push(Ir::Label(_loop_end));
544                _break_stack.pop();
545                _continue_stack.pop();
546            }}
547        }
548        // break → jump to innermost loop end
549        Expr::Break(_) => quote! {
550            ir.push(Ir::Jump(*_break_stack.last().expect("break outside loop")));
551        },
552        // continue → jump to innermost loop start
553        Expr::Continue(_) => quote! {
554            ir.push(Ir::Jump(*_continue_stack.last().expect("continue outside loop")));
555        },
556        // bare call
557        Expr::Call(call) => {
558            let dst = quote! { { _vreg += 1; _vreg } };
559            lower_call_to_vreg(call, dst, slots, helpers)
560        }
561        _ => quote! {},
562    }
563}
564
565/// Lower an expression into a vreg identified by `dst_expr` (a TokenStream that evaluates to u32).
566fn lower_expr_to_vreg(expr: &Expr, dst_expr: TS2, helpers: &[HelperFn]) -> TS2 {
567    match expr {
568        // Variable reference
569        Expr::Path(p) if p.path.get_ident().is_some() => {
570            let name = p.path.get_ident().unwrap().to_string();
571            quote! {
572                {
573                    let _src = *_vars.get(#name).expect(concat!("undefined variable: ", #name));
574                    ir.push(Ir::Mov(#dst_expr, _src));
575                }
576            }
577        }
578        // Integer literal
579        Expr::Lit(syn::ExprLit {
580            lit: syn::Lit::Int(i),
581            ..
582        }) => {
583            if let Ok(v) = i.base10_parse::<u64>() {
584                quote! { ir.push(Ir::LoadImm64(#dst_expr, #v)); }
585            } else {
586                quote! {}
587            }
588        }
589        // context::caller() etc.
590        Expr::Call(call) if is_path_call(call, &["context", "caller"]) => {
591            quote! { ir.push(Ir::GetCaller(#dst_expr)); }
592        }
593        Expr::Call(call) if is_path_call(call, &["context", "owner"]) => {
594            quote! { ir.push(Ir::GetOwner(#dst_expr)); }
595        }
596        Expr::Call(call) if is_path_call(call, &["context", "height"]) => {
597            quote! { ir.push(Ir::GetHeight(#dst_expr)); }
598        }
599        Expr::Call(call) if is_path_call(call, &["context", "value"]) => {
600            quote! { ir.push(Ir::GetValue(#dst_expr)); }
601        }
602        Expr::Call(call) if is_path_call(call, &["context", "timestamp"]) => {
603            quote! { ir.push(Ir::GetTimestamp(#dst_expr)); }
604        }
605        // storage::get(&KEY)
606        Expr::Call(call) if is_path_call(call, &["storage", "get"]) => {
607            if let Some(key_name) = extract_ref_ident(&call.args) {
608                let key_str = key_name.to_string();
609                quote! {
610                    {
611                        let _kv = { _vreg += 1; _vreg };
612                        let key = truthlinked_axiom_sdk::hashing::namespace(#key_str);
613                        ir.push(Ir::LoadConst(_kv, key.to_vec()));
614                        ir.push(Ir::SLoad(#dst_expr, _kv));
615                    }
616                }
617            } else {
618                quote! {}
619            }
620        }
621        // Binary operations: a + b, a == b, a > b, etc.
622        Expr::Binary(bin) => {
623            let lhs_eval = lower_expr_to_vreg(&bin.left, quote! { _lhs_r }, helpers);
624            let rhs_eval = lower_expr_to_vreg(&bin.right, quote! { _rhs_r }, helpers);
625            let op_ir = match &bin.op {
626                BinOp::Add(_) => quote! { ir.push(Ir::Add(#dst_expr, _lhs_r, _rhs_r)); },
627                BinOp::Sub(_) => quote! { ir.push(Ir::Sub(#dst_expr, _lhs_r, _rhs_r)); },
628                BinOp::Mul(_) => quote! { ir.push(Ir::Mul(#dst_expr, _lhs_r, _rhs_r)); },
629                BinOp::Div(_) => quote! { ir.push(Ir::Div(#dst_expr, _lhs_r, _rhs_r)); },
630                BinOp::Rem(_) => quote! { ir.push(Ir::Mod(#dst_expr, _lhs_r, _rhs_r)); },
631                BinOp::BitAnd(_) => quote! { ir.push(Ir::And(#dst_expr, _lhs_r, _rhs_r)); },
632                BinOp::BitOr(_) => quote! { ir.push(Ir::Or(#dst_expr, _lhs_r, _rhs_r)); },
633                BinOp::BitXor(_) => quote! { ir.push(Ir::Xor(#dst_expr, _lhs_r, _rhs_r)); },
634                BinOp::Eq(_) => quote! { ir.push(Ir::Eq(#dst_expr, _lhs_r, _rhs_r)); },
635                BinOp::Ne(_) => quote! { ir.push(Ir::Ne(#dst_expr, _lhs_r, _rhs_r)); },
636                BinOp::Lt(_) => quote! { ir.push(Ir::Lt(#dst_expr, _lhs_r, _rhs_r)); },
637                BinOp::Le(_) => quote! { ir.push(Ir::Lte(#dst_expr, _lhs_r, _rhs_r)); },
638                BinOp::Gt(_) => quote! { ir.push(Ir::Gt(#dst_expr, _lhs_r, _rhs_r)); },
639                BinOp::Ge(_) => quote! { ir.push(Ir::Gte(#dst_expr, _lhs_r, _rhs_r)); },
640                _ => quote! {},
641            };
642            // Wrap in a block so nested binary ops do not shadow _lhs_r/_rhs_r
643            quote! {{
644                let _lhs_r = { _vreg += 1; _vreg };
645                let _rhs_r = { _vreg += 1; _vreg };
646                #lhs_eval
647                #rhs_eval
648                #op_ir
649            }}
650        }
651        // Unary: !x → IsZero
652        Expr::Unary(u) if matches!(u.op, UnOp::Not(_)) => {
653            let inner_eval = lower_expr_to_vreg(&u.expr, quote! { _inner_r }, helpers);
654            quote! {{
655                let _inner_r = { _vreg += 1; _vreg };
656                #inner_eval
657                ir.push(Ir::IsZero(#dst_expr, _inner_r));
658            }}
659        }
660        // if/else expression (ternary pattern: let x = if cond { a } else { b })
661        Expr::If(ei) => lower_if_expr(ei, &[], helpers, Some(dst_expr)),
662        // Parenthesised
663        Expr::Paren(p) => lower_expr_to_vreg(&p.expr, dst_expr, helpers),
664        _ => quote! {},
665    }
666}
667
668/// Lower `if cond { then } else { else }` to JumpIfNot/Jump IR.
669/// If `result_vreg` is Some, the result of each branch is moved into it (ternary).
670fn lower_if_expr(
671    ei: &syn::ExprIf,
672    slots: &[StorageSlot],
673    helpers: &[HelperFn],
674    result_vreg: Option<TS2>,
675) -> TS2 {
676    let cond_eval = lower_expr_to_vreg(&ei.cond, quote! { _cond_if }, helpers);
677
678    // For ternary (let x = if ...), each branch must move its last expression
679    // into result_vreg. We lower branch stmts normally; if result_vreg is set
680    // and the last stmt is an expression, we emit a Mov into result_vreg.
681    let lower_branch = |stmts: &[Stmt], helpers: &[HelperFn]| -> TS2 {
682        if stmts.is_empty() {
683            return quote! {};
684        }
685        let (last, rest) = stmts.split_last().unwrap();
686        let rest_code: Vec<TS2> = rest.iter().map(|s| lower_stmt(s, slots, helpers)).collect();
687        let last_code = if let Some(ref rv) = result_vreg {
688            // If last stmt is a bare expression, evaluate it into result_vreg
689            match last {
690                Stmt::Expr(expr, None) => {
691                    let eval = lower_expr_to_vreg(expr, quote! { _branch_res }, helpers);
692                    quote! {
693                        let _branch_res = { _vreg += 1; _vreg };
694                        #eval
695                        ir.push(Ir::Mov(#rv, _branch_res));
696                    }
697                }
698                _ => lower_stmt(last, slots, helpers),
699            }
700        } else {
701            lower_stmt(last, slots, helpers)
702        };
703        quote! { #(#rest_code)* #last_code }
704    };
705
706    let then_code = lower_branch(&ei.then_branch.stmts, helpers);
707
708    let else_code = if let Some((_, else_expr)) = &ei.else_branch {
709        match else_expr.as_ref() {
710            Expr::Block(b) => lower_branch(&b.block.stmts, helpers),
711            Expr::If(nested) => lower_if_expr(nested, slots, helpers, result_vreg.clone()),
712            _ => quote! {},
713        }
714    } else {
715        quote! {}
716    };
717
718    quote! {{
719        let _cond_if = { _vreg += 1; _vreg };
720        #cond_eval
721        let _else_lbl = { _label += 1; _label };
722        let _end_lbl  = { _label += 1; _label };
723        ir.push(Ir::JumpIfNot(_cond_if, _else_lbl));
724        #then_code
725        ir.push(Ir::Jump(_end_lbl));
726        ir.push(Ir::Label(_else_lbl));
727        #else_code
728        ir.push(Ir::Label(_end_lbl));
729    }}
730}
731
732fn lower_call_to_vreg(
733    call: &ExprCall,
734    dst: TS2,
735    _slots: &[StorageSlot],
736    helpers: &[HelperFn],
737) -> TS2 {
738    if is_path_call(call, &["context", "caller"]) {
739        return quote! { let _d=#dst; ir.push(Ir::GetCaller(_d)); };
740    }
741    if is_path_call(call, &["context", "owner"]) {
742        return quote! { let _d=#dst; ir.push(Ir::GetOwner(_d)); };
743    }
744    if is_path_call(call, &["context", "height"]) {
745        return quote! { let _d=#dst; ir.push(Ir::GetHeight(_d)); };
746    }
747    if is_path_call(call, &["context", "value"]) {
748        return quote! { let _d=#dst; ir.push(Ir::GetValue(_d)); };
749    }
750    if is_path_call(call, &["storage", "get"]) {
751        if let Some(key_name) = extract_ref_ident(&call.args) {
752            let key_str = key_name.to_string();
753            return quote! {
754                let _d = #dst;
755                let _kv = { _vreg += 1; _vreg };
756                let key = truthlinked_axiom_sdk::hashing::namespace(#key_str);
757                ir.push(Ir::LoadConst(_kv, key.to_vec()));
758                ir.push(Ir::SLoad(_d, _kv));
759            };
760        }
761    }
762    // Intra-cell helper fn call: foo(arg0, arg1, ...)
763    if let Expr::Path(p) = call.func.as_ref() {
764        if let Some(fn_ident) = p.path.get_ident() {
765            let fn_name = fn_ident.to_string();
766            if let Some(helper) = helpers.iter().find(|h| h.name == fn_name) {
767                let fn_label_id = helper.label_id;
768                let ret_vreg = helper.ret_vreg;
769                let arg_loads: Vec<TS2> = call
770                    .args
771                    .iter()
772                    .zip(helper.arg_vregs.iter())
773                    .map(|(arg_expr, &arg_vreg)| {
774                        let eval = lower_expr_to_vreg(arg_expr, quote! { #arg_vreg }, helpers);
775                        quote! { #eval }
776                    })
777                    .collect();
778                return quote! {
779                    #(#arg_loads)*
780                    ir.push(Ir::Call(#fn_label_id));
781                    let _call_dst = #dst;
782                    ir.push(Ir::Mov(_call_dst, #ret_vreg));
783                };
784            }
785        }
786    }
787    quote! {}
788}
789
790// ── Helpers ───────────────────────────────────────────────────────────────────
791
792fn is_path_call(call: &ExprCall, path: &[&str]) -> bool {
793    if let Expr::Path(p) = call.func.as_ref() {
794        let segs: Vec<_> = p
795            .path
796            .segments
797            .iter()
798            .map(|s| s.ident.to_string())
799            .collect();
800        let want: Vec<String> = path.iter().map(|s| s.to_string()).collect();
801        return segs.ends_with(&want);
802    }
803    false
804}
805
806fn extract_ref_ident(
807    args: &syn::punctuated::Punctuated<Expr, syn::Token![,]>,
808) -> Option<&syn::Ident> {
809    if let Some(Expr::Reference(r)) = args.first() {
810        if let Expr::Path(p) = r.expr.as_ref() {
811            return p.path.get_ident();
812        }
813    }
814    None
815}
816
817fn is_ok_unit(expr: &Expr) -> bool {
818    if let Expr::Call(c) = expr {
819        if let Expr::Path(p) = c.func.as_ref() {
820            if p.path.is_ident("Ok") {
821                if let Some(Expr::Tuple(t)) = c.args.first() {
822                    return t.elems.is_empty();
823                }
824            }
825        }
826    }
827    false
828}
829
830fn is_err_code(expr: &Expr) -> Option<u16> {
831    if let Expr::Call(c) = expr {
832        if let Expr::Path(p) = c.func.as_ref() {
833            if p.path.is_ident("Err") {
834                if let Some(Expr::Lit(syn::ExprLit {
835                    lit: syn::Lit::Int(i),
836                    ..
837                })) = c.args.first()
838                {
839                    // base10_parse handles decimal; for hex/octal use the token string directly
840                    let s = i.to_string();
841                    let v: Option<u16> = if s.starts_with("0x") || s.starts_with("0X") {
842                        u16::from_str_radix(&s[2..], 16).ok()
843                    } else {
844                        s.parse().ok()
845                    };
846                    if let Some(code) = v {
847                        return Some(code);
848                    }
849                }
850                return Some(0x0002); // fallback for Err(non-literal)
851            }
852        }
853    }
854    None
855}
856
857#[proc_macro_attribute]
858pub fn storage(_attr: TokenStream, item: TokenStream) -> TokenStream {
859    item
860}
861
862#[proc_macro_attribute]
863pub fn selector(_attr: TokenStream, item: TokenStream) -> TokenStream {
864    item
865}