Skip to main content

leo_parser/
rowan.rs

1// Copyright (C) 2019-2026 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17//! Rowan-based parser implementation.
18//!
19//! This module uses `leo-parser-rowan` (rowan) and converts its output
20//! to the Leo AST via the `ConversionContext`.
21
22// Implementation notes:
23//
24// - All user-reachable errors should be emitted via the error handler (mostly
25//   `ERROR` nodes).
26// - All implementation bugs (e.g. unexpected node structure from
27//   `leo_parser_rowan`) should `panic!` in order to catch logic bugs in the
28//   compiler.
29
30use itertools::Itertools as _;
31use snarkvm::prelude::{Address, Signature, TestnetV0};
32
33use leo_ast::{NetworkName, NodeBuilder, NodeID, TypeInterner};
34use leo_errors::{Handler, Result};
35use leo_parser_rowan::{SyntaxElement, SyntaxKind, SyntaxKind::*, SyntaxNode, SyntaxToken, TextRange};
36use leo_span::{
37    Span,
38    Symbol,
39    source_map::{FileName, SourceFile},
40    sym,
41};
42
43/// Type parameters and const arguments extracted from a `CONST_ARG_LIST` node.
44type ConstArgList = (Vec<(leo_ast::TypeKind, Span)>, Vec<leo_ast::Expression>);
45
46/// Annotated types with visibility modes, used for `_dynamic_call` input/return types.
47type AnnotatedTypes = Vec<(leo_ast::Mode, leo_ast::TypeKind, Span)>;
48
49/// Parent declarations for inheritance
50type Parents = Vec<(Span, leo_ast::TypeKind)>;
51
52// =============================================================================
53// ConversionContext
54// =============================================================================
55
56/// Context for converting rowan syntax nodes to Leo AST nodes.
57struct ConversionContext<'a> {
58    handler: &'a Handler,
59    builder: &'a NodeBuilder,
60    interner: &'a TypeInterner,
61    /// The absolute start position to offset spans by.
62    start_pos: u32,
63    /// When true, suppress `unexpected_str` errors during conversion.
64    ///
65    /// These errors are always downstream of parse/lex errors (the conversion
66    /// only fails when the CST contains ERROR nodes from parse recovery), so
67    /// reporting them would be duplicative.
68    suppress_cascade: bool,
69}
70
71impl<'a> ConversionContext<'a> {
72    /// Create a new conversion context.
73    fn new(
74        handler: &'a Handler,
75        builder: &'a NodeBuilder,
76        interner: &'a TypeInterner,
77        start_pos: u32,
78        suppress_cascade: bool,
79    ) -> Self {
80        Self { handler, builder, interner, start_pos, suppress_cascade }
81    }
82
83    /// Emit an `unexpected_str` error, unless cascade suppression is active.
84    fn emit_unexpected_str(&self, expected: &str, found: impl std::fmt::Display, span: Span) {
85        if !self.suppress_cascade {
86            self.handler.emit_err(crate::errors::unexpected_str(expected, found, span));
87        }
88    }
89
90    // =========================================================================
91    // Utility Methods
92    // =========================================================================
93
94    /// Convert a rowan TextRange to a leo_span::Span.
95    fn to_span(&self, node: &SyntaxNode) -> Span {
96        let range = node.text_range();
97        Span::new(u32::from(range.start()) + self.start_pos, u32::from(range.end()) + self.start_pos)
98    }
99
100    /// Convert a token's text range to a Span.
101    fn token_span(&self, token: &SyntaxToken) -> Span {
102        let range = token.text_range();
103        Span::new(u32::from(range.start()) + self.start_pos, u32::from(range.end()) + self.start_pos)
104    }
105
106    /// Like `to_span` but starts at the first non-trivia direct token,
107    /// excluding leading whitespace/comments from the span.
108    fn non_trivia_span(&self, node: &SyntaxNode) -> Span {
109        let start = first_non_trivia_token(node).map(|t| t.text_range().start()).unwrap_or(node.text_range().start());
110        let end = node.text_range().end();
111        Span::new(u32::from(start) + self.start_pos, u32::from(end) + self.start_pos)
112    }
113
114    /// Span that excludes both leading and trailing trivia. Suitable for
115    /// leaf-like nodes (type paths, single tokens) where trailing whitespace
116    /// is not significant.
117    fn trimmed_span(&self, node: &SyntaxNode) -> Span {
118        let start = first_non_trivia_token(node).map(|t| t.text_range().start()).unwrap_or(node.text_range().start());
119        let end = last_non_trivia_token(node).map(|t| t.text_range().end()).unwrap_or(node.text_range().end());
120        Span::new(u32::from(start) + self.start_pos, u32::from(end) + self.start_pos)
121    }
122
123    /// Span that excludes leading and trailing trivia by scanning all
124    /// descendant tokens (deep traversal). Use for expression and statement
125    /// nodes whose trailing trivia may be nested inside child nodes.
126    fn content_span(&self, node: &SyntaxNode) -> Span {
127        let mut first = node.text_range().start();
128        let mut last = node.text_range().end();
129        let mut found_first = false;
130        for elem in node.descendants_with_tokens() {
131            if let Some(t) = elem.as_token()
132                && !t.kind().is_trivia()
133            {
134                if !found_first {
135                    first = t.text_range().start();
136                    found_first = true;
137                }
138                last = t.text_range().end();
139            }
140        }
141        Span::new(u32::from(first) + self.start_pos, u32::from(last) + self.start_pos)
142    }
143
144    /// Extend span to include leading annotations, if any.
145    fn span_including_annotations(&self, node: &SyntaxNode, span: Span) -> Span {
146        children(node)
147            .find(|n| n.kind() == ANNOTATION)
148            .map(|ann| Span::new(self.trimmed_span(&ann).lo, span.hi))
149            .unwrap_or(span)
150    }
151
152    /// Convert an IDENT token to a leo_ast::Identifier.
153    fn to_identifier(&self, token: &SyntaxToken) -> leo_ast::Identifier {
154        debug_assert_eq!(token.kind(), IDENT);
155        leo_ast::Identifier {
156            name: Symbol::intern(token.text()),
157            span: self.token_span(token),
158            id: self.builder.next_id(),
159        }
160    }
161
162    /// Create a placeholder identifier for error recovery.
163    fn error_identifier(&self, span: Span) -> leo_ast::Identifier {
164        leo_ast::Identifier { name: Symbol::intern("_error"), span, id: self.builder.next_id() }
165    }
166
167    /// Create a placeholder expression for error recovery.
168    fn error_expression(&self, span: Span) -> leo_ast::Expression {
169        leo_ast::ErrExpression { span, id: self.builder.next_id() }.into()
170    }
171
172    /// Create an `IntrinsicExpression` with no type parameters.
173    fn intrinsic_expression(
174        &self,
175        name: Symbol,
176        arguments: Vec<leo_ast::Expression>,
177        span: Span,
178    ) -> leo_ast::Expression {
179        leo_ast::IntrinsicExpression {
180            name,
181            type_parameters: Vec::new(),
182            input_types: Vec::new(),
183            return_types: Vec::new(),
184            arguments,
185            span,
186            id: self.builder.next_id(),
187        }
188        .into()
189    }
190
191    /// Create an empty block for error recovery.
192    fn error_block(&self, span: Span) -> leo_ast::Block {
193        leo_ast::Block { statements: Vec::new(), span, id: self.builder.next_id() }
194    }
195
196    /// Emit an error if the literal text has a hex, octal, or binary prefix,
197    /// which is not allowed for non-integer types (field, group, scalar).
198    fn validate_hexbin_literal(&self, text: &str, suffix_len: u32, span: Span) {
199        if text.starts_with("0x") || text.starts_with("0o") || text.starts_with("0b") {
200            self.handler.emit_err(crate::errors::hexbin_literal_nonintegers(Span::new(span.lo, span.hi - suffix_len)));
201        }
202    }
203
204    /// Find an IDENT token in `node` or emit an error and return a placeholder.
205    fn require_ident(&self, node: &SyntaxNode, label: &str) -> leo_ast::Identifier {
206        let span = self.to_span(node);
207        match tokens(node).find(|t| t.kind() == IDENT) {
208            Some(token) => self.to_identifier(&token),
209            None => {
210                self.emit_unexpected_str(label, node.text(), span);
211                self.error_identifier(span)
212            }
213        }
214    }
215
216    /// Find an expression child node or emit an error and return `ErrExpression`.
217    fn require_expression(&self, node: &SyntaxNode, label: &str) -> Result<leo_ast::Expression> {
218        match children(node).find(|n| n.kind().is_expression()) {
219            Some(expr_node) => self.to_expression(&expr_node),
220            None => {
221                let span = self.to_span(node);
222                self.emit_unexpected_str(label, node.text(), span);
223                Ok(self.error_expression(span))
224            }
225        }
226    }
227
228    /// Validate an identifier, checking for double underscores and length.
229    fn validate_identifier(&self, ident: &leo_ast::Identifier) {
230        const MAX_IDENTIFIER_LEN: usize = 31;
231        let text = ident.name.to_string();
232        if text.len() > MAX_IDENTIFIER_LEN {
233            self.handler.emit_err(crate::errors::identifier_too_long(
234                &text,
235                text.len(),
236                MAX_IDENTIFIER_LEN,
237                ident.span,
238            ));
239        }
240        if text.contains("__") {
241            self.handler.emit_err(crate::errors::identifier_cannot_contain_double_underscore(&text, ident.span));
242        }
243    }
244
245    /// Validate an identifier in a definition position (struct field, variable,
246    /// function name, etc.). In addition to the general identifier checks, this
247    /// rejects identifiers that start with `_` or that are reserved keywords.
248    fn validate_definition_identifier(&self, ident: &leo_ast::Identifier) {
249        // Skip validation for error-recovery placeholders.
250        if ident.name == Symbol::intern("_error") {
251            return;
252        }
253        self.validate_identifier(ident);
254        let text = ident.name.to_string();
255        if text.starts_with('_') {
256            self.handler.emit_err(crate::errors::identifier_cannot_start_with_underscore(ident.span));
257        }
258        if leo_parser_rowan::is_keyword(&text) {
259            self.emit_unexpected_str("an identifier", &text, ident.span);
260        }
261    }
262
263    // =========================================================================
264    // Type Conversions
265    // =========================================================================
266
267    /// Convert a type syntax node to a `TypeKind`.
268    fn to_type(&self, node: &SyntaxNode) -> Result<leo_ast::TypeKind> {
269        let ty = match node.kind() {
270            TYPE_PRIMITIVE => self.type_primitive_to_type(node)?,
271            TYPE_LOCATOR => self.type_locator_to_type(node)?,
272            TYPE_PATH => self.type_path_to_type(node)?,
273            TYPE_ARRAY => self.type_array_to_type(node)?,
274            TYPE_VECTOR => self.type_vector_to_type(node)?,
275            TYPE_TUPLE => self.type_tuple_to_type(node)?,
276            TYPE_OPTIONAL => self.type_optional_to_type(node)?,
277            TYPE_FINAL => self.type_final_to_type(node)?,
278            TYPE_MAPPING => self.type_mapping_to_type(node)?,
279            TYPE_DYN_RECORD => leo_ast::TypeKind::DynRecord,
280            ERROR => {
281                // Parse errors already emitted by emit_parse_errors().
282                leo_ast::TypeKind::Err
283            }
284            kind => panic!("unexpected type node kind: {:?}", kind),
285        };
286        Ok(ty)
287    }
288
289    /// Convert a type syntax node to a `TypeNode`, carrying the node's span.
290    fn to_type_repr(&self, node: &SyntaxNode) -> Result<leo_ast::TypeNode> {
291        let span = self.to_span(node);
292        let kind = self.to_type(node)?;
293        Ok(leo_ast::TypeNode::new(self.interner, kind, span))
294    }
295
296    /// Find a type child node or emit an error and return a node with `TypeKind::Err`.
297    fn require_type_repr(&self, node: &SyntaxNode, label: &str) -> Result<leo_ast::TypeNode> {
298        match children(node).find(|n| n.kind().is_type()) {
299            Some(type_node) => self.to_type_repr(&type_node),
300            None => {
301                let span = self.to_span(node);
302                self.emit_unexpected_str(label, node.text(), span);
303                Ok(leo_ast::TypeNode::new(self.interner, leo_ast::TypeKind::Err, span))
304            }
305        }
306    }
307
308    /// Convert a TYPE_PRIMITIVE node to a `TypeKind`.
309    fn type_primitive_to_type(&self, node: &SyntaxNode) -> Result<leo_ast::TypeKind> {
310        debug_assert_eq!(node.kind(), TYPE_PRIMITIVE);
311        let prim = tokens(node)
312            .next()
313            .and_then(|t| keyword_to_primitive_type(t.kind()))
314            .expect("TYPE_PRIMITIVE should contain a type keyword");
315        Ok(prim)
316    }
317
318    /// Convert a TYPE_LOCATOR node to a `TypeKind`.
319    ///
320    /// TYPE_LOCATOR represents `program.aleo::Type` or `program.aleo::module::Type`.
321    fn type_locator_to_type(&self, node: &SyntaxNode) -> Result<leo_ast::TypeKind> {
322        debug_assert_eq!(node.kind(), TYPE_LOCATOR);
323
324        // Collect all IDENT tokens; the first is the program name and the rest
325        // form qualifier segments plus the final type name.
326        let all_idents: Vec<_> = tokens(node).filter(|t| t.kind() == IDENT).collect();
327        let Some(program_token) = all_idents.first() else {
328            panic!("TYPE_LOCATOR should contain at least a program IDENT: {:?}", node.text())
329        };
330
331        // Find KW_ALEO in the tokens — always present in a TYPE_LOCATOR node.
332        let kw_aleo_token =
333            tokens(node).find(|t| t.kind() == KW_ALEO).expect("TYPE_LOCATOR should contain `aleo` keyword");
334
335        let network_ident = leo_ast::Identifier {
336            name: Symbol::intern("aleo"),
337            span: self.token_span(&kw_aleo_token),
338            id: self.builder.next_id(),
339        };
340
341        let program_ident = self.to_identifier(program_token);
342        let program_id = leo_ast::ProgramId { name: program_ident, network: network_ident };
343
344        if all_idents.len() < 2 {
345            // program.aleo without `::Type` — a program ID used as a type reference.
346            let span = self.content_span(node);
347            let path = leo_ast::Path::new(Some(program_id), Vec::new(), program_ident, span, self.builder.next_id());
348            Ok(leo_ast::CompositeType { path, const_arguments: Vec::new() }.into())
349        } else {
350            // program.aleo::Type or program.aleo::module::Type:
351            //   last IDENT = type name, everything before (excluding program) = qualifier segments.
352            let name_token = all_idents.last().unwrap(); // safe: len >= 2
353            let qualifier: Vec<_> = all_idents[1..all_idents.len() - 1].iter().map(|t| self.to_identifier(t)).collect();
354            let type_ident = self.to_identifier(name_token);
355            let path_span = Span::new(program_id.name.span.lo, type_ident.span.hi);
356            let path = leo_ast::Path::new(Some(program_id), qualifier, type_ident, path_span, self.builder.next_id());
357            let (_type_parameters, const_arguments) = self.extract_const_arg_list(node)?;
358            Ok(leo_ast::CompositeType { path, const_arguments }.into())
359        }
360    }
361
362    /// Convert a TYPE_PATH node to a `TypeKind`.
363    ///
364    /// TYPE_PATH represents named/composite types: `Foo`, `Foo::Bar`, `Foo::[N]`.
365    fn type_path_to_type(&self, node: &SyntaxNode) -> Result<leo_ast::TypeKind> {
366        debug_assert_eq!(node.kind(), TYPE_PATH);
367
368        // Regular path: collect identifiers and const generic args
369        let mut path_components = Vec::new();
370
371        // Collect IDENT tokens that are direct children of TYPE_PATH (not inside CONST_ARG_LIST)
372        for token in tokens(node) {
373            match token.kind() {
374                IDENT => {
375                    path_components.push(self.to_identifier(&token));
376                }
377                // Skip punctuation
378                COLON_COLON | L_BRACKET | R_BRACKET | LT | GT | COMMA | INTEGER => {}
379                kind if kind.is_trivia() => {}
380                kind => panic!("unexpected token in TYPE_PATH: {:?}", kind),
381            }
382        }
383
384        // Extract const arguments from CONST_ARG_LIST child node
385        let (_type_parameters, const_arguments) = self.extract_const_arg_list(node)?;
386
387        // The last component is the type name, rest are path segments.
388        // Path span covers only the identifier tokens, not the const arg list.
389        let name = path_components.pop().expect("TYPE_PATH should have at least one identifier");
390        let path_span =
391            if let Some(first) = path_components.first() { Span::new(first.span.lo, name.span.hi) } else { name.span };
392        let path = leo_ast::Path::new(None, path_components, name, path_span, self.builder.next_id());
393        Ok(leo_ast::CompositeType { path, const_arguments }.into())
394    }
395
396    /// Convert a TYPE_ARRAY node to an ArrayType.
397    fn type_array_to_type(&self, node: &SyntaxNode) -> Result<leo_ast::TypeKind> {
398        debug_assert_eq!(node.kind(), TYPE_ARRAY);
399
400        match children(node).find(|n| n.kind().is_type()) {
401            Some(element_node) => {
402                let element_type = self.to_type(&element_node)?;
403                let length_expr = self.array_length_to_expression(node)?;
404                Ok(leo_ast::ArrayType { element_type: Box::new(element_type), length: Box::new(length_expr) }.into())
405            }
406            None => {
407                // Error recovery: the rowan parser produced a TYPE_ARRAY with no element type.
408                let span = self.to_span(node);
409                self.emit_unexpected_str("element type", node.text(), span);
410                Ok(leo_ast::TypeKind::Err)
411            }
412        }
413    }
414
415    /// Convert a TYPE_VECTOR node to a VectorType.
416    fn type_vector_to_type(&self, node: &SyntaxNode) -> Result<leo_ast::TypeKind> {
417        debug_assert_eq!(node.kind(), TYPE_VECTOR);
418
419        match children(node).find(|n| n.kind().is_type()) {
420            Some(element_node) => {
421                let element_type = self.to_type(&element_node)?;
422                Ok(leo_ast::VectorType { element_type: Box::new(element_type) }.into())
423            }
424            None => {
425                // Error recovery: the rowan parser produced a TYPE_VECTOR with no element type
426                // (e.g. `[]` or `[1]` where the content is not a valid type).
427                let span = self.to_span(node);
428                self.emit_unexpected_str("element type", node.text(), span);
429                Ok(leo_ast::TypeKind::Err)
430            }
431        }
432    }
433
434    /// Extract the array length expression from a TYPE_ARRAY node.
435    fn array_length_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
436        match children(node).find(|n| n.kind() == ARRAY_LENGTH) {
437            Some(length_node) => self.require_expression(&length_node, "array length"),
438            None => {
439                // Error recovery: TYPE_ARRAY without ARRAY_LENGTH (e.g. `[T, N]` typo).
440                let span = self.to_span(node);
441                self.emit_unexpected_str("array length", node.text(), span);
442                Ok(self.error_expression(span))
443            }
444        }
445    }
446
447    /// Convert an INTEGER token to an Expression.
448    fn integer_token_to_expression(&self, token: &SyntaxToken) -> Result<leo_ast::Expression> {
449        debug_assert_eq!(token.kind(), INTEGER);
450        let text = token.text();
451        let span = self.token_span(token);
452        let id = self.builder.next_id();
453
454        // Check for integer type suffix
455        let suffixes = [
456            ("u128", leo_ast::IntegerType::U128),
457            ("u64", leo_ast::IntegerType::U64),
458            ("u32", leo_ast::IntegerType::U32),
459            ("u16", leo_ast::IntegerType::U16),
460            ("u8", leo_ast::IntegerType::U8),
461            ("i128", leo_ast::IntegerType::I128),
462            ("i64", leo_ast::IntegerType::I64),
463            ("i32", leo_ast::IntegerType::I32),
464            ("i16", leo_ast::IntegerType::I16),
465            ("i8", leo_ast::IntegerType::I8),
466        ];
467
468        for (suffix, int_type) in suffixes {
469            if text.ends_with(suffix) {
470                // Suffixed integer - preserve underscores in value
471                let value = text.strip_suffix(suffix).unwrap().to_string();
472                return Ok(leo_ast::Literal::integer(int_type, value, span, id).into());
473            }
474        }
475
476        // No suffix - use Unsuffixed variant (preserving underscores)
477        Ok(leo_ast::Literal::unsuffixed(text.to_string(), span, id).into())
478    }
479
480    /// Convert a TYPE_TUPLE node to a TupleType or Unit.
481    fn type_tuple_to_type(&self, node: &SyntaxNode) -> Result<leo_ast::TypeKind> {
482        debug_assert_eq!(node.kind(), TYPE_TUPLE);
483        let span = self.to_span(node);
484
485        let type_nodes: Vec<_> = children(node).filter(|n| n.kind().is_type()).collect();
486
487        if type_nodes.is_empty() {
488            // Unit type: ()
489            return Ok(leo_ast::TypeKind::Unit);
490        }
491
492        let elements = type_nodes.iter().map(|n| self.to_type(n)).collect::<Result<Vec<_>>>()?;
493
494        if elements.len() == 1 {
495            // Single-element tuple type is invalid - emit error
496            self.handler.emit_err(crate::errors::tuple_must_have_at_least_two_elements("type", span));
497            // Return the single element for error recovery
498            return Ok(elements.into_iter().next().unwrap());
499        }
500
501        Ok(leo_ast::TupleType::new(elements).into())
502    }
503
504    /// Convert a TYPE_OPTIONAL node to an OptionalType.
505    fn type_optional_to_type(&self, node: &SyntaxNode) -> Result<leo_ast::TypeKind> {
506        debug_assert_eq!(node.kind(), TYPE_OPTIONAL);
507
508        let inner_node = children(node).find(|n| n.kind().is_type()).expect("optional type should have inner type");
509
510        let inner = self.to_type(&inner_node)?;
511        Ok(leo_ast::TypeKind::Optional(leo_ast::OptionalType { inner: Box::new(inner) }))
512    }
513
514    /// Convert a TYPE_FINAL node to a FutureType.
515    fn type_final_to_type(&self, node: &SyntaxNode) -> Result<leo_ast::TypeKind> {
516        debug_assert_eq!(node.kind(), TYPE_FINAL);
517
518        // Collect any type children (for Future<fn(T) -> R> syntax)
519        let type_nodes: Vec<_> = children(node).filter(|n| n.kind().is_type()).collect();
520
521        if type_nodes.is_empty() {
522            // Simple Future with no explicit signature
523            return Ok(leo_ast::FutureType::default().into());
524        }
525
526        // Future with explicit signature: Future<fn(T1, T2) -> R>
527        let types = type_nodes.iter().map(|n| self.to_type(n)).collect::<Result<Vec<_>>>()?;
528
529        Ok(leo_ast::FutureType::new(types, None, true).into())
530    }
531
532    fn type_mapping_to_type(&self, node: &SyntaxNode) -> Result<leo_ast::TypeKind> {
533        debug_assert_eq!(node.kind(), TYPE_MAPPING);
534        let mut type_nodes = children(node).filter(|n| n.kind().is_type());
535        let key = type_nodes.next().map(|n| self.to_type(&n)).transpose()?.unwrap_or(leo_ast::TypeKind::Err);
536        let value = type_nodes.next().map(|n| self.to_type(&n)).transpose()?.unwrap_or(leo_ast::TypeKind::Err);
537        Ok(leo_ast::TypeKind::Mapping(leo_ast::MappingType { key: Box::new(key), value: Box::new(value) }))
538    }
539
540    // =========================================================================
541    // Expression Conversions
542    // =========================================================================
543
544    /// Convert a syntax node to an expression.
545    fn to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
546        let span = self.content_span(node);
547
548        let expr = match node.kind() {
549            LITERAL_FIELD => self.suffixed_literal_to_expression(node, "field", leo_ast::Literal::field)?,
550            LITERAL_GROUP => self.suffixed_literal_to_expression(node, "group", leo_ast::Literal::group)?,
551            LITERAL_SCALAR => self.suffixed_literal_to_expression(node, "scalar", leo_ast::Literal::scalar)?,
552            LITERAL_INT => self.int_literal_to_expression(node)?,
553            LITERAL_STRING => self.string_literal_to_expression(node)?,
554            LITERAL_ADDRESS => self.address_literal_to_expression(node)?,
555            LITERAL_BOOL => self.bool_literal_to_expression(node)?,
556            LITERAL_NONE => leo_ast::Literal::none(span, self.builder.next_id()).into(),
557            LITERAL_IDENT => self.identifier_literal_to_expression(node)?,
558            BINARY_EXPR => self.binary_expr_to_expression(node)?,
559            UNARY_EXPR => self.unary_expr_to_expression(node)?,
560            CALL_EXPR => self.call_expr_to_expression(node)?,
561            DYNAMIC_OP_EXPR => self.dynamic_op_expr_to_expression(node)?,
562            METHOD_CALL_EXPR => self.method_call_expr_to_expression(node)?,
563            FIELD_EXPR => self.field_expr_to_expression(node)?,
564            TUPLE_ACCESS_EXPR => self.tuple_access_expr_to_expression(node)?,
565            INDEX_EXPR => self.index_expr_to_expression(node)?,
566            CAST_EXPR => self.cast_expr_to_expression(node)?,
567            TERNARY_EXPR => self.ternary_expr_to_expression(node)?,
568            ARRAY_EXPR => self.array_expr_to_expression(node)?,
569            REPEAT_EXPR => self.repeat_expr_to_expression(node)?,
570            TUPLE_EXPR => self.tuple_expr_to_expression(node)?,
571            STRUCT_EXPR => self.struct_expr_to_expression(node)?,
572            STRUCT_LOCATOR_EXPR => self.struct_locator_expr_to_expression(node)?,
573            PATH_EXPR => self.path_expr_to_expression(node)?,
574            PATH_LOCATOR_EXPR => self.path_locator_expr_to_expression(node)?,
575            PROGRAM_REF_EXPR => self.program_ref_expr_to_expression(node)?,
576            SELF_EXPR => self.error_removed_context_keyword(node, sym::SelfLower),
577            BLOCK_KW_EXPR => self.error_removed_context_keyword(node, sym::block),
578            NETWORK_KW_EXPR => self.error_removed_context_keyword(node, sym::network),
579            SELF_UPPER_EXPR => {
580                self.handler.emit_err(crate::errors::reserved_identifier("Self", self.trimmed_span(node)));
581                self.error_expression(span)
582            }
583            PAREN_EXPR => {
584                // Parenthesized expression - just unwrap
585                if let Some(inner) = children(node).find(|n| n.kind().is_expression()) {
586                    self.to_expression(&inner)?
587                } else {
588                    // No inner expression found - likely parse error
589                    self.emit_unexpected_str("expression in parentheses", node.text(), span);
590                    self.error_expression(span)
591                }
592            }
593            // Final expression block
594            FINAL_EXPR => self.final_expr_to_expression(node)?,
595            // For ROOT nodes that wrap an expression (from parse_expression_entry)
596            ROOT => {
597                if let Some(inner) = children(node).find(|n| n.kind().is_expression()) {
598                    self.to_expression(&inner)?
599                } else {
600                    // Parse errors already emitted by emit_parse_errors().
601                    self.error_expression(span)
602                }
603            }
604            // Error recovery: return ErrExpression for ERROR nodes.
605            // Parse errors already emitted by emit_parse_errors().
606            ERROR => self.error_expression(span),
607            kind => panic!("unexpected expression kind: {:?}", kind),
608        };
609
610        Ok(expr)
611    }
612
613    /// Convert a suffixed literal node (field, group, scalar) to an expression.
614    fn suffixed_literal_to_expression(
615        &self,
616        node: &SyntaxNode,
617        suffix: &str,
618        ctor: fn(String, Span, leo_ast::NodeID) -> leo_ast::Literal,
619    ) -> Result<leo_ast::Expression> {
620        let span = self.content_span(node);
621        let id = self.builder.next_id();
622        let token = tokens(node).next().expect("literal node should have a token");
623        let text = token.text();
624        self.validate_hexbin_literal(text, suffix.len() as u32, span);
625        let value = text.strip_suffix(suffix).unwrap();
626        Ok(ctor(value.to_string(), span, id).into())
627    }
628
629    /// Convert a LITERAL_INT node to an expression.
630    fn int_literal_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
631        let token = tokens(node).next().expect("LITERAL_INT should have a token");
632        self.integer_token_to_expression(&token)
633    }
634
635    /// Convert a LITERAL_STRING node to an expression.
636    fn string_literal_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
637        let span = self.content_span(node);
638        let id = self.builder.next_id();
639        let token = tokens(node).next().expect("LITERAL_STRING should have a token");
640        Ok(leo_ast::Literal::string(token.text().to_string(), span, id).into())
641    }
642
643    /// Convert a LITERAL_IDENT node to an expression.
644    fn identifier_literal_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
645        let span = self.content_span(node);
646        let id = self.builder.next_id();
647        let token = tokens(node).next().expect("LITERAL_IDENT should have a token");
648        // Strip the surrounding single quotes.
649        let text = token.text();
650        let content = &text[1..text.len() - 1];
651        Ok(leo_ast::Literal::identifier(content.to_string(), span, id).into())
652    }
653
654    /// Convert a LITERAL_ADDRESS node to an expression.
655    fn address_literal_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
656        let span = self.content_span(node);
657        let id = self.builder.next_id();
658        let token = tokens(node).next().expect("LITERAL_ADDRESS should have a token");
659        let text = token.text();
660        // Validate address literal (skip program addresses like "program.aleo")
661        if !text.contains(".aleo") && text.parse::<Address<TestnetV0>>().is_err() {
662            self.handler.emit_err(crate::errors::invalid_address_lit(text, span));
663        }
664        Ok(leo_ast::Literal::address(text.to_string(), span, id).into())
665    }
666
667    /// Convert a LITERAL_BOOL node to an expression.
668    fn bool_literal_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
669        let span = self.content_span(node);
670        let id = self.builder.next_id();
671        let token = tokens(node).next().expect("LITERAL_BOOL should have a token");
672        let value = token.kind() == KW_TRUE;
673        Ok(leo_ast::Literal::boolean(value, span, id).into())
674    }
675
676    /// Convert a BINARY_EXPR node to a BinaryExpression.
677    fn binary_expr_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
678        debug_assert_eq!(node.kind(), BINARY_EXPR);
679        let span = self.content_span(node);
680        let id = self.builder.next_id();
681
682        let mut operands = children(node).filter(|n| n.kind().is_expression() || n.kind().is_type());
683
684        // Find the operator token
685        let op_token = match tokens(node).find(|t| t.kind().is_operator() || t.kind() == KW_AS) {
686            Some(token) => token,
687            None => {
688                self.emit_unexpected_str("operator in binary expression", node.text(), span);
689                return Ok(self.error_expression(span));
690            }
691        };
692
693        let op = token_to_binary_op(op_token.kind());
694
695        // Get left operand
696        let left = match operands.next() {
697            Some(left_node) => self.to_expression(&left_node)?,
698            None => {
699                self.emit_unexpected_str("left operand in binary expression", node.text(), span);
700                return Ok(self.error_expression(span));
701            }
702        };
703
704        // KW_AS should be CAST_EXPR, not binary.
705        if op_token.kind() == KW_AS {
706            self.emit_unexpected_str("cast expression", "binary AS expression", span);
707            return Ok(self.error_expression(span));
708        }
709
710        // Get right operand
711        let right = match operands.next() {
712            Some(right_node) => self.to_expression(&right_node)?,
713            None => {
714                self.emit_unexpected_str("right operand in binary expression", node.text(), span);
715                return Ok(self.error_expression(span));
716            }
717        };
718
719        Ok(leo_ast::BinaryExpression { left, right, op, span, id }.into())
720    }
721
722    /// Convert a UNARY_EXPR node to a UnaryExpression.
723    fn unary_expr_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
724        debug_assert_eq!(node.kind(), UNARY_EXPR);
725        let span = self.content_span(node);
726        let id = self.builder.next_id();
727
728        // Get the operator
729        let Some(op_token) = tokens(node).find(|t| matches!(t.kind(), BANG | MINUS)) else {
730            self.emit_unexpected_str("operator in unary expression", node.text(), span);
731            return Ok(self.error_expression(span));
732        };
733
734        let op = if op_token.kind() == BANG { leo_ast::UnaryOperation::Not } else { leo_ast::UnaryOperation::Negate };
735
736        // Get the operand
737        let Some(operand) = children(node).find(|n| n.kind().is_expression()) else {
738            self.emit_unexpected_str("operand in unary expression", node.text(), span);
739            return Ok(self.error_expression(span));
740        };
741
742        let mut receiver = self.to_expression(&operand)?;
743
744        // Fold negation into numeric literals
745        if op == leo_ast::UnaryOperation::Negate
746            && let leo_ast::Expression::Literal(leo_ast::Literal {
747                variant:
748                    leo_ast::LiteralVariant::Integer(_, ref mut string)
749                    | leo_ast::LiteralVariant::Field(ref mut string)
750                    | leo_ast::LiteralVariant::Group(ref mut string)
751                    | leo_ast::LiteralVariant::Scalar(ref mut string),
752                span: ref mut lit_span,
753                ..
754            }) = receiver
755            && !string.starts_with('-')
756        {
757            string.insert(0, '-');
758            *lit_span = span;
759            return Ok(receiver);
760        }
761
762        Ok(leo_ast::UnaryExpression { receiver, op, span, id }.into())
763    }
764
765    /// Extract type parameters and const arguments from a CONST_ARG_LIST child, if present.
766    ///
767    /// In the rowan CST, `CONST_ARG_LIST` children are either type nodes (for
768    /// intrinsic type parameters like `Deserialize::[u32]`), `DYNAMIC_CALL_RETURN_TYPE`
769    /// nodes (for visibility-annotated types like `public u64`), or expression nodes
770    /// (for const generic arguments like `Foo::[N]`).
771    fn extract_const_arg_list(&self, node: &SyntaxNode) -> Result<ConstArgList> {
772        let mut type_parameters = Vec::new();
773        let mut const_arguments = Vec::new();
774        if let Some(arg_list) = children(node).find(|n| n.kind() == CONST_ARG_LIST) {
775            for child in children(&arg_list) {
776                if child.kind() == DYNAMIC_CALL_RETURN_TYPE {
777                    // Visibility-annotated type: extract the inner type (visibility
778                    // is extracted separately when building return_types).
779                    if let Some(type_node) = children(&child).find(|n| n.kind().is_type()) {
780                        let span = self.content_span(&child);
781                        let ty = self.to_type(&type_node)?;
782                        type_parameters.push((ty, span));
783                    }
784                } else if child.kind().is_type() {
785                    let span = self.content_span(&child);
786                    let ty = self.to_type(&child)?;
787                    type_parameters.push((ty, span));
788                } else if child.kind().is_expression() {
789                    let expr = self.to_expression(&child)?;
790                    const_arguments.push(expr);
791                }
792            }
793        }
794        Ok((type_parameters, const_arguments))
795    }
796
797    /// Extract input and return types with visibility for `_dynamic_call` from the CST.
798    ///
799    /// Rule: the last type parameter is the return type, all preceding are input types.
800    /// - `_dynamic_call::[u64](...)` — return u64, no input annotations
801    /// - `_dynamic_call::[public u32, u64](...)` — input: public u32, return: u64
802    /// - `_dynamic_call::[public u32, public u32, (u32, u32)](...)` — two inputs, tuple return
803    ///
804    /// Tuple return types are unpacked into individual elements.
805    /// Visibility prefixes (public/private/constant) are extracted from `DYNAMIC_CALL_RETURN_TYPE` nodes.
806    fn extract_dynamic_call_types(
807        &self,
808        callee_node: &SyntaxNode,
809        type_parameters: &[(leo_ast::TypeKind, Span)],
810    ) -> Result<(AnnotatedTypes, AnnotatedTypes)> {
811        let Some(arg_list) = children(callee_node).find(|n| n.kind() == CONST_ARG_LIST) else {
812            return Ok((Vec::new(), Vec::new()));
813        };
814
815        // Collect all annotated type entries with their modes.
816        let mut all_entries = Vec::new();
817
818        for child in children(&arg_list) {
819            if child.kind() == DYNAMIC_CALL_RETURN_TYPE {
820                let mode = tokens(&child).find_map(|tok| token_kind_to_mode(tok.kind())).unwrap_or(leo_ast::Mode::None);
821                all_entries.push(mode);
822            } else if child.kind().is_type() {
823                all_entries.push(leo_ast::Mode::None);
824            }
825        }
826
827        // Split: everything except the last is input types, the last is return type.
828        if type_parameters.is_empty() {
829            return Ok((Vec::new(), Vec::new()));
830        }
831
832        let mut input_types = Vec::new();
833        let mut return_types = Vec::new();
834
835        let last_idx = type_parameters.len() - 1;
836        for (i, ((ty, sp), mode)) in type_parameters.iter().zip(all_entries.iter()).enumerate() {
837            if i < last_idx {
838                // Input type entry.
839                input_types.push((*mode, ty.clone(), *sp));
840            } else {
841                // Last entry: return type.
842                // - Unit `()` means void return (empty return_types).
843                // - Tuple `(T1, T2)` is unpacked into individual elements.
844                // - Anything else is a single return type.
845                match ty {
846                    leo_ast::TypeKind::Unit => {}
847                    leo_ast::TypeKind::Tuple(tuple) => {
848                        for elem in tuple.elements() {
849                            return_types.push((*mode, elem.clone(), *sp));
850                        }
851                    }
852                    _ => {
853                        return_types.push((*mode, ty.clone(), *sp));
854                    }
855                }
856            }
857        }
858
859        Ok((input_types, return_types))
860    }
861
862    /// Convert a CALL_EXPR node to a CallExpression.
863    fn call_expr_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
864        debug_assert_eq!(node.kind(), CALL_EXPR);
865        let span = self.content_span(node);
866        let id = self.builder.next_id();
867
868        // The first child should be the function being called (PATH_EXPR or PATH_LOCATOR_EXPR).
869        let mut child_iter = children(node);
870        let callee_node = child_iter.next().expect("call expr should have callee");
871
872        let function = match callee_node.kind() {
873            PATH_LOCATOR_EXPR => self.locator_tokens_to_path(&callee_node)?,
874            _ => self.path_expr_to_path(&callee_node)?,
875        };
876
877        // Collect arguments (remaining expression children)
878        let arguments = children(node)
879            .skip(1)  // Skip the callee
880            .filter(|n| n.kind().is_expression())
881            .map(|n| self.to_expression(&n))
882            .collect::<Result<Vec<_>>>()?;
883
884        // Extract type parameters and const arguments from CONST_ARG_LIST.
885        // In the rowan CST, CONST_ARG_LIST is a child of the PATH_EXPR callee node.
886        let (type_parameters, const_arguments) = self.extract_const_arg_list(&callee_node)?;
887
888        // If the path has exactly one qualifier (e.g. `group::to_x_coordinate`),
889        // try to canonicalize to an intrinsic. Non-intrinsic qualified calls
890        // fall through to the normal CallExpression below.
891        if function.user_program().is_none() && function.qualifier().len() == 1 {
892            let module = function.qualifier()[0].name;
893            let name = function.identifier().name;
894            if let Some(intrinsic_name) = leo_ast::Intrinsic::convert_path_symbols(module, name) {
895                return Ok(leo_ast::IntrinsicExpression {
896                    name: intrinsic_name,
897                    type_parameters,
898                    input_types: Vec::new(),
899                    return_types: Vec::new(),
900                    arguments,
901                    span,
902                    id,
903                }
904                .into());
905            }
906            // `Program::*` used to desugar to the program-metadata intrinsics. The replacement
907            // lives in `std::prog::*`; emit a tailored migration error.
908            if module == sym::ProgramCore {
909                let replacement = match name {
910                    sym::checksum => Some("std::prog::checksum::[PROG_ID]()"),
911                    sym::edition => Some("std::prog::edition::[PROG_ID]()"),
912                    sym::program_owner => Some("std::prog::program_owner::[PROG_ID]()"),
913                    sym::function_checksum => Some("std::prog::function_checksum::[PROG_ID, FN_NAME]()"),
914                    _ => None,
915                };
916                if let Some(replacement) = replacement {
917                    self.handler.emit_err(crate::errors::obsolete_context_access(
918                        format!("Program::{}", name),
919                        replacement,
920                        span,
921                    ));
922                    return Ok(self.error_expression(span));
923                }
924            }
925        }
926
927        // Bare intrinsic calls (e.g. `_dynamic_call::[u64](args)`).
928        // These have no qualifier and the identifier starts with `_`.
929        if function.user_program().is_none() && function.qualifier().is_empty() {
930            let name = function.identifier().name;
931            if leo_ast::Intrinsic::from_symbol(name, &type_parameters).is_some() {
932                // For _dynamic_call, split type parameters into input_types and return_types.
933                // Rule: last type param is the return type, all preceding are input types.
934                let (input_types, return_types) = if name == leo_span::sym::_dynamic_call {
935                    self.extract_dynamic_call_types(&callee_node, &type_parameters)?
936                } else {
937                    (Vec::new(), Vec::new())
938                };
939                return Ok(leo_ast::IntrinsicExpression {
940                    name,
941                    type_parameters,
942                    input_types,
943                    return_types,
944                    arguments,
945                    span,
946                    id,
947                }
948                .into());
949            }
950        }
951
952        Ok(leo_ast::CallExpression { function, const_arguments, arguments, span, id }.into())
953    }
954
955    /// Convert a `DYNAMIC_OP_EXPR` node to a `DynamicOpExpression`.
956    ///
957    /// Three surface forms distinguished by the presence of `DOT` / `L_PAREN` after `::`:
958    /// - `Interface@(target[, net])::func(args)` → `DynamicOpKind::Call`
959    /// - `Interface@(target[, net])::member.op(args)` → `DynamicOpKind::Op`
960    /// - `Interface@(target[, net])::storage_name` → `DynamicOpKind::Read`
961    fn dynamic_op_expr_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
962        debug_assert_eq!(node.kind(), DYNAMIC_OP_EXPR);
963        let span = self.content_span(node);
964        let id = self.builder.next_id();
965
966        let interface = children(node)
967            .filter(|n| n.kind().is_type())
968            .map(|n| self.to_type(&n))
969            .next()
970            .expect("Parser guarantees a type")?;
971
972        // The last `::` separates the target/network expressions from the member/function name.
973        // (Earlier `::` tokens may belong to the interface type path, e.g. `foo.aleo::Interface`.)
974        let separator_offset = tokens(node).filter(|t| t.kind() == COLON_COLON).last().map(|t| t.text_range().start());
975
976        // A DOT token inside the node (after `::`) indicates the `::member.op(args)` form.
977        let dot_offset = tokens(node).find(|t| t.kind() == DOT).map(|t| t.text_range().start());
978
979        // Whether a call-argument list is present after `::name` (or `::member.op`).
980        let has_call_parens =
981            tokens(node).any(|t| t.kind() == L_PAREN && Some(t.text_range().start()) > separator_offset);
982
983        let expr_children: Vec<_> = children(node).filter(|n| n.kind().is_expression()).collect();
984
985        let (pre_sep, post_sep): (Vec<_>, Vec<_>) = expr_children.iter().partition(|child| match separator_offset {
986            Some(sep_off) => child.text_range().start() < sep_off,
987            None => true,
988        });
989
990        let target = match pre_sep.first() {
991            Some(target_node) => self.to_expression(target_node)?,
992            None => self.error_expression(span),
993        };
994
995        let network =
996            if let Some(network_node) = pre_sep.get(1) { Some(self.to_expression(network_node)?) } else { None };
997
998        let arguments = post_sep.iter().map(|n| self.to_expression(n)).collect::<Result<Vec<_>>>()?;
999
1000        let kind = if let Some(dot_pos) = dot_offset {
1001            // `::member.op(args)` form.
1002            let member = tokens(node)
1003                .find(|t| t.kind() == IDENT && t.text_range().start() < dot_pos)
1004                .map(|t| self.to_identifier(&t))
1005                .unwrap_or_else(|| self.error_identifier(span));
1006            let op = tokens(node)
1007                .find(|t| t.kind() == IDENT && t.text_range().start() > dot_pos)
1008                .map(|t| self.to_identifier(&t))
1009                .unwrap_or_else(|| self.error_identifier(span));
1010            leo_ast::DynamicOpKind::Op { member, op, arguments }
1011        } else if has_call_parens {
1012            // `::func(args)` form.
1013            let function = tokens(node)
1014                .find(|t| t.kind() == IDENT)
1015                .map(|t| self.to_identifier(&t))
1016                .unwrap_or_else(|| self.error_identifier(span));
1017            leo_ast::DynamicOpKind::Call { function, arguments }
1018        } else {
1019            // `::storage` bare read form.
1020            let storage = tokens(node)
1021                .find(|t| t.kind() == IDENT)
1022                .map(|t| self.to_identifier(&t))
1023                .unwrap_or_else(|| self.error_identifier(span));
1024            leo_ast::DynamicOpKind::Read { storage }
1025        };
1026
1027        Ok(leo_ast::DynamicOpExpression { interface, target_program: target, network, kind, span, id }.into())
1028    }
1029
1030    /// Convert a METHOD_CALL_EXPR node to the appropriate expression.
1031    ///
1032    /// Structure: `receiver DOT method_name L_PAREN args R_PAREN`
1033    fn method_call_expr_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
1034        debug_assert_eq!(node.kind(), METHOD_CALL_EXPR);
1035        let span = self.content_span(node);
1036        let id = self.builder.next_id();
1037
1038        // First expression child is the receiver.
1039        let mut expr_children = children(node).filter(|n| n.kind().is_expression());
1040        let receiver = match expr_children.next() {
1041            Some(receiver_node) => self.to_expression(&receiver_node)?,
1042            None => {
1043                self.emit_unexpected_str("receiver in method call", node.text(), span);
1044                return Ok(self.error_expression(span));
1045            }
1046        };
1047
1048        // Get the method name (IDENT or keyword token after DOT).
1049        // `find_name_after_dot` can return keyword tokens because the grammar
1050        // accepts them in field/method name position for error recovery (e.g.
1051        // `x.assert()`). `to_identifier` asserts IDENT, so handle keywords
1052        // separately: emit a diagnostic and use an error placeholder.
1053        let method_name = match find_name_after_dot(node) {
1054            Some(method_token) if method_token.kind() == IDENT => self.to_identifier(&method_token),
1055            Some(method_token) => {
1056                let token_span = self.token_span(&method_token);
1057                self.emit_unexpected_str("identifier", method_token.text(), token_span);
1058                self.error_identifier(token_span)
1059            }
1060            None => {
1061                self.emit_unexpected_str("method name in method call", node.text(), span);
1062                return Ok(self.error_expression(span));
1063            }
1064        };
1065
1066        // Remaining expression children are the arguments.
1067        let mut args: Vec<_> = expr_children.map(|n| self.to_expression(&n)).collect::<Result<Vec<_>>>()?;
1068
1069        // Check for known methods that map to unary/binary operations or intrinsics
1070        if args.is_empty() {
1071            if let Some(op) = leo_ast::UnaryOperation::from_symbol(method_name.name) {
1072                return Ok(leo_ast::UnaryExpression { span, op, receiver, id }.into());
1073            }
1074        } else if args.len() == 1
1075            && let Some(op) = leo_ast::BinaryOperation::from_symbol(method_name.name)
1076        {
1077            return Ok(leo_ast::BinaryExpression { span, op, left: receiver, right: args.pop().unwrap(), id }.into());
1078        }
1079
1080        // Check for known intrinsic method calls.
1081        // Ordering follows the lossless parser (conversions.rs):
1082        // 1. Specific intrinsics (signature, Future, Optional)
1083        // 2. Unresolved `.get()`/`.set()` (deferred to type checker)
1084        // 3. Vector/Mapping methods
1085        let method = method_name.name;
1086        let all_args = || std::iter::once(receiver.clone()).chain(args.clone()).collect::<Vec<_>>();
1087
1088        // Known module-specific intrinsics matched by name and arg count.
1089        let intrinsic_name = match args.len() {
1090            2 => leo_ast::Intrinsic::convert_path_symbols(sym::signature, method),
1091            0 => leo_ast::Intrinsic::convert_path_symbols(sym::Final, method)
1092                .or_else(|| leo_ast::Intrinsic::convert_path_symbols(sym::Optional, method)),
1093            1 => leo_ast::Intrinsic::convert_path_symbols(sym::Optional, method),
1094            _ => None,
1095        };
1096        if let Some(intrinsic_name) = intrinsic_name {
1097            return Ok(self.intrinsic_expression(intrinsic_name, all_args(), span));
1098        }
1099
1100        // Unresolved `.get()` / `.set()` — the receiver type is unknown at
1101        // parse time, so defer resolution to the type checker.
1102        if method == sym::get && args.len() == 1 {
1103            return Ok(self.intrinsic_expression(Symbol::intern("__unresolved_get"), all_args(), span));
1104        }
1105        if method == sym::set && args.len() == 2 {
1106            return Ok(self.intrinsic_expression(Symbol::intern("__unresolved_set"), all_args(), span));
1107        }
1108
1109        // Remaining Vector/Mapping method intrinsics.
1110        for module in [sym::Vector, sym::Mapping] {
1111            if let Some(intrinsic_name) = leo_ast::Intrinsic::convert_path_symbols(module, method) {
1112                return Ok(self.intrinsic_expression(intrinsic_name, all_args(), span));
1113            }
1114        }
1115
1116        // Unknown method call - emit error
1117        self.handler.emit_err(crate::errors::invalid_method_call(receiver, method_name, args.len(), span));
1118        Ok(self.error_expression(span))
1119    }
1120
1121    /// Convert a TUPLE_ACCESS_EXPR node to a TupleAccess expression.
1122    fn tuple_access_expr_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
1123        debug_assert_eq!(node.kind(), TUPLE_ACCESS_EXPR);
1124        let span = self.content_span(node);
1125        let id = self.builder.next_id();
1126
1127        let inner = if let Some(inner_node) = children(node).find(|n| n.kind().is_expression()) {
1128            self.to_expression(&inner_node)?
1129        } else {
1130            self.emit_unexpected_str("expression in tuple access", node.text(), span);
1131            return Ok(self.error_expression(span));
1132        };
1133
1134        let index_token = match tokens(node).find(|t| t.kind() == INTEGER) {
1135            Some(token) => token,
1136            None => {
1137                self.emit_unexpected_str("tuple index", node.text(), span);
1138                return Ok(self.error_expression(span));
1139            }
1140        };
1141
1142        let index_text = index_token.text().replace('_', "");
1143        let index: usize = match index_text.parse() {
1144            Ok(idx) => idx,
1145            Err(_) => {
1146                self.emit_unexpected_str("valid tuple index", index_text, span);
1147                return Ok(self.error_expression(span));
1148            }
1149        };
1150        Ok(leo_ast::TupleAccess { tuple: inner, index: index.into(), span, id }.into())
1151    }
1152
1153    /// Convert a FIELD_EXPR node to a MemberAccess expression.
1154    fn field_expr_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
1155        debug_assert_eq!(node.kind(), FIELD_EXPR);
1156        let span = self.content_span(node);
1157        let id = self.builder.next_id();
1158
1159        // Get the inner expression and its CST kind (used for special-access dispatch).
1160        let (inner, first_child_kind) = match children(node).find(|n| n.kind().is_expression()) {
1161            Some(n) => {
1162                let kind = n.kind();
1163                let lowered = if matches!(kind, SELF_EXPR | BLOCK_KW_EXPR | NETWORK_KW_EXPR) {
1164                    self.error_expression(self.trimmed_span(&n))
1165                } else {
1166                    self.to_expression(&n)?
1167                };
1168                (lowered, kind)
1169            }
1170            None => {
1171                self.emit_unexpected_str("expression in field access", node.text(), span);
1172                return Ok(self.error_expression(span));
1173            }
1174        };
1175
1176        // Get the field name (token after DOT).
1177        // Field names can be identifiers or keywords.
1178        let field_token = match find_name_after_dot(node) {
1179            Some(token) => token,
1180            None => {
1181                self.emit_unexpected_str("field name in field access", node.text(), span);
1182                return Ok(self.error_expression(span));
1183            }
1184        };
1185
1186        // Check for `name.aleo` program ID references. The rowan parser
1187        // creates FIELD_EXPR for these, but the reference parser treats
1188        // them as address literals (program addresses).
1189        if field_token.kind() == KW_ALEO
1190            && let leo_ast::Expression::Path(ref path) = inner
1191            && path.user_program().is_none()
1192            && path.qualifier().is_empty()
1193        {
1194            let full_name = format!("{}.aleo", path.identifier().name);
1195            return Ok(leo_ast::Literal::address(full_name, span, id).into());
1196        }
1197
1198        // `self.X`, `block.X`, and `network.X` used to be sugar for execution-context
1199        // intrinsics. The sugar has been removed; the same values are now reached
1200        // through the `std::ctx` module. Emit a targeted migration error that names
1201        // the replacement.
1202        let field_name = Symbol::intern(field_token.text());
1203        let removed_access = match (first_child_kind, field_name) {
1204            (SELF_EXPR, sym::address) => Some(("self.address", "std::ctx::addr()")),
1205            (SELF_EXPR, sym::caller) => Some(("self.caller", "std::ctx::caller()")),
1206            (SELF_EXPR, sym::checksum) => Some(("self.checksum", "std::ctx::checksum()")),
1207            (SELF_EXPR, sym::edition) => Some(("self.edition", "std::ctx::edition()")),
1208            (SELF_EXPR, sym::id) => Some(("self.id", "std::ctx::id()")),
1209            (SELF_EXPR, sym::program_owner) => Some(("self.program_owner", "std::ctx::program_owner()")),
1210            (SELF_EXPR, sym::signer) => Some(("self.signer", "std::ctx::signer()")),
1211            (BLOCK_KW_EXPR, sym::height) => Some(("block.height", "std::ctx::block_height()")),
1212            (BLOCK_KW_EXPR, sym::timestamp) => Some(("block.timestamp", "std::ctx::block_timestamp()")),
1213            (NETWORK_KW_EXPR, sym::id) => Some(("network.id", "std::ctx::network_id()")),
1214            _ => None,
1215        };
1216        if let Some((old, replacement)) = removed_access {
1217            self.handler.emit_err(crate::errors::obsolete_context_access(old, replacement, span));
1218            return Ok(self.error_expression(span));
1219        }
1220        if matches!(first_child_kind, SELF_EXPR | BLOCK_KW_EXPR | NETWORK_KW_EXPR) {
1221            let keyword = match first_child_kind {
1222                SELF_EXPR => "self",
1223                BLOCK_KW_EXPR => "block",
1224                NETWORK_KW_EXPR => "network",
1225                _ => unreachable!(),
1226            };
1227            self.handler.emit_err(crate::errors::obsolete_context_keyword(keyword, span));
1228            return Ok(self.error_expression(span));
1229        }
1230
1231        // Field token may be an identifier or keyword (e.g. `obj.aleo`).
1232        let field_span = self.token_span(&field_token);
1233        let name = leo_ast::Identifier { name: field_name, span: field_span, id: self.builder.next_id() };
1234        Ok(leo_ast::MemberAccess { inner, name, span, id }.into())
1235    }
1236
1237    /// Convert an INDEX_EXPR node to an ArrayAccess expression.
1238    fn index_expr_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
1239        debug_assert_eq!(node.kind(), INDEX_EXPR);
1240        let span = self.content_span(node);
1241        let id = self.builder.next_id();
1242
1243        let mut exprs = children(node).filter(|n| n.kind().is_expression());
1244
1245        let array = match exprs.next() {
1246            Some(n) => self.to_expression(&n)?,
1247            None => {
1248                self.emit_unexpected_str("array in index expression", node.text(), span);
1249                return Ok(self.error_expression(span));
1250            }
1251        };
1252
1253        let index = match exprs.next() {
1254            Some(n) => self.to_expression(&n)?,
1255            None => {
1256                self.emit_unexpected_str("index in index expression", node.text(), span);
1257                return Ok(self.error_expression(span));
1258            }
1259        };
1260
1261        Ok(leo_ast::ArrayAccess { array, index, span, id }.into())
1262    }
1263
1264    /// Convert a CAST_EXPR node to a CastExpression.
1265    fn cast_expr_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
1266        debug_assert_eq!(node.kind(), CAST_EXPR);
1267        let span = self.content_span(node);
1268        let id = self.builder.next_id();
1269
1270        // Get the expression being cast
1271        let Some(expr_node) = children(node).find(|n| n.kind().is_expression()) else {
1272            self.emit_unexpected_str("expression in cast", node.text(), span);
1273            return Ok(self.error_expression(span));
1274        };
1275        let expression = self.to_expression(&expr_node)?;
1276
1277        // Get the target type
1278        let Some(type_node) = children(node).find(|n| n.kind().is_type()) else {
1279            self.emit_unexpected_str("type in cast expression", node.text(), span);
1280            return Ok(self.error_expression(span));
1281        };
1282        let type_ = self.to_type_repr(&type_node)?;
1283
1284        Ok(leo_ast::CastExpression { expression, type_, span, id }.into())
1285    }
1286
1287    /// Convert a TERNARY_EXPR node to a TernaryExpression.
1288    fn ternary_expr_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
1289        debug_assert_eq!(node.kind(), TERNARY_EXPR);
1290        let span = self.content_span(node);
1291        let id = self.builder.next_id();
1292
1293        let mut exprs = children(node).filter(|n| n.kind().is_expression());
1294
1295        let condition = match exprs.next() {
1296            Some(n) => self.to_expression(&n)?,
1297            None => {
1298                self.emit_unexpected_str("condition in ternary expression", node.text(), span);
1299                return Ok(self.error_expression(span));
1300            }
1301        };
1302
1303        let if_true = match exprs.next() {
1304            Some(n) => self.to_expression(&n)?,
1305            None => {
1306                self.emit_unexpected_str("true branch in ternary expression", node.text(), span);
1307                return Ok(self.error_expression(span));
1308            }
1309        };
1310
1311        let if_false = match exprs.next() {
1312            Some(n) => self.to_expression(&n)?,
1313            None => {
1314                self.emit_unexpected_str("false branch in ternary expression", node.text(), span);
1315                return Ok(self.error_expression(span));
1316            }
1317        };
1318
1319        Ok(leo_ast::TernaryExpression { condition, if_true, if_false, span, id }.into())
1320    }
1321
1322    /// Convert an ARRAY_EXPR node to an ArrayExpression.
1323    fn array_expr_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
1324        debug_assert_eq!(node.kind(), ARRAY_EXPR);
1325        let span = self.content_span(node);
1326        let id = self.builder.next_id();
1327
1328        let elements = children(node)
1329            .filter(|n| n.kind().is_expression())
1330            .map(|n| self.to_expression(&n))
1331            .collect::<Result<Vec<_>>>()?;
1332
1333        Ok(leo_ast::ArrayExpression { elements, span, id }.into())
1334    }
1335
1336    /// Convert a REPEAT_EXPR node to a RepeatExpression.
1337    fn repeat_expr_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
1338        debug_assert_eq!(node.kind(), REPEAT_EXPR);
1339        let span = self.content_span(node);
1340        let id = self.builder.next_id();
1341
1342        let mut exprs = children(node).filter(|n| n.kind().is_expression());
1343        let expr = match exprs.next() {
1344            Some(n) => self.to_expression(&n)?,
1345            None => {
1346                self.emit_unexpected_str("expression in repeat", node.text(), span);
1347                return Ok(self.error_expression(span));
1348            }
1349        };
1350        let count = match exprs.next() {
1351            Some(n) => self.to_expression(&n)?,
1352            None => {
1353                self.emit_unexpected_str("repeat count", node.text(), span);
1354                return Ok(self.error_expression(span));
1355            }
1356        };
1357
1358        Ok(leo_ast::RepeatExpression { expr, count, span, id }.into())
1359    }
1360
1361    /// Convert a TUPLE_EXPR node to a TupleExpression or UnitExpression.
1362    fn tuple_expr_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
1363        debug_assert_eq!(node.kind(), TUPLE_EXPR);
1364        let span = self.content_span(node);
1365        let id = self.builder.next_id();
1366
1367        let elements: Vec<_> = children(node)
1368            .filter(|n| n.kind().is_expression())
1369            .map(|n| self.to_expression(&n))
1370            .collect::<Result<Vec<_>>>()?;
1371
1372        match elements.len() {
1373            0 => {
1374                // Empty tuple is invalid - emit error
1375                self.handler.emit_err(crate::errors::tuple_must_have_at_least_two_elements("expression", span));
1376                // Return unit expression for error recovery
1377                Ok(leo_ast::UnitExpression { span, id }.into())
1378            }
1379            1 => {
1380                // Single-element tuple is invalid - emit error
1381                self.handler.emit_err(crate::errors::tuple_must_have_at_least_two_elements("expression", span));
1382                // Return the single element for error recovery
1383                Ok(elements.into_iter().next().unwrap())
1384            }
1385            _ => Ok(leo_ast::TupleExpression { elements, span, id }.into()),
1386        }
1387    }
1388
1389    /// Build a `CompositeExpression` from a path, collecting field initializers
1390    /// and const arguments from the node.
1391    fn composite_expression_from_path(
1392        &self,
1393        node: &SyntaxNode,
1394        path: leo_ast::Path,
1395        span: Span,
1396        id: leo_ast::NodeID,
1397    ) -> Result<leo_ast::Expression> {
1398        let members = children(node)
1399            .filter(|n| matches!(n.kind(), STRUCT_FIELD_INIT | STRUCT_FIELD_SHORTHAND))
1400            .map(|n| self.struct_field_init_to_member(&n))
1401            .collect::<Result<Vec<_>>>()?;
1402        let base = children(node)
1403            .find(|n| n.kind() == STRUCT_BASE_UPDATE)
1404            .and_then(|n| children(&n).find(|c| c.kind().is_expression()))
1405            .map(|n| self.to_expression(&n).map(Box::new))
1406            .transpose()?;
1407        let (_type_parameters, const_arguments) = self.extract_const_arg_list(node)?;
1408        Ok(leo_ast::CompositeExpression { path, const_arguments, members, base, span, id }.into())
1409    }
1410
1411    /// Convert a STRUCT_EXPR node to a CompositeExpression.
1412    fn struct_expr_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
1413        debug_assert_eq!(node.kind(), STRUCT_EXPR);
1414        let span = self.content_span(node);
1415        let id = self.builder.next_id();
1416        let path = self.struct_expr_to_path(node)?;
1417        self.composite_expression_from_path(node, path, span, id)
1418    }
1419
1420    /// Extract a Path from a STRUCT_EXPR node's name tokens.
1421    fn struct_expr_to_path(&self, node: &SyntaxNode) -> Result<leo_ast::Path> {
1422        let fallback_span = self.content_span(node);
1423
1424        // Collect IDENT tokens before L_BRACE, deriving span from identifiers.
1425        let mut path_components = Vec::new();
1426        for token in tokens(node) {
1427            if token.kind() == L_BRACE {
1428                break;
1429            }
1430            if token.kind() == IDENT {
1431                path_components.push(self.to_identifier(&token));
1432            }
1433        }
1434
1435        let path_span = match (path_components.first(), path_components.last()) {
1436            (Some(first), Some(last)) => Span::new(first.span.lo, last.span.hi),
1437            _ => fallback_span,
1438        };
1439
1440        let name = match path_components.pop() {
1441            Some(name) => name,
1442            None => {
1443                self.emit_unexpected_str("type name in struct expression", node.text(), fallback_span);
1444                self.error_identifier(fallback_span)
1445            }
1446        };
1447        Ok(leo_ast::Path::new(None, path_components, name, path_span, self.builder.next_id()))
1448    }
1449
1450    /// Convert a STRUCT_LOCATOR_EXPR node to an Expression.
1451    fn struct_locator_expr_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
1452        let span = self.content_span(node);
1453        let id = self.builder.next_id();
1454        let path = self.locator_tokens_to_path(node)?;
1455        self.composite_expression_from_path(node, path, span, id)
1456    }
1457
1458    /// Convert a PATH_LOCATOR_EXPR node to an Expression.
1459    fn path_locator_expr_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
1460        let path = self.locator_tokens_to_path(node)?;
1461        Ok(leo_ast::Expression::Path(path))
1462    }
1463
1464    /// Extract program and type name from a locator node's IDENT tokens.
1465    ///
1466    /// Locator nodes have the structure:
1467    ///   `IDENT DOT KW_ALEO COLON_COLON IDENT [COLON_COLON IDENT]* [COLON_COLON CONST_ARG_LIST]`.
1468    /// The first IDENT is the program name, the remaining IDENTs form qualifier segments and the
1469    /// final item name (e.g. `program.aleo::sub::item` → qualifier: `[sub]`, name: `item`).
1470    fn locator_tokens_to_path(&self, node: &SyntaxNode) -> Result<leo_ast::Path> {
1471        let span = self.to_span(node);
1472        let all_idents: Vec<_> = tokens(node).filter(|t| t.kind() == IDENT).collect();
1473
1474        // First IDENT is the program name — with error recovery if absent.
1475        let program_ident = match all_idents.first() {
1476            Some(t) => self.to_identifier(t),
1477            None => {
1478                self.emit_unexpected_str("program name", node.text(), span);
1479                self.error_identifier(span)
1480            }
1481        };
1482
1483        // KW_ALEO is always present; fall back to `span` if somehow absent.
1484        let network_ident = leo_ast::Identifier {
1485            name: Symbol::intern("aleo"),
1486            span: tokens(node).find(|t| t.kind() == KW_ALEO).map(|t| self.token_span(&t)).unwrap_or(span),
1487            id: self.builder.next_id(),
1488        };
1489
1490        // Wrap into ProgramId — network is never None.
1491        let program = leo_ast::ProgramId { name: program_ident, network: network_ident };
1492
1493        // Last IDENT is the item name; [1..len-1] are qualifiers.
1494        // Error recovery: the rowan parser only bumps the name IDENT `if self.at(IDENT)`,
1495        // so a non-IDENT token after `::` (e.g. `a.aleo::;`) produces a locator with no name.
1496        let (qualifier, name) = if all_idents.len() < 2 {
1497            self.emit_unexpected_str("identifier", node.text(), span);
1498            (Vec::new(), self.error_identifier(span))
1499        } else {
1500            let name = self.to_identifier(all_idents.last().unwrap()); // safe: len >= 2
1501            let qualifier: Vec<_> = all_idents[1..all_idents.len() - 1].iter().map(|t| self.to_identifier(t)).collect();
1502            (qualifier, name)
1503        };
1504
1505        let path_span = Span::new(program.name.span.lo, name.span.hi);
1506        Ok(leo_ast::Path::new(Some(program), qualifier, name, path_span, self.builder.next_id()))
1507    }
1508
1509    /// Convert a STRUCT_FIELD_INIT or STRUCT_FIELD_SHORTHAND node to a CompositeFieldInitializer.
1510    fn struct_field_init_to_member(&self, node: &SyntaxNode) -> Result<leo_ast::CompositeFieldInitializer> {
1511        debug_assert!(matches!(node.kind(), STRUCT_FIELD_INIT | STRUCT_FIELD_SHORTHAND));
1512        let span = self.content_span(node);
1513        let id = self.builder.next_id();
1514
1515        let Some(ident_token) = tokens(node).find(|t| t.kind() == IDENT) else {
1516            self.emit_unexpected_str("identifier in struct field", node.text(), span);
1517            return Ok(leo_ast::CompositeFieldInitializer {
1518                identifier: self.error_identifier(span),
1519                expression: None,
1520                span,
1521                id,
1522            });
1523        };
1524        let identifier = self.to_identifier(&ident_token);
1525
1526        let expression = if node.kind() == STRUCT_FIELD_INIT {
1527            children(node).find(|n| n.kind().is_expression()).map(|n| self.to_expression(&n)).transpose()?
1528        } else {
1529            None
1530        };
1531
1532        Ok(leo_ast::CompositeFieldInitializer { identifier, expression, span, id })
1533    }
1534
1535    /// Convert a PROGRAM_REF_EXPR node (`name.aleo`) to an address literal.
1536    fn program_ref_expr_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
1537        debug_assert_eq!(node.kind(), PROGRAM_REF_EXPR);
1538        let span = self.content_span(node);
1539        let id = self.builder.next_id();
1540        let text: String = tokens(node).map(|t| t.text().to_string()).collect();
1541        Ok(leo_ast::Literal::address(text, span, id).into())
1542    }
1543
1544    /// Convert a PATH_EXPR node to an Expression.
1545    fn path_expr_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
1546        debug_assert_eq!(node.kind(), PATH_EXPR);
1547
1548        let path = self.path_expr_to_path(node)?;
1549        let span = self.trimmed_span(node);
1550        let id = self.builder.next_id();
1551
1552        // Detect `group::GEN` → IntrinsicExpression.
1553        if path.user_program().is_none()
1554            && path.qualifier().len() == 1
1555            && path.qualifier()[0].name == sym::group
1556            && path.identifier().name == sym::GEN
1557        {
1558            return Ok(self.intrinsic_expression(sym::_group_gen, Vec::new(), span));
1559        }
1560
1561        // Detect signature literals (identifiers starting with `sign1` that
1562        // parse as valid `Signature<TestnetV0>`).
1563        if path.user_program().is_none() && path.qualifier().is_empty() {
1564            let name_text = path.identifier().name.to_string();
1565            if name_text.starts_with("sign1") && name_text.parse::<Signature<TestnetV0>>().is_ok() {
1566                return Ok(leo_ast::Literal::signature(name_text, span, id).into());
1567            }
1568            // Reject standalone `_ident` in expression context -- these are only
1569            // valid as the start of intrinsic calls (e.g. `_self_caller()`).
1570            if name_text.starts_with('_') {
1571                self.handler.emit_err(crate::errors::identifier_cannot_start_with_underscore(span));
1572                return Ok(self.error_expression(span));
1573            }
1574        }
1575
1576        Ok(leo_ast::Expression::Path(path))
1577    }
1578
1579    /// Emit the migration error for a bare reference to one of the removed context keywords
1580    /// (`self`, `block`, `network`) and return an error placeholder expression. The sugar that
1581    /// turned these into expressions has been replaced by the `std::ctx` module.
1582    fn error_removed_context_keyword(&self, node: &SyntaxNode, name: Symbol) -> leo_ast::Expression {
1583        let span = self.trimmed_span(node);
1584        let keyword = match name {
1585            sym::SelfLower => "self",
1586            sym::block => "block",
1587            sym::network => "network",
1588            _ => unreachable!("error_removed_context_keyword called with non-context keyword"),
1589        };
1590        self.handler.emit_err(crate::errors::obsolete_context_keyword(keyword, span));
1591        self.error_expression(span)
1592    }
1593
1594    /// Convert a FINAL_EXPR node to an Expression.
1595    fn final_expr_to_expression(&self, node: &SyntaxNode) -> Result<leo_ast::Expression> {
1596        debug_assert_eq!(node.kind(), FINAL_EXPR);
1597        let span = self.content_span(node);
1598        let id = self.builder.next_id();
1599
1600        // Find the block inside the final expression
1601        if let Some(block_node) = children(node).find(|n| n.kind() == BLOCK) {
1602            let block = self.to_block(&block_node)?;
1603            Ok(leo_ast::AsyncExpression { block, span, id }.into())
1604        } else {
1605            // No block found - emit error
1606            self.emit_unexpected_str("block in final expression", node.text(), span);
1607            Ok(self.error_expression(span))
1608        }
1609    }
1610
1611    /// Convert a PATH_EXPR node to a Path.
1612    ///
1613    /// Note: The lexer produces single IDENT tokens for associated function paths
1614    /// like `group::to_x_coordinate` or `signature::verify` (via the `PathSpecial`
1615    /// regex). These coalesced tokens must be split on `::` to build correct path
1616    /// components. This is a fundamental lexer constraint — `group` and `signature`
1617    /// are type keywords that must also work as associated function path prefixes.
1618    fn path_expr_to_path(&self, node: &SyntaxNode) -> Result<leo_ast::Path> {
1619        let span = self.trimmed_span(node);
1620
1621        // Regular path: collect identifiers
1622        let mut path_components = Vec::new();
1623        for token in tokens(node) {
1624            match token.kind() {
1625                IDENT => {
1626                    let text = token.text();
1627                    // The lexer produces single IDENT tokens for associated function
1628                    // paths like "group::to_x_coordinate" or "signature::verify".
1629                    // Split these on "::" to build the correct path components.
1630                    if text.contains("::") {
1631                        let token_span = self.token_span(&token);
1632                        let mut offset = token_span.lo;
1633                        for (i, segment) in text.split("::").enumerate() {
1634                            if i > 0 {
1635                                offset += 2; // skip "::"
1636                            }
1637                            let seg_span = Span::new(offset, offset + segment.len() as u32);
1638                            path_components.push(leo_ast::Identifier {
1639                                name: Symbol::intern(segment),
1640                                span: seg_span,
1641                                id: self.builder.next_id(),
1642                            });
1643                            offset += segment.len() as u32;
1644                        }
1645                    } else {
1646                        path_components.push(self.to_identifier(&token));
1647                    }
1648                }
1649                kind => {
1650                    if let Some(name) = keyword_to_path_symbol(kind) {
1651                        path_components.push(leo_ast::Identifier {
1652                            name,
1653                            span: self.token_span(&token),
1654                            id: self.builder.next_id(),
1655                        });
1656                    }
1657                }
1658            }
1659        }
1660
1661        let name = match path_components.pop() {
1662            Some(name) => name,
1663            None => {
1664                self.emit_unexpected_str("identifier in path", node.text(), span);
1665                self.error_identifier(span)
1666            }
1667        };
1668        Ok(leo_ast::Path::new(None, path_components, name, span, self.builder.next_id()))
1669    }
1670
1671    // =========================================================================
1672    // Statement Conversions
1673    // =========================================================================
1674
1675    /// Convert a syntax node to a statement.
1676    fn to_statement(&self, node: &SyntaxNode) -> Result<leo_ast::Statement> {
1677        let span = self.to_span(node);
1678        let id = self.builder.next_id();
1679
1680        let stmt = match node.kind() {
1681            LET_STMT => self.let_stmt_to_statement(node)?,
1682            CONST_STMT => self.const_stmt_to_statement(node)?,
1683            RETURN_STMT => self.return_stmt_to_statement(node)?,
1684            EXPR_STMT => self.expr_stmt_to_statement(node)?,
1685            ASSIGN_STMT => self.simple_assign_to_statement(node)?,
1686            COMPOUND_ASSIGN_STMT => self.compound_assign_to_statement(node)?,
1687            IF_STMT => self.if_stmt_to_statement(node)?,
1688            FOR_STMT | FOR_INCLUSIVE_STMT => self.for_stmt_to_statement(node)?,
1689            BLOCK => self.to_block(node)?.into(),
1690            ASSERT_STMT => {
1691                let expression = self.require_expression(node, "expression in assert")?;
1692                leo_ast::AssertStatement { variant: leo_ast::AssertVariant::Assert(expression), span, id }.into()
1693            }
1694            ASSERT_EQ_STMT => {
1695                self.assert_binary_to_statement(node, "assert_eq", span, id, leo_ast::AssertVariant::AssertEq)?
1696            }
1697            ASSERT_NEQ_STMT => {
1698                self.assert_binary_to_statement(node, "assert_neq", span, id, leo_ast::AssertVariant::AssertNeq)?
1699            }
1700            // For ROOT nodes that wrap a statement (from parse_statement_entry)
1701            ROOT => {
1702                if let Some(inner) = children(node).find(|n| n.kind().is_statement()) {
1703                    self.to_statement(&inner)?
1704                } else {
1705                    // Parse errors already emitted by emit_parse_errors().
1706                    leo_ast::ExpressionStatement { expression: self.error_expression(span), span, id }.into()
1707                }
1708            }
1709            // Error recovery for ERROR nodes.
1710            // Parse errors already emitted by emit_parse_errors().
1711            ERROR => leo_ast::ExpressionStatement { expression: self.error_expression(span), span, id }.into(),
1712            kind => panic!("unexpected statement kind: {:?}", kind),
1713        };
1714
1715        Ok(stmt)
1716    }
1717
1718    /// Convert an ASSERT_EQ_STMT or ASSERT_NEQ_STMT node to an AssertStatement.
1719    fn assert_binary_to_statement(
1720        &self,
1721        node: &SyntaxNode,
1722        label: &str,
1723        span: Span,
1724        id: leo_ast::NodeID,
1725        make_variant: fn(leo_ast::Expression, leo_ast::Expression) -> leo_ast::AssertVariant,
1726    ) -> Result<leo_ast::Statement> {
1727        let mut exprs = children(node).filter(|n| n.kind().is_expression());
1728        let e0 = match exprs.next() {
1729            Some(expr) => self.to_expression(&expr)?,
1730            None => {
1731                self.emit_unexpected_str(&format!("first expression in {label}"), node.text(), span);
1732                self.error_expression(span)
1733            }
1734        };
1735        let e1 = match exprs.next() {
1736            Some(expr) => self.to_expression(&expr)?,
1737            None => {
1738                self.emit_unexpected_str(&format!("second expression in {label}"), node.text(), span);
1739                self.error_expression(span)
1740            }
1741        };
1742        Ok(leo_ast::AssertStatement { variant: make_variant(e0, e1), span, id }.into())
1743    }
1744
1745    /// Convert a BLOCK node to a Block.
1746    fn to_block(&self, node: &SyntaxNode) -> Result<leo_ast::Block> {
1747        debug_assert_eq!(node.kind(), BLOCK);
1748        let span = self.to_span(node);
1749        let id = self.builder.next_id();
1750
1751        let statements = children(node)
1752            .filter(|n| n.kind().is_statement())
1753            .map(|n| self.to_statement(&n))
1754            .collect::<Result<Vec<_>>>()?;
1755
1756        Ok(leo_ast::Block { statements, span, id })
1757    }
1758
1759    /// Convert a LET_STMT node to a DefinitionStatement.
1760    fn let_stmt_to_statement(&self, node: &SyntaxNode) -> Result<leo_ast::Statement> {
1761        debug_assert_eq!(node.kind(), LET_STMT);
1762        let span = self.to_span(node);
1763        let id = self.builder.next_id();
1764
1765        // Find the pattern
1766        let place = match children(node).find(|n| matches!(n.kind(), IDENT_PATTERN | TUPLE_PATTERN | WILDCARD_PATTERN))
1767        {
1768            Some(pattern_node) => self.pattern_to_definition_place(&pattern_node)?,
1769            None => {
1770                self.emit_unexpected_str("pattern in let statement", node.text(), span);
1771                leo_ast::DefinitionPlace::Single(self.error_identifier(span))
1772            }
1773        };
1774
1775        // Find type annotation if present
1776        let type_ = children(node).find(|n| n.kind().is_type()).map(|n| self.to_type_repr(&n)).transpose()?;
1777
1778        let value = self.require_expression(node, "value in let statement")?;
1779
1780        Ok(leo_ast::DefinitionStatement { place, type_, value, span, id }.into())
1781    }
1782
1783    /// Convert a pattern node to a DefinitionPlace.
1784    fn pattern_to_definition_place(&self, node: &SyntaxNode) -> Result<leo_ast::DefinitionPlace> {
1785        let span = self.to_span(node);
1786        match node.kind() {
1787            IDENT_PATTERN => {
1788                let ident = self.require_ident(node, "identifier in pattern");
1789                self.validate_definition_identifier(&ident);
1790                Ok(leo_ast::DefinitionPlace::Single(ident))
1791            }
1792            TUPLE_PATTERN => {
1793                let names = children(node)
1794                    .filter(|n| matches!(n.kind(), IDENT_PATTERN | WILDCARD_PATTERN))
1795                    .map(|n| {
1796                        if n.kind() == WILDCARD_PATTERN {
1797                            // Use a placeholder identifier for wildcard
1798                            let span = self.to_span(&n);
1799                            leo_ast::Identifier { name: Symbol::intern("_"), span, id: self.builder.next_id() }
1800                        } else {
1801                            let ident = self.require_ident(&n, "identifier in pattern");
1802                            self.validate_definition_identifier(&ident);
1803                            ident
1804                        }
1805                    })
1806                    .collect();
1807                Ok(leo_ast::DefinitionPlace::Multiple(names))
1808            }
1809            WILDCARD_PATTERN => {
1810                let ident = leo_ast::Identifier { name: Symbol::intern("_"), span, id: self.builder.next_id() };
1811                Ok(leo_ast::DefinitionPlace::Single(ident))
1812            }
1813            _ => {
1814                self.emit_unexpected_str("valid pattern", node.text(), span);
1815                let ident = self.error_identifier(span);
1816                Ok(leo_ast::DefinitionPlace::Single(ident))
1817            }
1818        }
1819    }
1820
1821    /// Convert a CONST_STMT node to a ConstDeclaration.
1822    fn const_stmt_to_statement(&self, node: &SyntaxNode) -> Result<leo_ast::Statement> {
1823        debug_assert_eq!(node.kind(), CONST_STMT);
1824        let span = self.to_span(node);
1825        let id = self.builder.next_id();
1826
1827        let place = self.require_ident(node, "name in const declaration");
1828
1829        let type_ = self.require_type_repr(node, "type in const declaration")?;
1830
1831        let value = self.require_expression(node, "value in const declaration")?;
1832
1833        // Visibility doesn't apply to statement-level `const`s.
1834        Ok(leo_ast::ConstDeclaration { is_exported: None, place, type_, value, span, id }.into())
1835    }
1836
1837    /// Convert a RETURN_STMT node to a ReturnStatement.
1838    fn return_stmt_to_statement(&self, node: &SyntaxNode) -> Result<leo_ast::Statement> {
1839        debug_assert_eq!(node.kind(), RETURN_STMT);
1840        let span = self.to_span(node);
1841        let id = self.builder.next_id();
1842
1843        // Get optional expression
1844        let expression = children(node)
1845            .find(|n| n.kind().is_expression())
1846            .map(|n| self.to_expression(&n))
1847            .transpose()?
1848            .unwrap_or_else(|| leo_ast::UnitExpression { span, id: self.builder.next_id() }.into());
1849
1850        Ok(leo_ast::ReturnStatement { expression, span, id }.into())
1851    }
1852
1853    /// Convert an EXPR_STMT node to an ExpressionStatement.
1854    fn expr_stmt_to_statement(&self, node: &SyntaxNode) -> Result<leo_ast::Statement> {
1855        debug_assert_eq!(node.kind(), EXPR_STMT);
1856        let span = self.to_span(node);
1857        let id = self.builder.next_id();
1858
1859        let expression = self.require_expression(node, "expression in expression statement")?;
1860
1861        Ok(leo_ast::ExpressionStatement { expression, span, id }.into())
1862    }
1863
1864    /// Convert a simple ASSIGN_STMT (`x = expr;`) to a Statement.
1865    fn simple_assign_to_statement(&self, node: &SyntaxNode) -> Result<leo_ast::Statement> {
1866        debug_assert_eq!(node.kind(), ASSIGN_STMT);
1867        let span = self.to_span(node);
1868        let id = self.builder.next_id();
1869
1870        let mut exprs = children(node).filter(|n| n.kind().is_expression());
1871
1872        let place = match exprs.next() {
1873            Some(n) => self.to_expression(&n)?,
1874            None => {
1875                self.emit_unexpected_str("left side in assignment", node.text(), span);
1876                return Ok(leo_ast::ExpressionStatement { expression: self.error_expression(span), span, id }.into());
1877            }
1878        };
1879
1880        let value = match exprs.next() {
1881            Some(n) => self.to_expression(&n)?,
1882            None => {
1883                self.emit_unexpected_str("right side in assignment", node.text(), span);
1884                self.error_expression(span)
1885            }
1886        };
1887
1888        Ok(leo_ast::AssignStatement { place, value, span, id }.into())
1889    }
1890
1891    /// Convert a COMPOUND_ASSIGN_STMT (`x += expr;`) to a Statement.
1892    ///
1893    /// Desugars `x op= rhs` into `x = x op rhs`.
1894    fn compound_assign_to_statement(&self, node: &SyntaxNode) -> Result<leo_ast::Statement> {
1895        debug_assert_eq!(node.kind(), COMPOUND_ASSIGN_STMT);
1896        let span = self.to_span(node);
1897        let id = self.builder.next_id();
1898
1899        let mut exprs = children(node).filter(|n| n.kind().is_expression());
1900
1901        let left = match exprs.next() {
1902            Some(n) => self.to_expression(&n)?,
1903            None => {
1904                self.emit_unexpected_str("left side in compound assignment", node.text(), span);
1905                return Ok(leo_ast::ExpressionStatement { expression: self.error_expression(span), span, id }.into());
1906            }
1907        };
1908
1909        let right = match exprs.next() {
1910            Some(n) => self.to_expression(&n)?,
1911            None => {
1912                self.emit_unexpected_str("right side in compound assignment", node.text(), span);
1913                self.error_expression(span)
1914            }
1915        };
1916
1917        let op_token =
1918            tokens(node).find(|t| is_assign_op(t.kind())).expect("COMPOUND_ASSIGN_STMT should have operator");
1919
1920        let binary_op = match op_token.kind() {
1921            PLUS_EQ => leo_ast::BinaryOperation::Add,
1922            MINUS_EQ => leo_ast::BinaryOperation::Sub,
1923            STAR_EQ => leo_ast::BinaryOperation::Mul,
1924            SLASH_EQ => leo_ast::BinaryOperation::Div,
1925            PERCENT_EQ => leo_ast::BinaryOperation::Rem,
1926            STAR2_EQ => leo_ast::BinaryOperation::Pow,
1927            AMP_EQ => leo_ast::BinaryOperation::BitwiseAnd,
1928            PIPE_EQ => leo_ast::BinaryOperation::BitwiseOr,
1929            CARET_EQ => leo_ast::BinaryOperation::Xor,
1930            SHL_EQ => leo_ast::BinaryOperation::Shl,
1931            SHR_EQ => leo_ast::BinaryOperation::Shr,
1932            AMP2_EQ => leo_ast::BinaryOperation::And,
1933            PIPE2_EQ => leo_ast::BinaryOperation::Or,
1934            k => panic!("unexpected compound assignment operator: {k:?}"),
1935        };
1936
1937        let value =
1938            leo_ast::BinaryExpression { left: left.clone(), right, op: binary_op, span, id: self.builder.next_id() }
1939                .into();
1940
1941        Ok(leo_ast::AssignStatement { place: left, value, span, id }.into())
1942    }
1943
1944    /// Convert an IF_STMT node to a ConditionalStatement.
1945    fn if_stmt_to_statement(&self, node: &SyntaxNode) -> Result<leo_ast::Statement> {
1946        debug_assert_eq!(node.kind(), IF_STMT);
1947        let span = self.to_span(node);
1948        let id = self.builder.next_id();
1949
1950        let condition = self.require_expression(node, "condition in if statement")?;
1951
1952        // Single-pass: first BLOCK or IF_STMT child is the then-block,
1953        // second (if any) is the else clause.
1954        let mut block_or_if = children(node).filter(|n| n.kind() == BLOCK || n.kind() == IF_STMT);
1955
1956        let then = match block_or_if.next() {
1957            Some(n) if n.kind() == BLOCK => self.to_block(&n)?,
1958            _ => {
1959                self.emit_unexpected_str("then block in if statement", node.text(), span);
1960                self.error_block(span)
1961            }
1962        };
1963
1964        let otherwise = block_or_if.next().map(|n| self.to_statement(&n)).transpose()?.map(Box::new);
1965
1966        Ok(leo_ast::ConditionalStatement { condition, then, otherwise, span, id }.into())
1967    }
1968
1969    /// Convert a FOR_STMT or FOR_INCLUSIVE_STMT node to an IterationStatement.
1970    fn for_stmt_to_statement(&self, node: &SyntaxNode) -> Result<leo_ast::Statement> {
1971        debug_assert!(matches!(node.kind(), FOR_STMT | FOR_INCLUSIVE_STMT));
1972        let span = self.to_span(node);
1973        let id = self.builder.next_id();
1974
1975        let variable = self.require_ident(node, "variable in for statement");
1976
1977        // Get optional type annotation
1978        let type_ = children(node).find(|n| n.kind().is_type()).map(|n| self.to_type_repr(&n)).transpose()?;
1979
1980        // Get range expressions (before and after ..)
1981        let mut exprs = children(node).filter(|n| n.kind().is_expression());
1982
1983        let start = match exprs.next() {
1984            Some(n) => self.to_expression(&n)?,
1985            None => {
1986                self.emit_unexpected_str("start expression in for statement", node.text(), span);
1987                self.error_expression(span)
1988            }
1989        };
1990
1991        let stop = match exprs.next() {
1992            Some(n) => self.to_expression(&n)?,
1993            None => {
1994                self.emit_unexpected_str("stop expression in for statement", node.text(), span);
1995                self.error_expression(span)
1996            }
1997        };
1998
1999        // Get body block
2000        let block = match children(node).find(|n| n.kind() == BLOCK) {
2001            Some(block_node) => self.to_block(&block_node)?,
2002            None => {
2003                self.emit_unexpected_str("block in for statement", node.text(), span);
2004                self.error_block(span)
2005            }
2006        };
2007
2008        let inclusive = node.kind() == FOR_INCLUSIVE_STMT;
2009
2010        Ok(leo_ast::IterationStatement { variable, type_, start, stop, inclusive, block, span, id }.into())
2011    }
2012
2013    // =========================================================================
2014    // Item/Program Conversions
2015    // =========================================================================
2016
2017    /// Collect a single program item (function, struct/record, const, interface) into the given vectors.
2018    fn collect_program_item(
2019        &self,
2020        item: &SyntaxNode,
2021        is_in_program_block: bool,
2022        functions: &mut Vec<(Symbol, leo_ast::Function)>,
2023        composites: &mut Vec<(Symbol, leo_ast::Composite)>,
2024        consts: &mut Vec<(Symbol, leo_ast::ConstDeclaration)>,
2025        interfaces: &mut Vec<(Symbol, leo_ast::Interface)>,
2026    ) -> Result<()> {
2027        match item.kind() {
2028            FUNCTION_DEF | FINAL_FN_DEF | VIEW_FN_DEF => {
2029                if item.kind() == VIEW_FN_DEF && !is_in_program_block {
2030                    // `view fn` is a program-only entry point; module files cannot declare them.
2031                    let span = self.to_span(item);
2032                    self.handler.emit_err(crate::errors::custom(
2033                        "`view fn` is only allowed inside a `program { ... }` block.",
2034                        span,
2035                    ));
2036                }
2037                let func = self.to_function(item, is_in_program_block)?;
2038                functions.push((func.identifier.name, func));
2039            }
2040            STRUCT_DEF | RECORD_DEF => {
2041                if item.kind() == STRUCT_DEF && is_in_program_block {
2042                    let span = self.non_trivia_span(item);
2043                    self.handler.emit_err(
2044                        crate::errors::custom(
2045                            "`struct` definitions are not allowed inside a `program { ... }` block.",
2046                            span,
2047                        )
2048                        .with_help("Move the declaration outside the `program` block, to the top level of the file."),
2049                    );
2050                }
2051                let composite = self.to_composite(item, is_in_program_block)?;
2052                composites.push((composite.identifier.name, composite));
2053            }
2054            GLOBAL_CONST => {
2055                if is_in_program_block {
2056                    let span = self.non_trivia_span(item);
2057                    self.handler.emit_err(
2058                        crate::errors::custom(
2059                            "`const` declarations are not allowed inside a `program { ... }` block.",
2060                            span,
2061                        )
2062                        .with_help("Move the declaration outside the `program` block, to the top level of the file."),
2063                    );
2064                }
2065                let global_const = self.to_global_const(item, is_in_program_block)?;
2066                consts.push((global_const.place.name, global_const));
2067            }
2068            INTERFACE_DEF => {
2069                if is_in_program_block {
2070                    let span = self.non_trivia_span(item);
2071                    self.handler.emit_err(
2072                        crate::errors::custom(
2073                            "`interface` definitions are not allowed inside a `program { ... }` block.",
2074                            span,
2075                        )
2076                        .with_help("Move the declaration outside the `program` block, to the top level of the file."),
2077                    );
2078                }
2079                let interface = self.to_interface(item, is_in_program_block)?;
2080                interfaces.push((interface.identifier.name, interface));
2081            }
2082            _ => {}
2083        }
2084        Ok(())
2085    }
2086
2087    /// Collect a single library item into the given vectors.
2088    fn collect_library_item(
2089        &self,
2090        item: &SyntaxNode,
2091        consts: &mut Vec<(Symbol, leo_ast::ConstDeclaration)>,
2092        structs: &mut Vec<(Symbol, leo_ast::Composite)>,
2093        functions: &mut Vec<(Symbol, leo_ast::Function)>,
2094        interfaces: &mut Vec<(Symbol, leo_ast::Interface)>,
2095    ) -> Result<()> {
2096        if is_library_item(item.kind()) {
2097            match item.kind() {
2098                GLOBAL_CONST => {
2099                    let global_const = self.to_global_const(item, false)?;
2100                    consts.push((global_const.place.name, global_const));
2101                }
2102                STRUCT_DEF => {
2103                    let composite = self.to_composite(item, false)?;
2104                    structs.push((composite.identifier.name, composite));
2105                }
2106                FUNCTION_DEF => {
2107                    // `is_in_program_block = false` so the variant is always `Fn` (not EntryPoint).
2108                    let func = self.to_function(item, false)?;
2109                    functions.push((func.identifier.name, func));
2110                }
2111                INTERFACE_DEF => {
2112                    let interface = self.to_interface(item, false)?;
2113                    interfaces.push((interface.identifier.name, interface));
2114                }
2115                _ => {}
2116            }
2117        } else if item.kind() == VIEW_FN_DEF {
2118            // `view fn` is a program-only entry point; libraries cannot declare them.
2119            let span = self.to_span(item);
2120            self.handler
2121                .emit_err(crate::errors::custom("`view fn` is only allowed inside a `program { ... }` block.", span));
2122        } else if is_program_item(item.kind()) {
2123            // A recognized program-only item appeared in a library file.
2124            let span = self.to_span(item);
2125            self.handler.emit_err(crate::errors::custom(
2126                "Only `const` declarations, `struct` definitions, `fn` functions, and `interface` definitions are allowed in a library.",
2127                span,
2128            ));
2129        }
2130        // Other node kinds (e.g. ERROR nodes from CST-level parse failures) are
2131        // already reported by the rowan parser; nothing to do here.
2132        Ok(())
2133    }
2134
2135    /// Convert a syntax node to a module.
2136    fn to_module(&self, node: &SyntaxNode, program_name: Symbol, path: Vec<Symbol>) -> Result<leo_ast::Module> {
2137        // Module nodes are ROOT nodes containing items (functions, structs, consts, interfaces)
2138        let mut functions = Vec::new();
2139        let mut composites = Vec::new();
2140        let mut consts = Vec::new();
2141        let mut interfaces = Vec::new();
2142
2143        for child in children(node) {
2144            if child.kind() == PROGRAM_DECL {
2145                for item in children(&child) {
2146                    self.collect_program_item(
2147                        &item,
2148                        true,
2149                        &mut functions,
2150                        &mut composites,
2151                        &mut consts,
2152                        &mut interfaces,
2153                    )?;
2154                }
2155            } else {
2156                self.collect_program_item(
2157                    &child,
2158                    false,
2159                    &mut functions,
2160                    &mut composites,
2161                    &mut consts,
2162                    &mut interfaces,
2163                )?;
2164            }
2165        }
2166
2167        // Sort functions: entry points first
2168        functions.sort_by_key(|func| if func.1.variant.is_entry() { 0u8 } else { 1u8 });
2169
2170        Ok(leo_ast::Module { unit_name: program_name, path, consts, composites, functions, interfaces })
2171    }
2172
2173    /// Convert a syntax node to a program (main file).
2174    fn to_main(&self, node: &SyntaxNode) -> Result<leo_ast::Program> {
2175        // The main file contains imports and a program declaration
2176        let mut imports = indexmap::IndexMap::new();
2177        let mut functions = Vec::new();
2178        let mut composites = Vec::new();
2179        let mut consts = Vec::new();
2180        let mut mappings = Vec::new();
2181        let mut storage_variables = Vec::new();
2182        let mut constructors = Vec::new();
2183        let mut interfaces = Vec::new();
2184        let mut program_name = None;
2185        let mut network = None;
2186        let mut parents = Vec::new();
2187        let mut span = None;
2188
2189        for child in children(node) {
2190            match child.kind() {
2191                IMPORT => {
2192                    let program_id = self.import_to_program_id(&child)?;
2193                    imports.insert(program_id.as_symbol(), program_id);
2194                }
2195                PROGRAM_DECL => {
2196                    if program_name.is_some() {
2197                        self.handler
2198                            .emit_err(crate::errors::multiple_program_declarations(self.non_trivia_span(&child)));
2199                        continue;
2200                    }
2201                    // Extract program name, network, and optional parent interface
2202                    let (pname, pnetwork, pparents) = self.program_decl_to_name_with_parent(&child)?;
2203                    program_name = Some(pname);
2204                    network = Some(pnetwork);
2205                    parents = pparents;
2206                    span = Some(self.to_span(&child));
2207
2208                    // Process items inside program decl
2209                    for item in children(&child) {
2210                        self.collect_program_item(
2211                            &item,
2212                            true,
2213                            &mut functions,
2214                            &mut composites,
2215                            &mut consts,
2216                            &mut interfaces,
2217                        )?;
2218                        match item.kind() {
2219                            MAPPING_DEF => {
2220                                let mapping = self.to_mapping(&item)?;
2221                                mappings.push((mapping.identifier.name, mapping));
2222                            }
2223                            STORAGE_DEF => {
2224                                let storage = self.to_storage(&item)?;
2225                                storage_variables.push((storage.identifier.name, storage));
2226                            }
2227                            CONSTRUCTOR_DEF => {
2228                                constructors.push(self.to_constructor(&item)?);
2229                            }
2230                            _ => {}
2231                        }
2232                    }
2233                }
2234                _ => {
2235                    self.collect_program_item(
2236                        &child,
2237                        false,
2238                        &mut functions,
2239                        &mut composites,
2240                        &mut consts,
2241                        &mut interfaces,
2242                    )?;
2243                }
2244            }
2245        }
2246
2247        if let Some(extra) = constructors.get(1) {
2248            return Err(crate::errors::custom("A program can only have one constructor.", extra.span).into());
2249        }
2250
2251        let (Some(program_name), Some(network), Some(span)) = (program_name, network, span) else {
2252            return Err(crate::errors::missing_program_declaration(self.to_span(node)).into());
2253        };
2254
2255        // Sort functions: entry points first
2256        functions.sort_by_key(|func| if func.1.variant.is_entry() { 0u8 } else { 1u8 });
2257
2258        let program_id = leo_ast::ProgramId { name: program_name, network };
2259        let program_id_as_symbol = program_id.as_symbol();
2260        let program_scope = leo_ast::ProgramScope {
2261            program_id,
2262            parents,
2263            consts,
2264            composites,
2265            mappings,
2266            storage_variables,
2267            functions,
2268            interfaces,
2269            constructor: constructors.pop(),
2270            span,
2271        };
2272
2273        Ok(leo_ast::Program {
2274            imports,
2275            modules: indexmap::IndexMap::new(),
2276            stubs: indexmap::IndexMap::new(),
2277            program_scopes: vec![(program_id_as_symbol, program_scope)].into_iter().collect(),
2278        })
2279    }
2280
2281    /// Convert a syntax node to a library (`lib.leo` file).
2282    fn to_library(&self, name: Symbol, node: &SyntaxNode) -> Result<leo_ast::Library> {
2283        let mut consts = Vec::new();
2284        let mut structs = Vec::new();
2285        let mut functions = Vec::new();
2286        let mut interfaces = Vec::new();
2287
2288        for child in children(node) {
2289            self.collect_library_item(&child, &mut consts, &mut structs, &mut functions, &mut interfaces)?;
2290        }
2291
2292        Ok(leo_ast::Library {
2293            name,
2294            modules: indexmap::IndexMap::new(),
2295            consts,
2296            structs,
2297            functions,
2298            interfaces,
2299            stubs: indexmap::IndexMap::new(),
2300        })
2301    }
2302
2303    /// Extract a ProgramId from an IMPORT node. Guarantees `network` is always present.
2304    fn import_to_program_id(&self, node: &SyntaxNode) -> Result<leo_ast::ProgramId> {
2305        debug_assert_eq!(node.kind(), IMPORT);
2306        let span = self.to_span(node);
2307
2308        // Extract the first IDENT as the program name
2309        let program_name_text = match tokens(node).find(|t| t.kind() == IDENT) {
2310            Some(name_token) => name_token.text().to_string(),
2311            None => {
2312                self.emit_unexpected_str("import name", node.text(), span);
2313                "_error".to_string()
2314            }
2315        };
2316
2317        // Check if KW_ALEO exists in tokens
2318        let network_span = tokens(node).find(|t| t.kind() == KW_ALEO).map(|t| self.token_span(&t)).unwrap_or(span); // fallback span if missing
2319
2320        let program_id = leo_ast::ProgramId {
2321            name: leo_ast::Identifier { name: Symbol::intern(&program_name_text), span, id: self.builder.next_id() },
2322            network: leo_ast::Identifier {
2323                name: Symbol::intern("aleo"),
2324                span: network_span,
2325                id: self.builder.next_id(),
2326            },
2327        };
2328
2329        // Extra validation: if no KW_ALEO token but there is some invalid network, emit error
2330        if tokens(node).all(|t| t.kind() != KW_ALEO)
2331            && let Some(net_token) = find_invalid_network(node)
2332        {
2333            self.handler.emit_err(crate::errors::invalid_network(self.token_span(&net_token)));
2334        }
2335
2336        Ok(program_id)
2337    }
2338
2339    /// Extract program name and network from a PROGRAM_DECL node.
2340    fn program_decl_to_name(&self, node: &SyntaxNode) -> Result<(leo_ast::Identifier, leo_ast::Identifier)> {
2341        debug_assert_eq!(node.kind(), PROGRAM_DECL);
2342        let span = self.to_span(node);
2343
2344        // Program format: program name.aleo { ... }
2345        let program_name = self.require_ident(node, "program name");
2346
2347        let network = match tokens(node).find(|t| t.kind() == KW_ALEO) {
2348            Some(aleo_token) => leo_ast::Identifier {
2349                name: Symbol::intern("aleo"),
2350                span: self.token_span(&aleo_token),
2351                id: self.builder.next_id(),
2352            },
2353            None => {
2354                // Check for an invalid network identifier (e.g. `program test.eth`).
2355                if let Some(net_token) = find_invalid_network(node) {
2356                    self.handler.emit_err(crate::errors::invalid_network(self.token_span(&net_token)));
2357                } else {
2358                    self.emit_unexpected_str(".aleo network", node.text(), span);
2359                }
2360                leo_ast::Identifier { name: Symbol::intern("aleo"), span, id: self.builder.next_id() }
2361            }
2362        };
2363
2364        Ok((program_name, network))
2365    }
2366
2367    /// Extract program name, network, and optional parent interface from a PROGRAM_DECL node.
2368    fn program_decl_to_name_with_parent(
2369        &self,
2370        node: &SyntaxNode,
2371    ) -> Result<(leo_ast::Identifier, leo_ast::Identifier, Parents)> {
2372        debug_assert_eq!(node.kind(), PROGRAM_DECL);
2373        let (program_name, network) = self.program_decl_to_name(node)?;
2374
2375        let parents = if let Some(parent_list) = children(node).find(|n| n.kind() == PARENT_LIST) {
2376            self.collect_parent_list(&parent_list)?
2377        } else {
2378            vec![]
2379        };
2380
2381        Ok((program_name, network, parents))
2382    }
2383
2384    fn collect_parent_list(&self, node: &SyntaxNode) -> Result<Parents> {
2385        debug_assert_eq!(node.kind(), PARENT_LIST);
2386        children(node)
2387            .filter(|n| n.kind().is_type())
2388            .map(|n| self.to_type(&n).map(|t| (self.to_span(&n), t)))
2389            .collect::<Result<Vec<_>>>()
2390    }
2391
2392    /// Collect all ANNOTATION children from a node.
2393    fn collect_annotations(&self, node: &SyntaxNode) -> Result<Vec<leo_ast::Annotation>> {
2394        children(node).filter(|n| n.kind() == ANNOTATION).map(|n| self.to_annotation(&n)).collect()
2395    }
2396
2397    /// Find a BLOCK child or produce an error block for recovery.
2398    fn require_block(&self, node: &SyntaxNode, span: Span) -> Result<leo_ast::Block> {
2399        Ok(children(node)
2400            .find(|n| n.kind() == BLOCK)
2401            .map(|n| self.to_block(&n))
2402            .transpose()?
2403            .unwrap_or_else(|| self.error_block(span)))
2404    }
2405
2406    /// Convert a FUNCTION_DEF / FINAL_FN_DEF / VIEW_FN_DEF / CONSTRUCTOR_DEF node to a Function.
2407    fn to_function(&self, node: &SyntaxNode, is_in_program_block: bool) -> Result<leo_ast::Function> {
2408        debug_assert!(matches!(node.kind(), FUNCTION_DEF | FINAL_FN_DEF | VIEW_FN_DEF | CONSTRUCTOR_DEF));
2409        let span = self.span_including_annotations(node, self.non_trivia_span(node));
2410        let id = self.builder.next_id();
2411
2412        let annotations = self.collect_annotations(node)?;
2413
2414        // A `view fn` outside a program block is grammatically allowed but rejected later by
2415        // `collect_library_item`; we map it to `Variant::Fn` here so downstream passes stay
2416        // well-formed until that diagnostic fires. `final fn` inside a program block can only
2417        // appear via parser error recovery for inputs like `final view fn` and likewise
2418        // produces a diagnostic, so we map it to `Variant::FinalFn` for the same reason.
2419        let variant = if is_in_program_block {
2420            match node.kind() {
2421                VIEW_FN_DEF => leo_ast::Variant::View,
2422                FINAL_FN_DEF => leo_ast::Variant::FinalFn,
2423                FUNCTION_DEF | CONSTRUCTOR_DEF => leo_ast::Variant::EntryPoint,
2424                kind => unreachable!("unexpected function node kind in program block: {kind:?}"),
2425            }
2426        } else {
2427            match node.kind() {
2428                FINAL_FN_DEF => leo_ast::Variant::FinalFn,
2429                FUNCTION_DEF | VIEW_FN_DEF => leo_ast::Variant::Fn,
2430                kind => unreachable!("unexpected function node kind outside program block: {kind:?}"),
2431            }
2432        };
2433
2434        let identifier = self.require_ident(node, "function name");
2435        self.validate_identifier(&identifier);
2436
2437        let const_parameters = self.extract_const_parameters(node)?;
2438
2439        // Get input parameters
2440        let input = children(node)
2441            .find(|n| n.kind() == PARAM_LIST)
2442            .map(|n| self.param_list_to_inputs(&n))
2443            .transpose()?
2444            .unwrap_or_default();
2445
2446        // Get return type and build output declarations.
2447        //
2448        // Two structures are possible:
2449        // - Single return: FUNCTION_DEF > ... ARROW [KW_PUBLIC|KW_PRIVATE|KW_CONSTANT]? TYPE_* BLOCK
2450        // - Tuple return:  FUNCTION_DEF > ... ARROW RETURN_TYPE(L_PAREN [vis TYPE_*]+ R_PAREN) BLOCK
2451        let (output, output_type) = if let Some(return_type_node) = children(node).find(|n| n.kind() == RETURN_TYPE) {
2452            // Tuple return type.
2453            self.return_type_to_outputs(&return_type_node)?
2454        } else if let Some(type_node) = children(node).find(|n| n.kind().is_type()) {
2455            // Single return type (direct child of FUNCTION_DEF).
2456            let type_ = self.to_type(&type_node)?;
2457            // Check for visibility keyword before the type node.
2458            let (mode, mode_start) = self.return_mode_before(node, &type_node);
2459            let type_span = self.content_span(&type_node);
2460            let output_span = match mode_start {
2461                Some(start) => Span::new(start, type_span.hi),
2462                None => type_span,
2463            };
2464            let output = vec![leo_ast::Output {
2465                mode,
2466                type_: leo_ast::TypeNode::new(self.interner, type_.clone(), type_span),
2467                span: output_span,
2468                id: self.builder.next_id(),
2469            }];
2470            (output, type_)
2471        } else {
2472            (Vec::new(), leo_ast::TypeKind::Unit)
2473        };
2474
2475        let block = self.require_block(node, span)?;
2476
2477        let is_exported = if is_in_program_block { None } else { Some(has_export(node)) };
2478
2479        Ok(leo_ast::Function {
2480            is_exported,
2481            annotations,
2482            variant,
2483            identifier,
2484            const_parameters,
2485            input,
2486            output,
2487            output_type,
2488            block,
2489            span,
2490            id,
2491        })
2492    }
2493
2494    /// Extract the visibility mode keyword that precedes a type node within a parent.
2495    ///
2496    /// Scans tokens of the parent, looking for a visibility keyword that
2497    /// appears immediately before the type node's text range.
2498    /// Returns the mode and optionally the mode token's offset-adjusted span start.
2499    fn return_mode_before(&self, parent: &SyntaxNode, type_node: &SyntaxNode) -> (leo_ast::Mode, Option<u32>) {
2500        let type_start = type_node.text_range().start();
2501        let mut mode = leo_ast::Mode::None;
2502        let mut mode_start = None;
2503        for token in tokens(parent) {
2504            let token_end = token.text_range().end();
2505            if token_end > type_start {
2506                break;
2507            }
2508            if let Some(m) = token_kind_to_mode(token.kind()) {
2509                mode = m;
2510                mode_start = Some(u32::from(token.text_range().start()) + self.start_pos);
2511            }
2512        }
2513        (mode, mode_start)
2514    }
2515
2516    /// Convert a RETURN_TYPE node (tuple return) to `(Vec<Output>, TypeKind)`.
2517    fn return_type_to_outputs(&self, node: &SyntaxNode) -> Result<(Vec<leo_ast::Output>, leo_ast::TypeKind)> {
2518        debug_assert_eq!(node.kind(), RETURN_TYPE);
2519
2520        // RETURN_TYPE contains: L_PAREN [vis? TYPE_*]+ R_PAREN
2521        // Iterate children, tracking the last-seen visibility keyword.
2522        let mut outputs = Vec::new();
2523        let mut current_mode = leo_ast::Mode::None;
2524        let mut current_mode_start: Option<u32> = None;
2525
2526        for child in node.children_with_tokens() {
2527            match &child {
2528                SyntaxElement::Token(token) if !token.kind().is_trivia() => {
2529                    if let Some(m) = token_kind_to_mode(token.kind()) {
2530                        current_mode = m;
2531                        current_mode_start = Some(u32::from(token.text_range().start()) + self.start_pos);
2532                    }
2533                }
2534                SyntaxElement::Node(child_node) if child_node.kind().is_type() => {
2535                    let type_ = self.to_type(child_node)?;
2536                    let type_span = self.content_span(child_node);
2537                    let output_span = match current_mode_start.take() {
2538                        Some(start) => Span::new(start, type_span.hi),
2539                        None => type_span,
2540                    };
2541                    outputs.push(leo_ast::Output {
2542                        mode: current_mode,
2543                        type_: leo_ast::TypeNode::new(self.interner, type_, type_span),
2544                        span: output_span,
2545                        id: self.builder.next_id(),
2546                    });
2547                    current_mode = leo_ast::Mode::None;
2548                }
2549                _ => {}
2550            }
2551        }
2552
2553        let output_type = match outputs.len() {
2554            0 => leo_ast::TypeKind::Unit,
2555            1 => outputs[0].type_.kind().clone(),
2556            _ => leo_ast::TupleType::new(outputs.iter().map(|o| o.type_.kind().clone()).collect()).into(),
2557        };
2558
2559        Ok((outputs, output_type))
2560    }
2561
2562    /// Convert an ANNOTATION node to an Annotation.
2563    fn to_annotation(&self, node: &SyntaxNode) -> Result<leo_ast::Annotation> {
2564        debug_assert_eq!(node.kind(), ANNOTATION);
2565        let span = self.trimmed_span(node);
2566        let id = self.builder.next_id();
2567
2568        // Annotation names can be identifiers or keywords (e.g. @program, @test).
2569        // The name is the first IDENT or keyword token after `@`.
2570        let identifier = match tokens(node).find(|t| t.kind() == IDENT || t.kind().is_keyword()) {
2571            Some(name_token) => {
2572                let name = Symbol::intern(name_token.text());
2573                let name_span = self.token_span(&name_token);
2574                leo_ast::Identifier { name, span: name_span, id: self.builder.next_id() }
2575            }
2576            None => {
2577                self.emit_unexpected_str("annotation name", node.text(), span);
2578                self.error_identifier(span)
2579            }
2580        };
2581
2582        // Parse annotation key-value pairs from ANNOTATION_PAIR child nodes.
2583        let map = children(node)
2584            .filter(|n| n.kind() == ANNOTATION_PAIR)
2585            .filter_map(|pair| {
2586                let key =
2587                    tokens(&pair).find(|t| t.kind() == IDENT || t.kind() == KW_ADDRESS || t.kind() == KW_MAPPING)?;
2588                let val = tokens(&pair).find(|t| t.kind() == STRING)?;
2589                let text = val.text();
2590                Some((Symbol::intern(key.text()), text[1..text.len() - 1].to_string()))
2591            })
2592            .collect();
2593
2594        Ok(leo_ast::Annotation { identifier, map, span, id })
2595    }
2596
2597    /// Convert a PARAM_LIST node to function inputs.
2598    fn param_list_to_inputs(&self, node: &SyntaxNode) -> Result<Vec<leo_ast::Input>> {
2599        debug_assert_eq!(node.kind(), PARAM_LIST);
2600
2601        children(node)
2602            .filter(|n| matches!(n.kind(), PARAM | PARAM_PUBLIC | PARAM_PRIVATE | PARAM_CONSTANT))
2603            .map(|n| self.param_to_input(&n))
2604            .collect()
2605    }
2606
2607    /// Convert a PARAM node to an Input.
2608    fn param_to_input(&self, node: &SyntaxNode) -> Result<leo_ast::Input> {
2609        debug_assert!(matches!(node.kind(), PARAM | PARAM_PUBLIC | PARAM_PRIVATE | PARAM_CONSTANT));
2610        let span = self.non_trivia_span(node);
2611        let id = self.builder.next_id();
2612
2613        let mode = node_kind_to_mode(node.kind());
2614
2615        let identifier = self.require_ident(node, "parameter name");
2616        self.validate_identifier(&identifier);
2617
2618        let type_ = self.require_type_repr(node, "parameter type")?;
2619
2620        Ok(leo_ast::Input { identifier, mode, type_, span, id })
2621    }
2622
2623    /// Convert a const parameter list.
2624    fn to_const_parameters(&self, node: &SyntaxNode) -> Result<Vec<leo_ast::ConstParameter>> {
2625        debug_assert_eq!(node.kind(), CONST_PARAM_LIST);
2626
2627        children(node)
2628            .filter(|n| n.kind() == CONST_PARAM)
2629            .map(|n| {
2630                let span = self.non_trivia_span(&n);
2631                let id = self.builder.next_id();
2632
2633                let identifier = self.require_ident(&n, "const parameter name");
2634
2635                let type_ = self.require_type_repr(&n, "const parameter type")?;
2636
2637                Ok(leo_ast::ConstParameter { identifier, type_, span, id })
2638            })
2639            .collect()
2640    }
2641
2642    /// Extract optional const parameters from a node with a CONST_PARAM_LIST child.
2643    fn extract_const_parameters(&self, node: &SyntaxNode) -> Result<Vec<leo_ast::ConstParameter>> {
2644        children(node)
2645            .find(|n| n.kind() == CONST_PARAM_LIST)
2646            .map(|n| self.to_const_parameters(&n))
2647            .transpose()
2648            .map(|opt| opt.unwrap_or_default())
2649    }
2650
2651    /// Convert a STRUCT_DEF or RECORD_DEF node to a Composite.
2652    fn to_composite(&self, node: &SyntaxNode, is_in_program_block: bool) -> Result<leo_ast::Composite> {
2653        debug_assert!(matches!(node.kind(), STRUCT_DEF | RECORD_DEF));
2654        let span = self.non_trivia_span(node);
2655        let id = self.builder.next_id();
2656
2657        let is_record = node.kind() == RECORD_DEF;
2658
2659        let identifier = self.require_ident(node, "struct/record name");
2660        self.validate_identifier(&identifier);
2661
2662        let const_parameters = self.extract_const_parameters(node)?;
2663
2664        // Get members
2665        let members = children(node)
2666            .filter(|n| {
2667                matches!(
2668                    n.kind(),
2669                    STRUCT_MEMBER | STRUCT_MEMBER_PUBLIC | STRUCT_MEMBER_PRIVATE | STRUCT_MEMBER_CONSTANT
2670                )
2671            })
2672            .map(|n| self.struct_member_to_member(&n))
2673            .collect::<Result<Vec<_>>>()?;
2674
2675        let is_exported = if is_record || is_in_program_block { None } else { Some(has_export(node)) };
2676
2677        Ok(leo_ast::Composite { is_exported, identifier, const_parameters, members, is_record, span, id })
2678    }
2679
2680    /// Convert a STRUCT_MEMBER node to a Member.
2681    fn struct_member_to_member(&self, node: &SyntaxNode) -> Result<leo_ast::Member> {
2682        debug_assert!(matches!(
2683            node.kind(),
2684            STRUCT_MEMBER | STRUCT_MEMBER_PUBLIC | STRUCT_MEMBER_PRIVATE | STRUCT_MEMBER_CONSTANT
2685        ));
2686        let span = self.non_trivia_span(node);
2687        let id = self.builder.next_id();
2688
2689        let mode = node_kind_to_mode(node.kind());
2690
2691        let identifier = self.require_ident(node, "member name");
2692        self.validate_identifier(&identifier);
2693
2694        let type_ = self.require_type_repr(node, "member type")?;
2695
2696        Ok(leo_ast::Member { mode, identifier, type_, span, id })
2697    }
2698
2699    /// Convert a GLOBAL_CONST node to a ConstDeclaration.
2700    fn to_global_const(&self, node: &SyntaxNode, is_in_program_block: bool) -> Result<leo_ast::ConstDeclaration> {
2701        debug_assert_eq!(node.kind(), GLOBAL_CONST);
2702        let span = self.non_trivia_span(node);
2703        let id = self.builder.next_id();
2704
2705        let place = self.require_ident(node, "const name");
2706        self.validate_definition_identifier(&place);
2707
2708        let type_ = self.require_type_repr(node, "const type")?;
2709
2710        let value = self.require_expression(node, "const value")?;
2711
2712        let is_exported = if is_in_program_block { None } else { Some(has_export(node)) };
2713
2714        Ok(leo_ast::ConstDeclaration { is_exported, place, type_, value, span, id })
2715    }
2716
2717    /// Parse a MAPPING_DEF node, returning its constituent parts.
2718    fn parse_mapping_def(
2719        &self,
2720        node: &SyntaxNode,
2721    ) -> Result<(leo_ast::Identifier, leo_ast::TypeKind, leo_ast::TypeKind, Span, NodeID)> {
2722        debug_assert_eq!(node.kind(), MAPPING_DEF);
2723        let span = self.non_trivia_span(node);
2724        let id = self.builder.next_id();
2725        let identifier = self.require_ident(node, "name in mapping");
2726        let mut type_nodes = children(node).filter(|n| n.kind().is_type());
2727        let key_type = match type_nodes.next() {
2728            Some(key_node) => self.to_type(&key_node)?,
2729            None => {
2730                self.emit_unexpected_str("key type in mapping", node.text(), span);
2731                leo_ast::TypeKind::Err
2732            }
2733        };
2734        let value_type = match type_nodes.next() {
2735            Some(value_node) => self.to_type(&value_node)?,
2736            None => {
2737                self.emit_unexpected_str("value type in mapping", node.text(), span);
2738                leo_ast::TypeKind::Err
2739            }
2740        };
2741        Ok((identifier, key_type, value_type, span, id))
2742    }
2743
2744    /// Convert a MAPPING_DEF node to a Mapping.
2745    fn to_mapping(&self, node: &SyntaxNode) -> Result<leo_ast::Mapping> {
2746        let (identifier, key_type, value_type, span, id) = self.parse_mapping_def(node)?;
2747        Ok(leo_ast::Mapping { identifier, key_type, value_type, span, id })
2748    }
2749
2750    /// Convert a MAPPING_DEF node inside an interface to a MappingPrototype.
2751    fn to_mapping_prototype(&self, node: &SyntaxNode) -> Result<leo_ast::MappingPrototype> {
2752        let (identifier, key_type, value_type, span, id) = self.parse_mapping_def(node)?;
2753        Ok(leo_ast::MappingPrototype { identifier, key_type, value_type, span, id })
2754    }
2755
2756    /// Parse a STORAGE_DEF node, returning its constituent parts.
2757    fn parse_storage_def(&self, node: &SyntaxNode) -> Result<(leo_ast::Identifier, leo_ast::TypeNode, Span, NodeID)> {
2758        debug_assert_eq!(node.kind(), STORAGE_DEF);
2759        let span = self.non_trivia_span(node);
2760        let id = self.builder.next_id();
2761        let identifier = self.require_ident(node, "name in storage");
2762        let type_ = self.require_type_repr(node, "type in storage")?;
2763        Ok((identifier, type_, span, id))
2764    }
2765
2766    /// Convert a STORAGE_DEF node to a StorageVariable.
2767    fn to_storage(&self, node: &SyntaxNode) -> Result<leo_ast::StorageVariable> {
2768        let (identifier, type_, span, id) = self.parse_storage_def(node)?;
2769        Ok(leo_ast::StorageVariable { identifier, type_, span, id })
2770    }
2771
2772    /// Convert a STORAGE_DEF node inside an interface to a StorageVariablePrototype.
2773    fn to_storage_prototype(&self, node: &SyntaxNode) -> Result<leo_ast::StorageVariablePrototype> {
2774        let (identifier, type_, span, id) = self.parse_storage_def(node)?;
2775        Ok(leo_ast::StorageVariablePrototype { identifier, type_, span, id })
2776    }
2777
2778    /// Convert a CONSTRUCTOR_DEF node to a Constructor.
2779    fn to_constructor(&self, node: &SyntaxNode) -> Result<leo_ast::Constructor> {
2780        debug_assert_eq!(node.kind(), CONSTRUCTOR_DEF);
2781        let span = self.span_including_annotations(node, self.non_trivia_span(node));
2782        let id = self.builder.next_id();
2783
2784        let annotations = self.collect_annotations(node)?;
2785        let block = self.require_block(node, span)?;
2786
2787        Ok(leo_ast::Constructor { annotations, block, span, id })
2788    }
2789
2790    // =========================================================================
2791    // Interface Conversions
2792    // =========================================================================
2793
2794    /// Convert an INTERFACE_DEF node to an Interface.
2795    fn to_interface(&self, node: &SyntaxNode, is_in_program_block: bool) -> Result<leo_ast::Interface> {
2796        debug_assert_eq!(node.kind(), INTERFACE_DEF);
2797        let span = self.to_span(node);
2798
2799        // Get interface name (first IDENT)
2800        let identifier = self.require_ident(node, "interface name");
2801
2802        // Check for parent interface after COLON
2803        // Format: `interface Name : ParentName { ... }`
2804        let parents = if let Some(parent_list) = children(node).find(|n| n.kind() == PARENT_LIST) {
2805            self.collect_parent_list(&parent_list)?
2806        } else {
2807            vec![]
2808        };
2809
2810        let mut functions = Vec::new();
2811        let mut records = Vec::new();
2812        let mut mappings = Vec::new();
2813        let mut storages = Vec::new();
2814
2815        for child in children(node) {
2816            match child.kind() {
2817                FN_PROTOTYPE_DEF => {
2818                    let proto = self.to_function_prototype(&child)?;
2819                    functions.push((proto.identifier.name, proto));
2820                }
2821                RECORD_PROTOTYPE_DEF => {
2822                    let proto = self.to_record_prototype(&child)?;
2823                    records.push((proto.identifier.name, proto));
2824                }
2825                MAPPING_DEF => {
2826                    let mapping = self.to_mapping_prototype(&child)?;
2827                    mappings.push(mapping);
2828                }
2829                STORAGE_DEF => {
2830                    let storage = self.to_storage_prototype(&child)?;
2831                    storages.push(storage);
2832                }
2833                _ => {}
2834            }
2835        }
2836
2837        let is_exported = if is_in_program_block { None } else { Some(has_export(node)) };
2838
2839        Ok(leo_ast::Interface {
2840            is_exported,
2841            identifier,
2842            parents,
2843            span,
2844            id: self.builder.next_id(),
2845            functions,
2846            records,
2847            mappings,
2848            storages,
2849        })
2850    }
2851
2852    /// Convert an FN_PROTOTYPE_DEF node to a FunctionPrototype.
2853    fn to_function_prototype(&self, node: &SyntaxNode) -> Result<leo_ast::FunctionPrototype> {
2854        debug_assert_eq!(node.kind(), FN_PROTOTYPE_DEF);
2855        let span = self.to_span(node);
2856
2857        let is_view = tokens(node).any(|t| t.kind() == KW_VIEW);
2858        let variant = if is_view { leo_ast::Variant::View } else { leo_ast::Variant::EntryPoint };
2859        let identifier = self.require_ident(node, "function name");
2860
2861        // Collect const generic parameters
2862        let const_parameters = self.extract_const_parameters(node)?;
2863
2864        // Collect input parameters
2865        let input = children(node)
2866            .find(|n| n.kind() == PARAM_LIST)
2867            .map(|n| self.param_list_to_inputs(&n))
2868            .transpose()?
2869            .unwrap_or_default();
2870
2871        // Get return type and build output declarations.
2872        // Same logic as to_function but for prototypes (no block)
2873        let output = if let Some(return_type_node) = children(node).find(|n| n.kind() == RETURN_TYPE) {
2874            // Tuple return type.
2875            self.return_type_to_outputs(&return_type_node)?.0
2876        } else if let Some(type_node) = children(node).find(|n| n.kind().is_type()) {
2877            // Single return type (direct child of FN_PROTOTYPE_DEF).
2878            let type_ = self.to_type(&type_node)?;
2879            // Check for visibility keyword before the type node.
2880            let (mode, mode_start) = self.return_mode_before(node, &type_node);
2881            let type_span = self.content_span(&type_node);
2882            let output_span = match mode_start {
2883                Some(start) => Span::new(start, type_span.hi),
2884                None => type_span,
2885            };
2886            vec![leo_ast::Output {
2887                mode,
2888                type_: leo_ast::TypeNode::new(self.interner, type_, type_span),
2889                span: output_span,
2890                id: self.builder.next_id(),
2891            }]
2892        } else {
2893            Vec::new()
2894        };
2895
2896        Ok(leo_ast::FunctionPrototype::new(
2897            vec![], // annotations (not supported in prototypes)
2898            variant,
2899            identifier,
2900            const_parameters,
2901            input,
2902            output,
2903            span,
2904            self.builder.next_id(),
2905        ))
2906    }
2907
2908    /// Convert a RECORD_PROTOTYPE_DEF node to a RecordPrototype.
2909    fn to_record_prototype(&self, node: &SyntaxNode) -> Result<leo_ast::RecordPrototype> {
2910        debug_assert_eq!(node.kind(), RECORD_PROTOTYPE_DEF);
2911
2912        let span = self.to_span(node);
2913        let identifier = self.require_ident(node, "record name");
2914        let members = children(node)
2915            .filter(|n| {
2916                matches!(
2917                    n.kind(),
2918                    STRUCT_MEMBER | STRUCT_MEMBER_PUBLIC | STRUCT_MEMBER_PRIVATE | STRUCT_MEMBER_CONSTANT
2919                )
2920            })
2921            .map(|n| self.struct_member_to_member(&n))
2922            .collect::<Result<Vec<_>>>()?;
2923
2924        // Check for redundant prototypes: `{ .. }` or `{ owner: address, .. }`.
2925        let is_redundant = members.is_empty()
2926            || members.iter().all(|m| {
2927                m.identifier.name == sym::owner
2928                    && *m.type_.kind() == leo_ast::TypeKind::Address
2929                    && m.mode == leo_ast::Mode::None
2930            });
2931        if is_redundant && !members.is_empty() {
2932            // Strip the owner-only members since they are implicit.
2933            self.handler.emit_warning(crate::errors::record_prototype_redundant(identifier.name, span));
2934            return Ok(leo_ast::RecordPrototype { identifier, span, members: Vec::new(), id: self.builder.next_id() });
2935        } else if is_redundant {
2936            // members is empty but we had braces — check if the node actually had braces.
2937            // If there's a L_BRACE token child, it means `{ .. }` was used.
2938            let had_braces = node.children_with_tokens().any(|c| c.kind() == L_BRACE);
2939            if had_braces {
2940                self.handler.emit_warning(crate::errors::record_prototype_redundant(identifier.name, span));
2941            }
2942        }
2943
2944        Ok(leo_ast::RecordPrototype { identifier, span, members, id: self.builder.next_id() })
2945    }
2946}
2947
2948// =============================================================================
2949// Public Parse Functions
2950// =============================================================================
2951
2952/// Create a span from a rowan `TextRange`, clamping to source bounds and ensuring `hi >= lo`.
2953fn clamped_span(range: TextRange, start_pos: u32, source_len: u32) -> Span {
2954    let end = start_pos + source_len;
2955    let lo = (u32::from(range.start()) + start_pos).min(end);
2956    let hi = (u32::from(range.end()) + start_pos).min(end).max(lo);
2957    Span::new(lo, hi)
2958}
2959
2960/// Emit lexer errors to the handler with appropriate error types.
2961fn emit_lex_errors(handler: &Handler, lex_errors: &[leo_parser_rowan::LexError], start_pos: u32, source_len: u32) {
2962    use leo_parser_rowan::LexErrorKind;
2963    for error in lex_errors {
2964        let span = clamped_span(error.range, start_pos, source_len);
2965
2966        match &error.kind {
2967            LexErrorKind::InvalidDigit { digit, radix, token } => {
2968                handler.emit_err(crate::errors::wrong_digit_for_radix_span(*digit, *radix, token, span));
2969            }
2970            LexErrorKind::CouldNotLex { content } => {
2971                handler.emit_err(crate::errors::could_not_lex_span(content, span));
2972            }
2973            LexErrorKind::BidiOverride => {
2974                handler.emit_err(crate::errors::lexer_bidi_override_span(span));
2975            }
2976        }
2977    }
2978}
2979
2980/// Emit parse errors to the handler, using structured error types when available.
2981/// Duplicate errors at the same location are filtered out to prevent cascading errors.
2982fn emit_parse_errors(
2983    handler: &Handler,
2984    errors: &[leo_parser_rowan::ParseError],
2985    start_pos: u32,
2986    source_len: u32,
2987    lex_errors: &[leo_parser_rowan::LexError],
2988) {
2989    use std::collections::HashSet;
2990
2991    let has_lex_errors = !lex_errors.is_empty();
2992
2993    // Collect lex error byte ranges so we can skip overlapping parse errors.
2994    let lex_ranges: Vec<(u32, u32)> = lex_errors
2995        .iter()
2996        .map(|e| {
2997            let lo = u32::from(e.range.start()).saturating_add(start_pos);
2998            let hi = u32::from(e.range.end()).saturating_add(start_pos);
2999            (lo, hi)
3000        })
3001        .collect();
3002
3003    // Track emitted error ranges to prevent duplicate errors at the same location
3004    let mut emitted_ranges: HashSet<(u32, u32)> = HashSet::new();
3005    let mut count = 0;
3006    let max_errors = 10;
3007
3008    for error in errors {
3009        if count >= max_errors {
3010            break;
3011        }
3012
3013        let span = clamped_span(error.range, start_pos, source_len);
3014        let range_key = (span.lo, span.hi);
3015
3016        // Skip if we already emitted an error at this exact range
3017        if emitted_ranges.contains(&range_key) {
3018            continue;
3019        }
3020
3021        // When there are lex errors, skip parse errors at EOF since
3022        // they are secondary effects of the lex failure.
3023        if has_lex_errors && span.lo == span.hi && span.hi == start_pos + source_len {
3024            continue;
3025        }
3026
3027        // Skip parse errors that overlap with lex error ranges — these
3028        // are secondary effects of the lex failure already reported.
3029        if lex_ranges.iter().any(|&(lo, hi)| span.lo < hi && span.hi > lo) {
3030            continue;
3031        }
3032
3033        emitted_ranges.insert(range_key);
3034
3035        // Detect EOF errors: the found token is empty or "end of file", or the
3036        // span sits at/past the end of source.
3037        let is_eof_error = match &error.found {
3038            Some(f) => f.is_empty() || f == "end of file",
3039            None => false,
3040        } || (span.lo == span.hi && span.hi >= start_pos + source_len);
3041
3042        if is_eof_error {
3043            handler.emit_err(crate::errors::unexpected_eof(span));
3044            count += 1;
3045            continue;
3046        }
3047
3048        // Use crate::errors::unexpected if we have structured found/expected info
3049        if let Some(found) = &error.found {
3050            if error.expected.is_empty() {
3051                // No structured expected tokens — use the message as a custom error.
3052                // This covers errors from `error()` like "expected field name".
3053                handler.emit_err(crate::errors::custom(&error.message, span));
3054            } else {
3055                let expected_str = error.expected.join(", ");
3056                handler.emit_err(crate::errors::unexpected(found, expected_str, span));
3057            }
3058            count += 1;
3059            continue;
3060        }
3061
3062        // Fall back to custom error for unstructured errors
3063        handler.emit_err(crate::errors::custom(&error.message, span));
3064        count += 1;
3065    }
3066}
3067
3068/// Emit lex and parse errors, then create a `ConversionContext`.
3069fn conversion_context<'a>(
3070    handler: &'a Handler,
3071    node_builder: &'a NodeBuilder,
3072    interner: &'a TypeInterner,
3073    lex_errors: &[leo_parser_rowan::LexError],
3074    parse_errors: &[leo_parser_rowan::ParseError],
3075    start_pos: u32,
3076    source_len: u32,
3077) -> ConversionContext<'a> {
3078    emit_lex_errors(handler, lex_errors, start_pos, source_len);
3079    emit_parse_errors(handler, parse_errors, start_pos, source_len, lex_errors);
3080    let has_errors = !parse_errors.is_empty() || !lex_errors.is_empty();
3081    ConversionContext::new(handler, node_builder, interner, start_pos, has_errors)
3082}
3083
3084/// Parses a single expression from source code.
3085pub fn parse_expression(
3086    handler: Handler,
3087    node_builder: &NodeBuilder,
3088    interner: &TypeInterner,
3089    source: &str,
3090    start_pos: u32,
3091    _network: NetworkName,
3092) -> Result<leo_ast::Expression> {
3093    let parse = leo_parser_rowan::parse_expression_entry(source);
3094    let ctx = conversion_context(
3095        &handler,
3096        node_builder,
3097        interner,
3098        parse.lex_errors(),
3099        parse.errors(),
3100        start_pos,
3101        source.len() as u32,
3102    );
3103    ctx.to_expression(&parse.syntax())
3104}
3105
3106/// Parses a single statement from source code.
3107pub fn parse_statement(
3108    handler: Handler,
3109    node_builder: &NodeBuilder,
3110    interner: &TypeInterner,
3111    source: &str,
3112    start_pos: u32,
3113    _network: NetworkName,
3114) -> Result<leo_ast::Statement> {
3115    let parse = leo_parser_rowan::parse_statement_entry(source);
3116    let ctx = conversion_context(
3117        &handler,
3118        node_builder,
3119        interner,
3120        parse.lex_errors(),
3121        parse.errors(),
3122        start_pos,
3123        source.len() as u32,
3124    );
3125    ctx.to_statement(&parse.syntax())
3126}
3127
3128/// Parses a module (non-main source file) into a Module AST.
3129#[allow(clippy::too_many_arguments)]
3130pub fn parse_module(
3131    handler: Handler,
3132    node_builder: &NodeBuilder,
3133    interner: &TypeInterner,
3134    source: &str,
3135    start_pos: u32,
3136    program_name: Symbol,
3137    path: Vec<Symbol>,
3138    _network: NetworkName,
3139) -> Result<leo_ast::Module> {
3140    let parse = leo_parser_rowan::parse_module_entry(source);
3141    let ctx = conversion_context(
3142        &handler,
3143        node_builder,
3144        interner,
3145        parse.lex_errors(),
3146        parse.errors(),
3147        start_pos,
3148        source.len() as u32,
3149    );
3150    ctx.to_module(&parse.syntax(), program_name, path)
3151}
3152
3153/// Parses a complete program with its modules into a Program AST.
3154pub fn parse_program(
3155    handler: Handler,
3156    node_builder: &NodeBuilder,
3157    interner: &TypeInterner,
3158    source: &SourceFile,
3159    modules: &[std::rc::Rc<SourceFile>],
3160    _network: NetworkName,
3161) -> Result<leo_ast::Program> {
3162    // Parse main program file
3163    let parse = leo_parser_rowan::parse_file(&source.src);
3164    let main_context = conversion_context(
3165        &handler,
3166        node_builder,
3167        interner,
3168        parse.lex_errors(),
3169        parse.errors(),
3170        source.absolute_start,
3171        source.src.len() as u32,
3172    );
3173    let mut program = main_context.to_main(&parse.syntax())?;
3174    let program_name = *program.program_scopes.first().unwrap().0;
3175
3176    // Determine the root directory of the main file (for module resolution)
3177    let root_dir = match &source.name {
3178        FileName::Real(path) => path.parent().map(|p| p.to_path_buf()),
3179        _ => None,
3180    };
3181
3182    for module in modules {
3183        let module_parse = leo_parser_rowan::parse_module_entry(&module.src);
3184        let module_context = conversion_context(
3185            &handler,
3186            node_builder,
3187            interner,
3188            module_parse.lex_errors(),
3189            module_parse.errors(),
3190            module.absolute_start,
3191            module.src.len() as u32,
3192        );
3193
3194        if let Some(key) = compute_module_key(&module.name, root_dir.as_deref()) {
3195            for segment in &key {
3196                if leo_parser_rowan::is_keyword(&segment.to_string()) {
3197                    return Err(crate::errors::keyword_used_as_module_name(key.iter().format("::"), segment).into());
3198                }
3199            }
3200            let module_ast = module_context.to_module(&module_parse.syntax(), program_name, key.clone())?;
3201            program.modules.insert(key, module_ast);
3202        }
3203    }
3204
3205    Ok(program)
3206}
3207
3208/// Parses a complete library with its submodules into a Library AST.
3209pub fn parse_library(
3210    handler: Handler,
3211    node_builder: &NodeBuilder,
3212    interner: &TypeInterner,
3213    library_name: Symbol,
3214    source: &SourceFile,
3215    modules: &[std::rc::Rc<SourceFile>],
3216    _network: NetworkName,
3217) -> Result<leo_ast::Library> {
3218    // Parse `lib.leo` library file.
3219    let parse = leo_parser_rowan::parse_file(&source.src);
3220    let main_context = conversion_context(
3221        &handler,
3222        node_builder,
3223        interner,
3224        parse.lex_errors(),
3225        parse.errors(),
3226        source.absolute_start,
3227        source.src.len() as u32,
3228    );
3229
3230    let mut library = main_context.to_library(library_name, &parse.syntax())?;
3231
3232    // Determine the root directory of `lib.leo` for module key computation.
3233    let root_dir = match &source.name {
3234        FileName::Real(path) => path.parent().map(|p| p.to_path_buf()),
3235        _ => None,
3236    };
3237
3238    // Parse each submodule source file and insert it into the library.
3239    for module_sf in modules {
3240        let module_parse = leo_parser_rowan::parse_module_entry(&module_sf.src);
3241        let module_context = conversion_context(
3242            &handler,
3243            node_builder,
3244            interner,
3245            module_parse.lex_errors(),
3246            module_parse.errors(),
3247            module_sf.absolute_start,
3248            module_sf.src.len() as u32,
3249        );
3250
3251        if let Some(key) = compute_module_key(&module_sf.name, root_dir.as_deref()) {
3252            for segment in &key {
3253                if leo_parser_rowan::is_keyword(&segment.to_string()) {
3254                    return Err(crate::errors::keyword_used_as_module_name(key.iter().format("::"), segment).into());
3255                }
3256            }
3257            // Library modules use library_name as the owner (analogous to program_name in
3258            // program modules). Items inside are registered under Location::new(library_name, path).
3259            let module_ast = module_context.to_module(&module_parse.syntax(), library_name, key.clone())?;
3260            library.modules.insert(key, module_ast);
3261        }
3262    }
3263
3264    Ok(library)
3265}
3266
3267// =============================================================================
3268// Helper Functions
3269// =============================================================================
3270
3271/// Get non-trivia children of a node.
3272fn children(node: &SyntaxNode) -> impl Iterator<Item = SyntaxNode> + '_ {
3273    node.children().filter(|n| !n.kind().is_trivia())
3274}
3275
3276/// Get non-trivia tokens from a node.
3277fn tokens(node: &SyntaxNode) -> impl Iterator<Item = SyntaxToken> + '_ {
3278    node.children_with_tokens().filter_map(|elem| elem.into_token()).filter(|t| !t.kind().is_trivia())
3279}
3280
3281/// True when `node` carries a direct `export` keyword child.
3282fn has_export(node: &SyntaxNode) -> bool {
3283    tokens(node).any(|t| t.kind() == KW_EXPORT)
3284}
3285
3286/// Find the first IDENT or keyword token after the DOT in a node.
3287fn find_name_after_dot(node: &SyntaxNode) -> Option<SyntaxToken> {
3288    let dot_end = tokens(node).find(|t| t.kind() == DOT)?.text_range().end();
3289    tokens(node).filter(|t| t.text_range().start() >= dot_end).find(|t| t.kind() == IDENT || t.kind().is_keyword())
3290}
3291
3292/// First non-trivia direct token of a node.
3293fn first_non_trivia_token(node: &SyntaxNode) -> Option<SyntaxToken> {
3294    node.children_with_tokens().find_map(|e| e.into_token().filter(|t| !t.kind().is_trivia()))
3295}
3296
3297/// Last non-trivia direct token of a node.
3298fn last_non_trivia_token(node: &SyntaxNode) -> Option<SyntaxToken> {
3299    node.children_with_tokens().filter_map(|e| e.into_token().filter(|t| !t.kind().is_trivia())).last()
3300}
3301
3302/// Find an invalid network identifier (IDENT after DOT) in a node's tokens.
3303fn find_invalid_network(node: &SyntaxNode) -> Option<SyntaxToken> {
3304    let mut saw_dot = false;
3305    tokens(node).find(|t| {
3306        if t.kind() == DOT {
3307            saw_dot = true;
3308            return false;
3309        }
3310        saw_dot && t.kind() == IDENT
3311    })
3312}
3313
3314/// Convert a visibility keyword token kind to a `Mode`.
3315fn token_kind_to_mode(kind: SyntaxKind) -> Option<leo_ast::Mode> {
3316    match kind {
3317        KW_PUBLIC => Some(leo_ast::Mode::Public),
3318        KW_PRIVATE => Some(leo_ast::Mode::Private),
3319        KW_CONSTANT => Some(leo_ast::Mode::Constant),
3320        _ => None,
3321    }
3322}
3323
3324/// Convert a parameter or struct member node kind to a `Mode`.
3325fn node_kind_to_mode(kind: SyntaxKind) -> leo_ast::Mode {
3326    match kind {
3327        PARAM_PUBLIC | STRUCT_MEMBER_PUBLIC => leo_ast::Mode::Public,
3328        PARAM_PRIVATE | STRUCT_MEMBER_PRIVATE => leo_ast::Mode::Private,
3329        PARAM_CONSTANT | STRUCT_MEMBER_CONSTANT => leo_ast::Mode::Constant,
3330        _ => leo_ast::Mode::None,
3331    }
3332}
3333
3334/// Convert a keyword token kind to the corresponding path symbol, if applicable.
3335fn keyword_to_path_symbol(kind: SyntaxKind) -> Option<Symbol> {
3336    match kind {
3337        KW_SELF => Some(sym::SelfLower),
3338        KW_BLOCK => Some(sym::block),
3339        KW_NETWORK => Some(sym::network),
3340        KW_FINAL_UPPER => Some(sym::Final),
3341        _ => None,
3342    }
3343}
3344
3345/// Check if a SyntaxKind is an assignment operator.
3346fn is_assign_op(kind: SyntaxKind) -> bool {
3347    matches!(
3348        kind,
3349        EQ | PLUS_EQ
3350            | MINUS_EQ
3351            | STAR_EQ
3352            | SLASH_EQ
3353            | PERCENT_EQ
3354            | STAR2_EQ
3355            | AMP_EQ
3356            | PIPE_EQ
3357            | CARET_EQ
3358            | SHL_EQ
3359            | SHR_EQ
3360            | AMP2_EQ
3361            | PIPE2_EQ
3362    )
3363}
3364/// Convert a type keyword to a primitive `TypeKind`, if applicable.
3365fn keyword_to_primitive_type(kind: SyntaxKind) -> Option<leo_ast::TypeKind> {
3366    let ty = match kind {
3367        KW_ADDRESS => leo_ast::TypeKind::Address,
3368        KW_BOOL => leo_ast::TypeKind::Boolean,
3369        KW_FIELD => leo_ast::TypeKind::Field,
3370        KW_GROUP => leo_ast::TypeKind::Group,
3371        KW_SCALAR => leo_ast::TypeKind::Scalar,
3372        KW_SIGNATURE => leo_ast::TypeKind::Signature,
3373        KW_STRING => leo_ast::TypeKind::String,
3374        KW_DYN => leo_ast::TypeKind::DynRecord,
3375        KW_IDENTIFIER => leo_ast::TypeKind::Identifier,
3376        KW_U8 => leo_ast::TypeKind::Integer(leo_ast::IntegerType::U8),
3377        KW_U16 => leo_ast::TypeKind::Integer(leo_ast::IntegerType::U16),
3378        KW_U32 => leo_ast::TypeKind::Integer(leo_ast::IntegerType::U32),
3379        KW_U64 => leo_ast::TypeKind::Integer(leo_ast::IntegerType::U64),
3380        KW_U128 => leo_ast::TypeKind::Integer(leo_ast::IntegerType::U128),
3381        KW_I8 => leo_ast::TypeKind::Integer(leo_ast::IntegerType::I8),
3382        KW_I16 => leo_ast::TypeKind::Integer(leo_ast::IntegerType::I16),
3383        KW_I32 => leo_ast::TypeKind::Integer(leo_ast::IntegerType::I32),
3384        KW_I64 => leo_ast::TypeKind::Integer(leo_ast::IntegerType::I64),
3385        KW_I128 => leo_ast::TypeKind::Integer(leo_ast::IntegerType::I128),
3386        _ => return None,
3387    };
3388    Some(ty)
3389}
3390
3391/// Convert a SyntaxKind operator to BinaryOperation.
3392fn token_to_binary_op(kind: SyntaxKind) -> leo_ast::BinaryOperation {
3393    match kind {
3394        EQ2 => leo_ast::BinaryOperation::Eq,
3395        BANG_EQ => leo_ast::BinaryOperation::Neq,
3396        LT => leo_ast::BinaryOperation::Lt,
3397        LT_EQ => leo_ast::BinaryOperation::Lte,
3398        GT => leo_ast::BinaryOperation::Gt,
3399        GT_EQ => leo_ast::BinaryOperation::Gte,
3400        PLUS => leo_ast::BinaryOperation::Add,
3401        MINUS => leo_ast::BinaryOperation::Sub,
3402        STAR => leo_ast::BinaryOperation::Mul,
3403        SLASH => leo_ast::BinaryOperation::Div,
3404        PERCENT => leo_ast::BinaryOperation::Rem,
3405        PIPE2 => leo_ast::BinaryOperation::Or,
3406        AMP2 => leo_ast::BinaryOperation::And,
3407        PIPE => leo_ast::BinaryOperation::BitwiseOr,
3408        AMP => leo_ast::BinaryOperation::BitwiseAnd,
3409        STAR2 => leo_ast::BinaryOperation::Pow,
3410        SHL => leo_ast::BinaryOperation::Shl,
3411        SHR => leo_ast::BinaryOperation::Shr,
3412        CARET => leo_ast::BinaryOperation::Xor,
3413        _ => panic!("unexpected binary operator: {:?}", kind),
3414    }
3415}
3416
3417/// Computes a module key from a `FileName`, optionally relative to a root directory.
3418fn compute_module_key(name: &FileName, root_dir: Option<&std::path::Path>) -> Option<Vec<Symbol>> {
3419    let path = match name {
3420        FileName::Custom(name) => std::path::Path::new(name).to_path_buf(),
3421        FileName::Real(path) => {
3422            let root = root_dir?;
3423            path.strip_prefix(root).ok()?.to_path_buf()
3424        }
3425    };
3426
3427    let mut key: Vec<Symbol> =
3428        path.components().map(|comp| Symbol::intern(&comp.as_os_str().to_string_lossy())).collect();
3429
3430    if let Some(last) = path.file_name()
3431        && let Some(stem) = std::path::Path::new(last).file_stem()
3432    {
3433        key.pop();
3434        key.push(Symbol::intern(&stem.to_string_lossy()));
3435    }
3436
3437    Some(key)
3438}
3439
3440/// Returns `true` for syntax node kinds that are valid inside a library (`lib.leo`).
3441fn is_library_item(kind: SyntaxKind) -> bool {
3442    matches!(kind, GLOBAL_CONST | STRUCT_DEF | FUNCTION_DEF | INTERFACE_DEF)
3443}
3444
3445/// Returns `true` for syntax node kinds that are valid inside a program (`main.leo`).
3446fn is_program_item(kind: SyntaxKind) -> bool {
3447    matches!(
3448        kind,
3449        GLOBAL_CONST
3450            | FUNCTION_DEF
3451            | FINAL_FN_DEF
3452            | VIEW_FN_DEF
3453            | STRUCT_DEF
3454            | RECORD_DEF
3455            | INTERFACE_DEF
3456            | MAPPING_DEF
3457            | STORAGE_DEF
3458            | CONSTRUCTOR_DEF
3459            | PROGRAM_DECL
3460            | IMPORT
3461    )
3462}