Skip to main content

lux/convert/
mod.rs

1//! Translate a parsed lux program into a target language's source.
2//!
3//! Each backend — `rust`, `swift`, `go` — walks the same ast the interpreter
4//! runs and emits idiomatic source for its target: `func` becomes `fn` / `func`,
5//! lux's enums become Rust variants, Swift cases, or a Go interface, and the
6//! top-level statements are wrapped in a `main`. The point is for a learner to
7//! watch their own program turn into the language they're growing toward, so the
8//! output is meant to be read.
9//!
10//! lux has no separate type checker yet, so to decide the handful of places
11//! where the same lux syntax must emit different code — string `+` versus
12//! numeric `+`, `length` on a string versus an array, how a value prints — the
13//! shared `Types` below carries a small `type_of` that infers an expression's
14//! type on demand from the declared signatures. It assumes a well-formed
15//! program; the target compiler is the backstop for anything it can't see.
16
17mod go;
18mod rust;
19mod swift;
20
21pub use go::to_go;
22pub use rust::to_rust;
23pub use swift::to_swift;
24
25use std::collections::HashMap;
26
27use crate::ast::*;
28use crate::diagnostic::Span;
29
30/// A lux type, inferred during translation. `User` covers both structs and
31/// enums (each backend emits them by name); `Unknown` is the honest answer when
32/// a value doesn't pin its own type, like a bare `none`, and lets the target's
33/// own inference take over.
34#[derive(Clone, PartialEq)]
35pub(crate) enum Ty {
36    Int,
37    Float,
38    Str,
39    Bool,
40    Array(Box<Ty>),
41    User(String),
42    Option(Box<Ty>),
43    Result(Box<Ty>, Box<Ty>),
44    Range,
45    Unit,
46    Unknown,
47}
48
49impl Ty {
50    fn has_unknown(&self) -> bool {
51        match self {
52            Ty::Unknown => true,
53            Ty::Array(t) | Ty::Option(t) => t.has_unknown(),
54            Ty::Result(a, b) => a.has_unknown() || b.has_unknown(),
55            _ => false,
56        }
57    }
58
59    /// Does this type involve `int`? lux's `int` is 64-bit, but a bare Rust
60    /// integer literal defaults to `i32`, so the Rust backend annotates any
61    /// binding whose type involves an int to keep the two from drifting apart.
62    fn has_int(&self) -> bool {
63        match self {
64            Ty::Int => true,
65            Ty::Array(t) | Ty::Option(t) => t.has_int(),
66            Ty::Result(a, b) => a.has_int() || b.has_int(),
67            _ => false,
68        }
69    }
70
71    /// Scalars print plainly; compound values need a debug-style format.
72    fn is_scalar(&self) -> bool {
73        matches!(self, Ty::Int | Ty::Float | Ty::Str | Ty::Bool)
74    }
75}
76
77/// Turn a written annotation into an inferred type. Struct and enum names both
78/// land as `User`; the built-in generics are recognised by name.
79fn ty_from_ann(a: &TypeAnn) -> Ty {
80    match &a.kind {
81        TypeKind::Named(n) => match n.as_str() {
82            "int" => Ty::Int,
83            "float" => Ty::Float,
84            "string" => Ty::Str,
85            "bool" => Ty::Bool,
86            "Unit" => Ty::Unit,
87            _ => Ty::User(n.clone()),
88        },
89        TypeKind::Array(inner) => Ty::Array(Box::new(ty_from_ann(inner))),
90        TypeKind::Generic(name, args) => match (name.as_str(), args.as_slice()) {
91            ("Option", [t]) => Ty::Option(Box::new(ty_from_ann(t))),
92            ("Result", [a, b]) => Ty::Result(Box::new(ty_from_ann(a)), Box::new(ty_from_ann(b))),
93            _ => Ty::Unknown,
94        },
95    }
96}
97
98/// What the translator knows about the program's declared names, gathered in
99/// one pass up front so a call or field access can be typed wherever it appears.
100pub(crate) struct Env {
101    structs: HashMap<String, Vec<FieldDef>>,
102    enums: HashMap<String, Vec<VariantDef>>,
103    funcs: HashMap<String, (Vec<Param>, Option<TypeAnn>)>,
104}
105
106/// The declared names plus the running scope stack. Every backend shares this:
107/// it tracks what's in scope as emission walks the tree, so `type_of` can answer
108/// the type of any expression on demand. It does no emitting of its own.
109pub(crate) struct Types {
110    env: Env,
111    scopes: Vec<HashMap<String, Ty>>,
112}
113
114impl Types {
115    fn new(program: &[Stmt]) -> Self {
116        let mut env = Env {
117            structs: HashMap::new(),
118            enums: HashMap::new(),
119            funcs: HashMap::new(),
120        };
121        for stmt in program {
122            match stmt {
123                Stmt::Struct { name, fields, .. } => {
124                    env.structs.insert(name.clone(), fields.clone());
125                }
126                Stmt::Enum { name, variants, .. } => {
127                    env.enums.insert(name.clone(), variants.clone());
128                }
129                Stmt::Func {
130                    name, params, ret, ..
131                } => {
132                    env.funcs
133                        .insert(name.clone(), (params.clone(), ret.clone()));
134                }
135                _ => {}
136            }
137        }
138        // `Output` is the built-in struct `run` returns. Registering its fields
139        // here lets a field access like `result.status` type correctly in every
140        // backend, the same as a struct the program declared itself.
141        env.structs.insert("Output".to_string(), output_fields());
142        Types {
143            env,
144            scopes: vec![HashMap::new()],
145        }
146    }
147
148    fn push_scope(&mut self) {
149        self.scopes.push(HashMap::new());
150    }
151
152    fn pop_scope(&mut self) {
153        self.scopes.pop();
154    }
155
156    fn declare(&mut self, name: String, ty: Ty) {
157        self.scopes.last_mut().unwrap().insert(name, ty);
158    }
159
160    fn lookup(&self, name: &str) -> Ty {
161        for scope in self.scopes.iter().rev() {
162            if let Some(t) = scope.get(name) {
163                return t.clone();
164            }
165        }
166        Ty::Unknown
167    }
168
169    fn type_of(&self, e: &Expr) -> Ty {
170        match e {
171            Expr::Int(..) => Ty::Int,
172            Expr::Float(..) => Ty::Float,
173            Expr::Str(..) => Ty::Str,
174            Expr::Bool(..) => Ty::Bool,
175            Expr::Ident(name, _) => {
176                if name == "none" {
177                    Ty::Option(Box::new(Ty::Unknown))
178                } else {
179                    self.lookup(name)
180                }
181            }
182            Expr::Array(els, _) => match els.first() {
183                Some(first) => Ty::Array(Box::new(self.type_of(first))),
184                None => Ty::Array(Box::new(Ty::Unknown)),
185            },
186            Expr::Unary { op, rhs, .. } => match op {
187                UnOp::Neg => self.type_of(rhs),
188                UnOp::Not => Ty::Bool,
189            },
190            Expr::Binary { op, lhs, .. } => match op {
191                BinOp::Eq
192                | BinOp::Ne
193                | BinOp::Lt
194                | BinOp::Gt
195                | BinOp::Le
196                | BinOp::Ge
197                | BinOp::And
198                | BinOp::Or => Ty::Bool,
199                _ => self.type_of(lhs),
200            },
201            Expr::Index { base, .. } => match self.type_of(base) {
202                Ty::Array(t) => *t,
203                other => other,
204            },
205            Expr::Range { .. } => Ty::Range,
206            Expr::Call { name, args, .. } => self.call_type(name, args),
207            Expr::StructLit { name, .. } => Ty::User(name.clone()),
208            Expr::EnumLit { enum_name, .. } => Ty::User(enum_name.clone()),
209            Expr::Field { base, field, .. } => self.field_type(base, field),
210            Expr::Match { arms, .. } => arms
211                .first()
212                .map(|a| self.type_of(&a.body))
213                .unwrap_or(Ty::Unknown),
214        }
215    }
216
217    fn call_type(&self, name: &str, args: &[Expr]) -> Ty {
218        match name {
219            "print" => Ty::Unit,
220            "string" => Ty::Str,
221            "int" | "length" => Ty::Int,
222            "float" => Ty::Float,
223            "some" => Ty::Option(Box::new(self.type_of(&args[0]))),
224            "ok" => Ty::Result(Box::new(self.type_of(&args[0])), Box::new(Ty::Unknown)),
225            "err" => Ty::Result(Box::new(Ty::Unknown), Box::new(self.type_of(&args[0]))),
226            // The outside world: each fallible call hands its failure back as a
227            // value, so its type is what `match` reads to pick the right arms.
228            "readFile" => Ty::Result(Box::new(Ty::Str), Box::new(Ty::Str)),
229            "writeFile" => Ty::Result(Box::new(Ty::Unit), Box::new(Ty::Str)),
230            "args" => Ty::Array(Box::new(Ty::Str)),
231            "readLine" => Ty::Option(Box::new(Ty::Str)),
232            // Parsing text into a number can fail, so it answers with an Option.
233            "parseInt" => Ty::Option(Box::new(Ty::Int)),
234            "parseFloat" => Ty::Option(Box::new(Ty::Float)),
235            "eprint" => Ty::Unit,
236            // `run` is the one built-in that succeeds with a struct: the captured
237            // status and streams, or a string reason it could not launch.
238            "run" => Ty::Result(Box::new(Ty::User("Output".into())), Box::new(Ty::Str)),
239            _ => match self.env.funcs.get(name) {
240                Some((_, Some(ret))) => ty_from_ann(ret),
241                Some((_, None)) => Ty::Unit,
242                None => Ty::Unknown,
243            },
244        }
245    }
246
247    fn field_type(&self, base: &Expr, field: &str) -> Ty {
248        if let Expr::Ident(n, _) = base
249            && let Some(variants) = self.env.enums.get(n)
250            && variants.iter().any(|v| v.name == *field)
251        {
252            return Ty::User(n.clone());
253        }
254        match self.type_of(base) {
255            Ty::User(s) => self
256                .env
257                .structs
258                .get(&s)
259                .and_then(|fields| fields.iter().find(|f| f.name == *field))
260                .map(|f| ty_from_ann(&f.ty))
261                .unwrap_or(Ty::Unknown),
262            _ => Ty::Unknown,
263        }
264    }
265}
266
267/// The fields of the built-in `Output` struct, in declared order, so every
268/// backend types and emits `run`'s result identically.
269fn output_fields() -> Vec<FieldDef> {
270    let field = |name: &str, ty: &str| FieldDef {
271        name: name.to_string(),
272        ty: TypeAnn {
273            kind: TypeKind::Named(ty.to_string()),
274            span: Span::new(0, 0),
275        },
276        span: Span::new(0, 0),
277    };
278    vec![
279        field("status", "int"),
280        field("stdout", "string"),
281        field("stderr", "string"),
282    ]
283}
284
285// --- helpers shared across backends ----------------------------------------
286
287/// Binding strength of a binary operator, loosest (`||`) to tightest (`*`).
288/// Used to decide which operands actually need parentheses. The three targets
289/// share C's precedence, so they share this table.
290fn bin_prec(op: BinOp) -> u8 {
291    match op {
292        BinOp::Or => 1,
293        BinOp::And => 2,
294        BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Gt | BinOp::Le | BinOp::Ge => 3,
295        BinOp::Add | BinOp::Sub => 4,
296        BinOp::Mul | BinOp::Div | BinOp::Mod => 5,
297    }
298}
299
300/// The operator's source spelling — identical in Rust, Swift, and Go.
301fn op_str(op: BinOp) -> &'static str {
302    match op {
303        BinOp::Add => "+",
304        BinOp::Sub => "-",
305        BinOp::Mul => "*",
306        BinOp::Div => "/",
307        BinOp::Mod => "%",
308        BinOp::Eq => "==",
309        BinOp::Ne => "!=",
310        BinOp::Lt => "<",
311        BinOp::Gt => ">",
312        BinOp::Le => "<=",
313        BinOp::Ge => ">=",
314        BinOp::And => "&&",
315        BinOp::Or => "||",
316    }
317}
318
319fn indent(n: usize) -> String {
320    "    ".repeat(n)
321}
322
323/// `firstEven` becomes `first_even` — lux's camelCase identifiers become
324/// snake_case for the Rust backend's functions, variables, and fields.
325fn to_snake(s: &str) -> String {
326    let mut out = String::new();
327    for (i, c) in s.chars().enumerate() {
328        if c.is_uppercase() {
329            if i != 0 {
330                out.push('_');
331            }
332            out.extend(c.to_lowercase());
333        } else {
334            out.push(c);
335        }
336    }
337    out
338}
339
340/// `circle` becomes `Circle` — used for Rust's PascalCase enum variants and for
341/// Go's per-case struct names.
342fn to_pascal(s: &str) -> String {
343    let mut out = String::new();
344    let mut upper = true;
345    for c in s.chars() {
346        if c == '_' {
347            upper = true;
348        } else if upper {
349            out.extend(c.to_uppercase());
350            upper = false;
351        } else {
352            out.push(c);
353        }
354    }
355    out
356}
357
358/// Escape a string's contents for a double-quoted literal. The three targets
359/// share C's escape conventions for the characters lux can hold.
360fn escape(s: &str) -> String {
361    let mut out = String::new();
362    for c in s.chars() {
363        match c {
364            '\\' => out.push_str("\\\\"),
365            '"' => out.push_str("\\\""),
366            '\n' => out.push_str("\\n"),
367            '\t' => out.push_str("\\t"),
368            '\r' => out.push_str("\\r"),
369            _ => out.push(c),
370        }
371    }
372    out
373}
374
375/// Render a float so it always carries a decimal point, the way a float literal
376/// must in all three targets: `2.0`, not `2`.
377fn format_float(f: f64) -> String {
378    let s = format!("{}", f);
379    if s.contains('.') || s.contains('e') || s.contains("inf") || s.contains("NaN") {
380        s
381    } else {
382        format!("{}.0", s)
383    }
384}
385
386/// A lux identifier that collides with a reserved word in the target language
387/// gets a trailing `_` so the generated program still compiles — `where`
388/// becomes `where_`, `go` becomes `go_`. lux's own keywords never reach here
389/// (they aren't legal lux identifiers either), so each list below holds only the
390/// target words lux does *not* itself reserve.
391///
392/// This guards *value* names: functions, parameters, and locals. Type names,
393/// struct fields, and enum cases are left as written — a type called `map` is a
394/// documented rough edge (see learn-lux.md's scope notes), not a supported name.
395fn reserve(name: &str, words: &[&str]) -> String {
396    if words.contains(&name) {
397        format!("{name}_")
398    } else {
399        name.to_string()
400    }
401}
402
403/// Go keywords, plus the predeclared names the generated code itself relies on
404/// (`append`, `len`, `ptr`, …) where a user function of the same name would
405/// silently shadow the one the emitter emits.
406const GO_RESERVED: &[&str] = &[
407    "break",
408    "case",
409    "chan",
410    "const",
411    "continue",
412    "default",
413    "defer",
414    "else",
415    "fallthrough",
416    "for",
417    "func",
418    "go",
419    "goto",
420    "if",
421    "import",
422    "interface",
423    "map",
424    "package",
425    "range",
426    "return",
427    "select",
428    "struct",
429    "switch",
430    "type",
431    "var",
432    "append",
433    "cap",
434    "copy",
435    "delete",
436    "len",
437    "make",
438    "new",
439    "panic",
440    "recover",
441    "ptr",
442    "any",
443    "nil",
444    "iota",
445];
446
447fn go_ident(name: &str) -> String {
448    reserve(name, GO_RESERVED)
449}
450
451/// Rust's strict and reserved keywords (2021 edition).
452const RUST_RESERVED: &[&str] = &[
453    "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern",
454    "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub",
455    "ref", "return", "self", "Self", "static", "struct", "super", "trait", "true", "type", "union",
456    "unsafe", "use", "where", "while", "abstract", "become", "box", "do", "final", "gen", "macro",
457    "override", "priv", "try", "typeof", "unsized", "virtual", "yield",
458];
459
460fn rust_ident(name: &str) -> String {
461    reserve(name, RUST_RESERVED)
462}
463
464/// Swift's keywords (declaration, statement, and expression).
465const SWIFT_RESERVED: &[&str] = &[
466    "associatedtype",
467    "class",
468    "deinit",
469    "enum",
470    "extension",
471    "fileprivate",
472    "func",
473    "import",
474    "init",
475    "inout",
476    "internal",
477    "let",
478    "open",
479    "operator",
480    "private",
481    "protocol",
482    "public",
483    "rethrows",
484    "static",
485    "struct",
486    "subscript",
487    "typealias",
488    "var",
489    "actor",
490    "break",
491    "case",
492    "continue",
493    "default",
494    "defer",
495    "do",
496    "else",
497    "fallthrough",
498    "for",
499    "guard",
500    "if",
501    "in",
502    "repeat",
503    "return",
504    "switch",
505    "where",
506    "while",
507    "as",
508    "Any",
509    "catch",
510    "false",
511    "is",
512    "nil",
513    "super",
514    "self",
515    "Self",
516    "throw",
517    "throws",
518    "true",
519    "try",
520    "await",
521    "async",
522    "some",
523    "any",
524];
525
526fn swift_ident(name: &str) -> String {
527    reserve(name, SWIFT_RESERVED)
528}