plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


plum-core/src/parser.rs
use tree_sitter::Node;
use crate::ast::*;

pub struct AstParser<'a> {
    source: &'a [u8],
}

impl<'a> AstParser<'a> {
    pub fn new(source: &'a str) -> Self {
        AstParser { source: source.as_bytes() }
    }

    fn text(&self, node: Node) -> String {
        node.utf8_text(self.source).unwrap_or("").to_string()
    }

    /// Peel transparent `expression` / `primary_expression` wrapper nodes.
    fn unwrapExprNode<'b>(&self, node: Node<'b>) -> Node<'b> {
        match node.kind() {
            "expression" | "primary_expression" => {
                node.named_child(0).map(|c| self.unwrapExprNode(c)).unwrap_or(node)
            }
            _ => node,
        }
    }

    /// Collect named children of `node` that have the given `kind`.
    fn childrenOfKind(&self, node: Node<'a>, kind: &str) -> Vec<Node<'a>> {
        let mut cursor = node.walk();
        node.named_children(&mut cursor)
            .filter(|n| n.kind() == kind)
            .collect()
    }

    // ---- top level --------------------------------------------------------

    pub fn parseSource(&self, node: Node) -> Source {
        assert_eq!(node.kind(), "source");
        let mut module = None;
        let mut imports = Vec::new();
        let mut items = Vec::new();
        let mut cursor = node.walk();
        for child in node.named_children(&mut cursor) {
            match child.kind() {
                "module" => module = Some(self.parseModule(child)),
                "import" => imports.push(self.parseImport(child)),
                "class" => {
                    let c = self.parseClass(child);
                    let nested = self.collectNestedFns(child, &c.name);
                    items.push(Item::Class(c));
                    items.extend(nested.into_iter().map(Item::Fn));
                }
                "trait" => items.push(Item::Trait(self.parseTrait(child))),
                "enum" => {
                    let e = self.parseEnum(child);
                    let nested = self.collectNestedFns(child, &e.name);
                    items.push(Item::Enum(e));
                    items.extend(nested.into_iter().map(Item::Fn));
                }
                "fn" => items.push(Item::Fn(self.parseFn(child))),
                "const" => items.push(Item::Const(self.parseConst(child))),
                _ => {}
            }
        }
        Source { module, imports, items }
    }

    fn parseModule(&self, node: Node) -> Module {
        // module: "module" mod_identifier
        // named_child(0) = mod_identifier
        let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
        Module { name }
    }

    fn parseImport(&self, node: Node) -> Import {
        // import: "import" url — url is the only named child
        let path = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
        Import { path }
    }

    // ---- class / trait / enum ---------------------------------------------

    /// Collects any `fn` named children nested directly inside a class/enum body and
    /// parses each as an ordinary top-level `Fn`, with `type_param` forced to `owner`
    /// regardless of whatever the nested `fn` itself parsed (a nested method's receiver
    /// is implicit from its enclosing declaration; if it also carries its own explicit,
    /// redundant `<Receiver>` annotation, that's simply overridden, not treated as a
    /// conflict/error).
    fn collectNestedFns(&self, node: Node, owner: &str) -> Vec<Fn> {
        self.childrenOfKind(node, "fn")
            .into_iter()
            .map(|n| {
                let mut f = self.parseFn(n);
                f.type_param = Some(owner.to_string());
                f
            })
            .collect()
    }

    fn parseClass(&self, node: Node) -> Class {
        // class: "type" type_identifier generics? ("(" type_identifier,* ")")? "=" body
        // Named children in order: type_identifier (name), generics? (declaration), type_identifier* (implements), field*
        let mut cursor = node.walk();
        let named: Vec<Node> = node.named_children(&mut cursor).collect();

        let name = named.first().map(|n| self.text(*n)).unwrap_or_default();

        // Skip the optional `generics` declaration node before looking for implements.
        let after_generics = if named.get(1).map(|n| n.kind()) == Some("generics") { 2 } else { 1 };

        // implements = type_identifiers that appear before any `field` node
        let implements: Vec<String> = named[after_generics..]
            .iter()
            .take_while(|n| n.kind() == "type_identifier")
            .map(|n| self.text(*n))
            .collect();

        let generics = self.parseGenericsField(node);

        let fields: Vec<Field> = named
            .iter()
            .filter(|n| n.kind() == "field")
            .map(|n| self.parseField(*n))
            .collect();

        Class { name, implements, generics, fields }
    }

    fn parseGenericsField(&self, node: Node) -> Vec<GenericParam> {
        // generics: "[" generic_type,* "]"  where  generic_type: generic (":" sep1(type_identifier, "+"))?
        //
        // `generic_type` is `inline`d in the grammar, so the `generics` node has NO
        // `generic_type` children — its named children are the single-uppercase-letter
        // `generic` nodes, each optionally followed by their bound `type_identifier`
        // nodes, all flattened together. Reconstruct each `GenericParam` by starting a
        // new one at every `generic` node and attaching any following
        // `type_identifier`s as its bounds until the next `generic` node.
        let Some(generics_node) = self.childrenOfKind(node, "generics").into_iter().next() else {
            return Vec::new();
        };
        let mut cursor = generics_node.walk();
        let mut params: Vec<GenericParam> = Vec::new();
        for child in generics_node.named_children(&mut cursor) {
            match child.kind() {
                "generic" => {
                    params.push(GenericParam { name: self.text(child), bounds: Vec::new() });
                }
                "type_identifier" => {
                    if let Some(last) = params.last_mut() {
                        last.bounds.push(self.text(child));
                    }
                }
                _ => {}
            }
        }
        params
    }

    fn parseField(&self, node: Node) -> Field {
        // class_field (aliased to field): var_identifier ":" type
        let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
        let ty = node
            .named_child(1)
            .map(|n| self.parseType(n))
            .unwrap_or(Type { name: String::new(), generics: vec![] });
        Field { name, ty }
    }

    fn parseTrait(&self, node: Node) -> Trait {
        // trait: "trait" type_identifier generics? "=" body
        let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
        let generics = self.parseGenericsField(node);
        let methods = self.childrenOfKind(node, "field")
            .into_iter()
            .map(|f| self.parseTraitMethod(f))
            .collect();
        Trait { name, generics, methods }
    }

    fn parseTraitMethod(&self, node: Node) -> TraitMethod {
        // trait_field (aliased to field): fn_identifier "(" params ")" ("->" type)?
        let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
        let params = self.collectParamsFrom(node);

        // As with `parseFn`'s `returns` field, the grammar's `returns` field
        // wraps the whole `optional(seq("->", $.type))`, so
        // `child_by_field_name("returns")` resolves to the anonymous "->"
        // token, not the `type` node. Unlike `parseFn`, a trait method has
        // no receiver annotation, so there's at most one `type`-kind named
        // child here, and it's unambiguously the return type when present.
        let returns = node
            .named_children(&mut node.walk())
            .find(|n| n.kind() == "type")
            .map(|n| self.parseType(n));

        TraitMethod { name, params, returns }
    }

    fn parseEnum(&self, node: Node) -> Enum {
        let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
        let params = self.childrenOfKind(node, "enum_param")
            .into_iter()
            .map(|n| self.parseEnumParam(n))
            .collect();
        let variants = self.childrenOfKind(node, "field")
            .into_iter()
            .map(|f| self.parseEnumVariant(f))
            .collect();
        Enum { name, params, variants }
    }

    fn parseEnumParam(&self, node: Node) -> EnumParam {
        // enum_param: var_identifier ":" type
        let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
        let ty = node
            .named_child(1)
            .map(|n| self.parseType(n))
            .unwrap_or(Type { name: String::new(), generics: vec![] });
        EnumParam { name, ty }
    }

    fn parseEnumVariant(&self, node: Node) -> EnumVariant {
        // enum_field (aliased to field): "|" type_identifier
        //   ("[" (type_identifier | generic),* "]")?     -- existing: generic type payload
        //   | ("(" expression,* ")")?                    -- new: discriminant value literals
        // named children after the name: either type_identifier/generic (fields) or
        // expression (values) — the two are disjoint child-kind sets, never mixed.
        let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
        let rest: Vec<Node> = (1..node.named_child_count())
            .filter_map(|i| node.named_child(i as u32))
            .collect();
        let fields: Vec<String> = rest.iter()
            .filter(|n| matches!(n.kind(), "type_identifier" | "generic"))
            .map(|n| self.text(*n))
            .collect();
        let values: Vec<Expr> = rest.iter()
            .filter(|n| n.kind() == "expression")
            .map(|n| { let u = self.unwrapExprNode(*n); self.parseExpression(u) })
            .collect();
        EnumVariant { name, fields, values }
    }

    // ---- functions --------------------------------------------------------

    fn parseFn(&self, node: Node) -> Fn {
        // fn: "fun" fn_identifier "(" param,* ")" ("->" type)? "=" body_or_expr
        // Named children: fn_identifier, param*, type?, body/expr
        //
        // `type_param` (the method's receiver, e.g. `Cat` in a method nested inside
        // `type Cat = ...`) is never set here — a bare `fn` node has no receiver of
        // its own; `parseSource`'s `collectNestedFns` forces it afterward for any
        // `fn` nested inside a `class`/`enum` body. There is no top-level
        // `<Receiver>` annotation syntax to parse.
        let mut cursor = node.walk();
        let named: Vec<Node> = node.named_children(&mut cursor).collect();

        let name = named.first().map(|n| self.text(*n)).unwrap_or_default();

        let params: Vec<Param> = named
            .iter()
            .filter(|n| n.kind() == "param")
            .map(|n| self.parseParam(*n))
            .collect();

        // The grammar's `returns` field wraps the whole `optional(seq("->", $.type))`,
        // so `child_by_field_name("returns")` resolves to the anonymous `"->"` token,
        // not the `type` node — find the `type`-kind named child instead.
        let returns = named
            .iter()
            .find(|n| n.kind() == "type")
            .map(|n| self.parseType(*n));

        // body is the last named child — it is either a `body` node (block)
        // or an expression node when the body is a single expression. Genuinely
        // absent (`extern fun foo(...)` with no `=`) parses as `FnBody::Extern`;
        // whether that's actually valid here is `plum-checker`'s job, not the
        // parser's — it must agree with `is_extern` below.
        let body = named.last().and_then(|last| {
            match last.kind() {
                // Skip non-body trailing nodes
                "fn_identifier" | "type" | "param" | "self" => None,
                "body" => Some(FnBody::Block(self.parseBlock(*last))),
                _ => {
                    let unwrapped = self.unwrapExprNode(*last);
                    Some(FnBody::Expr(self.parseExpression(unwrapped)))
                }
            }
        }).unwrap_or(FnBody::Extern);

        let is_extern = node.child_by_field_name("externKw").is_some();

        Fn { name, type_param: None, is_extern, params, returns, body }
    }

    fn parseConst(&self, node: Node) -> Const {
        // const: const_identifier "=" expression
        let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
        let value = node
            .named_child(1)
            .map(|n| {
                let unwrapped = self.unwrapExprNode(n);
                self.parseExpression(unwrapped)
            })
            .unwrap_or(Expr::Int(0));
        Const { name, value }
    }

    // ---- params / return type ---------------------------------------------

    /// Collect `param` named children from any node that has them.
    fn collectParamsFrom(&self, node: Node) -> Vec<Param> {
        self.childrenOfKind(node, "param")
            .into_iter()
            .map(|n| self.parseParam(n))
            .collect()
    }

    fn parseParam(&self, node: Node) -> Param {
        // param: var_identifier ":" (type | variadic_type | fn_value_type) ("=" expression)?
        let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
        let ty = node.named_child(1).map(|n| match n.kind() {
            "variadic_type" => {
                let inner = n.named_child(0)
                    .map(|t| self.parseType(t))
                    .unwrap_or(Type { name: String::new(), generics: vec![] });
                ParamType::Variadic(inner)
            }
            "fn_value_type" => self.parseFnValueType(n),
            _ => ParamType::Type(self.parseType(n)),
        }).unwrap_or(ParamType::Type(Type { name: String::new(), generics: vec![] }));
        let default = node.named_child(2).map(|n| {
            let unwrapped = self.unwrapExprNode(n);
            self.parseExpression(unwrapped)
        });
        Param { name, ty, default }
    }

    fn parseFnValueType(&self, node: Node) -> ParamType {
        // fn_value_type: "fn" "(" field("params", type,*) ")" ("->" field("returns", type))?
        // The "returns" field (if present) is a distinct field from "params", so the
        // two are disambiguated unambiguously by field name, not by counting/position
        // among same-kind "type" children — the same idiom `fn`'s own `returns` field
        // already uses.
        let returns_node = node.child_by_field_name("returns");
        let param_types: Vec<Type> = self.childrenOfKind(node, "type")
            .into_iter()
            .filter(|n| Some(*n) != returns_node)
            .map(|n| self.parseType(n))
            .collect();
        let ret = returns_node.map(|n| Box::new(self.parseType(n)));
        ParamType::Fn(param_types, ret)
    }

    fn parseType(&self, node: Node) -> Type {
        // type: type_identifier ("[" type,* "]")? | generic | "[" "]" element:type
        // A slice type (e.g. `[]Byte`) is flattened to a single reserved name
        // `"[]" + element_name` rather than a real generics list — the checker
        // treats it as a fixed builtin (only `[]Byte` is accepted), not a
        // monomorphized generic, so there's no template to carry args for.
        if let Some(element) = node.child_by_field_name("element") {
            let elem = self.parseType(element);
            return Type { name: format!("[]{}", elem.name), generics: vec![] };
        }
        // named_child(0) = type_identifier
        let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_else(|| self.text(node));
        let mut cursor = node.walk();
        let generics: Vec<Type> = node
            .named_children(&mut cursor)
            .skip(1)
            .filter(|n| n.kind() == "type")
            .map(|n| self.parseType(n))
            .collect();
        Type { name, generics }
    }

    // ---- statements -------------------------------------------------------

    fn parseBlock(&self, node: Node) -> Block {
        let mut cursor = node.walk();
        let stmts = node
            .named_children(&mut cursor)
            .filter_map(|n| self.parseStmt(n))
            .collect();
        Block { stmts }
    }

    fn parseStmt(&self, node: Node) -> Option<Stmt> {
        let node = self.unwrapExprNode(node);
        Some(match node.kind() {
            "assign" => Stmt::Assign(self.parseAssign(node)),
            "break" => Stmt::Break,
            "continue" => Stmt::Continue,
            "return" => {
                let expr = node.named_child(0).map(|n| {
                    let u = self.unwrapExprNode(n);
                    self.parseExpression(u)
                });
                Stmt::Return(expr)
            }
            "todo" => Stmt::Todo,
            "assert" => {
                let expr = node.named_child(0)
                    .map(|n| { let u = self.unwrapExprNode(n); self.parseExpression(u) })
                    .unwrap_or(Expr::Int(0));
                Stmt::Assert(expr)
            }
            "for" => Stmt::For(self.parseFor(node)),
            "while" => Stmt::While(self.parseWhile(node)),
            "if" => Stmt::If(self.parseIf(node)),
            "match" => Stmt::Match(self.parseMatch(node)),
            kind if isExpressionKind(kind) => Stmt::Expr(self.parseExpression(node)),
            _ => return None,
        })
    }

    fn parseAssign(&self, node: Node) -> Assign {
        // assign: commaSep1(choice(var_identifier, field_target)) "=" commaSep1(expression)
        // Named children are all targets (var_identifier | field_target) then all
        // expressions. We split at the first child that is neither.
        let mut cursor = node.walk();
        let named: Vec<Node> = node.named_children(&mut cursor).collect();
        let split = named
            .iter()
            .position(|n| n.kind() != "var_identifier" && n.kind() != "field_target")
            .unwrap_or(named.len());
        let targets = named[..split]
            .iter()
            .map(|n| self.parseAssignTarget(*n))
            .collect();
        let values = named[split..]
            .iter()
            .map(|n| { let u = self.unwrapExprNode(*n); self.parseExpression(u) })
            .collect();
        let declare = node.child_by_field_name("op").map(|n| self.text(n) == ":=").unwrap_or(false);
        Assign { targets, values, declare }
    }

    fn parseAssignTarget(&self, node: Node) -> AssignTarget {
        match node.kind() {
            "field_target" => {
                // field_target: object: primary_expression "." member: fn_identifier
                let object_node = node.child_by_field_name("object").expect("field_target has an object");
                let member = node
                    .child_by_field_name("member")
                    .map(|n| self.text(n))
                    .unwrap_or_default();
                let object = self.parsePrimaryExpression(self.unwrapExprNode(object_node));
                AssignTarget::Field(Box::new(object), member)
            }
            _ => AssignTarget::Var(self.text(node)),
        }
    }

    fn parseFor(&self, node: Node) -> For {
        // for: "for" commaSep1(var_identifier) "in" primary_expression body
        // Named children: var_identifier+, primary_expression (iter), body
        let mut cursor = node.walk();
        let named: Vec<Node> = node.named_children(&mut cursor).collect();

        let split = named.iter().position(|n| n.kind() != "var_identifier").unwrap_or(0);
        let vars = named[..split].iter().map(|n| self.text(*n)).collect();

        let iter = named.get(split)
            .map(|n| { let u = self.unwrapExprNode(*n); self.parsePrimaryExpression(u) })
            .unwrap_or(Expr::Int(0));

        let body = named.last()
            .filter(|n| n.kind() == "body")
            .map(|n| self.parseBlock(*n))
            .unwrap_or(Block { stmts: vec![] });

        For { vars, iter, body }
    }

    fn parseWhile(&self, node: Node) -> While {
        // while: "while" expression body
        let mut cursor = node.walk();
        let named: Vec<Node> = node.named_children(&mut cursor).collect();
        let condition = named.first()
            .map(|n| { let u = self.unwrapExprNode(*n); self.parseExpression(u) })
            .unwrap_or(Expr::Int(0));
        let body = named.last()
            .filter(|n| n.kind() == "body")
            .map(|n| self.parseBlock(*n))
            .unwrap_or(Block { stmts: vec![] });
        While { condition, body }
    }

    fn parseIf(&self, node: Node) -> If {
        // if: "if" expression body else_if* else?
        let mut cursor = node.walk();
        let named: Vec<Node> = node.named_children(&mut cursor).collect();

        let condition = named.first()
            .map(|n| { let u = self.unwrapExprNode(*n); self.parseExpression(u) })
            .unwrap_or(Expr::Int(0));
        let body = named.get(1)
            .filter(|n| n.kind() == "body")
            .map(|n| self.parseBlock(*n))
            .unwrap_or(Block { stmts: vec![] });
        let else_ifs = named.iter()
            .filter(|n| n.kind() == "else_if")
            .map(|n| self.parseElseIf(*n))
            .collect();
        let else_ = named.iter()
            .find(|n| n.kind() == "else")
            .and_then(|n| n.named_child(0))
            .map(|n| self.parseBlock(n));
        If { condition, body, else_ifs, else_ }
    }

    fn parseElseIf(&self, node: Node) -> ElseIf {
        // else_if: "else if" expression body
        let mut cursor = node.walk();
        let named: Vec<Node> = node.named_children(&mut cursor).collect();
        let condition = named.first()
            .map(|n| { let u = self.unwrapExprNode(*n); self.parseExpression(u) })
            .unwrap_or(Expr::Int(0));
        let body = named.last()
            .filter(|n| n.kind() == "body")
            .map(|n| self.parseBlock(*n))
            .unwrap_or(Block { stmts: vec![] });
        ElseIf { condition, body }
    }

    fn parseMatch(&self, node: Node) -> Match {
        // match: "match" commaSep1(expression) "is" case+
        let mut cursor = node.walk();
        let named: Vec<Node> = node.named_children(&mut cursor).collect();
        let split = named.iter().position(|n| n.kind() == "case").unwrap_or(named.len());
        let subjects = named[..split]
            .iter()
            .map(|n| { let u = self.unwrapExprNode(*n); self.parseExpression(u) })
            .collect();
        let cases = named[split..]
            .iter()
            .filter(|n| n.kind() == "case")
            .map(|n| self.parseCase(*n))
            .collect();
        Match { subjects, cases }
    }

    fn parseCase(&self, node: Node) -> Case {
        // case: commaSep1(case_pattern) "=>" (expression | body)
        let mut cursor = node.walk();
        let named: Vec<Node> = node.named_children(&mut cursor).collect();
        let patterns = named.iter()
            .filter(|n| n.kind() == "case_pattern")
            .map(|n| self.parseCasePattern(*n))
            .collect();
        let body = named.iter()
            .find(|n| n.kind() != "case_pattern")
            .map(|n| {
                if n.kind() == "body" {
                    self.parseBlock(*n)
                } else {
                    let unwrapped = self.unwrapExprNode(*n);
                    Block { stmts: vec![Stmt::Expr(self.parseExpression(unwrapped))] }
                }
            })
            .unwrap_or(Block { stmts: vec![] });
        Case { patterns, body }
    }

    fn parseCasePattern(&self, node: Node) -> CasePattern {
        // case_pattern wraps: class_pattern | string | integer | float | dotted_name | "_"
        let inner = node.named_child(0).unwrap_or(node);
        match inner.kind() {
            "class_pattern" => {
                // class_pattern: dotted_name "(" case_pattern,* ")"
                let name = inner.named_child(0).map(|n| self.text(n)).unwrap_or_default();
                let fields = (1..inner.named_child_count())
                    .filter_map(|i| inner.named_child(i as u32))
                    .filter(|n| n.kind() == "case_pattern")
                    .map(|n| self.parseCasePattern(n))
                    .collect();
                CasePattern::Class { name, fields }
            }
            "string" => CasePattern::String(self.parseStringRaw(inner)),
            "integer" => CasePattern::Int(self.parseInteger(inner)),
            "float" => CasePattern::Float(self.parseFloat(inner)),
            "dotted_name" => CasePattern::Name(self.text(inner)),
            _ => {
                let t = self.text(inner);
                if t == "_" { CasePattern::Wildcard } else { CasePattern::Name(t) }
            }
        }
    }

    // ---- expressions ------------------------------------------------------

    pub fn parseExpression(&self, node: Node) -> Expr {
        let node = self.unwrapExprNode(node);
        match node.kind() {
            "comparison_operator" => self.parseCompare(node),
            "not_operator" => {
                let arg = node.named_child(0)
                    .map(|n| { let u = self.unwrapExprNode(n); self.parseExpression(u) })
                    .unwrap_or(Expr::Int(0));
                Expr::Not(Box::new(arg))
            }
            "boolean_operator" => self.parseBoolOp(node),
            "ternary_expression" => self.parseTernary(node),
            "closure" => Expr::Closure(Box::new(self.parseClosure(node))),
            _ => self.parsePrimaryExpression(node),
        }
    }

    fn parseClosure(&self, node: Node) -> Closure {
        // closure: "|" var_identifier,* "|" (expression | body)
        let params: Vec<String> = self.childrenOfKind(node, "var_identifier")
            .into_iter()
            .map(|n| self.text(n))
            .collect();
        // The body is either an indented `body` block or a single inline expression
        // (`|v| v`); normalize the inline form into a one-statement block so codegen and
        // the checker only ever see a `Block`.
        let body = match self.childrenOfKind(node, "body").into_iter().next() {
            Some(block_node) => self.parseBlock(block_node),
            None => match node.child_by_field_name("body") {
                Some(expr_node) => {
                    let unwrapped = self.unwrapExprNode(expr_node);
                    Block { stmts: vec![Stmt::Expr(self.parseExpression(unwrapped))] }
                }
                None => Block { stmts: vec![] },
            },
        };
        Closure { params, body }
    }

    pub fn parsePrimaryExpression(&self, node: Node) -> Expr {
        let node = self.unwrapExprNode(node);
        match node.kind() {
            "binary_operator" => self.parseBinary(node),
            "unary_operator" => self.parseUnary(node),
            "attribute" => self.parseAttribute(node),
            "fn_call" => Expr::FnCall(self.parseFnCall(node)),
            "class_call" => Expr::ClassCall(self.parseClassCall(node)),
            "parenthesized_expression" => {
                // parenthesized_expression: "{" expression "}"
                let inner = node.named_child(0)
                    .map(|n| { let u = self.unwrapExprNode(n); self.parseExpression(u) })
                    .unwrap_or(Expr::Int(0));
                Expr::Paren(Box::new(inner))
            }
            "string" => Expr::String(self.parseString(node)),
            "integer" => Expr::Int(self.parseInteger(node)),
            "float" => Expr::Float(self.parseFloat(node)),
            "self" => Expr::Self_,
            "var_identifier" => Expr::Var(self.text(node)),
            "type_identifier" => Expr::TypeName(self.text(node)),
            // A SCREAMING_CASE const reference (e.g. `MAX_FLOAT_VALUE`) — reuses the
            // `TypeName` path, which already resolves a matching top-level const's
            // real type/value (see plum-checker's `inferExpr` and
            // plum-wasm-codegen's `CURRENT_CONSTS`).
            "const_identifier" => Expr::TypeName(self.text(node)),
            _ => Expr::Var(self.text(node)),
        }
    }

    fn parseBinary(&self, node: Node) -> Expr {
        // binary_operator: primary_expression op primary_expression
        // "operator" is an unnamed child; left/right are field-named
        let op_text = self.findUnnamedOperator(node);
        let op = match op_text.as_str() {
            "+" => BinOp::Add,
            "-" => BinOp::Sub,
            "*" => BinOp::Mul,
            "/" => BinOp::Div,
            "%" => BinOp::Mod,
            "|" => BinOp::BitOr,
            "&" => BinOp::BitAnd,
            "^" => BinOp::Xor,
            "<<" => BinOp::Shl,
            ">>" => BinOp::Shr,
            _ => BinOp::Add,
        };
        let mut cursor = node.walk();
        let named: Vec<Node> = node.named_children(&mut cursor).collect();
        let left = named.first()
            .map(|n| { let u = self.unwrapExprNode(*n); self.parsePrimaryExpression(u) })
            .unwrap_or(Expr::Int(0));
        let right = named.last()
            .map(|n| { let u = self.unwrapExprNode(*n); self.parsePrimaryExpression(u) })
            .unwrap_or(Expr::Int(0));
        Expr::Binary(Box::new(BinaryExpr { op, left, right }))
    }

    fn parseUnary(&self, node: Node) -> Expr {
        let op_text = self.findUnnamedOperator(node);
        let op = if op_text == "-" { UnOp::Neg } else { UnOp::Pos };
        let operand = node.named_child(0)
            .map(|n| { let u = self.unwrapExprNode(n); self.parsePrimaryExpression(u) })
            .unwrap_or(Expr::Int(0));
        Expr::Unary(Box::new(UnaryExpr { op, operand }))
    }

    fn parseBoolOp(&self, node: Node) -> Expr {
        let op_text = self.findUnnamedOperator(node);
        let op = if op_text == "&&" { BoolOp::And } else { BoolOp::Or };
        let mut cursor = node.walk();
        let named: Vec<Node> = node.named_children(&mut cursor).collect();
        let left = named.first()
            .map(|n| { let u = self.unwrapExprNode(*n); self.parseExpression(u) })
            .unwrap_or(Expr::Int(0));
        let right = named.last()
            .map(|n| { let u = self.unwrapExprNode(*n); self.parseExpression(u) })
            .unwrap_or(Expr::Int(0));
        Expr::Bool(Box::new(BoolExpr { op, left, right }))
    }

    fn parseCompare(&self, node: Node) -> Expr {
        let op_text = self.findUnnamedOperator(node);
        let op = match op_text.as_str() {
            "<" => CmpOp::Lt,
            "<=" => CmpOp::Lte,
            "==" => CmpOp::Eq,
            "!=" => CmpOp::Neq,
            ">=" => CmpOp::Gte,
            ">" => CmpOp::Gt,
            "<>" => CmpOp::NotEq2,
            _ => CmpOp::Eq,
        };
        let mut cursor = node.walk();
        let named: Vec<Node> = node.named_children(&mut cursor).collect();
        let left = named.first()
            .map(|n| { let u = self.unwrapExprNode(*n); self.parsePrimaryExpression(u) })
            .unwrap_or(Expr::Int(0));
        let right = named.last()
            .map(|n| { let u = self.unwrapExprNode(*n); self.parsePrimaryExpression(u) })
            .unwrap_or(Expr::Int(0));
        Expr::Compare(Box::new(CompareExpr { op, left, right }))
    }

    fn parseTernary(&self, node: Node) -> Expr {
        // ternary_expression: expression "?" expression ":" expression
        let mut cursor = node.walk();
        let named: Vec<Node> = node.named_children(&mut cursor).collect();
        let condition = named.first()
            .map(|n| { let u = self.unwrapExprNode(*n); self.parseExpression(u) })
            .unwrap_or(Expr::Int(0));
        let then = named.get(1)
            .map(|n| { let u = self.unwrapExprNode(*n); self.parseExpression(u) })
            .unwrap_or(Expr::Int(0));
        let else_ = named.get(2)
            .map(|n| { let u = self.unwrapExprNode(*n); self.parseExpression(u) })
            .unwrap_or(Expr::Int(0));
        Expr::Ternary(Box::new(TernaryExpr { condition, then, else_ }))
    }

    fn parseAttribute(&self, node: Node) -> Expr {
        // attribute: primary_expression "." fn_identifier fn_argument_list?
        // The member name is always fn_identifier (a superset of var_identifier); an
        // optional trailing argument list distinguishes a method call from field access.
        let mut cursor = node.walk();
        let named: Vec<Node> = node.named_children(&mut cursor).collect();
        let object = named.first()
            .map(|n| { let u = self.unwrapExprNode(*n); self.parsePrimaryExpression(u) })
            .unwrap_or(Expr::Int(0));
        let member = named.get(1).map(|n| self.text(*n)).unwrap_or_default();
        let attr = match named.get(2) {
            Some(args_node) => {
                let mut acursor = args_node.walk();
                let args = args_node.named_children(&mut acursor)
                    .map(|n| self.parseArg(n))
                    .collect();
                AttrKind::Method(FnCall { name: member, args })
            }
            None => AttrKind::Field(member),
        };
        Expr::Attribute(Box::new(AttributeExpr { object, attr }))
    }

    fn parseFnCall(&self, node: Node) -> FnCall {
        // fn_call: var_identifier fn_argument_list (the callee lexes as var_identifier
        // to avoid an identifier-token tie with all-lowercase, no-underscore names)
        let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
        let args = node.named_child(1)
            .map(|args_node| {
                let mut cursor = args_node.walk();
                args_node.named_children(&mut cursor)
                    .map(|n| self.parseArg(n))
                    .collect()
            })
            .unwrap_or_default();
        FnCall { name, args }
    }

    fn parseArg(&self, node: Node) -> Arg {
        match node.kind() {
            "keyword_argument" => {
                // keyword_argument: var_identifier "=" expression
                let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
                let value = node.named_child(1)
                    .map(|n| { let u = self.unwrapExprNode(n); self.parseExpression(u) })
                    .unwrap_or(Expr::Int(0));
                Arg::Keyword { name, value }
            }
            "pair_argument" => {
                // pair_argument: string "=>" expression
                let key = node.named_child(0).map(|n| self.parseStringRaw(n)).unwrap_or_default();
                let value = node.named_child(1)
                    .map(|n| { let u = self.unwrapExprNode(n); self.parseExpression(u) })
                    .unwrap_or(Expr::Int(0));
                Arg::Pair { key, value }
            }
            _ => {
                let u = self.unwrapExprNode(node);
                Arg::Positional(self.parseExpression(u))
            }
        }
    }

    fn parseClassCall(&self, node: Node) -> ClassCall {
        // class_call: type_identifier ("[" type,* "]")? class_argument_list
        let type_name = node.child_by_field_name("type").map(|n| self.text(n)).unwrap_or_default();
        // Filtering by NODE KIND (rather than the "generics" field, which
        // tree-sitter attaches once per repeated element, not as a single
        // group) — mirrors `parseType`'s own handling of a type's nested
        // generics list, and sidesteps the optional bracket entirely: if it
        // wasn't written, there are simply no "type" children to find.
        let generics: Vec<Type> = {
            let mut cursor = node.walk();
            node.named_children(&mut cursor)
                .filter(|n| n.kind() == "type")
                .map(|n| self.parseType(n))
                .collect()
        };
        let fields = node.child_by_field_name("arguments")
            .map(|args_node| {
                // class_argument_list: "(" (var_identifier ":" expression),* ")"
                // Named children alternate: var_identifier, expression, ...
                let mut cursor = args_node.walk();
                let named: Vec<Node> = args_node.named_children(&mut cursor).collect();
                named.chunks(2).filter_map(|chunk| {
                    if chunk.len() == 2 {
                        let name = self.text(chunk[0]);
                        let u = self.unwrapExprNode(chunk[1]);
                        Some(FieldArg { name, value: self.parseExpression(u) })
                    } else {
                        None
                    }
                }).collect()
            })
            .unwrap_or_default();
        ClassCall { type_name, fields, generics }
    }

    // ---- string literals --------------------------------------------------

    fn parseString(&self, node: Node) -> StringExpr {
        let mut cursor = node.walk();
        let parts = node
            .named_children(&mut cursor)
            .filter_map(|n| match n.kind() {
                "string_content" => Some(StringPart::Text(decodeEscapes(&self.text(n)))),
                "interpolation" => {
                    n.named_child(0).map(|e| {
                        let u = self.unwrapExprNode(e);
                        StringPart::Interp(self.parsePrimaryExpression(u))
                    })
                }
                _ => None,
            })
            .collect();
        StringExpr { parts }
    }

    fn parseStringRaw(&self, node: Node) -> String {
        let full = self.text(node);
        full.trim_matches('"').to_string()
    }

    // ---- numeric literals -------------------------------------------------

    fn parseInteger(&self, node: Node) -> i64 {
        let s = self.text(node).replace('_', "");
        if s.starts_with("0x") || s.starts_with("0X") {
            i64::from_str_radix(&s[2..], 16).unwrap_or(0)
        } else if s.starts_with("0b") || s.starts_with("0B") {
            i64::from_str_radix(&s[2..], 2).unwrap_or(0)
        } else {
            s.parse().unwrap_or(0)
        }
    }

    fn parseFloat(&self, node: Node) -> f64 {
        let s = self.text(node).trim_end_matches(['f', 'F']).replace('_', "");
        s.parse().unwrap_or(0.0)
    }

    // ---- helpers ----------------------------------------------------------

    /// Find the text of the first unnamed (punctuation/operator) non-whitespace child.
    fn findUnnamedOperator(&self, node: Node) -> String {
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            if !child.is_named() {
                let t = self.text(child);
                if !t.trim().is_empty() {
                    return t;
                }
            }
        }
        String::new()
    }
}

/// Decodes a string literal's raw source text (the grammar's `escape_sequence`
/// is matched at the lexer level but never actually interpreted anywhere — the
/// parser just handed back the literal source bytes, backslashes and all)
/// into its real content: `\n`/`\t`/`\\`/`\"`/etc single-char escapes, `\NNN`
/// (1-3 decimal digits), `\xXX`, `\uXXXX`, `\UXXXXXXXX`. An unrecognized escape
/// (including `\N{...}`) is passed through unchanged rather than erroring —
/// this only ever runs on text the grammar already accepted as a valid
/// `escape_sequence`, so "unrecognized" only means "not decoded yet."
fn decodeEscapes(s: &str) -> String {
    let chars: Vec<char> = s.chars().collect();
    let mut out = String::with_capacity(chars.len());
    let mut i = 0;
    while i < chars.len() {
        if chars[i] != '\\' || i + 1 >= chars.len() {
            out.push(chars[i]);
            i += 1;
            continue;
        }
        let next = chars[i + 1];
        match next {
            'n' => { out.push('\n'); i += 2; }
            't' => { out.push('\t'); i += 2; }
            'r' => { out.push('\r'); i += 2; }
            'a' => { out.push('\u{07}'); i += 2; }
            'b' => { out.push('\u{08}'); i += 2; }
            'f' => { out.push('\u{0C}'); i += 2; }
            'v' => { out.push('\u{0B}'); i += 2; }
            '\\' => { out.push('\\'); i += 2; }
            '\'' => { out.push('\''); i += 2; }
            '"' => { out.push('"'); i += 2; }
            '\n' => { i += 2; } // escaped literal newline: line continuation, emits nothing
            'x' => match decodeHexEscape(&chars, i + 2, 2) {
                Some((ch, consumed)) => { out.push(ch); i += 2 + consumed; }
                None => { out.push(chars[i]); i += 1; }
            },
            'u' => match decodeHexEscape(&chars, i + 2, 4) {
                Some((ch, consumed)) => { out.push(ch); i += 2 + consumed; }
                None => { out.push(chars[i]); i += 1; }
            },
            'U' => match decodeHexEscape(&chars, i + 2, 8) {
                Some((ch, consumed)) => { out.push(ch); i += 2 + consumed; }
                None => { out.push(chars[i]); i += 1; }
            },
            d if d.is_ascii_digit() => {
                let mut j = i + 1;
                while j < chars.len() && j < i + 4 && chars[j].is_ascii_digit() {
                    j += 1;
                }
                let digits: String = chars[i + 1..j].iter().collect();
                match digits.parse::<u32>().ok().and_then(char::from_u32) {
                    Some(ch) => { out.push(ch); i = j; }
                    None => { out.push(chars[i]); i += 1; }
                }
            }
            _ => { out.push(chars[i]); i += 1; } // e.g. `\N{...}` — pass through raw
        }
    }
    out
}

/// Decodes exactly `width` hex digits starting at `start` into a `char`, if
/// `start..start+width` are all present and form a valid codepoint. Returns
/// `(decoded_char, width)` on success so the caller advances past all of them.
fn decodeHexEscape(chars: &[char], start: usize, width: usize) -> Option<(char, usize)> {
    if start + width > chars.len() {
        return None;
    }
    let hex: String = chars[start..start + width].iter().collect();
    u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32).map(|ch| (ch, width))
}

fn isExpressionKind(kind: &str) -> bool {
    matches!(
        kind,
        "binary_operator"
            | "unary_operator"
            | "boolean_operator"
            | "not_operator"
            | "comparison_operator"
            | "ternary_expression"
            | "attribute"
            | "fn_call"
            | "class_call"
            | "parenthesized_expression"
            | "string"
            | "integer"
            | "float"
            | "self"
            | "var_identifier"
            | "type_identifier"
    )
}