Skip to main content

candela/compiler/
expr.rs

1// This file is derived from keel (https://github.com/horacehoff/keel),
2// Copyright 2026 Horace Hoff, licensed under the Apache License, Version 2.0.
3// It has been modified by the candela authors. See the NOTICE file.
4use super::type_system::ReturnAnnotation;
5use super::type_system::TypeExpr;
6use super::type_system::TypeParams;
7pub use crate::rt::Span;
8use smol_strc::SmolStr;
9use smol_strc::ToSmolStr;
10use std::{hint::unreachable_unchecked, rc::Rc};
11
12/// Separator between a type name and a method name in a mangled method symbol.
13///
14/// `impl` methods lower to per-type-unique free functions named `Type#method`.
15/// `#` is not a legal character in a candela identifier (which is
16/// `[a-zA-Z_][a-zA-Z0-9_]*`) and is never produced by the lexer, so a mangled
17/// name can never collide with a user-written free function or with a method of
18/// another type: `Point#len` and `Str#len` are distinct symbols, and both are
19/// distinct from a free `fn len`.
20pub const METHOD_SEP: char = '#';
21
22/// Builds the mangled free-function symbol an `impl Type { fn method ... }`
23/// lowers to.
24///
25/// Used by the parser when lowering method declarations and by the
26/// compiler/type-checker when resolving a `recv.method(...)` call site.
27#[must_use]
28pub fn mangle_method(type_name: &str, method_name: &str) -> SmolStr {
29    format_args!("{type_name}{METHOD_SEP}{method_name}").to_smolstr()
30}
31
32/// What an import adds to the scope of the file that writes it.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum ImportBinding {
35    /// The module under one name, reached with `::`: the last segment of the
36    /// path (`import "std/json";` binds `json`) or the name after `as`.
37    Namespace(SmolStr),
38    /// `import "std/assert" { eq, ne };`: the named items, each with the span
39    /// of its name, merged into the file's own scope. The module itself is
40    /// bound under no name.
41    Items(Box<[(SmolStr, Span)]>),
42}
43
44/// Where a struct literal written with `..` takes the fields it does not
45/// write from.
46#[derive(Debug, Clone, PartialEq)]
47pub enum StructRest {
48    /// `..base`: from another value of the struct's type.
49    Base(Expr),
50    /// A bare `..`: from the struct's default.
51    Default,
52}
53
54impl StructRest {
55    /// The expression a `..base` names, `None` for a bare `..`.
56    #[must_use]
57    pub const fn base(&self) -> Option<&Expr> {
58        match self {
59            Self::Base(base) => Some(base),
60            Self::Default => None,
61        }
62    }
63}
64
65/// What the right side of `is` names.
66#[derive(Debug, Clone, PartialEq)]
67pub enum IsTarget {
68    /// A type: `int`, `Point`, `int[]`, `{string: any}`, a union. A bare name
69    /// that names no type but a variant without a payload (`None`) is a
70    /// variant test.
71    Type(TypeExpr),
72    /// A variant written the way a `match` arm writes it, `Some(x)`: an
73    /// [`Expr::FunctionCall`] whose arguments name the payload binders.
74    Variant(Expr),
75}
76
77/// The type a `let` declares for its variable: `let xs: int[] = [];`.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct LetAnnotation {
80    pub ty: TypeExpr,
81    /// Where the type was written.
82    pub span: Span,
83    /// Where the value the variable starts with was written.
84    pub value_span: Span,
85}
86
87#[derive(Debug, Clone, PartialEq)]
88pub enum Expr {
89    Float(f64),
90    Int(i64),
91    Bool(bool),
92    Null,
93    String(SmolStr),
94    Var(SmolStr, Span),
95    /// Array(contents, [entire_array, elem_spans...])
96    Array(Box<[Self]>, Box<[Span]>),
97    /// Map(key-value pairs, span)
98    Map(Box<[(Self, Span, Self, Span)]>, Span),
99    /// Struct(name, fields, span, type_args, base)
100    ///
101    /// `type_args` holds the arguments of a generic struct literal
102    /// (`Cell<int>{ value: 3 }`) and is empty otherwise. `base` is what a
103    /// literal written with `..` takes the fields it does not write from,
104    /// `..base` (`Opts { cwd: "a", ..base }`) or a bare `..` for the struct's
105    /// default, with the span it was written at.
106    Struct(
107        Box<[SmolStr]>,
108        Box<[(SmolStr, Self, Span, Span)]>,
109        Span,
110        Box<[TypeExpr]>,
111        Option<Box<(StructRest, Span)>>,
112    ),
113    /// StructDeclare(name, fields, span, type_params, defaults)
114    ///
115    /// `defaults` holds the `= value` each field was declared with and the
116    /// span it was written at, one entry per field, and is empty when no field
117    /// declares one.
118    StructDeclare(
119        SmolStr,
120        Box<[(SmolStr, TypeExpr, Span)]>,
121        Span,
122        TypeParams,
123        Box<[Option<(Self, Span)>]>,
124    ),
125    /// StructDefault(struct_id, span)
126    ///
127    /// The default value of a struct, by its id: what `S::default()` and a
128    /// field of a struct type left to its default lower to. Built by the
129    /// compiler, never by the parser.
130    StructDefault(u16, Span),
131    /// EnumDeclare(name, variants: [(variant_name, payload_types, name_span)], span, type_params)
132    EnumDeclare(
133        SmolStr,
134        Box<[(SmolStr, Box<[TypeExpr]>, Span)]>,
135        Span,
136        TypeParams,
137    ),
138    /// NamespacedRef(path, span, type_args)
139    ///
140    /// A bare namespaced identifier (`Color::Red`) with no call parentheses or
141    /// struct braces. The only such form candela has is a nullary enum-variant
142    /// construction; the compiler resolves it against the enum registry.
143    /// `type_args` names the instantiation of a generic enum
144    /// (`Slot<int>::Empty`) and is empty otherwise.
145    NamespacedRef(Box<[SmolStr]>, Span, Box<[TypeExpr]>),
146    /// Match(scrutinee, arms: [(patterns, body)], wildcard_body, span, is_value)
147    ///
148    /// The arm patterns are parsed as ordinary expressions; the compiler picks
149    /// the lowering by the scrutinee's static type. For an enum scrutinee each
150    /// pattern is a variant pattern (`Circle(r)` binds the payload); otherwise
151    /// each pattern is an equality test against the scrutinee. An arm holds one
152    /// pattern per `|` alternative and runs when any of them matches.
153    ///
154    /// `is_value` marks a `match` written where a value goes. Each of its
155    /// bodies is one expression, the arm's value, and it covers every case.
156    Match(
157        Box<Self>,
158        Box<[(Box<[Self]>, Box<[Self]>)]>,
159        Option<Box<[Self]>>,
160        Span,
161        bool,
162    ),
163    /// GetStructField(struct_expr, field, struct_span, field_span, value_span)
164    GetStructField(Box<Self>, SmolStr, Span, Span),
165    /// SetStructField(struct_expr, field, new_expr, struct_span, field_span, value_span)
166    SetStructField(Box<Self>, SmolStr, Box<Self>, Span, Span, Span),
167    /// VarDeclare(name, value, annotation)
168    ///
169    /// `annotation` is the type a `let x: T = value;` declares, `None` for a
170    /// `let` written without one.
171    VarDeclare(SmolStr, Box<Self>, Option<Box<LetAnnotation>>),
172    /// VarAssign(name, value, name_span, value_span)
173    VarAssign(SmolStr, Box<Self>, Span, Span),
174    /// Condition(condition, code (contains else_if_blocks and potentially
175    /// else_block), span, condition_span)
176    ///
177    /// The condition carries its own span so a condition whose type is not
178    /// `bool` is reported against the condition rather than the whole `if`.
179    Condition(Box<Self>, Box<[Self]>, Span, Span),
180    /// InlineCondition - expression-form if/else, always produces a value, must have an else branch
181    InlineCondition(Box<Self>, Box<[Self]>, Span, Span),
182    /// ElseIfBlock(condition, code, condition_span)
183    ElseIfBlock(Box<Self>, Box<[Self]>, Span),
184    ElseBlock(Box<[Self]>),
185
186    /// AnonymousFunction(args, code, span)
187    AnonymousFunction(Box<[SmolStr]>, Box<[Self]>, Span),
188    // AnonymousFunction(Box<[(SmolStr, SmolStr)]>, SmolStr, Box<[Self]>, Span),
189    /// WhileBlock(condition, code, condition_span)
190    WhileBlock(Box<Self>, Box<[Self]>, Span),
191    /// FunctionCall(args, (optional namespace + name), span, (arg_start,arg_end), type_args)
192    ///
193    /// `type_args` holds the type arguments of a call written with them
194    /// (`first<int>(nums)`) and is empty otherwise.
195    FunctionCall(
196        Box<[Self]>,
197        Box<[SmolStr]>,
198        Span,
199        Box<[Span]>,
200        Box<[TypeExpr]>,
201    ),
202    /// ObjFunctionCall(obj, args, namespace, obj_span, fn_span, arg_markers, type_args)
203    ///
204    /// `type_args` holds the type arguments a method call is written with
205    /// (`b.tagged<string>("hi")`), which bind the method's own type parameters,
206    /// and is empty otherwise.
207    ObjFunctionCall(
208        // Will be removed soon.
209        Box<Self>,
210        Box<[Self]>,
211        Box<[SmolStr]>,
212        // obj_span
213        Span,
214        // fn_span
215        Span,
216        Box<[Span]>,
217        Box<[TypeExpr]>,
218    ),
219    /// CallValue(callee, args, callee_span, arg_markers)
220    ///
221    /// A call whose callee is an expression rather than a name: what another
222    /// call handed back, an element of a list, a value read out of a map. The
223    /// callee's static `Fn` type names the function the same way the type of a
224    /// `let`-bound closure does.
225    CallValue(Box<Self>, Box<[Self]>, Span, Box<[Span]>),
226    /// FunctionDecl(name, args, code, name_span, return_type, type_params)
227    ///
228    /// `return_type` carries the optional `-> Type` annotation together with the
229    /// span of the annotation itself, which is what a mismatch between the
230    /// declared and the returned type is reported against. `type_params` names
231    /// the type parameters of a generic function (`fn first<T>`) and is empty
232    /// otherwise.
233    FunctionDecl(
234        SmolStr,
235        Box<[(SmolStr, Option<TypeExpr>)]>,
236        Rc<[Self]>,
237        Span,
238        ReturnAnnotation,
239        TypeParams,
240    ),
241
242    ReturnVal(Box<Option<Self>>),
243
244    ArrayGetIndex(Box<Self>, Box<Self>, Span),
245    /// Propagate(operand, span)
246    ///
247    /// `operand?`: the value inside a `Some` or an `Ok`, or an early return of
248    /// the `None` or the `Err` from the function it is written in.
249    Propagate(Box<Self>, Span),
250    /// Is(operand, target, operand_span, target_span)
251    ///
252    /// `operand is T` tests the operand's type and `operand is Some(x)` its
253    /// variant; either is a `bool`. Where the test is known to hold (the body
254    /// of an `if` or `while` it is the condition of, and the right of an `&&`
255    /// after it) a variable operand has the type tested for, and a variant
256    /// test's binders hold the payload.
257    Is(Box<Self>, Box<IsTarget>, Span, Span),
258    /// Cast(operand, type, operand_span, type_span)
259    ///
260    /// `operand as T`: the operand, typed `T`, checked while the program runs
261    /// where its static type does not already settle it. A value of another
262    /// type raises `bad_downcast`.
263    Cast(Box<Self>, Box<TypeExpr>, Span, Span),
264    /// ArrayGetSlice(array, range_start, range_end, span)
265    ///
266    /// `range_end` is `None` for an open-ended slice, `a[i..]`, which runs to
267    /// the end of the list or string.
268    ArrayGetSlice(Box<Self>, Box<Self>, Option<Box<Self>>, Span),
269    ArrayModify(Box<Self>, Box<Self>, Box<Self>, Span, Span),
270
271    /// ForLoop(loop_var_name, loop_array+code, obj_markers)
272    ForLoop(SmolStr, Box<Self>, Box<[Self]>, Span),
273    /// IntForLoop(loop_var_name, first_elem, final_elem, code)
274    IntForLoop(SmolStr, Box<Self>, Box<Self>, Box<[Self]>, Span, Span),
275    /// ImportDylib(lib_path, [(fn_name, fn_args, fn_return_type, fn_name_span)], (start, end))
276    ImportDylib(
277        SmolStr,
278        Box<[(SmolStr, Box<[TypeExpr]>, TypeExpr, Span)]>,
279        Span,
280    ),
281
282    /// HostBlock(namespace, [(fn_name, fn_args, fn_return_type, fn_name_span)], (start, end), structs)
283    ///
284    /// Declares a host namespace whose functions are backed by Rust closures
285    /// registered on the embedding [`crate::Engine`]. Mirrors [`Self::ImportDylib`]
286    /// but dispatches to a registered closure instead of a C symbol. `structs`
287    /// holds the [`Self::StructDeclare`]s the block declares, which a script
288    /// names behind the namespace (`process::StartOptions`).
289    HostBlock(
290        SmolStr,
291        Box<[(SmolStr, Box<[TypeExpr]>, TypeExpr, Span)]>,
292        Span,
293        Box<[Self]>,
294    ),
295
296    /// ImportFile(path, binding, is_logical, (start, end))
297    ///
298    /// `is_logical` marks a library import (`import "std/string";`, an
299    /// extensionless path): the resolver maps it straight to the shipped
300    /// library directory and never looks source-relative. A `.cdl` file import
301    /// (`import "./local.cdl";`) has `is_logical` false and resolves
302    /// source-relative first. `binding` says what the import adds to the
303    /// importing file's scope; see [`ImportBinding`].
304    ImportFile(SmolStr, ImportBinding, bool, Span),
305
306    Break,
307    Continue,
308
309    EvalBlock(Box<[Self]>),
310    LoopBlock(Box<[Self]>),
311
312    /// TryCatchBlock(try_code, err_var, catch_code)
313    TryCatchBlock(Box<[Self]>, SmolStr, Box<[Self]>),
314
315    Mul(Box<Self>, Box<Self>, Span, Span),
316    Div(Box<Self>, Box<Self>, Span, Span),
317    Add(Box<Self>, Box<Self>, Span, Span),
318    Sub(Box<Self>, Box<Self>, Span, Span),
319    Mod(Box<Self>, Box<Self>, Span, Span),
320    Pow(Box<Self>, Box<Self>, Span, Span),
321    BitAnd(Box<Self>, Box<Self>, Span, Span),
322    BitOr(Box<Self>, Box<Self>, Span, Span),
323    BitXor(Box<Self>, Box<Self>, Span, Span),
324    Shl(Box<Self>, Box<Self>, Span, Span),
325    Shr(Box<Self>, Box<Self>, Span, Span),
326    BitNot(Box<Self>, Span, Span),
327    Eq(Box<Self>, Box<Self>, Span, Span),
328    NotEq(Box<Self>, Box<Self>, Span, Span),
329    Sup(Box<Self>, Box<Self>, Span, Span),
330    SupEq(Box<Self>, Box<Self>, Span, Span),
331    Inf(Box<Self>, Box<Self>, Span, Span),
332    InfEq(Box<Self>, Box<Self>, Span, Span),
333    BoolAnd(Box<Self>, Box<Self>, Span, Span),
334    BoolOr(Box<Self>, Box<Self>, Span, Span),
335    BoolNeg(Box<Self>, Span, Span),
336    Neg(Box<Self>, Span, Span),
337}
338
339impl Expr {
340    /// Whether the only way to compile this form is as a value, so the
341    /// register it lands in has to be read back.
342    ///
343    /// [`Expr::compile`] takes a `uses_id` flag saying whether the caller
344    /// reads the value produced, and the two answers reach different arms. A
345    /// literal, a variable, an operator and a field read have a value arm and
346    /// nothing else; a declaration, a loop, an assignment and a control-flow
347    /// form have a statement arm and nothing else; a call has both, and its
348    /// statement arm is the one that skips the result register. A form in the
349    /// first group still compiles as a value when it stands alone as a
350    /// statement, and the register is freed straight away.
351    ///
352    /// The list is the arms of [`Expr::compile_with_code_context`] that open
353    /// with `debug_assert!(uses_id)`, and the two are kept in step by hand: an
354    /// arm that gains or loses that assertion belongs here or leaves here in
355    /// the same change, or a statement of that form stops on the assertion it
356    /// no longer matches.
357    #[must_use]
358    pub const fn is_value_only(&self) -> bool {
359        matches!(
360            self,
361            Self::Float(_)
362                | Self::Int(_)
363                | Self::Bool(_)
364                | Self::Null
365                | Self::String(_)
366                | Self::Var(_, _)
367                | Self::Array(_, _)
368                | Self::Map(_, _)
369                | Self::Struct(..)
370                | Self::StructDefault(..)
371                | Self::NamespacedRef(_, _, _)
372                | Self::GetStructField(_, _, _, _)
373                | Self::InlineCondition(_, _, _, _)
374                | Self::Match(_, _, _, _, true)
375                | Self::AnonymousFunction(_, _, _)
376                | Self::ArrayGetIndex(_, _, _)
377                | Self::Propagate(_, _)
378                | Self::Is(..)
379                | Self::Cast(..)
380                | Self::ArrayGetSlice(..)
381                | Self::Mul(..)
382                | Self::Div(..)
383                | Self::Add(..)
384                | Self::Sub(..)
385                | Self::Mod(..)
386                | Self::Pow(..)
387                | Self::BitAnd(..)
388                | Self::BitOr(..)
389                | Self::BitXor(..)
390                | Self::Shl(..)
391                | Self::Shr(..)
392                | Self::BitNot(_, _, _)
393                | Self::Eq(..)
394                | Self::NotEq(..)
395                | Self::Sup(..)
396                | Self::SupEq(..)
397                | Self::Inf(..)
398                | Self::InfEq(..)
399                | Self::BoolAnd(..)
400                | Self::BoolOr(..)
401                | Self::BoolNeg(_, _, _)
402                | Self::Neg(_, _, _)
403        )
404    }
405}
406
407#[cold]
408#[inline(never)]
409#[must_use]
410pub const fn symbol_of_expr(expr: &Expr) -> &'static str {
411    match expr {
412        Expr::Mul(_, _, _, _) => "*",
413        Expr::Div(_, _, _, _) => "/",
414        Expr::Add(_, _, _, _) => "+",
415        Expr::Sub(_, _, _, _) | Expr::Neg(_, _, _) => "-",
416        Expr::Mod(_, _, _, _) => "%",
417        Expr::Pow(_, _, _, _) => "^",
418        Expr::BitAnd(_, _, _, _) => "&",
419        Expr::BitOr(_, _, _, _) => "|",
420        Expr::BitXor(_, _, _, _) => "^^",
421        Expr::Shl(_, _, _, _) => "<<",
422        Expr::Shr(_, _, _, _) => ">>",
423        Expr::BitNot(_, _, _) => "~",
424        Expr::Eq(..) => "==",
425        Expr::NotEq(..) => "!=",
426        Expr::Sup(_, _, _, _) => ">",
427        Expr::SupEq(_, _, _, _) => ">=",
428        Expr::Inf(_, _, _, _) => "<",
429        Expr::InfEq(_, _, _, _) => "<=",
430        Expr::BoolAnd(_, _, _, _) => "&&",
431        Expr::BoolOr(_, _, _, _) => "||",
432        _ => unsafe { unreachable_unchecked() },
433    }
434}
435
436/// The method name unary minus takes.
437///
438/// `-` is the one operator with two forms, and both may be defined on one
439/// type, so negation cannot share subtraction's symbol as a method name.
440/// Subtraction keeps `-` and negation takes this, which no identifier and no
441/// other operator spells.
442pub const UNARY_MINUS_METHOD: &str = "neg-";
443
444/// The operator symbols a method may be named after, as a diagnostic lists
445/// them. Unary minus is written `-` like subtraction; the parameter count
446/// tells the two apart.
447pub const OPERATOR_SYMBOLS: &str = "+ - * / % ^ & | ^^ << >> == < <= ~";
448
449/// How an operator reaches the method a type defines.
450#[derive(Debug, Clone, Copy, PartialEq, Eq)]
451pub enum OperatorForm {
452    /// The method named by this operator, operands in the order they are
453    /// written.
454    Direct,
455    /// `a != b` is `a == b` negated: the `==` method, then the bool flipped.
456    Negated,
457    /// `a > b` is `b < a`: the `<` method with the operands swapped, which is
458    /// why the right operand is evaluated first.
459    Swapped,
460}
461
462/// The method an operator symbol resolves to on a type that defines one, and
463/// the form the call takes.
464///
465/// `!=`, `>` and `>=` have no method of their own: they are derived from `==`,
466/// `<` and `<=`, so a type defines three comparisons and gets six operators.
467/// `&&`, `||`, `!` and assignment are not overloadable and answer `None`.
468#[must_use]
469pub fn operator_method(symbol: &str) -> Option<(&'static str, OperatorForm)> {
470    Some(match symbol {
471        "+" => ("+", OperatorForm::Direct),
472        "-" => ("-", OperatorForm::Direct),
473        "*" => ("*", OperatorForm::Direct),
474        "/" => ("/", OperatorForm::Direct),
475        "%" => ("%", OperatorForm::Direct),
476        "^" => ("^", OperatorForm::Direct),
477        "&" => ("&", OperatorForm::Direct),
478        "|" => ("|", OperatorForm::Direct),
479        "^^" => ("^^", OperatorForm::Direct),
480        "<<" => ("<<", OperatorForm::Direct),
481        ">>" => (">>", OperatorForm::Direct),
482        "==" => ("==", OperatorForm::Direct),
483        "<" => ("<", OperatorForm::Direct),
484        "<=" => ("<=", OperatorForm::Direct),
485        "~" => ("~", OperatorForm::Direct),
486        UNARY_MINUS_METHOD => (UNARY_MINUS_METHOD, OperatorForm::Direct),
487        "!=" => ("==", OperatorForm::Negated),
488        ">" => ("<", OperatorForm::Swapped),
489        ">=" => ("<=", OperatorForm::Swapped),
490        _ => return None,
491    })
492}
493
494/// Whether a method name is one of the operators, so it is declared with an
495/// operator symbol rather than an identifier.
496#[must_use]
497pub fn is_operator_method(name: &str) -> bool {
498    matches!(operator_method(name), Some((_, OperatorForm::Direct)))
499}
500
501/// Whether an operator method takes one operand rather than two.
502#[must_use]
503pub fn is_unary_operator_method(name: &str) -> bool {
504    name == UNARY_MINUS_METHOD || name == "~"
505}
506
507/// Whether an operator method has to hand back a `bool`.
508///
509/// `==`, `<` and `<=` answer a question about two values, and `!=`, `>` and
510/// `>=` are derived from them, so a program reading one of the six as a
511/// condition has to get a `bool` whatever the operands are.
512#[must_use]
513pub fn operator_method_answers_bool(name: &str) -> bool {
514    matches!(name, "==" | "<" | "<=")
515}
516
517/// The operator method this node reaches on the body's own receiver, for a
518/// node whose receiver is `self`.
519///
520/// A method that applies its own operator to `self` calls itself, and the
521/// recursion check works off the names a body calls; without this a method
522/// like `fn +(self, o)` returning `self + o` under a guard is inlined into
523/// itself forever. The derived forms swap or negate, so `>` looks at the
524/// right operand and `!=` at the left.
525#[must_use]
526pub fn self_operator_method(expr: &Expr) -> Option<&'static str> {
527    fn is_self(expr: &Expr) -> bool {
528        matches!(expr, Expr::Var(name, _) if name.as_str() == "self")
529    }
530    let (symbol, receiver) = match expr {
531        Expr::Neg(x, _, _) => (UNARY_MINUS_METHOD, &**x),
532        Expr::BitNot(x, _, _) => ("~", &**x),
533        Expr::Mul(l, r, _, _)
534        | Expr::Div(l, r, _, _)
535        | Expr::Add(l, r, _, _)
536        | Expr::Sub(l, r, _, _)
537        | Expr::Mod(l, r, _, _)
538        | Expr::Pow(l, r, _, _)
539        | Expr::BitAnd(l, r, _, _)
540        | Expr::BitOr(l, r, _, _)
541        | Expr::BitXor(l, r, _, _)
542        | Expr::Shl(l, r, _, _)
543        | Expr::Shr(l, r, _, _)
544        | Expr::Eq(l, r, _, _)
545        | Expr::NotEq(l, r, _, _)
546        | Expr::Inf(l, r, _, _)
547        | Expr::InfEq(l, r, _, _)
548        | Expr::Sup(l, r, _, _)
549        | Expr::SupEq(l, r, _, _) => {
550            let symbol = symbol_of_expr(expr);
551            let (method, form) = operator_method(symbol)?;
552            let receiver = if form == OperatorForm::Swapped { r } else { l };
553            return is_self(receiver).then_some(method);
554        }
555        _ => return None,
556    };
557    let (method, _) = operator_method(symbol)?;
558    is_self(receiver).then_some(method)
559}
560
561#[must_use]
562pub fn code_modifies_variable(var_name: &SmolStr, code: &[Expr]) -> bool {
563    code.iter().any(|expr| match expr {
564        Expr::VarAssign(n, _, _, _) => n == var_name,
565        Expr::Condition(_, body, _, _)
566        | Expr::WhileBlock(_, body, _)
567        | Expr::EvalBlock(body)
568        | Expr::LoopBlock(body)
569        | Expr::InlineCondition(_, body, _, _)
570        | Expr::ElseIfBlock(_, body, _)
571        | Expr::ElseBlock(body)
572        | Expr::ForLoop(_, _, body, _)
573        | Expr::IntForLoop(_, _, _, body, _, _) => code_modifies_variable(var_name, body),
574        // A catch binds its own name for the error; an assignment to a
575        // variable of the same name there writes the error binding instead.
576        Expr::TryCatchBlock(try_code, err_var, catch_code) => {
577            code_modifies_variable(var_name, try_code)
578                || (err_var != var_name && code_modifies_variable(var_name, catch_code))
579        }
580        Expr::Match(_, arms, wildcard, _, _) => {
581            arms.iter()
582                .any(|(_, body)| code_modifies_variable(var_name, body))
583                || wildcard
584                    .as_ref()
585                    .is_some_and(|body| code_modifies_variable(var_name, body))
586        }
587        _ => false,
588    })
589}
590
591#[must_use]
592pub fn var_assign(target: Expr, value: Expr, expr_span: Span, value_span: Span) -> Expr {
593    if let Expr::Var(n, s) = target {
594        Expr::VarAssign(n, Box::from(value), s, value_span)
595    } else if let Expr::ArrayGetIndex(base, idx, _) = target {
596        Expr::ArrayModify(base, idx, Box::from(value), expr_span, value_span)
597    } else if let Expr::GetStructField(obj, field, obj_span, field_span) = target {
598        Expr::SetStructField(
599            obj,
600            field,
601            Box::from(value),
602            obj_span,
603            field_span,
604            value_span,
605        )
606    } else {
607        unsafe { unreachable_unchecked() }
608    }
609}
610
611/// Records a name the code read or wrote without binding it first, when the
612/// walk is inside an anonymous function.
613fn use_free_name(name: &SmolStr, depth: u32, bound: &[SmolStr], out: &mut Vec<SmolStr>) {
614    if depth > 0 && !bound.contains(name) && !out.contains(name) {
615        out.push(name.clone());
616    }
617}
618
619/// Walks a block in its own scope: the names it declares go out of scope with
620/// it, so a closure written after it does not see them.
621fn scan_block_free_names(
622    code: &[Expr],
623    depth: u32,
624    bound: &mut Vec<SmolStr>,
625    out: &mut Vec<SmolStr>,
626) {
627    let bound_len = bound.len();
628    for expr in code {
629        scan_free_names(expr, depth, bound, out);
630    }
631    bound.truncate(bound_len);
632}
633
634/// Collects, in the order they first appear, the names the anonymous
635/// functions in `expr` read or write from the scope around them.
636///
637/// `bound` holds what is in scope at the point being walked, so a name it does
638/// not hold comes from further out, which is what a closure captures. `depth`
639/// counts the anonymous functions the walk is inside: a name used outside one
640/// is resolved where it stands and is not recorded. A call name counts as a
641/// use, since the callee may be a closure held in a variable; a name that
642/// turns out to name a declared function is dropped by the caller, which keeps
643/// only the names that resolve to variables. A nested function declaration has
644/// a scope of its own and reads nothing from around it, so its body is
645/// skipped.
646fn scan_free_names(expr: &Expr, depth: u32, bound: &mut Vec<SmolStr>, out: &mut Vec<SmolStr>) {
647    match expr {
648        Expr::Var(name, _) => use_free_name(name, depth, bound, out),
649        Expr::VarAssign(name, value, _, _) => {
650            scan_free_names(value, depth, bound, out);
651            use_free_name(name, depth, bound, out);
652        }
653        Expr::VarDeclare(name, value, _) => {
654            scan_free_names(value, depth, bound, out);
655            bound.push(name.clone());
656        }
657        Expr::AnonymousFunction(params, body, _) => {
658            let bound_len = bound.len();
659            bound.extend(params.iter().cloned());
660            scan_block_free_names(body, depth + 1, bound, out);
661            bound.truncate(bound_len);
662        }
663        Expr::FunctionCall(args, namespace, _, _, _) => {
664            for arg in args {
665                scan_free_names(arg, depth, bound, out);
666            }
667            if let [name] = &**namespace {
668                use_free_name(name, depth, bound, out);
669            }
670        }
671        Expr::ObjFunctionCall(obj, args, _, _, _, _, _) => {
672            scan_free_names(obj, depth, bound, out);
673            for arg in args {
674                scan_free_names(arg, depth, bound, out);
675            }
676        }
677        Expr::CallValue(callee, args, _, _) => {
678            scan_free_names(callee, depth, bound, out);
679            for arg in args {
680                scan_free_names(arg, depth, bound, out);
681            }
682        }
683        Expr::Array(items, _) => {
684            for item in items {
685                scan_free_names(item, depth, bound, out);
686            }
687        }
688        Expr::Map(pairs, _) => {
689            for (key, _, value, _) in pairs {
690                scan_free_names(key, depth, bound, out);
691                scan_free_names(value, depth, bound, out);
692            }
693        }
694        Expr::Struct(_, fields, _, _, base) => {
695            for (_, value, _, _) in fields {
696                scan_free_names(value, depth, bound, out);
697            }
698            if let Some(base) = base.as_ref().and_then(|b| b.0.base()) {
699                scan_free_names(base, depth, bound, out);
700            }
701        }
702        Expr::Match(scrutinee, arms, wildcard, _, _) => {
703            scan_free_names(scrutinee, depth, bound, out);
704            for (patterns, body) in arms {
705                let bound_len = bound.len();
706                // An enum pattern binds its payload for the arm body; any
707                // other pattern is a value the scrutinee is compared against.
708                for pattern in patterns {
709                    if let Expr::FunctionCall(binders, _, _, _, _) = pattern {
710                        for binder in binders {
711                            if let Expr::Var(name, _) = binder {
712                                bound.push(name.clone());
713                            }
714                        }
715                    } else {
716                        scan_free_names(pattern, depth, bound, out);
717                    }
718                }
719                scan_block_free_names(body, depth, bound, out);
720                bound.truncate(bound_len);
721            }
722            if let Some(body) = wildcard {
723                scan_block_free_names(body, depth, bound, out);
724            }
725        }
726        Expr::Condition(condition, body, ..)
727        | Expr::InlineCondition(condition, body, ..)
728        | Expr::ElseIfBlock(condition, body, ..)
729        | Expr::WhileBlock(condition, body, ..) => {
730            // What an `is` in the condition binds is in scope for the
731            // condition and the body, and goes out of scope with them.
732            let bound_len = bound.len();
733            scan_free_names(condition, depth, bound, out);
734            scan_block_free_names(body, depth, bound, out);
735            bound.truncate(bound_len);
736        }
737        Expr::Is(operand, target, _, _) => {
738            scan_free_names(operand, depth, bound, out);
739            if let IsTarget::Variant(Expr::FunctionCall(binders, _, _, _, _)) = &**target {
740                for binder in binders {
741                    if let Expr::Var(name, _) = binder {
742                        bound.push(name.clone());
743                    }
744                }
745            }
746        }
747        Expr::ElseBlock(body) | Expr::EvalBlock(body) | Expr::LoopBlock(body) => {
748            scan_block_free_names(body, depth, bound, out);
749        }
750        Expr::ForLoop(var_name, iterated, body, _) => {
751            scan_free_names(iterated, depth, bound, out);
752            let bound_len = bound.len();
753            bound.push(var_name.clone());
754            scan_block_free_names(body, depth, bound, out);
755            bound.truncate(bound_len);
756        }
757        Expr::IntForLoop(var_name, start, end, body, _, _) => {
758            scan_free_names(start, depth, bound, out);
759            scan_free_names(end, depth, bound, out);
760            let bound_len = bound.len();
761            bound.push(var_name.clone());
762            scan_block_free_names(body, depth, bound, out);
763            bound.truncate(bound_len);
764        }
765        Expr::TryCatchBlock(try_code, err_var, catch_code) => {
766            scan_block_free_names(try_code, depth, bound, out);
767            let bound_len = bound.len();
768            bound.push(err_var.clone());
769            scan_block_free_names(catch_code, depth, bound, out);
770            bound.truncate(bound_len);
771        }
772        Expr::ReturnVal(value) => {
773            if let Some(value) = value.as_ref() {
774                scan_free_names(value, depth, bound, out);
775            }
776        }
777        Expr::GetStructField(obj, _, _, _)
778        | Expr::Propagate(obj, _)
779        | Expr::Cast(obj, _, _, _)
780        | Expr::BoolNeg(obj, _, _)
781        | Expr::Neg(obj, _, _)
782        | Expr::BitNot(obj, _, _) => {
783            scan_free_names(obj, depth, bound, out);
784        }
785        Expr::SetStructField(obj, _, value, _, _, _) => {
786            scan_free_names(obj, depth, bound, out);
787            scan_free_names(value, depth, bound, out);
788        }
789        Expr::ArrayGetIndex(base, index, _) => {
790            scan_free_names(base, depth, bound, out);
791            scan_free_names(index, depth, bound, out);
792        }
793        Expr::ArrayGetSlice(base, start, end, _) => {
794            scan_free_names(base, depth, bound, out);
795            scan_free_names(start, depth, bound, out);
796            if let Some(end) = end {
797                scan_free_names(end, depth, bound, out);
798            }
799        }
800        Expr::ArrayModify(base, start, end, _, _) => {
801            scan_free_names(base, depth, bound, out);
802            scan_free_names(start, depth, bound, out);
803            scan_free_names(end, depth, bound, out);
804        }
805        Expr::Mul(l, r, _, _)
806        | Expr::Div(l, r, _, _)
807        | Expr::Add(l, r, _, _)
808        | Expr::Sub(l, r, _, _)
809        | Expr::Mod(l, r, _, _)
810        | Expr::Pow(l, r, _, _)
811        | Expr::BitAnd(l, r, _, _)
812        | Expr::BitOr(l, r, _, _)
813        | Expr::BitXor(l, r, _, _)
814        | Expr::Shl(l, r, _, _)
815        | Expr::Shr(l, r, _, _)
816        | Expr::Eq(l, r, _, _)
817        | Expr::NotEq(l, r, _, _)
818        | Expr::Sup(l, r, _, _)
819        | Expr::SupEq(l, r, _, _)
820        | Expr::Inf(l, r, _, _)
821        | Expr::InfEq(l, r, _, _)
822        | Expr::BoolAnd(l, r, _, _)
823        | Expr::BoolOr(l, r, _, _) => {
824            scan_free_names(l, depth, bound, out);
825            scan_free_names(r, depth, bound, out);
826        }
827        // A literal, a type declaration, an import and a nested function
828        // declaration read nothing from the scope around them.
829        Expr::Float(_)
830        | Expr::Int(_)
831        | Expr::Bool(_)
832        | Expr::Null
833        | Expr::String(_)
834        | Expr::NamespacedRef(_, _, _)
835        | Expr::StructDefault(_, _)
836        | Expr::StructDeclare(..)
837        | Expr::EnumDeclare(_, _, _, _)
838        | Expr::FunctionDecl(_, _, _, _, _, _)
839        | Expr::ImportDylib(_, _, _)
840        | Expr::HostBlock(..)
841        | Expr::ImportFile(_, _, _, _)
842        | Expr::Break
843        | Expr::Continue => {}
844    }
845}
846
847/// The names an anonymous function reads or writes from the scope it is
848/// written in, in the order they first appear.
849///
850/// The caller keeps the ones that name a variable there: those are the
851/// closure's captures, and the rest name declared functions or nothing at all.
852#[must_use]
853pub fn closure_free_names(params: &[SmolStr], body: &[Expr]) -> Vec<SmolStr> {
854    let mut bound: Vec<SmolStr> = params.to_vec();
855    let mut out = Vec::new();
856    scan_block_free_names(body, 1, &mut bound, &mut out);
857    out
858}
859
860/// Whether an anonymous function written anywhere in `code` reads or writes
861/// `name` from the scope around it.
862///
863/// This is what decides whether a variable lives in a cell: one no closure
864/// names keeps its register and costs what it costs today.
865#[must_use]
866pub fn code_captures_variable(name: &SmolStr, code: &[Expr]) -> bool {
867    let mut bound = Vec::new();
868    let mut out = Vec::new();
869    scan_block_free_names(code, 0, &mut bound, &mut out);
870    out.contains(name)
871}