Skip to main content

lua2hcb_compiler/
compile.rs

1use crate::ir::{Item, Label, OpKind};
2use crate::lua::{Function, GlobalDecl, GlobalKind, Program, Stmt};
3use crate::meta::Meta;
4use anyhow::{anyhow, bail, Result};
5use regex::Regex;
6use std::collections::{HashMap, HashSet};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9enum CondKind {
10    NonZero,
11    Zero,
12    AlwaysTrue,
13    AlwaysFalse,
14    Generic,
15}
16
17fn parse_cond(cond: &str) -> CondKind {
18    let c = cond.trim();
19    if c == "true" {
20        return CondKind::AlwaysTrue;
21    }
22    if c == "false" || c == "nil" {
23        return CondKind::AlwaysFalse;
24    }
25
26    let c = c.trim_start_matches('(').trim_end_matches(')').trim();
27
28    let re_ne0 = Regex::new(r"^S\d+\s*~=\s*0$").unwrap();
29    let re_eq0 = Regex::new(r"^S\d+\s*==\s*0$").unwrap();
30    let re_s = Regex::new(r"^S\d+$").unwrap();
31
32    if re_ne0.is_match(c) {
33        return CondKind::NonZero;
34    }
35    if re_eq0.is_match(c) {
36        return CondKind::Zero;
37    }
38    if re_s.is_match(c) {
39        return CondKind::NonZero;
40    }
41
42    CondKind::Generic
43}
44
45#[derive(Clone, Debug)]
46pub struct GlobalLayout {
47    pub non_volatile_count: u16,
48    pub volatile_count: u16,
49    name_to_idx: HashMap<String, u16>,
50    declared: HashSet<String>,
51}
52
53impl GlobalLayout {
54    fn from_globals(globals: &[GlobalDecl]) -> Result<Self> {
55        let mut max_g: Option<u16> = None;
56        let mut max_vg: Option<u16> = None;
57        let re_g = Regex::new(r"^g(\d+)$").unwrap();
58        let re_vg = Regex::new(r"^vg(\d+)$").unwrap();
59        let mut declared = HashSet::new();
60
61        for g in globals {
62            if !declared.insert(g.name.clone()) {
63                bail!("duplicate global declaration: {}", g.name);
64            }
65            match g.kind {
66                GlobalKind::NonVolatile => {
67                    let caps = re_g
68                        .captures(&g.name)
69                        .ok_or_else(|| anyhow!("invalid non-volatile global name: {}", g.name))?;
70                    let idx: u16 = caps.get(1).unwrap().as_str().parse()?;
71                    max_g = Some(max_g.map(|x| x.max(idx)).unwrap_or(idx));
72                }
73                GlobalKind::Volatile => {
74                    let caps = re_vg
75                        .captures(&g.name)
76                        .ok_or_else(|| anyhow!("invalid volatile global name: {}", g.name))?;
77                    let idx: u16 = caps.get(1).unwrap().as_str().parse()?;
78                    max_vg = Some(max_vg.map(|x| x.max(idx)).unwrap_or(idx));
79                }
80            }
81        }
82
83        let non_volatile_count = max_g.map(|x| x + 1).unwrap_or(0);
84        let volatile_count = max_vg.map(|x| x + 1).unwrap_or(0);
85        let mut name_to_idx = HashMap::new();
86        for g in globals {
87            let idx = match g.kind {
88                GlobalKind::NonVolatile => g.name[1..].parse::<u16>()?,
89                GlobalKind::Volatile => non_volatile_count + g.name[2..].parse::<u16>()?,
90            };
91            name_to_idx.insert(g.name.clone(), idx);
92        }
93
94        Ok(Self {
95            non_volatile_count,
96            volatile_count,
97            name_to_idx,
98            declared,
99        })
100    }
101
102    fn global_idx(&self, name: &str) -> Option<u16> {
103        self.name_to_idx.get(name).copied()
104    }
105
106    fn is_declared(&self, name: &str) -> bool {
107        self.declared.contains(name)
108    }
109}
110
111fn slot_to_stack_idx(var: &str, args_count: i8) -> Result<i8> {
112    let re_a = Regex::new(r"^a(\d+)$").unwrap();
113    let re_l = Regex::new(r"^l(\d+)$").unwrap();
114
115    if let Some(c) = re_a.captures(var) {
116        let v: i16 = c.get(1).unwrap().as_str().parse()?;
117        let argc = i16::from(args_count);
118        if v >= argc {
119            bail!("argument index out of range: {var} for {args_count} args");
120        }
121        // RFVP Context::call pushes the arguments first and then a
122        // SavedStackInfo frame.  The callee base points immediately after
123        // that frame, so -1 is the frame itself, the last argument is -2,
124        // and a0 is -(args_count + 1).
125        let idx = v - argc - 1;
126        return Ok(i8::try_from(idx).map_err(|_| anyhow!("stack index out of i8"))?);
127    }
128    if let Some(c) = re_l.captures(var) {
129        let v: i16 = c.get(1).unwrap().as_str().parse()?;
130        // InitStack allocates locals starting exactly at the callee base.
131        // Arguments live below the SavedStackInfo frame and therefore do not
132        // shift local-variable offsets.
133        return Ok(i8::try_from(v).map_err(|_| anyhow!("stack index out of i8"))?);
134    }
135    bail!("not a frame slot: {var}")
136}
137
138fn push_int(v: i64) -> Result<OpKind> {
139    if v < i64::from(i32::MIN) || v > i64::from(i32::MAX) {
140        bail!("integer out of i32 range: {v}");
141    }
142    if v >= -128 && v <= 127 {
143        return Ok(OpKind::PushI8(v as i8));
144    }
145    if v >= -32768 && v <= 32767 {
146        return Ok(OpKind::PushI16(v as i16));
147    }
148    Ok(OpKind::PushI32(v as i32))
149}
150
151fn lua_unescape_string(lit: &str) -> String {
152    let mut out = String::new();
153    let mut chars = lit.chars();
154    while let Some(ch) = chars.next() {
155        if ch != '\\' {
156            out.push(ch);
157            continue;
158        }
159        match chars.next() {
160            Some('n') => out.push('\n'),
161            Some('r') => out.push('\r'),
162            Some('t') => out.push('\t'),
163            Some('\\') => out.push('\\'),
164            Some('"') => out.push('"'),
165            Some(other) => out.push(other),
166            None => break,
167        }
168    }
169    out
170}
171
172#[derive(Clone, Debug, PartialEq)]
173enum Tok {
174    Ident(String),
175    Int(i64),
176    Float(f32),
177    Str(String),
178    Nil,
179    True,
180    False,
181    LParen,
182    RParen,
183    LBracket,
184    RBracket,
185    Comma,
186    Plus,
187    Minus,
188    Star,
189    Slash,
190    Percent,
191    Amp,
192    EqEq,
193    NotEq,
194    Lt,
195    Le,
196    Gt,
197    Ge,
198    And,
199    Or,
200}
201
202fn tokenize_expr(s: &str) -> Result<Vec<Tok>> {
203    let mut toks = Vec::new();
204    let b = s.as_bytes();
205    let mut i = 0usize;
206    while i < b.len() {
207        let ch = b[i] as char;
208        if ch.is_ascii_whitespace() {
209            i += 1;
210            continue;
211        }
212        match ch {
213            '(' => {
214                toks.push(Tok::LParen);
215                i += 1;
216            }
217            ')' => {
218                toks.push(Tok::RParen);
219                i += 1;
220            }
221            '[' => {
222                toks.push(Tok::LBracket);
223                i += 1;
224            }
225            ']' => {
226                toks.push(Tok::RBracket);
227                i += 1;
228            }
229            ',' => {
230                toks.push(Tok::Comma);
231                i += 1;
232            }
233            '+' => {
234                toks.push(Tok::Plus);
235                i += 1;
236            }
237            '-' => {
238                if i + 1 < b.len() && (b[i + 1] as char).is_ascii_digit() {
239                    let start = i;
240                    i += 1;
241                    while i < b.len() && (b[i] as char).is_ascii_digit() {
242                        i += 1;
243                    }
244                    let mut is_float = false;
245                    if i < b.len() && b[i] as char == '.' {
246                        is_float = true;
247                        i += 1;
248                        while i < b.len() && (b[i] as char).is_ascii_digit() {
249                            i += 1;
250                        }
251                    }
252                    let lit = &s[start..i];
253                    if is_float {
254                        toks.push(Tok::Float(lit.parse()?));
255                    } else {
256                        toks.push(Tok::Int(lit.parse()?));
257                    }
258                } else {
259                    toks.push(Tok::Minus);
260                    i += 1;
261                }
262            }
263            '*' => {
264                toks.push(Tok::Star);
265                i += 1;
266            }
267            '/' => {
268                toks.push(Tok::Slash);
269                i += 1;
270            }
271            '%' => {
272                toks.push(Tok::Percent);
273                i += 1;
274            }
275            '&' => {
276                toks.push(Tok::Amp);
277                i += 1;
278            }
279            '=' => {
280                if i + 1 < b.len() && b[i + 1] as char == '=' {
281                    toks.push(Tok::EqEq);
282                    i += 2;
283                } else {
284                    bail!("unexpected '=' inside expression: {s}");
285                }
286            }
287            '~' => {
288                if i + 1 < b.len() && b[i + 1] as char == '=' {
289                    toks.push(Tok::NotEq);
290                    i += 2;
291                } else {
292                    bail!("unexpected '~' inside expression: {s}");
293                }
294            }
295            '<' => {
296                if i + 1 < b.len() && b[i + 1] as char == '=' {
297                    toks.push(Tok::Le);
298                    i += 2;
299                } else {
300                    toks.push(Tok::Lt);
301                    i += 1;
302                }
303            }
304            '>' => {
305                if i + 1 < b.len() && b[i + 1] as char == '=' {
306                    toks.push(Tok::Ge);
307                    i += 2;
308                } else {
309                    toks.push(Tok::Gt);
310                    i += 1;
311                }
312            }
313            '"' => {
314                i += 1;
315                let start = i;
316                let mut out = String::new();
317                while i < b.len() {
318                    let c = b[i] as char;
319                    if c == '\\' {
320                        if i + 1 >= b.len() {
321                            bail!("unterminated string literal");
322                        }
323                        let esc = b[i + 1] as char;
324                        match esc {
325                            'n' => out.push('\n'),
326                            'r' => out.push('\r'),
327                            't' => out.push('\t'),
328                            '\\' => out.push('\\'),
329                            '"' => out.push('"'),
330                            other => out.push(other),
331                        }
332                        i += 2;
333                        continue;
334                    }
335                    if c == '"' {
336                        break;
337                    }
338                    out.push(c);
339                    i += 1;
340                }
341                if i >= b.len() || b[i] as char != '"' {
342                    bail!(
343                        "unterminated string literal starting at: {}",
344                        &s[start - 1..]
345                    );
346                }
347                i += 1;
348                toks.push(Tok::Str(out));
349            }
350            c if c.is_ascii_digit() => {
351                let start = i;
352                i += 1;
353                while i < b.len() && (b[i] as char).is_ascii_digit() {
354                    i += 1;
355                }
356                let mut is_float = false;
357                if i < b.len() && b[i] as char == '.' {
358                    is_float = true;
359                    i += 1;
360                    while i < b.len() && (b[i] as char).is_ascii_digit() {
361                        i += 1;
362                    }
363                }
364                let lit = &s[start..i];
365                if is_float {
366                    toks.push(Tok::Float(lit.parse()?));
367                } else {
368                    toks.push(Tok::Int(lit.parse()?));
369                }
370            }
371            c if c.is_ascii_alphabetic() || c == '_' => {
372                let start = i;
373                i += 1;
374                while i < b.len() {
375                    let c = b[i] as char;
376                    if c.is_ascii_alphanumeric() || c == '_' {
377                        i += 1;
378                    } else {
379                        break;
380                    }
381                }
382                let ident = &s[start..i];
383                match ident {
384                    "nil" => toks.push(Tok::Nil),
385                    "true" => toks.push(Tok::True),
386                    "false" => toks.push(Tok::False),
387                    "and" => toks.push(Tok::And),
388                    "or" => toks.push(Tok::Or),
389                    _ => toks.push(Tok::Ident(ident.to_string())),
390                }
391            }
392            _ => bail!("unsupported character in expression: {ch}"),
393        }
394    }
395    Ok(toks)
396}
397
398#[derive(Clone, Debug)]
399enum UnaryOp {
400    Neg,
401}
402
403#[derive(Clone, Debug, PartialEq, Eq)]
404enum BinaryOp {
405    Add,
406    Sub,
407    Mul,
408    Div,
409    Mod,
410    BitAnd,
411    Eq,
412    Ne,
413    Lt,
414    Le,
415    Gt,
416    Ge,
417    And,
418    Or,
419}
420
421#[derive(Clone, Debug)]
422enum Expr {
423    Nil,
424    True,
425    False,
426    Int(i64),
427    Float(f32),
428    Str(String),
429    Var(String),
430    Call {
431        name: String,
432        args: Vec<Expr>,
433    },
434    GlobalTable {
435        idx: u16,
436        key: Box<Expr>,
437    },
438    LocalTable {
439        idx: i8,
440        key: Box<Expr>,
441    },
442    Unary {
443        op: UnaryOp,
444        expr: Box<Expr>,
445    },
446    Binary {
447        op: BinaryOp,
448        left: Box<Expr>,
449        right: Box<Expr>,
450    },
451}
452
453struct ExprParser {
454    toks: Vec<Tok>,
455    pos: usize,
456}
457
458impl ExprParser {
459    fn new(toks: Vec<Tok>) -> Self {
460        Self { toks, pos: 0 }
461    }
462
463    fn peek(&self) -> Option<&Tok> {
464        self.toks.get(self.pos)
465    }
466
467    fn bump(&mut self) -> Option<Tok> {
468        let t = self.toks.get(self.pos).cloned();
469        if t.is_some() {
470            self.pos += 1;
471        }
472        t
473    }
474
475    fn eat(&mut self, tok: &Tok) -> bool {
476        if self.peek() == Some(tok) {
477            self.pos += 1;
478            true
479        } else {
480            false
481        }
482    }
483
484    fn parse(mut self) -> Result<Expr> {
485        let expr = self.parse_or()?;
486        if self.pos != self.toks.len() {
487            bail!("unexpected trailing tokens in expression");
488        }
489        Ok(expr)
490    }
491
492    fn parse_or(&mut self) -> Result<Expr> {
493        let mut expr = self.parse_and()?;
494        while self.eat(&Tok::Or) {
495            let rhs = self.parse_and()?;
496            expr = Expr::Binary {
497                op: BinaryOp::Or,
498                left: Box::new(expr),
499                right: Box::new(rhs),
500            };
501        }
502        Ok(expr)
503    }
504
505    fn parse_and(&mut self) -> Result<Expr> {
506        let mut expr = self.parse_cmp()?;
507        while self.eat(&Tok::And) {
508            let rhs = self.parse_cmp()?;
509            expr = Expr::Binary {
510                op: BinaryOp::And,
511                left: Box::new(expr),
512                right: Box::new(rhs),
513            };
514        }
515        Ok(expr)
516    }
517
518    fn parse_cmp(&mut self) -> Result<Expr> {
519        let mut expr = self.parse_add()?;
520        loop {
521            let op = match self.peek() {
522                Some(Tok::EqEq) => BinaryOp::Eq,
523                Some(Tok::NotEq) => BinaryOp::Ne,
524                Some(Tok::Lt) => BinaryOp::Lt,
525                Some(Tok::Le) => BinaryOp::Le,
526                Some(Tok::Gt) => BinaryOp::Gt,
527                Some(Tok::Ge) => BinaryOp::Ge,
528                _ => break,
529            };
530            self.bump();
531            let rhs = self.parse_add()?;
532            expr = Expr::Binary {
533                op,
534                left: Box::new(expr),
535                right: Box::new(rhs),
536            };
537        }
538        Ok(expr)
539    }
540
541    fn parse_add(&mut self) -> Result<Expr> {
542        let mut expr = self.parse_mul()?;
543        loop {
544            let op = match self.peek() {
545                Some(Tok::Plus) => BinaryOp::Add,
546                Some(Tok::Minus) => BinaryOp::Sub,
547                _ => break,
548            };
549            self.bump();
550            let rhs = self.parse_mul()?;
551            expr = Expr::Binary {
552                op,
553                left: Box::new(expr),
554                right: Box::new(rhs),
555            };
556        }
557        Ok(expr)
558    }
559
560    fn parse_mul(&mut self) -> Result<Expr> {
561        let mut expr = self.parse_bitand()?;
562        loop {
563            let op = match self.peek() {
564                Some(Tok::Star) => BinaryOp::Mul,
565                Some(Tok::Slash) => BinaryOp::Div,
566                Some(Tok::Percent) => BinaryOp::Mod,
567                _ => break,
568            };
569            self.bump();
570            let rhs = self.parse_bitand()?;
571            expr = Expr::Binary {
572                op,
573                left: Box::new(expr),
574                right: Box::new(rhs),
575            };
576        }
577        Ok(expr)
578    }
579
580    fn parse_bitand(&mut self) -> Result<Expr> {
581        let mut expr = self.parse_unary()?;
582        while self.eat(&Tok::Amp) {
583            let rhs = self.parse_unary()?;
584            expr = Expr::Binary {
585                op: BinaryOp::BitAnd,
586                left: Box::new(expr),
587                right: Box::new(rhs),
588            };
589        }
590        Ok(expr)
591    }
592
593    fn parse_unary(&mut self) -> Result<Expr> {
594        if self.eat(&Tok::Minus) {
595            let expr = self.parse_unary()?;
596            return Ok(Expr::Unary {
597                op: UnaryOp::Neg,
598                expr: Box::new(expr),
599            });
600        }
601        self.parse_primary()
602    }
603
604    fn parse_primary(&mut self) -> Result<Expr> {
605        match self
606            .bump()
607            .ok_or_else(|| anyhow!("unexpected end of expression"))?
608        {
609            Tok::Nil => Ok(Expr::Nil),
610            Tok::True => Ok(Expr::True),
611            Tok::False => Ok(Expr::False),
612            Tok::Int(v) => Ok(Expr::Int(v)),
613            Tok::Float(v) => Ok(Expr::Float(v)),
614            Tok::Str(s) => Ok(Expr::Str(s)),
615            Tok::LParen => {
616                let e = self.parse_or()?;
617                if !self.eat(&Tok::RParen) {
618                    bail!("missing ')' in expression");
619                }
620                Ok(e)
621            }
622            Tok::Ident(name) => {
623                if self.eat(&Tok::LParen) {
624                    let mut args = Vec::new();
625                    if !self.eat(&Tok::RParen) {
626                        loop {
627                            args.push(self.parse_or()?);
628                            if self.eat(&Tok::Comma) {
629                                continue;
630                            }
631                            if !self.eat(&Tok::RParen) {
632                                bail!("missing ')' after call arguments");
633                            }
634                            break;
635                        }
636                    }
637                    return Ok(Expr::Call { name, args });
638                }
639
640                if (name == "GT" || name == "LT") && self.eat(&Tok::LBracket) {
641                    let idx = match self.bump() {
642                        Some(Tok::Int(v)) => v,
643                        Some(Tok::Minus) => match self.bump() {
644                            Some(Tok::Int(v)) => -v,
645                            _ => bail!("table index must be integer"),
646                        },
647                        _ => bail!("table index must be integer"),
648                    };
649                    if !self.eat(&Tok::RBracket) || !self.eat(&Tok::LBracket) {
650                        bail!("table access must be GT[idx][key] or LT[idx][key]");
651                    }
652                    let key = self.parse_or()?;
653                    if !self.eat(&Tok::RBracket) {
654                        bail!("table access missing closing ']'");
655                    }
656                    if name == "GT" {
657                        if idx < 0 || idx > i64::from(u16::MAX) {
658                            bail!("GT index out of range: {idx}");
659                        }
660                        return Ok(Expr::GlobalTable {
661                            idx: idx as u16,
662                            key: Box::new(key),
663                        });
664                    }
665                    if idx < i64::from(i8::MIN) || idx > i64::from(i8::MAX) {
666                        bail!("LT index out of range: {idx}");
667                    }
668                    return Ok(Expr::LocalTable {
669                        idx: idx as i8,
670                        key: Box::new(key),
671                    });
672                }
673
674                Ok(Expr::Var(name))
675            }
676            other => bail!("unexpected token in expression: {:?}", other),
677        }
678    }
679}
680
681fn parse_expr(expr: &str) -> Result<Expr> {
682    let toks = tokenize_expr(expr)?;
683    ExprParser::new(toks).parse()
684}
685
686fn emit_call(
687    name: &str,
688    args: &[Expr],
689    meta: &Meta,
690    user_fns: &HashSet<String>,
691    layout: &GlobalLayout,
692    args_count: i8,
693    out: &mut Vec<Item>,
694) -> Result<()> {
695    for arg in args {
696        compile_expr(arg, args_count, meta, user_fns, layout, out)?;
697    }
698
699    if let Some(sid) = meta.syscall_id_by_name(name) {
700        if let Some(expect) = meta.syscall_args_by_id(sid) {
701            if usize::from(expect) != args.len() {
702                bail!("syscall {name} expects {expect} args, got {}", args.len());
703            }
704        }
705        out.push(Item::Op(OpKind::Syscall { id: sid }));
706        return Ok(());
707    }
708
709    if name.starts_with("f_") || user_fns.contains(name) {
710        out.push(Item::Op(OpKind::CallFn {
711            name: name.to_string(),
712        }));
713        return Ok(());
714    }
715
716    bail!("unknown callee: {name}")
717}
718
719fn compile_expr(
720    expr: &Expr,
721    args_count: i8,
722    meta: &Meta,
723    user_fns: &HashSet<String>,
724    layout: &GlobalLayout,
725    out: &mut Vec<Item>,
726) -> Result<()> {
727    match expr {
728        Expr::Nil => out.push(Item::Op(OpKind::PushNil)),
729        Expr::True => out.push(Item::Op(OpKind::PushTrue)),
730        Expr::False => bail!("false is not supported as a runtime value in this compiler"),
731        Expr::Int(v) => out.push(Item::Op(push_int(*v)?)),
732        Expr::Float(v) => out.push(Item::Op(OpKind::PushF32(*v))),
733        Expr::Str(s) => out.push(Item::Op(OpKind::PushString(s.clone()))),
734        Expr::Var(name) => {
735            let re_s = Regex::new(r"^S\d+$").unwrap();
736            let re_slot = Regex::new(r"^(a\d+|l\d+)$").unwrap();
737            if name == "__ret" {
738                out.push(Item::Op(OpKind::PushReturn));
739            } else if let Some(idx) = layout.global_idx(name) {
740                out.push(Item::Op(OpKind::PushGlobal(idx)));
741            } else if re_slot.is_match(name) {
742                let idx = slot_to_stack_idx(name, args_count)?;
743                out.push(Item::Op(OpKind::PushStack(idx)));
744            } else if re_s.is_match(name) {
745                out.push(Item::Op(OpKind::PushTop));
746            } else {
747                bail!("unsupported variable reference: {name}");
748            }
749        }
750        Expr::Call { name, args } => {
751            emit_call(name, args, meta, user_fns, layout, args_count, out)?;
752            out.push(Item::Op(OpKind::PushReturn));
753        }
754        Expr::GlobalTable { idx, key } => {
755            compile_expr(key, args_count, meta, user_fns, layout, out)?;
756            out.push(Item::Op(OpKind::PushGlobalTable(*idx)));
757        }
758        Expr::LocalTable { idx, key } => {
759            compile_expr(key, args_count, meta, user_fns, layout, out)?;
760            out.push(Item::Op(OpKind::PushLocalTable(*idx)));
761        }
762        Expr::Unary {
763            op: UnaryOp::Neg,
764            expr,
765        } => {
766            compile_expr(expr, args_count, meta, user_fns, layout, out)?;
767            out.push(Item::Op(OpKind::Neg));
768        }
769        Expr::Binary { op, left, right } => {
770            if *op == BinaryOp::Ne {
771                if let Expr::Binary {
772                    op: BinaryOp::BitAnd,
773                    left: bleft,
774                    right: bright,
775                } = &**left
776                {
777                    if matches!(&**right, Expr::Int(0)) {
778                        compile_expr(bleft, args_count, meta, user_fns, layout, out)?;
779                        compile_expr(bright, args_count, meta, user_fns, layout, out)?;
780                        out.push(Item::Op(OpKind::BitTest));
781                        return Ok(());
782                    }
783                }
784            }
785            if *op == BinaryOp::And || *op == BinaryOp::Or {
786                compile_expr(left, args_count, meta, user_fns, layout, out)?;
787                compile_expr(right, args_count, meta, user_fns, layout, out)?;
788                out.push(Item::Op(match op {
789                    BinaryOp::And => OpKind::And,
790                    BinaryOp::Or => OpKind::Or,
791                    _ => unreachable!(),
792                }));
793                return Ok(());
794            }
795            if *op == BinaryOp::BitAnd {
796                bail!("plain bitwise '&' values are not supported, use '(x & y) ~= 0'");
797            }
798            compile_expr(left, args_count, meta, user_fns, layout, out)?;
799            compile_expr(right, args_count, meta, user_fns, layout, out)?;
800            let inst = match op {
801                BinaryOp::Add => OpKind::Add,
802                BinaryOp::Sub => OpKind::Sub,
803                BinaryOp::Mul => OpKind::Mul,
804                BinaryOp::Div => OpKind::Div,
805                BinaryOp::Mod => OpKind::Mod,
806                BinaryOp::Eq => OpKind::SetE,
807                BinaryOp::Ne => OpKind::SetNe,
808                BinaryOp::Lt => OpKind::SetL,
809                BinaryOp::Le => OpKind::SetLe,
810                BinaryOp::Gt => OpKind::SetG,
811                BinaryOp::Ge => OpKind::SetGe,
812                BinaryOp::BitAnd | BinaryOp::And | BinaryOp::Or => unreachable!(),
813            };
814            out.push(Item::Op(inst));
815        }
816    }
817    Ok(())
818}
819
820fn split_assignment(stmt: &str) -> Option<(String, String)> {
821    let b = stmt.as_bytes();
822    let mut depth_paren = 0i32;
823    let mut depth_brack = 0i32;
824    let mut in_string = false;
825    let mut i = 0usize;
826    while i < b.len() {
827        let c = b[i] as char;
828        if in_string {
829            if c == '\\' {
830                i += 2;
831                continue;
832            }
833            if c == '"' {
834                in_string = false;
835            }
836            i += 1;
837            continue;
838        }
839        match c {
840            '"' => in_string = true,
841            '(' => depth_paren += 1,
842            ')' => depth_paren -= 1,
843            '[' => depth_brack += 1,
844            ']' => depth_brack -= 1,
845            '=' if depth_paren == 0 && depth_brack == 0 => {
846                let prev = if i > 0 { Some(b[i - 1] as char) } else { None };
847                let next = if i + 1 < b.len() {
848                    Some(b[i + 1] as char)
849                } else {
850                    None
851                };
852                if prev != Some('=') && prev != Some('~') && next != Some('=') {
853                    let lhs = stmt[..i].trim().to_string();
854                    let rhs = stmt[i + 1..].trim().to_string();
855                    return Some((lhs, rhs));
856                }
857            }
858            _ => {}
859        }
860        i += 1;
861    }
862    None
863}
864
865enum AssignTarget {
866    Stack(i8),
867    Global(u16),
868    StackTemp,
869    GlobalTable(u16, Expr),
870    LocalTable(i8, Expr),
871}
872
873fn parse_assign_target(lhs: &str, args_count: i8, layout: &GlobalLayout) -> Result<AssignTarget> {
874    let re_slot = Regex::new(r"^(a\d+|l\d+)$").unwrap();
875    let re_s = Regex::new(r"^S\d+$").unwrap();
876    if re_slot.is_match(lhs) {
877        return Ok(AssignTarget::Stack(slot_to_stack_idx(lhs, args_count)?));
878    }
879    if re_s.is_match(lhs) {
880        return Ok(AssignTarget::StackTemp);
881    }
882    if let Some(idx) = layout.global_idx(lhs) {
883        return Ok(AssignTarget::Global(idx));
884    }
885
886    let re_gt = Regex::new(r"^GT\[(\d+)\]\[(.+)\]$").unwrap();
887    if let Some(c) = re_gt.captures(lhs) {
888        let idx: u16 = c.get(1).unwrap().as_str().parse()?;
889        let key = parse_expr(c.get(2).unwrap().as_str().trim())?;
890        return Ok(AssignTarget::GlobalTable(idx, key));
891    }
892    let re_lt = Regex::new(r"^LT\[(-?\d+)\]\[(.+)\]$").unwrap();
893    if let Some(c) = re_lt.captures(lhs) {
894        let idx: i8 = c.get(1).unwrap().as_str().parse()?;
895        let key = parse_expr(c.get(2).unwrap().as_str().trim())?;
896        return Ok(AssignTarget::LocalTable(idx, key));
897    }
898
899    bail!("unsupported assignment target: {lhs}")
900}
901
902fn compile_simple_stmt(
903    stmt: &str,
904    args_count: i8,
905    meta: &Meta,
906    user_fns: &HashSet<String>,
907    layout: &GlobalLayout,
908    out: &mut Vec<Item>,
909) -> Result<()> {
910    let s = stmt.trim();
911    if s.is_empty() {
912        return Ok(());
913    }
914
915    let ignore_re =
916        Regex::new(r#"^__ret\s*=\s*(nil|true|false|-?\d+(?:\.\d+)?|\"(?:\\.|[^\"])*\")\s*$"#)
917            .unwrap();
918    if ignore_re.is_match(s) {
919        return Ok(());
920    }
921
922    if let Some((lhs, rhs)) = split_assignment(s) {
923        if lhs == "__ret" {
924            let expr = parse_expr(&rhs)?;
925            if let Expr::Call { name, args } = expr {
926                emit_call(&name, &args, meta, user_fns, layout, args_count, out)?;
927                return Ok(());
928            }
929            bail!("__ret assignment requires a call: {s}");
930        }
931
932        let target = parse_assign_target(&lhs, args_count, layout)?;
933        let expr = parse_expr(&rhs)?;
934        match target {
935            AssignTarget::Stack(idx) => {
936                compile_expr(&expr, args_count, meta, user_fns, layout, out)?;
937                out.push(Item::Op(OpKind::PopStack(idx)));
938            }
939            AssignTarget::Global(idx) => {
940                compile_expr(&expr, args_count, meta, user_fns, layout, out)?;
941                out.push(Item::Op(OpKind::PopGlobal(idx)));
942            }
943            AssignTarget::StackTemp => {
944                compile_expr(&expr, args_count, meta, user_fns, layout, out)?;
945            }
946            AssignTarget::GlobalTable(idx, key) => {
947                compile_expr(&key, args_count, meta, user_fns, layout, out)?;
948                compile_expr(&expr, args_count, meta, user_fns, layout, out)?;
949                out.push(Item::Op(OpKind::PopGlobalTable(idx)));
950            }
951            AssignTarget::LocalTable(idx, key) => {
952                compile_expr(&key, args_count, meta, user_fns, layout, out)?;
953                compile_expr(&expr, args_count, meta, user_fns, layout, out)?;
954                out.push(Item::Op(OpKind::PopLocalTable(idx)));
955            }
956        }
957        return Ok(());
958    }
959
960    let expr = parse_expr(s)?;
961    if let Expr::Call { name, args } = expr {
962        emit_call(&name, &args, meta, user_fns, layout, args_count, out)?;
963        return Ok(());
964    }
965
966    bail!("unsupported statement: {s}")
967}
968
969struct LabelGen {
970    prefix: String,
971    n: u32,
972}
973
974impl LabelGen {
975    fn new(prefix: impl Into<String>) -> Self {
976        Self {
977            prefix: prefix.into(),
978            n: 0,
979        }
980    }
981
982    fn fresh(&mut self, kind: &str) -> String {
983        let id = self.n;
984        self.n += 1;
985        format!("{}:{}:{}", self.prefix, kind, id)
986    }
987}
988
989fn compile_cond_generic(
990    cond: &str,
991    args_count: i8,
992    meta: &Meta,
993    user_fns: &HashSet<String>,
994    layout: &GlobalLayout,
995    out: &mut Vec<Item>,
996) -> Result<()> {
997    let expr = parse_expr(cond)?;
998    compile_expr(&expr, args_count, meta, user_fns, layout, out)
999}
1000
1001fn compile_stmts(
1002    stmts: &[Stmt],
1003    args_count: i8,
1004    meta: &Meta,
1005    user_fns: &HashSet<String>,
1006    layout: &GlobalLayout,
1007    out: &mut Vec<Item>,
1008    lg: &mut LabelGen,
1009    break_stack: &mut Vec<String>,
1010) -> Result<()> {
1011    for st in stmts {
1012        match st {
1013            Stmt::Simple(s) => compile_simple_stmt(s, args_count, meta, user_fns, layout, out)?,
1014            Stmt::Return(None) => out.push(Item::Op(OpKind::Ret)),
1015            Stmt::Return(Some(expr)) => {
1016                let e = parse_expr(expr)?;
1017                compile_expr(&e, args_count, meta, user_fns, layout, out)?;
1018                out.push(Item::Op(OpKind::Retv));
1019            }
1020            Stmt::Break => {
1021                let tgt = break_stack
1022                    .last()
1023                    .ok_or_else(|| anyhow!("break outside of loop"))?
1024                    .clone();
1025                out.push(Item::Op(OpKind::JmpLabel { label: tgt }));
1026            }
1027            Stmt::If { arms, else_arm } => {
1028                let end_lbl = lg.fresh("if_end");
1029                for (idx, (cond, body)) in arms.iter().enumerate() {
1030                    let after_lbl = lg.fresh(&format!("if_next_{idx}"));
1031                    match parse_cond(cond) {
1032                        CondKind::AlwaysTrue => {
1033                            compile_stmts(
1034                                body,
1035                                args_count,
1036                                meta,
1037                                user_fns,
1038                                layout,
1039                                out,
1040                                lg,
1041                                break_stack,
1042                            )?;
1043                            out.push(Item::Op(OpKind::JmpLabel {
1044                                label: end_lbl.clone(),
1045                            }));
1046                            break;
1047                        }
1048                        CondKind::AlwaysFalse => {
1049                            out.push(Item::Label(Label::new(after_lbl.clone())));
1050                        }
1051                        CondKind::NonZero => {
1052                            out.push(Item::Op(OpKind::JzLabel {
1053                                label: after_lbl.clone(),
1054                            }));
1055                            compile_stmts(
1056                                body,
1057                                args_count,
1058                                meta,
1059                                user_fns,
1060                                layout,
1061                                out,
1062                                lg,
1063                                break_stack,
1064                            )?;
1065                            out.push(Item::Op(OpKind::JmpLabel {
1066                                label: end_lbl.clone(),
1067                            }));
1068                            out.push(Item::Label(Label::new(after_lbl)));
1069                        }
1070                        CondKind::Zero => {
1071                            let body_lbl = lg.fresh(&format!("if_body_{idx}"));
1072                            out.push(Item::Op(OpKind::JzLabel {
1073                                label: body_lbl.clone(),
1074                            }));
1075                            out.push(Item::Op(OpKind::JmpLabel {
1076                                label: after_lbl.clone(),
1077                            }));
1078                            out.push(Item::Label(Label::new(body_lbl)));
1079                            compile_stmts(
1080                                body,
1081                                args_count,
1082                                meta,
1083                                user_fns,
1084                                layout,
1085                                out,
1086                                lg,
1087                                break_stack,
1088                            )?;
1089                            out.push(Item::Op(OpKind::JmpLabel {
1090                                label: end_lbl.clone(),
1091                            }));
1092                            out.push(Item::Label(Label::new(after_lbl)));
1093                        }
1094                        CondKind::Generic => {
1095                            compile_cond_generic(cond, args_count, meta, user_fns, layout, out)?;
1096                            out.push(Item::Op(OpKind::JzLabel {
1097                                label: after_lbl.clone(),
1098                            }));
1099                            compile_stmts(
1100                                body,
1101                                args_count,
1102                                meta,
1103                                user_fns,
1104                                layout,
1105                                out,
1106                                lg,
1107                                break_stack,
1108                            )?;
1109                            out.push(Item::Op(OpKind::JmpLabel {
1110                                label: end_lbl.clone(),
1111                            }));
1112                            out.push(Item::Label(Label::new(after_lbl)));
1113                        }
1114                    }
1115                }
1116                if let Some(eb) = else_arm {
1117                    compile_stmts(eb, args_count, meta, user_fns, layout, out, lg, break_stack)?;
1118                }
1119                out.push(Item::Label(Label::new(end_lbl)));
1120            }
1121            Stmt::While { cond, body } => {
1122                let head = lg.fresh("while_head");
1123                let end = lg.fresh("while_end");
1124                let body_lbl = lg.fresh("while_body");
1125
1126                out.push(Item::Label(Label::new(head.clone())));
1127                break_stack.push(end.clone());
1128
1129                match parse_cond(cond) {
1130                    CondKind::AlwaysTrue => {
1131                        compile_stmts(
1132                            body,
1133                            args_count,
1134                            meta,
1135                            user_fns,
1136                            layout,
1137                            out,
1138                            lg,
1139                            break_stack,
1140                        )?;
1141                        out.push(Item::Op(OpKind::JmpLabel { label: head }));
1142                    }
1143                    CondKind::AlwaysFalse => {
1144                        out.push(Item::Op(OpKind::JmpLabel { label: end.clone() }));
1145                    }
1146                    CondKind::NonZero => {
1147                        out.push(Item::Op(OpKind::JzLabel { label: end.clone() }));
1148                        compile_stmts(
1149                            body,
1150                            args_count,
1151                            meta,
1152                            user_fns,
1153                            layout,
1154                            out,
1155                            lg,
1156                            break_stack,
1157                        )?;
1158                        out.push(Item::Op(OpKind::JmpLabel { label: head }));
1159                    }
1160                    CondKind::Zero => {
1161                        out.push(Item::Op(OpKind::JzLabel {
1162                            label: body_lbl.clone(),
1163                        }));
1164                        out.push(Item::Op(OpKind::JmpLabel { label: end.clone() }));
1165                        out.push(Item::Label(Label::new(body_lbl)));
1166                        compile_stmts(
1167                            body,
1168                            args_count,
1169                            meta,
1170                            user_fns,
1171                            layout,
1172                            out,
1173                            lg,
1174                            break_stack,
1175                        )?;
1176                        out.push(Item::Op(OpKind::JmpLabel { label: head }));
1177                    }
1178                    CondKind::Generic => {
1179                        compile_cond_generic(cond, args_count, meta, user_fns, layout, out)?;
1180                        out.push(Item::Op(OpKind::JzLabel { label: end.clone() }));
1181                        compile_stmts(
1182                            body,
1183                            args_count,
1184                            meta,
1185                            user_fns,
1186                            layout,
1187                            out,
1188                            lg,
1189                            break_stack,
1190                        )?;
1191                        out.push(Item::Op(OpKind::JmpLabel { label: head }));
1192                    }
1193                }
1194
1195                break_stack.pop();
1196                out.push(Item::Label(Label::new(end)));
1197            }
1198        }
1199    }
1200    Ok(())
1201}
1202
1203// ----------------------------
1204// __pc dispatcher mode
1205// ----------------------------
1206
1207fn looks_like_pc_dispatcher(raw: &[String]) -> bool {
1208    let mut saw_pc = false;
1209    let mut saw_case = false;
1210    let mut saw_while_true = false;
1211    for ln in raw {
1212        let t = ln.trim();
1213        if t.contains("__pc") {
1214            saw_pc = true;
1215        }
1216        if t == "while true do" {
1217            saw_while_true = true;
1218        }
1219        if t.starts_with("if __pc ==") || t.starts_with("elseif __pc ==") {
1220            saw_case = true;
1221        }
1222    }
1223    saw_pc && saw_while_true && saw_case
1224}
1225
1226fn is_comment_or_empty_line(t: &str) -> bool {
1227    let tt = t.trim();
1228    tt.is_empty() || tt.starts_with("--")
1229}
1230
1231fn is_if_start_line(t: &str) -> bool {
1232    let tt = t.trim();
1233    tt.starts_with("if ") && tt.ends_with(" then")
1234}
1235
1236fn is_while_start_line(t: &str) -> bool {
1237    let tt = t.trim();
1238    tt.starts_with("while ") && tt.ends_with(" do")
1239}
1240
1241fn is_for_start_line(t: &str) -> bool {
1242    let tt = t.trim();
1243    tt.starts_with("for ") && tt.ends_with(" do")
1244}
1245
1246fn is_repeat_start_line(t: &str) -> bool {
1247    t.trim() == "repeat"
1248}
1249
1250fn is_until_line(t: &str) -> bool {
1251    t.trim().starts_with("until ")
1252}
1253
1254fn is_end_line(t: &str) -> bool {
1255    t.trim() == "end"
1256}
1257
1258fn bb_label(fn_name: &str, pc: u32) -> String {
1259    format!("bb:{fn_name}:{pc}")
1260}
1261
1262fn parse_entry_pc(body: &[String]) -> u32 {
1263    let re = Regex::new(r"^(?:local\s+)?__pc\s*=\s*(\d+)\s*$").unwrap();
1264    for ln in body {
1265        let t = ln.trim();
1266        if let Some(c) = re.captures(t) {
1267            if let Ok(v) = c.get(1).unwrap().as_str().parse::<u32>() {
1268                return v;
1269            }
1270        }
1271        if t == "while true do" {
1272            break;
1273        }
1274    }
1275    0
1276}
1277
1278fn collect_case_body(body: &[String], mut i: usize, re_case: &Regex) -> (Vec<String>, usize) {
1279    let mut out: Vec<String> = Vec::new();
1280    let mut nest: i32 = 0;
1281
1282    while i < body.len() {
1283        let t = body[i].trim();
1284        if nest == 0 {
1285            if re_case.is_match(t) || t == "else" {
1286                break;
1287            }
1288        }
1289
1290        out.push(body[i].clone());
1291
1292        if is_if_start_line(t)
1293            || is_while_start_line(t)
1294            || is_for_start_line(t)
1295            || is_repeat_start_line(t)
1296        {
1297            nest += 1;
1298        } else if is_end_line(t) {
1299            nest -= 1;
1300        } else if is_until_line(t) {
1301            nest -= 1;
1302        }
1303
1304        i += 1;
1305    }
1306
1307    (out, i)
1308}
1309
1310fn compile_pc_case(
1311    pc: u32,
1312    lines: &[String],
1313    fn_name: &str,
1314    args_count: i8,
1315    meta: &Meta,
1316    user_fns: &HashSet<String>,
1317    layout: &GlobalLayout,
1318    out: &mut Vec<Item>,
1319) -> Result<()> {
1320    out.push(Item::Label(Label::new(bb_label(fn_name, pc))));
1321
1322    let re_pc_set = Regex::new(r"^__pc\s*=\s*(\d+)\s*$").unwrap();
1323    let re_term_if = Regex::new(r"^if\s+S\d+\s*(==|~=)\s*0\s+then\s*$").unwrap();
1324
1325    let mut i = 0usize;
1326    while i < lines.len() {
1327        let mut t = lines[i].trim().to_string();
1328        if is_comment_or_empty_line(&t) {
1329            i += 1;
1330            continue;
1331        }
1332
1333        if t.starts_with("local ") && !t.contains('=') {
1334            i += 1;
1335            continue;
1336        }
1337        if let Some(rest) = t.strip_prefix("local ") {
1338            t = rest.trim().to_string();
1339        }
1340
1341        if t == "return" {
1342            out.push(Item::Op(OpKind::Ret));
1343            return Ok(());
1344        }
1345
1346        if let Some(rest) = t.strip_prefix("return ") {
1347            let e = parse_expr(rest.trim())?;
1348            compile_expr(&e, args_count, meta, user_fns, layout, out)?;
1349            out.push(Item::Op(OpKind::Retv));
1350            return Ok(());
1351        }
1352
1353        if let Some(c) = re_pc_set.captures(&t) {
1354            let target: u32 = c.get(1).unwrap().as_str().parse()?;
1355            out.push(Item::Op(OpKind::JmpLabel {
1356                label: bb_label(fn_name, target),
1357            }));
1358            return Ok(());
1359        }
1360
1361        if let Some(c) = re_term_if.captures(&t) {
1362            let op = c.get(1).unwrap().as_str();
1363            let mut j = i + 1;
1364            while j < lines.len() && is_comment_or_empty_line(lines[j].trim()) {
1365                j += 1;
1366            }
1367            if j >= lines.len() {
1368                bail!("unterminated pc-if in bb {pc}");
1369            }
1370            let then_line = lines[j].trim();
1371            let then_pc: u32 = re_pc_set
1372                .captures(then_line)
1373                .ok_or_else(|| anyhow!("pc-if then arm must set __pc in bb {pc}"))?
1374                .get(1)
1375                .unwrap()
1376                .as_str()
1377                .parse()?;
1378
1379            j += 1;
1380            while j < lines.len() && is_comment_or_empty_line(lines[j].trim()) {
1381                j += 1;
1382            }
1383            if j >= lines.len() || lines[j].trim() != "else" {
1384                bail!("pc-if missing else in bb {pc}");
1385            }
1386
1387            j += 1;
1388            while j < lines.len() && is_comment_or_empty_line(lines[j].trim()) {
1389                j += 1;
1390            }
1391            if j >= lines.len() {
1392                bail!("pc-if missing else pc assignment in bb {pc}");
1393            }
1394            let else_line = lines[j].trim();
1395            let else_pc: u32 = re_pc_set
1396                .captures(else_line)
1397                .ok_or_else(|| anyhow!("pc-if else arm must set __pc in bb {pc}"))?
1398                .get(1)
1399                .unwrap()
1400                .as_str()
1401                .parse()?;
1402
1403            j += 1;
1404            while j < lines.len() && is_comment_or_empty_line(lines[j].trim()) {
1405                j += 1;
1406            }
1407            if j >= lines.len() || lines[j].trim() != "end" {
1408                bail!("pc-if missing end in bb {pc}");
1409            }
1410
1411            let (zero_target, nonzero_target) = if op == "==" {
1412                (then_pc, else_pc)
1413            } else {
1414                (else_pc, then_pc)
1415            };
1416            out.push(Item::Op(OpKind::JzLabel {
1417                label: bb_label(fn_name, zero_target),
1418            }));
1419            out.push(Item::Op(OpKind::JmpLabel {
1420                label: bb_label(fn_name, nonzero_target),
1421            }));
1422            return Ok(());
1423        }
1424
1425        compile_simple_stmt(&t, args_count, meta, user_fns, layout, out)?;
1426        i += 1;
1427    }
1428
1429    out.push(Item::Op(OpKind::Ret));
1430    Ok(())
1431}
1432
1433fn compile_pc_dispatcher_function(
1434    f: &Function,
1435    meta: &Meta,
1436    user_fns: &HashSet<String>,
1437    layout: &GlobalLayout,
1438    out: &mut Vec<Item>,
1439) -> Result<()> {
1440    if f.raw.len() < 2 {
1441        bail!("function {}: too short", f.name);
1442    }
1443    let body: Vec<String> = f.raw[1..f.raw.len() - 1].to_vec();
1444
1445    let entry_pc = parse_entry_pc(&body);
1446
1447    let re_case = Regex::new(r"^(if|elseif)\s+__pc\s*==\s*(\d+)\s+then\s*$").unwrap();
1448
1449    let mut i = 0usize;
1450    while i < body.len() {
1451        if re_case.is_match(body[i].trim()) {
1452            break;
1453        }
1454        i += 1;
1455    }
1456    if i >= body.len() {
1457        bail!("function {}: pc-dispatcher header not found", f.name);
1458    }
1459
1460    let mut cases: Vec<(u32, Vec<String>)> = Vec::new();
1461    while i < body.len() {
1462        let t = body[i].trim();
1463        if t == "else" {
1464            break;
1465        }
1466        if let Some(c) = re_case.captures(t) {
1467            let pc: u32 = c.get(2).unwrap().as_str().parse()?;
1468            i += 1;
1469            let (case_lines, next_i) = collect_case_body(&body, i, &re_case);
1470            cases.push((pc, case_lines));
1471            i = next_i;
1472            continue;
1473        }
1474        i += 1;
1475    }
1476
1477    if cases.is_empty() {
1478        bail!("function {}: no pc-dispatcher cases found", f.name);
1479    }
1480
1481    if let Some(pos) = cases.iter().position(|(pc, _)| *pc == entry_pc) {
1482        if pos != 0 {
1483            let entry = cases.remove(pos);
1484            cases.insert(0, entry);
1485        }
1486    }
1487
1488    for (pc, lines) in cases {
1489        compile_pc_case(
1490            pc,
1491            &lines,
1492            &f.name,
1493            f.args_count,
1494            meta,
1495            user_fns,
1496            layout,
1497            out,
1498        )?;
1499    }
1500
1501    Ok(())
1502}
1503
1504pub fn compile_program(meta: &Meta, program: &Program) -> Result<(Vec<Item>, GlobalLayout)> {
1505    let mut items: Vec<Item> = Vec::new();
1506    let layout = GlobalLayout::from_globals(&program.globals)?;
1507    let user_fns: HashSet<String> = program.functions.iter().map(|f| f.name.clone()).collect();
1508
1509    for f in &program.functions {
1510        items.push(Item::Label(Label::new(format!("fn:{}", f.name))));
1511        items.push(Item::Op(OpKind::InitStack {
1512            args: f.args_count,
1513            locals: f.locals_count,
1514        }));
1515
1516        if looks_like_pc_dispatcher(&f.raw) {
1517            compile_pc_dispatcher_function(f, meta, &user_fns, &layout, &mut items)?;
1518        } else {
1519            let mut lg = LabelGen::new(format!("fn:{}", f.name));
1520            let mut break_stack: Vec<String> = Vec::new();
1521            compile_stmts(
1522                &f.body,
1523                f.args_count,
1524                meta,
1525                &user_fns,
1526                &layout,
1527                &mut items,
1528                &mut lg,
1529                &mut break_stack,
1530            )?;
1531        }
1532
1533        if !matches!(items.last(), Some(Item::Op(OpKind::Ret | OpKind::Retv))) {
1534            items.push(Item::Op(OpKind::Ret));
1535        }
1536    }
1537
1538    Ok((items, layout))
1539}
1540
1541#[cfg(test)]
1542mod frame_slot_tests {
1543    use super::slot_to_stack_idx;
1544
1545    #[test]
1546    fn rfvp_call_frame_argument_and_local_offsets() {
1547        assert_eq!(slot_to_stack_idx("a0", 1).unwrap(), -2);
1548        assert_eq!(slot_to_stack_idx("l0", 1).unwrap(), 0);
1549
1550        assert_eq!(slot_to_stack_idx("a0", 3).unwrap(), -4);
1551        assert_eq!(slot_to_stack_idx("a1", 3).unwrap(), -3);
1552        assert_eq!(slot_to_stack_idx("a2", 3).unwrap(), -2);
1553        assert_eq!(slot_to_stack_idx("l0", 3).unwrap(), 0);
1554        assert_eq!(slot_to_stack_idx("l3", 3).unwrap(), 3);
1555
1556        assert!(slot_to_stack_idx("a1", 1).is_err());
1557    }
1558}