Skip to main content

lux/convert/
rust.rs

1//! The Rust backend: emit real Rust source.
2//!
3//! Rust is the closest match to lux's shape — enums with values, `Option` and
4//! `Result`, exhaustive `match`, value semantics — so most of the work is
5//! cosmetic: `func` becomes `fn`, lux's lowercase enum cases become PascalCase
6//! variants, camelCase names become snake_case. The one real wrinkle is that
7//! Rust *moves* a non-`Copy` value when you pass it, while lux copies, so a
8//! named place argument is cloned at the call site to preserve lux's semantics.
9
10use crate::ast::*;
11
12use super::{
13    Ty, Types, bin_prec, escape, format_float, indent, op_str, rust_ident, to_pascal, to_snake,
14    ty_from_ann,
15};
16
17struct Gen {
18    t: Types,
19    out: String,
20    indent: usize,
21    /// `readLine()` reads from the shared stdin handle the same way each call,
22    /// which is a few lines; emit it once as a helper when the program asks.
23    uses_read_line: bool,
24    /// `run` needs the built-in `Output` struct and a helper that spawns a
25    /// command, emitted once when the program reaches for it.
26    uses_run: bool,
27}
28
29/// Reading one line, returning `None` at end of input — the helper `readLine()`
30/// lowers to. Pulled out so a loop over input reads as one clean call.
31const READ_LINE_HELPER: &str = "\
32fn read_line() -> Option<String> {
33    let mut line = String::new();
34    match std::io::stdin().read_line(&mut line) {
35        Ok(0) | Err(_) => None,
36        Ok(_) => Some(line.trim_end_matches(['\\n', '\\r']).to_string()),
37    }
38}
39";
40
41/// The built-in `Output` struct and the helper `run` lowers to. Rust's
42/// `Command` mirrors lux's shape closely: a launch failure is the `Err`, and the
43/// exit code rides inside `Output` on success. The child's input is closed off.
44const RUN_HELPER: &str = "\
45#[derive(Debug, Clone, PartialEq)]
46struct Output {
47    status: i64,
48    stdout: String,
49    stderr: String,
50}
51
52fn run(program: String, args: Vec<String>) -> Result<Output, String> {
53    match std::process::Command::new(&program)
54        .args(&args)
55        .stdin(std::process::Stdio::null())
56        .output()
57    {
58        Ok(out) => Ok(Output {
59            status: out.status.code().unwrap_or(-1) as i64,
60            stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
61            stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
62        }),
63        Err(e) => Err(e.to_string()),
64    }
65}
66";
67
68/// Translate a whole program to Rust source text.
69pub fn to_rust(program: &[Stmt]) -> String {
70    let mut g = Gen {
71        t: Types::new(program),
72        out: String::new(),
73        indent: 0,
74        uses_read_line: false,
75        uses_run: false,
76    };
77
78    for stmt in program {
79        if let Stmt::Struct { name, fields, .. } = stmt {
80            g.emit_struct(name, fields);
81        }
82    }
83    for stmt in program {
84        if let Stmt::Enum { name, variants, .. } = stmt {
85            g.emit_enum(name, variants);
86        }
87    }
88    for stmt in program {
89        if let Stmt::Func {
90            name,
91            params,
92            ret,
93            body,
94            ..
95        } = stmt
96        {
97            g.emit_func(name, params, ret.as_ref(), body);
98        }
99    }
100
101    g.line("fn main() {".into());
102    g.indent += 1;
103    g.t.push_scope();
104    for stmt in program {
105        if !matches!(
106            stmt,
107            Stmt::Struct { .. } | Stmt::Enum { .. } | Stmt::Func { .. }
108        ) {
109            g.emit_stmt(stmt);
110        }
111    }
112    g.t.pop_scope();
113    g.indent -= 1;
114    g.line("}".into());
115
116    let mut preamble = String::new();
117    if g.uses_run {
118        preamble.push_str(RUN_HELPER);
119        preamble.push('\n');
120    }
121    if g.uses_read_line {
122        preamble.push_str(READ_LINE_HELPER);
123        preamble.push('\n');
124    }
125    format!("{}{}", preamble, g.out)
126}
127
128/// A lux type as Rust source text.
129fn ty_text(t: &Ty) -> String {
130    match t {
131        Ty::Int => "i64".into(),
132        Ty::Float => "f64".into(),
133        Ty::Str => "String".into(),
134        Ty::Bool => "bool".into(),
135        Ty::Array(t) => format!("Vec<{}>", ty_text(t)),
136        Ty::User(n) => n.clone(),
137        Ty::Option(t) => format!("Option<{}>", ty_text(t)),
138        Ty::Result(a, b) => format!("Result<{}, {}>", ty_text(a), ty_text(b)),
139        Ty::Range => "std::ops::Range<i64>".into(),
140        Ty::Unit => "()".into(),
141        Ty::Unknown => "_".into(),
142    }
143}
144
145/// The natural empty value for a `var` that was declared without one.
146fn zero(t: &Ty) -> String {
147    match t {
148        Ty::Int => "0".into(),
149        Ty::Float => "0.0".into(),
150        Ty::Bool => "false".into(),
151        Ty::Str => "String::new()".into(),
152        Ty::Array(_) => "Vec::new()".into(),
153        Ty::Option(_) => "None".into(),
154        _ => "Default::default()".into(),
155    }
156}
157
158impl Gen {
159    fn line(&mut self, s: String) {
160        self.out.push_str(&indent(self.indent));
161        self.out.push_str(&s);
162        self.out.push('\n');
163    }
164
165    fn blank(&mut self) {
166        self.out.push('\n');
167    }
168
169    // --- declarations ------------------------------------------------------
170
171    fn emit_struct(&mut self, name: &str, fields: &[FieldDef]) {
172        self.line("#[derive(Debug, Clone, PartialEq)]".into());
173        self.line(format!("struct {} {{", name));
174        for f in fields {
175            self.line(format!(
176                "    {}: {},",
177                to_snake(&f.name),
178                ty_text(&ty_from_ann(&f.ty))
179            ));
180        }
181        self.line("}".into());
182        self.blank();
183    }
184
185    fn emit_enum(&mut self, name: &str, variants: &[VariantDef]) {
186        self.line("#[derive(Debug, Clone, PartialEq)]".into());
187        self.line(format!("enum {} {{", name));
188        for v in variants {
189            if v.fields.is_empty() {
190                self.line(format!("    {},", to_pascal(&v.name)));
191            } else {
192                let tys: Vec<String> = v
193                    .fields
194                    .iter()
195                    .map(|f| ty_text(&ty_from_ann(&f.ty)))
196                    .collect();
197                self.line(format!("    {}({}),", to_pascal(&v.name), tys.join(", ")));
198            }
199        }
200        self.line("}".into());
201        self.blank();
202    }
203
204    fn emit_func(&mut self, name: &str, params: &[Param], ret: Option<&TypeAnn>, body: &[Stmt]) {
205        let ps: Vec<String> = params
206            .iter()
207            .map(|p| {
208                format!(
209                    "{}: {}",
210                    rust_ident(&to_snake(&p.name)),
211                    ty_text(&ty_from_ann(&p.ty))
212                )
213            })
214            .collect();
215        let r = ret
216            .map(|t| format!(" -> {}", ty_text(&ty_from_ann(t))))
217            .unwrap_or_default();
218        self.line(format!(
219            "fn {}({}){} {{",
220            rust_ident(&to_snake(name)),
221            ps.join(", "),
222            r
223        ));
224        self.indent += 1;
225        self.t.push_scope();
226        for p in params {
227            self.t.declare(p.name.clone(), ty_from_ann(&p.ty));
228        }
229        for stmt in body {
230            self.emit_stmt(stmt);
231        }
232        self.t.pop_scope();
233        self.indent -= 1;
234        self.line("}".into());
235        self.blank();
236    }
237
238    // --- statements --------------------------------------------------------
239
240    fn emit_stmt(&mut self, stmt: &Stmt) {
241        match stmt {
242            Stmt::Let {
243                name, ty, value, ..
244            } => self.emit_binding(name, ty.as_ref(), value, false),
245            Stmt::Var {
246                name,
247                ty,
248                value: Some(value),
249                ..
250            } => self.emit_binding(name, ty.as_ref(), value, true),
251            Stmt::Var {
252                name,
253                ty: Some(ann),
254                value: None,
255                ..
256            } => {
257                let vty = ty_from_ann(ann);
258                let z = zero(&vty);
259                let snake = rust_ident(&to_snake(name));
260                self.t.declare(name.clone(), vty.clone());
261                self.line(format!("let mut {}: {} = {};", snake, ty_text(&vty), z));
262            }
263            Stmt::Var { value: None, .. } => {} // a var with neither type nor value can't occur
264            Stmt::Assign {
265                name, op, value, ..
266            } => self.emit_assign(name, *op, value),
267            Stmt::Return { value, .. } => match value {
268                Some(v) => {
269                    let e = self.emit_expr(v);
270                    self.line(format!("return {};", e));
271                }
272                None => self.line("return;".into()),
273            },
274            Stmt::If {
275                cond,
276                then_body,
277                else_body,
278                ..
279            } => self.emit_if(cond, then_body, else_body.as_deref()),
280            Stmt::While { cond, body, .. } => {
281                let c = self.emit_expr(cond);
282                self.line(format!("while {} {{", c));
283                self.block(body);
284                self.line("}".into());
285            }
286            Stmt::For {
287                var, iter, body, ..
288            } => self.emit_for(var, iter, body),
289            Stmt::Expr(e) => {
290                let s = self.emit_expr(e);
291                self.line(format!("{};", s));
292            }
293            // Declarations are hoisted to module scope in the top-level pass.
294            Stmt::Func {
295                name,
296                params,
297                ret,
298                body,
299                ..
300            } => self.emit_func(name, params, ret.as_ref(), body),
301            Stmt::Struct { .. } | Stmt::Enum { .. } => {}
302        }
303    }
304
305    /// A block that owns its own scope, indented one level.
306    fn block(&mut self, body: &[Stmt]) {
307        self.indent += 1;
308        self.t.push_scope();
309        for stmt in body {
310            self.emit_stmt(stmt);
311        }
312        self.t.pop_scope();
313        self.indent -= 1;
314    }
315
316    fn emit_binding(&mut self, name: &str, ann: Option<&TypeAnn>, value: &Expr, mutable: bool) {
317        let snake = rust_ident(&to_snake(name));
318        let vty = ann
319            .map(ty_from_ann)
320            .unwrap_or_else(|| self.t.type_of(value));
321        // A bare `none` (or anything that leaves its type open) carries no type
322        // of its own, so when the source named one, write it down for Rust.
323        let value_open = self.t.type_of(value).has_unknown();
324        let annotate = !vty.has_unknown() && ((ann.is_some() && value_open) || vty.has_int());
325        let kw = if mutable { "let mut" } else { "let" };
326        let expr = self.emit_expr(value);
327        if annotate {
328            self.line(format!("{} {}: {} = {};", kw, snake, ty_text(&vty), expr));
329        } else {
330            self.line(format!("{} {} = {};", kw, snake, expr));
331        }
332        self.t.declare(name.to_string(), vty);
333    }
334
335    fn emit_assign(&mut self, name: &str, op: AssignOp, value: &Expr) {
336        let snake = rust_ident(&to_snake(name));
337        let lty = self.t.lookup(name);
338        match op {
339            AssignOp::Set => {
340                let e = self.emit_expr(value);
341                self.line(format!("{} = {};", snake, e));
342            }
343            AssignOp::Add => match lty {
344                Ty::Str => {
345                    // lux `+=` on a string appends text.
346                    if let Expr::Str(s, _) = value {
347                        self.line(format!("{}.push_str(\"{}\");", snake, escape(s)));
348                    } else {
349                        let e = self.emit_expr(value);
350                        self.line(format!("{}.push_str(&{});", snake, e));
351                    }
352                }
353                Ty::Array(_) => {
354                    // lux `+=` on an array appends one element.
355                    let e = self.emit_expr(value);
356                    self.line(format!("{}.push({});", snake, e));
357                }
358                _ => {
359                    let e = self.emit_expr(value);
360                    self.line(format!("{} += {};", snake, e));
361                }
362            },
363            AssignOp::Sub => {
364                let e = self.emit_expr(value);
365                self.line(format!("{} -= {};", snake, e));
366            }
367        }
368    }
369
370    fn emit_if(&mut self, cond: &Expr, then_body: &[Stmt], mut els: Option<&[Stmt]>) {
371        let c = self.emit_expr(cond);
372        self.line(format!("if {} {{", c));
373        self.block(then_body);
374        loop {
375            match els {
376                None => {
377                    self.line("}".into());
378                    break;
379                }
380                // A lone nested `if` is an `else if` — chain it on one line.
381                Some(e) if e.len() == 1 && matches!(e[0], Stmt::If { .. }) => {
382                    if let Stmt::If {
383                        cond,
384                        then_body,
385                        else_body,
386                        ..
387                    } = &e[0]
388                    {
389                        let c = self.emit_expr(cond);
390                        self.line(format!("}} else if {} {{", c));
391                        self.block(then_body);
392                        els = else_body.as_deref();
393                    }
394                }
395                Some(e) => {
396                    self.line("} else {".into());
397                    self.block(e);
398                    self.line("}".into());
399                    break;
400                }
401            }
402        }
403    }
404
405    fn emit_for(&mut self, var: &str, iter: &Expr, body: &[Stmt]) {
406        let svar = to_snake(var);
407        let (iter_str, elem_ty) = match self.t.type_of(iter) {
408            Ty::Range => (self.emit_expr(iter), Ty::Int),
409            Ty::Array(t) => {
410                let base = self.emit_expr(iter);
411                // Borrow the array and clone each element, so the loop never
412                // consumes what it walks and the body gets owned values.
413                (format!("{}.iter().cloned()", base), *t)
414            }
415            _ => (self.emit_expr(iter), Ty::Unknown),
416        };
417        self.line(format!("for {} in {} {{", svar, iter_str));
418        self.indent += 1;
419        self.t.push_scope();
420        self.t.declare(var.to_string(), elem_ty);
421        for stmt in body {
422            self.emit_stmt(stmt);
423        }
424        self.t.pop_scope();
425        self.indent -= 1;
426        self.line("}".into());
427    }
428
429    // --- expressions -------------------------------------------------------
430
431    fn emit_expr(&mut self, e: &Expr) -> String {
432        match e {
433            Expr::Int(n, _) => n.to_string(),
434            Expr::Float(f, _) => format_float(*f),
435            Expr::Str(s, _) => format!("\"{}\".to_string()", escape(s)),
436            Expr::Bool(b, _) => b.to_string(),
437            Expr::Ident(name, _) => {
438                if name == "none" {
439                    "None".to_string()
440                } else {
441                    rust_ident(&to_snake(name))
442                }
443            }
444            Expr::Array(els, _) => {
445                let parts: Vec<String> = els.iter().map(|x| self.emit_expr(x)).collect();
446                format!("vec![{}]", parts.join(", "))
447            }
448            Expr::Unary { op, rhs, .. } => {
449                // Unary binds tighter than any binary operator, so a binary
450                // operand needs parentheses: `-(a + b)`, not `-a + b`.
451                let r = if matches!(**rhs, Expr::Binary { .. }) {
452                    let inner = self.emit_expr(rhs);
453                    format!("({})", inner)
454                } else {
455                    self.emit_expr(rhs)
456                };
457                match op {
458                    UnOp::Neg => format!("-{}", r),
459                    UnOp::Not => format!("!{}", r),
460                }
461            }
462            Expr::Binary { op, lhs, rhs, .. } => {
463                if *op == BinOp::Add && self.t.type_of(lhs) == Ty::Str {
464                    let l = self.display_arg(lhs);
465                    let r = self.display_arg(rhs);
466                    format!("format!(\"{{}}{{}}\", {}, {})", l, r)
467                } else {
468                    let p = bin_prec(*op);
469                    let l = self.emit_child(lhs, p, false);
470                    let r = self.emit_child(rhs, p, true);
471                    format!("{} {} {}", l, op_str(*op), r)
472                }
473            }
474            Expr::Index { base, index, .. } => {
475                let b = self.emit_expr(base);
476                let idx = if let Expr::Int(n, _) = **index {
477                    n.to_string()
478                } else {
479                    let e = self.emit_expr(index);
480                    format!("({}) as usize", e)
481                };
482                format!("{}[{}]", b, idx)
483            }
484            Expr::Range { start, end, .. } => {
485                let s = self.emit_expr(start);
486                let e = self.emit_expr(end);
487                format!("{}..{}", s, e)
488            }
489            Expr::Call { name, args, .. } => self.emit_call(name, args),
490            Expr::StructLit { name, fields, .. } => {
491                let parts: Vec<String> = fields
492                    .iter()
493                    .map(|(k, v)| {
494                        let val = self.emit_expr(v);
495                        format!("{}: {}", to_snake(k), val)
496                    })
497                    .collect();
498                format!("{} {{ {} }}", name, parts.join(", "))
499            }
500            Expr::EnumLit {
501                enum_name,
502                variant,
503                fields,
504                ..
505            } => self.emit_enum_lit(enum_name, variant, fields),
506            Expr::Field { base, field, .. } => {
507                // `Shape.dot` parses as a field access but is a payload-less
508                // enum case — emit it as construction.
509                if let Expr::Ident(n, _) = &**base
510                    && let Some(variants) = self.t.env.enums.get(n)
511                    && variants.iter().any(|v| v.name == *field)
512                {
513                    return format!("{}::{}", n, to_pascal(field));
514                }
515                let b = self.emit_expr(base);
516                format!("{}.{}", b, to_snake(field))
517            }
518            Expr::Match {
519                scrutinee, arms, ..
520            } => self.emit_match(scrutinee, arms),
521        }
522    }
523
524    /// Emit a binary operand, wrapping it in parentheses only when its operator
525    /// binds more loosely than the parent's. The right operand of a left-
526    /// associative operator also needs them at equal precedence (`a - (b - c)`).
527    fn emit_child(&mut self, e: &Expr, parent: u8, is_right: bool) -> String {
528        let s = self.emit_expr(e);
529        if let Expr::Binary { op, .. } = e {
530            let p = bin_prec(*op);
531            let wrap = if is_right { p <= parent } else { p < parent };
532            if wrap {
533                return format!("({})", s);
534            }
535        }
536        s
537    }
538
539    /// A user-function argument. lux passes values by copy and the caller keeps
540    /// its own, so a named value of a non-`Copy` type is cloned at the call
541    /// site to preserve that — exactly what lux does under the hood.
542    fn emit_call_arg(&mut self, a: &Expr) -> String {
543        let clone = !is_copy(&self.t.type_of(a)) && is_place(a);
544        let s = self.emit_expr(a);
545        if clone { format!("{}.clone()", s) } else { s }
546    }
547
548    /// An argument in `print` or string concatenation, where a bare string
549    /// literal can stay a clean `&str` instead of an owned `String`.
550    fn display_arg(&mut self, e: &Expr) -> String {
551        if let Expr::Str(s, _) = e {
552            format!("\"{}\"", escape(s))
553        } else {
554            self.emit_expr(e)
555        }
556    }
557
558    /// `print` and `eprint` differ only in the macro they reach for — one writes
559    /// stdout, the other stderr — so they share how arguments become a format.
560    fn println_call(&mut self, mac: &str, args: &[Expr]) -> String {
561        let mut fmt = String::new();
562        let mut parts = Vec::new();
563        for (i, a) in args.iter().enumerate() {
564            if i > 0 {
565                fmt.push(' ');
566            }
567            // `{:?}` on an f64 keeps the decimal point (`9.0`), matching how lux
568            // prints floats; plain scalars use `{}`.
569            let ty = self.t.type_of(a);
570            fmt.push_str(if ty == Ty::Float || !ty.is_scalar() {
571                "{:?}"
572            } else {
573                "{}"
574            });
575            parts.push(self.display_arg(a));
576        }
577        if parts.is_empty() {
578            format!("{}!()", mac)
579        } else {
580            format!("{}!(\"{}\", {})", mac, fmt, parts.join(", "))
581        }
582    }
583
584    fn emit_call(&mut self, name: &str, args: &[Expr]) -> String {
585        match name {
586            "print" => self.println_call("println", args),
587            "eprint" => self.println_call("eprintln", args),
588            // Each fallible call turns the target's native error into a string, so
589            // the lux source stays `Result<_, string>` on this side too.
590            "readFile" => {
591                let p = self.emit_call_arg(&args[0]);
592                format!("std::fs::read_to_string({}).map_err(|e| e.to_string())", p)
593            }
594            "writeFile" => {
595                let p = self.emit_call_arg(&args[0]);
596                let c = self.emit_call_arg(&args[1]);
597                format!("std::fs::write({}, {}).map_err(|e| e.to_string())", p, c)
598            }
599            "args" => "std::env::args().collect::<Vec<String>>()".to_string(),
600            "readLine" => {
601                self.uses_read_line = true;
602                "read_line()".to_string()
603            }
604            "run" => {
605                self.uses_run = true;
606                let p = self.emit_call_arg(&args[0]);
607                let a = self.emit_call_arg(&args[1]);
608                format!("run({}, {})", p, a)
609            }
610            "string" => {
611                // `{:?}` keeps a whole float's decimal point, the way lux's
612                // `string(2.0)` yields "2.0" rather than "2".
613                let is_float = self.t.type_of(&args[0]) == Ty::Float;
614                let e = self.emit_expr(&args[0]);
615                if is_float {
616                    format!("format!(\"{{:?}}\", {})", e)
617                } else {
618                    format!("({}).to_string()", e)
619                }
620            }
621            "int" => {
622                let inner = self.t.type_of(&args[0]);
623                let e = self.emit_expr(&args[0]);
624                match inner {
625                    Ty::Int => e,
626                    _ => format!("({}) as i64", e),
627                }
628            }
629            "float" => {
630                let inner = self.t.type_of(&args[0]);
631                let e = self.emit_expr(&args[0]);
632                match inner {
633                    Ty::Float => e,
634                    _ => format!("({}) as f64", e),
635                }
636            }
637            // `.ok()` turns parse's Result into the Option lux returns.
638            "parseInt" => {
639                let e = self.emit_call_arg(&args[0]);
640                format!("{}.trim().parse::<i64>().ok()", e)
641            }
642            "parseFloat" => {
643                let e = self.emit_call_arg(&args[0]);
644                format!("{}.trim().parse::<f64>().ok()", e)
645            }
646            "length" => {
647                let inner = self.t.type_of(&args[0]);
648                let e = self.emit_expr(&args[0]);
649                if inner == Ty::Str {
650                    format!("({}).chars().count() as i64", e)
651                } else {
652                    format!("({}).len() as i64", e)
653                }
654            }
655            "some" => {
656                let e = self.emit_expr(&args[0]);
657                format!("Some({})", e)
658            }
659            "ok" => {
660                let e = self.emit_expr(&args[0]);
661                format!("Ok({})", e)
662            }
663            "err" => {
664                let e = self.emit_expr(&args[0]);
665                format!("Err({})", e)
666            }
667            _ => {
668                let parts: Vec<String> = args.iter().map(|a| self.emit_call_arg(a)).collect();
669                format!("{}({})", rust_ident(&to_snake(name)), parts.join(", "))
670            }
671        }
672    }
673
674    fn emit_enum_lit(
675        &mut self,
676        enum_name: &str,
677        variant: &str,
678        fields: &[(String, Expr)],
679    ) -> String {
680        // Tuple variants are positional, so emit the values in the order the
681        // enum declared its fields, not the order they were written.
682        let order: Option<Vec<String>> = self.t.env.enums.get(enum_name).and_then(|variants| {
683            variants
684                .iter()
685                .find(|v| v.name == variant)
686                .map(|v| v.fields.iter().map(|f| f.name.clone()).collect())
687        });
688        let args: Vec<String> = match order {
689            Some(names) => names
690                .iter()
691                .map(|fname| {
692                    let expr = fields.iter().find(|(k, _)| k == fname).map(|(_, e)| e);
693                    match expr {
694                        Some(e) => self.emit_expr(e),
695                        None => "()".to_string(),
696                    }
697                })
698                .collect(),
699            None => fields.iter().map(|(_, e)| self.emit_expr(e)).collect(),
700        };
701        if args.is_empty() {
702            format!("{}::{}", enum_name, to_pascal(variant))
703        } else {
704            format!("{}::{}({})", enum_name, to_pascal(variant), args.join(", "))
705        }
706    }
707
708    fn emit_match(&mut self, scrutinee: &Expr, arms: &[MatchArm]) -> String {
709        let base = self.indent;
710        let ind = indent(base);
711        let ind1 = indent(base + 1);
712        let st = self.t.type_of(scrutinee);
713        let needs_as_str = arms.iter().any(|a| matches!(a.pattern, Pattern::Str(..)));
714        let scrut = if needs_as_str {
715            let s = self.emit_expr(scrutinee);
716            format!("{}.as_str()", s)
717        } else {
718            self.emit_expr(scrutinee)
719        };
720        let mut s = format!("match {} {{\n", scrut);
721        for arm in arms {
722            let pat = self.emit_pattern(&arm.pattern, &st);
723            // Bring the pattern's captures into scope so the arm body types
724            // correctly (a captured string should print without quotes).
725            self.t.push_scope();
726            self.declare_bindings(&arm.pattern, &st);
727            // Arm bodies sit one level in, so a nested match nests cleanly.
728            self.indent = base + 1;
729            let body = self.emit_expr(&arm.body);
730            self.indent = base;
731            self.t.pop_scope();
732            s.push_str(&format!("{}{} => {},\n", ind1, pat, body));
733        }
734        s.push_str(&format!("{}}}", ind));
735        s
736    }
737
738    fn emit_pattern(&mut self, pat: &Pattern, st: &Ty) -> String {
739        match pat {
740            Pattern::Wildcard(_) => "_".to_string(),
741            Pattern::Int(n, _) => n.to_string(),
742            Pattern::Str(s, _) => format!("\"{}\"", escape(s)),
743            Pattern::Bool(b, _) => b.to_string(),
744            Pattern::Variant { name, bindings, .. } => {
745                let binds: Vec<String> = bindings.iter().map(|b| to_snake(b)).collect();
746                let inner = if binds.is_empty() {
747                    String::new()
748                } else {
749                    format!("({})", binds.join(", "))
750                };
751                match st {
752                    Ty::Option(_) => match name.as_str() {
753                        "some" => format!("Some{}", paren_or_empty(&binds)),
754                        _ => "None".to_string(),
755                    },
756                    Ty::Result(_, _) => match name.as_str() {
757                        "ok" => format!("Ok{}", paren_or_empty(&binds)),
758                        _ => format!("Err{}", paren_or_empty(&binds)),
759                    },
760                    Ty::User(en) => format!("{}::{}{}", en, to_pascal(name), inner),
761                    _ => format!("{}{}", to_pascal(name), inner),
762                }
763            }
764        }
765    }
766
767    /// Record the types of a pattern's captures in the current scope.
768    fn declare_bindings(&mut self, pat: &Pattern, st: &Ty) {
769        let Pattern::Variant { name, bindings, .. } = pat else {
770            return;
771        };
772        let types: Vec<Ty> = match st {
773            Ty::Option(t) if name == "some" => vec![(**t).clone()],
774            Ty::Result(o, _) if name == "ok" => vec![(**o).clone()],
775            Ty::Result(_, e) if name == "err" => vec![(**e).clone()],
776            Ty::User(en) => self
777                .t
778                .env
779                .enums
780                .get(en)
781                .and_then(|vs| vs.iter().find(|v| v.name == *name))
782                .map(|v| v.fields.iter().map(|f| ty_from_ann(&f.ty)).collect())
783                .unwrap_or_default(),
784            _ => Vec::new(),
785        };
786        for (b, t) in bindings.iter().zip(types) {
787            self.t.declare(b.clone(), t);
788        }
789    }
790}
791
792fn paren_or_empty(binds: &[String]) -> String {
793    if binds.is_empty() {
794        String::new()
795    } else {
796        format!("({})", binds.join(", "))
797    }
798}
799
800/// Types that copy on use in Rust, so passing them never moves the original.
801fn is_copy(t: &Ty) -> bool {
802    matches!(t, Ty::Int | Ty::Float | Ty::Bool)
803}
804
805/// A "place" — a named value that could still be used after a call, as opposed
806/// to a fresh temporary like a literal or another call's result.
807fn is_place(e: &Expr) -> bool {
808    match e {
809        Expr::Ident(n, _) => n != "none",
810        Expr::Field { .. } | Expr::Index { .. } => true,
811        _ => false,
812    }
813}