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