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 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 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
1203fn 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}