Skip to main content

runar_compiler_rust/frontend/
parser.rs

1//! Pass 1: Parse
2//!
3//! Uses SWC to parse a TypeScript source file and extract the SmartContract
4//! subclass into a Rúnar AST.
5
6use swc_common::sync::Lrc;
7use swc_common::{FileName, SourceMap};
8use swc_ecma_ast as swc;
9use swc_ecma_ast::{
10    Accessibility, AssignExpr, AssignOp, AssignTarget, CallExpr, Callee, Class, ClassDecl,
11    ClassMember, Decl, EsVersion, Expr, ForStmt, IfStmt, Lit, MemberExpr as SwcMemberExpr,
12    MemberProp, ModuleDecl, ModuleItem, Param, ParamOrTsParamProp, Pat, PropName, ReturnStmt,
13    SimpleAssignTarget, Stmt, SuperProp, TsEntityName, TsKeywordTypeKind, TsLit,
14    TsParamPropParam, TsType, UnaryExpr as SwcUnaryExpr, UpdateExpr, UpdateOp, VarDecl,
15    VarDeclKind, VarDeclOrExpr,
16};
17use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax, TsSyntax};
18
19use super::ast::{
20    BinaryOp, ContractNode, Expression, MethodNode, ParamNode, PrimitiveTypeName,
21    PropertyNode, SourceLocation, Statement, TypeNode, UnaryOp, Visibility,
22};
23use super::diagnostic::Diagnostic;
24
25// ---------------------------------------------------------------------------
26// Public API
27// ---------------------------------------------------------------------------
28
29/// Result of parsing a source file.
30pub struct ParseResult {
31    pub contract: Option<ContractNode>,
32    pub errors: Vec<Diagnostic>,
33}
34
35impl ParseResult {
36    /// Get error messages as plain strings (for backward compatibility).
37    pub fn error_strings(&self) -> Vec<String> {
38        self.errors.iter().map(|d| d.format_message()).collect()
39    }
40}
41
42/// Parse a TypeScript source string and extract the Rúnar contract AST.
43pub fn parse(source: &str, file_name: Option<&str>) -> ParseResult {
44    let mut errors: Vec<Diagnostic> = Vec::new();
45    let file = file_name.unwrap_or("contract.ts");
46
47    // Set up SWC parser
48    let cm: Lrc<SourceMap> = Lrc::new(SourceMap::default());
49    let fm = cm.new_source_file(Lrc::new(FileName::Custom(file.to_string())), source.to_string());
50    let lexer = Lexer::new(
51        Syntax::Typescript(TsSyntax {
52            tsx: false,
53            decorators: false,
54            ..Default::default()
55        }),
56        EsVersion::Es2022,
57        StringInput::from(&*fm),
58        None,
59    );
60    let mut parser = Parser::new_from(lexer);
61
62    let module = match parser.parse_module() {
63        Ok(m) => m,
64        Err(e) => {
65            errors.push(Diagnostic::error(format!("Parse error: {:?}", e), None));
66            return ParseResult {
67                contract: None,
68                errors,
69            };
70        }
71    };
72
73    // Collect any parser errors
74    for e in parser.take_errors() {
75        errors.push(Diagnostic::error(format!("Parse error: {:?}", e), None));
76    }
77
78    // Find the class that extends SmartContract or StatefulSmartContract
79    let mut contract_class: Option<&ClassDecl> = None;
80    let mut detected_parent_class: &str = "SmartContract";
81
82    for item in &module.body {
83        if let ModuleItem::Stmt(Stmt::Decl(Decl::Class(class_decl))) = item {
84            if let Some(super_class) = &class_decl.class.super_class {
85                if let Some(base_name) = get_base_class_name(super_class) {
86                    if contract_class.is_some() {
87                        errors.push(Diagnostic::error(
88                            "Only one SmartContract subclass is allowed per file", None,
89                        ));
90                    }
91                    contract_class = Some(class_decl);
92                    detected_parent_class = base_name;
93                }
94            }
95        }
96    }
97
98    // Also check export declarations
99    for item in &module.body {
100        if let ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) = item {
101            if let Decl::Class(class_decl) = &export_decl.decl {
102                if let Some(super_class) = &class_decl.class.super_class {
103                    if let Some(base_name) = get_base_class_name(super_class) {
104                        if contract_class.is_some() {
105                            errors.push(Diagnostic::error(
106                                "Only one SmartContract subclass is allowed per file", None,
107                            ));
108                        }
109                        contract_class = Some(class_decl);
110                        detected_parent_class = base_name;
111                    }
112                }
113            }
114        }
115    }
116
117    let class_decl = match contract_class {
118        Some(c) => c,
119        None => {
120            errors.push(Diagnostic::error("No class extending SmartContract or StatefulSmartContract found", None));
121            return ParseResult {
122                contract: None,
123                errors,
124            };
125        }
126    };
127
128    let contract_name = class_decl.ident.sym.to_string();
129    let class = &class_decl.class;
130
131    // Extract properties
132    let properties = parse_properties(class, file, &mut errors);
133
134    // Extract constructor
135    let constructor_node = parse_constructor(class, file, &mut errors);
136
137    // Extract methods
138    let methods = parse_methods(class, file, &mut errors);
139
140    let contract = ContractNode {
141        name: contract_name,
142        parent_class: detected_parent_class.to_string(),
143        properties,
144        constructor: constructor_node,
145        methods,
146        source_file: file.to_string(),
147    };
148
149    ParseResult {
150        contract: Some(contract),
151        errors,
152    }
153}
154
155// ---------------------------------------------------------------------------
156// Helpers
157// ---------------------------------------------------------------------------
158
159fn get_base_class_name(expr: &Expr) -> Option<&str> {
160    match expr {
161        Expr::Ident(ident) => {
162            let name = ident.sym.as_ref();
163            if name == "SmartContract" || name == "StatefulSmartContract" {
164                Some(name)
165            } else {
166                None
167            }
168        }
169        _ => None,
170    }
171}
172
173fn loc(file: &str, line: usize, column: usize) -> SourceLocation {
174    SourceLocation {
175        file: file.to_string(),
176        line,
177        column,
178    }
179}
180
181fn default_loc(file: &str) -> SourceLocation {
182    loc(file, 1, 0)
183}
184
185// ---------------------------------------------------------------------------
186// Properties
187// ---------------------------------------------------------------------------
188
189fn parse_properties(class: &Class, file: &str, errors: &mut Vec<Diagnostic>) -> Vec<PropertyNode> {
190    let mut result = Vec::new();
191
192    for member in &class.body {
193        if let ClassMember::ClassProp(prop) = member {
194            let name = match &prop.key {
195                PropName::Ident(ident) => ident.sym.to_string(),
196                _ => {
197                    errors.push(Diagnostic::error("Property must have an identifier name", None));
198                    continue;
199                }
200            };
201
202            let readonly = prop.readonly;
203
204            let prop_type = if let Some(ref ann) = prop.type_ann {
205                parse_type_node(&ann.type_ann, file, errors)
206            } else {
207                errors.push(Diagnostic::error(format!(
208                    "Property '{}' must have an explicit type annotation",
209                    name
210                ), None));
211                TypeNode::Custom("unknown".to_string())
212            };
213
214            // Parse initializer if present (SWC ClassProp.value)
215            let initializer = prop.value.as_ref().map(|v| parse_expression(v, file, errors));
216
217            result.push(PropertyNode {
218                name,
219                prop_type,
220                readonly,
221                initializer,
222                source_location: default_loc(file),
223            });
224        }
225    }
226
227    result
228}
229
230// ---------------------------------------------------------------------------
231// Constructor
232// ---------------------------------------------------------------------------
233
234fn parse_constructor(class: &Class, file: &str, errors: &mut Vec<Diagnostic>) -> MethodNode {
235    for member in &class.body {
236        if let ClassMember::Constructor(ctor) = member {
237            let params = parse_constructor_params(&ctor.params, file, errors);
238            let body = if let Some(ref block) = ctor.body {
239                parse_block_stmts(&block.stmts, file, errors)
240            } else {
241                Vec::new()
242            };
243
244            return MethodNode {
245                name: "constructor".to_string(),
246                params,
247                body,
248                visibility: Visibility::Public,
249                source_location: default_loc(file),
250            };
251        }
252    }
253
254    errors.push(Diagnostic::error("Contract must have a constructor", None));
255    MethodNode {
256        name: "constructor".to_string(),
257        params: Vec::new(),
258        body: Vec::new(),
259        visibility: Visibility::Public,
260        source_location: default_loc(file),
261    }
262}
263
264fn parse_constructor_params(
265    params: &[ParamOrTsParamProp],
266    file: &str,
267    errors: &mut Vec<Diagnostic>,
268) -> Vec<ParamNode> {
269    let mut result = Vec::new();
270
271    for param in params {
272        match param {
273            ParamOrTsParamProp::Param(p) => {
274                if let Some(ast_param) = parse_param_pat(&p.pat, file, errors) {
275                    result.push(ast_param);
276                }
277            }
278            ParamOrTsParamProp::TsParamProp(ts_param) => {
279                match &ts_param.param {
280                    TsParamPropParam::Ident(ident) => {
281                        let name = ident.id.sym.to_string();
282                        let param_type = if let Some(ref ann) = ident.type_ann {
283                            parse_type_node(&ann.type_ann, file, errors)
284                        } else {
285                            errors.push(Diagnostic::error(format!(
286                                "Parameter '{}' must have an explicit type annotation",
287                                name
288                            ), None));
289                            TypeNode::Custom("unknown".to_string())
290                        };
291                        result.push(ParamNode { name, param_type });
292                    }
293                    TsParamPropParam::Assign(_) => {
294                        errors.push(Diagnostic::error("Default parameter values are not supported", None));
295                    }
296                }
297            }
298        }
299    }
300
301    result
302}
303
304// ---------------------------------------------------------------------------
305// Methods
306// ---------------------------------------------------------------------------
307
308fn parse_methods(class: &Class, file: &str, errors: &mut Vec<Diagnostic>) -> Vec<MethodNode> {
309    let mut result = Vec::new();
310
311    for member in &class.body {
312        if let ClassMember::Method(method) = member {
313            let name = match &method.key {
314                PropName::Ident(ident) => ident.sym.to_string(),
315                _ => {
316                    errors.push(Diagnostic::error("Method must have an identifier name", None));
317                    continue;
318                }
319            };
320
321            let params = parse_method_params(&method.function.params, file, errors);
322
323            let visibility = if method.accessibility == Some(Accessibility::Public) {
324                Visibility::Public
325            } else {
326                Visibility::Private
327            };
328
329            let body = if let Some(ref block) = method.function.body {
330                parse_block_stmts(&block.stmts, file, errors)
331            } else {
332                Vec::new()
333            };
334
335            result.push(MethodNode {
336                name,
337                params,
338                body,
339                visibility,
340                source_location: default_loc(file),
341            });
342        }
343    }
344
345    result
346}
347
348fn parse_method_params(
349    params: &[Param],
350    file: &str,
351    errors: &mut Vec<Diagnostic>,
352) -> Vec<ParamNode> {
353    let mut result = Vec::new();
354    for param in params {
355        if let Some(ast_param) = parse_param_pat(&param.pat, file, errors) {
356            result.push(ast_param);
357        }
358    }
359    result
360}
361
362fn parse_param_pat(
363    pat: &Pat,
364    file: &str,
365    errors: &mut Vec<Diagnostic>,
366) -> Option<ParamNode> {
367    match pat {
368        Pat::Ident(ident) => {
369            let name = ident.id.sym.to_string();
370            let param_type = if let Some(ref ann) = ident.type_ann {
371                parse_type_node(&ann.type_ann, file, errors)
372            } else {
373                errors.push(Diagnostic::error(format!(
374                    "Parameter '{}' must have an explicit type annotation",
375                    name
376                ), None));
377                TypeNode::Custom("unknown".to_string())
378            };
379            Some(ParamNode { name, param_type })
380        }
381        _ => {
382            errors.push(Diagnostic::error("Unsupported parameter pattern", None));
383            None
384        }
385    }
386}
387
388// ---------------------------------------------------------------------------
389// Type nodes
390// ---------------------------------------------------------------------------
391
392fn parse_type_node(ts_type: &TsType, file: &str, errors: &mut Vec<Diagnostic>) -> TypeNode {
393    match ts_type {
394        // Keyword types
395        TsType::TsKeywordType(kw) => match kw.kind {
396            TsKeywordTypeKind::TsBigIntKeyword => TypeNode::Primitive(PrimitiveTypeName::Bigint),
397            TsKeywordTypeKind::TsBooleanKeyword => {
398                TypeNode::Primitive(PrimitiveTypeName::Boolean)
399            }
400            TsKeywordTypeKind::TsVoidKeyword => TypeNode::Primitive(PrimitiveTypeName::Void),
401            TsKeywordTypeKind::TsNumberKeyword => {
402                errors.push(Diagnostic::error("'number' type is not allowed in Rúnar contracts; use 'bigint' instead", None));
403                TypeNode::Primitive(PrimitiveTypeName::Bigint)
404            }
405            TsKeywordTypeKind::TsStringKeyword => {
406                errors.push(Diagnostic::error("'string' type is not allowed in Rúnar contracts; use 'ByteString' instead", None));
407                TypeNode::Primitive(PrimitiveTypeName::ByteString)
408            }
409            _ => {
410                errors.push(Diagnostic::error(format!("Unsupported keyword type: {:?}", kw.kind), None));
411                TypeNode::Custom("unknown".to_string())
412            }
413        },
414
415        // Type references: Sha256, PubKey, FixedArray<T, N>, etc.
416        TsType::TsTypeRef(type_ref) => {
417            let type_name = ts_entity_name_to_string(&type_ref.type_name);
418
419            // Check for FixedArray<T, N>
420            if type_name == "FixedArray" {
421                if let Some(ref type_params) = type_ref.type_params {
422                    let params = &type_params.params;
423                    if params.len() != 2 {
424                        errors.push(Diagnostic::error(
425                            "FixedArray requires exactly 2 type arguments: FixedArray<T, N>",
426                            None,
427                        ));
428                        return TypeNode::Custom(type_name);
429                    }
430
431                    let element = parse_type_node(&params[0], file, errors);
432
433                    // The second parameter should be a literal type for the length
434                    let length = extract_type_literal_number(&params[1]);
435                    if let Some(len) = length {
436                        return TypeNode::FixedArray {
437                            element: Box::new(element),
438                            length: len,
439                        };
440                    } else {
441                        errors.push(Diagnostic::error(
442                            "FixedArray size must be a non-negative integer literal",
443                            None,
444                        ));
445                        return TypeNode::Custom(type_name);
446                    }
447                } else {
448                    errors.push(Diagnostic::error("FixedArray requires type arguments", None));
449                    return TypeNode::Custom(type_name);
450                }
451            }
452
453            // Check for primitive types referenced by name
454            if let Some(prim) = PrimitiveTypeName::from_str(&type_name) {
455                return TypeNode::Primitive(prim);
456            }
457
458            // Unknown type reference
459            TypeNode::Custom(type_name)
460        }
461
462        _ => {
463            errors.push(Diagnostic::error("Unsupported type annotation", None));
464            TypeNode::Custom("unknown".to_string())
465        }
466    }
467}
468
469fn ts_entity_name_to_string(entity: &TsEntityName) -> String {
470    match entity {
471        TsEntityName::Ident(ident) => ident.sym.to_string(),
472        TsEntityName::TsQualifiedName(qual) => {
473            format!(
474                "{}.{}",
475                ts_entity_name_to_string(&qual.left),
476                qual.right.sym
477            )
478        }
479    }
480}
481
482/// Try to extract a literal number from a type node (e.g. `10` in `FixedArray<bigint, 10>`).
483fn extract_type_literal_number(ts_type: &TsType) -> Option<usize> {
484    match ts_type {
485        TsType::TsLitType(lit) => match &lit.lit {
486            TsLit::Number(n) => Some(n.value as usize),
487            _ => None,
488        },
489        _ => None,
490    }
491}
492
493// ---------------------------------------------------------------------------
494// Statements
495// ---------------------------------------------------------------------------
496
497fn parse_block_stmts(stmts: &[Stmt], file: &str, errors: &mut Vec<Diagnostic>) -> Vec<Statement> {
498    let mut result = Vec::new();
499    for stmt in stmts {
500        if let Some(parsed) = parse_statement(stmt, file, errors) {
501            result.push(parsed);
502        }
503    }
504    result
505}
506
507fn parse_statement(stmt: &Stmt, file: &str, errors: &mut Vec<Diagnostic>) -> Option<Statement> {
508    match stmt {
509        Stmt::Decl(Decl::Var(var_decl)) => parse_variable_statement(var_decl, file, errors),
510
511        Stmt::Expr(expr_stmt) => parse_expression_statement(&expr_stmt.expr, file, errors),
512
513        Stmt::If(if_stmt) => Some(parse_if_statement(if_stmt, file, errors)),
514
515        Stmt::For(for_stmt) => Some(parse_for_statement(for_stmt, file, errors)),
516
517        Stmt::Return(ret_stmt) => Some(parse_return_statement(ret_stmt, file, errors)),
518
519        Stmt::Block(block) => {
520            // Flatten block statements
521            let stmts = parse_block_stmts(&block.stmts, file, errors);
522            // Return as individual statements by wrapping in a block-like structure
523            // For simplicity, we report an error -- blocks should be part of if/for
524            if stmts.is_empty() {
525                None
526            } else {
527                errors.push(Diagnostic::error("Standalone block statements are not supported; use if/for", None));
528                None
529            }
530        }
531
532        _ => {
533            errors.push(Diagnostic::error(format!("Unsupported statement kind: {:?}", stmt), None));
534            None
535        }
536    }
537}
538
539fn parse_variable_statement(
540    var_decl: &VarDecl,
541    file: &str,
542    errors: &mut Vec<Diagnostic>,
543) -> Option<Statement> {
544    if var_decl.decls.is_empty() {
545        return None;
546    }
547
548    let decl = &var_decl.decls[0];
549    let name = match &decl.name {
550        Pat::Ident(ident) => ident.id.sym.to_string(),
551        _ => {
552            errors.push(Diagnostic::error("Destructuring patterns are not supported in variable declarations", None));
553            return None;
554        }
555    };
556
557    let is_const = var_decl.kind == VarDeclKind::Const;
558
559    let init = if let Some(ref init_expr) = decl.init {
560        parse_expression(init_expr, file, errors)
561    } else {
562        errors.push(Diagnostic::error(format!("Variable '{}' must have an initializer", name), None));
563        Expression::BigIntLiteral { value: 0 }
564    };
565
566    let var_type = if let Pat::Ident(ident) = &decl.name {
567        if let Some(ref ann) = ident.type_ann {
568            Some(parse_type_node(&ann.type_ann, file, errors))
569        } else {
570            None
571        }
572    } else {
573        None
574    };
575
576    Some(Statement::VariableDecl {
577        name,
578        var_type,
579        mutable: !is_const,
580        init,
581        source_location: default_loc(file),
582    })
583}
584
585fn parse_expression_statement(
586    expr: &Expr,
587    file: &str,
588    errors: &mut Vec<Diagnostic>,
589) -> Option<Statement> {
590    // Check if this is an assignment expression (a = b, this.x = b)
591    if let Expr::Assign(assign) = expr {
592        return Some(parse_assignment_expr(assign, file, errors));
593    }
594
595    let expression = parse_expression(expr, file, errors);
596    Some(Statement::ExpressionStatement {
597        expression,
598        source_location: default_loc(file),
599    })
600}
601
602fn parse_assignment_expr(
603    assign: &AssignExpr,
604    file: &str,
605    errors: &mut Vec<Diagnostic>,
606) -> Statement {
607    let target = parse_assign_target(&assign.left, file, errors);
608
609    match assign.op {
610        AssignOp::Assign => {
611            let value = parse_expression(&assign.right, file, errors);
612            Statement::Assignment {
613                target,
614                value,
615                source_location: default_loc(file),
616            }
617        }
618        // Compound assignments: +=, -=, *=, /=, %=
619        op => {
620            let bin_op = match op {
621                AssignOp::AddAssign => Some(BinaryOp::Add),
622                AssignOp::SubAssign => Some(BinaryOp::Sub),
623                AssignOp::MulAssign => Some(BinaryOp::Mul),
624                AssignOp::DivAssign => Some(BinaryOp::Div),
625                AssignOp::ModAssign => Some(BinaryOp::Mod),
626                _ => {
627                    errors.push(Diagnostic::error(format!("Unsupported compound assignment operator: {:?}", op), None));
628                    None
629                }
630            };
631
632            if let Some(bin_op) = bin_op {
633                let right = parse_expression(&assign.right, file, errors);
634                let target_for_rhs = parse_assign_target(&assign.left, file, errors);
635                let value = Expression::BinaryExpr {
636                    op: bin_op,
637                    left: Box::new(target_for_rhs),
638                    right: Box::new(right),
639                };
640                Statement::Assignment {
641                    target,
642                    value,
643                    source_location: default_loc(file),
644                }
645            } else {
646                let value = parse_expression(&assign.right, file, errors);
647                Statement::Assignment {
648                    target,
649                    value,
650                    source_location: default_loc(file),
651                }
652            }
653        }
654    }
655}
656
657fn parse_assign_target(
658    target: &AssignTarget,
659    file: &str,
660    errors: &mut Vec<Diagnostic>,
661) -> Expression {
662    match target {
663        AssignTarget::Simple(simple) => match simple {
664            SimpleAssignTarget::Ident(ident) => Expression::Identifier {
665                name: ident.id.sym.to_string(),
666            },
667            SimpleAssignTarget::Member(member) => {
668                parse_member_expression(member, file, errors)
669            }
670            _ => {
671                errors.push(Diagnostic::error("Unsupported assignment target", None));
672                Expression::Identifier {
673                    name: "_error".to_string(),
674                }
675            }
676        },
677        AssignTarget::Pat(_) => {
678            errors.push(Diagnostic::error("Destructuring assignment is not supported", None));
679            Expression::Identifier {
680                name: "_error".to_string(),
681            }
682        }
683    }
684}
685
686fn parse_if_statement(if_stmt: &IfStmt, file: &str, errors: &mut Vec<Diagnostic>) -> Statement {
687    let condition = parse_expression(&if_stmt.test, file, errors);
688    let then_branch = parse_stmt_or_block(&if_stmt.cons, file, errors);
689
690    let else_branch = if_stmt
691        .alt
692        .as_ref()
693        .map(|alt| parse_stmt_or_block(alt, file, errors));
694
695    Statement::IfStatement {
696        condition,
697        then_branch,
698        else_branch,
699        source_location: default_loc(file),
700    }
701}
702
703fn parse_stmt_or_block(
704    stmt: &Stmt,
705    file: &str,
706    errors: &mut Vec<Diagnostic>,
707) -> Vec<Statement> {
708    match stmt {
709        Stmt::Block(block) => parse_block_stmts(&block.stmts, file, errors),
710        _ => {
711            if let Some(s) = parse_statement(stmt, file, errors) {
712                vec![s]
713            } else {
714                Vec::new()
715            }
716        }
717    }
718}
719
720fn parse_for_statement(for_stmt: &ForStmt, file: &str, errors: &mut Vec<Diagnostic>) -> Statement {
721    // Parse initializer
722    let init = if let Some(ref init_expr) = for_stmt.init {
723        match init_expr {
724            VarDeclOrExpr::VarDecl(var_decl) => {
725                if let Some(stmt) = parse_variable_statement(var_decl, file, errors) {
726                    stmt
727                } else {
728                    make_default_for_init(file)
729                }
730            }
731            VarDeclOrExpr::Expr(_) => {
732                errors.push(Diagnostic::error(
733                    "For loop must have a variable declaration initializer",
734                    None,
735                ));
736                make_default_for_init(file)
737            }
738        }
739    } else {
740        errors.push(Diagnostic::error("For loop must have an initializer", None));
741        make_default_for_init(file)
742    };
743
744    // Parse condition
745    let condition = if let Some(ref cond) = for_stmt.test {
746        parse_expression(cond, file, errors)
747    } else {
748        errors.push(Diagnostic::error("For loop must have a condition", None));
749        Expression::BoolLiteral { value: false }
750    };
751
752    // Parse update
753    let update = if let Some(ref upd) = for_stmt.update {
754        parse_for_update(upd, file, errors)
755    } else {
756        errors.push(Diagnostic::error("For loop must have an update expression", None));
757        Statement::ExpressionStatement {
758            expression: Expression::BigIntLiteral { value: 0 },
759            source_location: default_loc(file),
760        }
761    };
762
763    // Parse body
764    let body = parse_stmt_or_block(&for_stmt.body, file, errors);
765
766    Statement::ForStatement {
767        init: Box::new(init),
768        condition,
769        update: Box::new(update),
770        body,
771        source_location: default_loc(file),
772    }
773}
774
775fn parse_for_update(
776    expr: &Expr,
777    file: &str,
778    errors: &mut Vec<Diagnostic>,
779) -> Statement {
780    match expr {
781        Expr::Update(update) => {
782            let operand = parse_expression(&update.arg, file, errors);
783            let is_increment = update.op == UpdateOp::PlusPlus;
784            let expression = if is_increment {
785                Expression::IncrementExpr {
786                    operand: Box::new(operand),
787                    prefix: update.prefix,
788                }
789            } else {
790                Expression::DecrementExpr {
791                    operand: Box::new(operand),
792                    prefix: update.prefix,
793                }
794            };
795            Statement::ExpressionStatement {
796                expression,
797                source_location: default_loc(file),
798            }
799        }
800        _ => {
801            let expression = parse_expression(expr, file, errors);
802            Statement::ExpressionStatement {
803                expression,
804                source_location: default_loc(file),
805            }
806        }
807    }
808}
809
810fn parse_return_statement(
811    ret_stmt: &ReturnStmt,
812    file: &str,
813    errors: &mut Vec<Diagnostic>,
814) -> Statement {
815    let value = ret_stmt
816        .arg
817        .as_ref()
818        .map(|e| parse_expression(e, file, errors));
819
820    Statement::ReturnStatement {
821        value,
822        source_location: default_loc(file),
823    }
824}
825
826fn make_default_for_init(file: &str) -> Statement {
827    Statement::VariableDecl {
828        name: "_i".to_string(),
829        var_type: None,
830        mutable: true,
831        init: Expression::BigIntLiteral { value: 0 },
832        source_location: default_loc(file),
833    }
834}
835
836// ---------------------------------------------------------------------------
837// Expressions
838// ---------------------------------------------------------------------------
839
840fn parse_expression(expr: &Expr, file: &str, errors: &mut Vec<Diagnostic>) -> Expression {
841    match expr {
842        Expr::Bin(bin) => parse_binary_expression(bin, file, errors),
843
844        Expr::Unary(unary) => parse_unary_expression(unary, file, errors),
845
846        Expr::Update(update) => parse_update_expression(update, file, errors),
847
848        Expr::Call(call) => parse_call_expression(call, file, errors),
849
850        Expr::Member(member) => parse_member_expression(member, file, errors),
851
852        Expr::SuperProp(super_prop) => {
853            // super.x -- unlikely in Rúnar but handle gracefully
854            match &super_prop.prop {
855                SuperProp::Ident(ident) => Expression::MemberExpr {
856                    object: Box::new(Expression::Identifier {
857                        name: "super".to_string(),
858                    }),
859                    property: ident.sym.to_string(),
860                },
861                SuperProp::Computed(comp) => {
862                    let _ = parse_expression(&comp.expr, file, errors);
863                    errors.push(Diagnostic::error("Computed super property access not supported", None));
864                    Expression::Identifier {
865                        name: "super".to_string(),
866                    }
867                }
868            }
869        }
870
871        Expr::Ident(ident) => Expression::Identifier {
872            name: ident.sym.to_string(),
873        },
874
875        Expr::Lit(Lit::BigInt(bigint)) => {
876            // Parse the BigInt value -- SWC gives the numeric part
877            let val = bigint_to_i64(bigint);
878            Expression::BigIntLiteral { value: val }
879        }
880
881        Expr::Lit(Lit::Num(num)) => {
882            // Plain numeric literal -- treat as bigint for Rúnar
883            Expression::BigIntLiteral {
884                value: num.value as i64,
885            }
886        }
887
888        Expr::Lit(Lit::Bool(b)) => Expression::BoolLiteral { value: b.value },
889
890        Expr::Lit(Lit::Str(s)) => {
891            // String literals are hex-encoded ByteString values
892            Expression::ByteStringLiteral {
893                value: s.value.to_string(),
894            }
895        }
896
897        Expr::Tpl(tpl) => {
898            // Template literal with no substitutions
899            if tpl.exprs.is_empty() && tpl.quasis.len() == 1 {
900                Expression::ByteStringLiteral {
901                    value: tpl.quasis[0].raw.to_string(),
902                }
903            } else {
904                errors.push(Diagnostic::error("Template literals with expressions are not supported", None));
905                Expression::ByteStringLiteral {
906                    value: String::new(),
907                }
908            }
909        }
910
911        Expr::Cond(cond) => {
912            let condition = parse_expression(&cond.test, file, errors);
913            let consequent = parse_expression(&cond.cons, file, errors);
914            let alternate = parse_expression(&cond.alt, file, errors);
915            Expression::TernaryExpr {
916                condition: Box::new(condition),
917                consequent: Box::new(consequent),
918                alternate: Box::new(alternate),
919            }
920        }
921
922        Expr::Paren(paren) => parse_expression(&paren.expr, file, errors),
923
924        Expr::This(_) => Expression::Identifier {
925            name: "this".to_string(),
926        },
927
928        Expr::TsAs(as_expr) => {
929            // Type assertions: ignore the type, parse the expression
930            parse_expression(&as_expr.expr, file, errors)
931        }
932
933        Expr::TsNonNull(nn) => {
934            // Non-null assertion: just parse the inner expression
935            parse_expression(&nn.expr, file, errors)
936        }
937
938        Expr::Assign(assign) => {
939            // Assignment expression in expression context -- should be handled
940            // at statement level, but in case it appears in an expression context
941            errors.push(Diagnostic::error("Assignment expressions in expression context are not recommended", None));
942            let value = parse_expression(&assign.right, file, errors);
943            value
944        }
945
946        _ => {
947            errors.push(Diagnostic::error(format!("Unsupported expression: {:?}", expr), None));
948            Expression::BigIntLiteral { value: 0 }
949        }
950    }
951}
952
953fn parse_binary_expression(
954    bin: &swc::BinExpr,
955    file: &str,
956    errors: &mut Vec<Diagnostic>,
957) -> Expression {
958    let left = parse_expression(&bin.left, file, errors);
959    let right = parse_expression(&bin.right, file, errors);
960
961    let op = match bin.op {
962        swc::BinaryOp::Add => BinaryOp::Add,
963        swc::BinaryOp::Sub => BinaryOp::Sub,
964        swc::BinaryOp::Mul => BinaryOp::Mul,
965        swc::BinaryOp::Div => BinaryOp::Div,
966        swc::BinaryOp::Mod => BinaryOp::Mod,
967        swc::BinaryOp::EqEqEq => BinaryOp::StrictEq,
968        swc::BinaryOp::NotEqEq => BinaryOp::StrictNe,
969        swc::BinaryOp::Lt => BinaryOp::Lt,
970        swc::BinaryOp::LtEq => BinaryOp::Le,
971        swc::BinaryOp::Gt => BinaryOp::Gt,
972        swc::BinaryOp::GtEq => BinaryOp::Ge,
973        swc::BinaryOp::LogicalAnd => BinaryOp::And,
974        swc::BinaryOp::LogicalOr => BinaryOp::Or,
975        swc::BinaryOp::BitAnd => BinaryOp::BitAnd,
976        swc::BinaryOp::BitOr => BinaryOp::BitOr,
977        swc::BinaryOp::BitXor => BinaryOp::BitXor,
978        swc::BinaryOp::EqEq => {
979            // Accept == and map to === (same as TS and Go parsers)
980            BinaryOp::StrictEq
981        }
982        swc::BinaryOp::NotEq => {
983            // Accept != and map to !== (same as TS and Go parsers)
984            BinaryOp::StrictNe
985        }
986        _ => {
987            errors.push(Diagnostic::error(format!("Unsupported binary operator: {:?}", bin.op), None));
988            BinaryOp::Add
989        }
990    };
991
992    Expression::BinaryExpr {
993        op,
994        left: Box::new(left),
995        right: Box::new(right),
996    }
997}
998
999fn parse_unary_expression(
1000    unary: &SwcUnaryExpr,
1001    file: &str,
1002    errors: &mut Vec<Diagnostic>,
1003) -> Expression {
1004    let operand = parse_expression(&unary.arg, file, errors);
1005
1006    let op = match unary.op {
1007        swc::UnaryOp::Bang => UnaryOp::Not,
1008        swc::UnaryOp::Minus => UnaryOp::Neg,
1009        swc::UnaryOp::Tilde => UnaryOp::BitNot,
1010        _ => {
1011            errors.push(Diagnostic::error(format!("Unsupported unary operator: {:?}", unary.op), None));
1012            UnaryOp::Neg
1013        }
1014    };
1015
1016    Expression::UnaryExpr {
1017        op,
1018        operand: Box::new(operand),
1019    }
1020}
1021
1022fn parse_update_expression(
1023    update: &UpdateExpr,
1024    file: &str,
1025    errors: &mut Vec<Diagnostic>,
1026) -> Expression {
1027    let operand = parse_expression(&update.arg, file, errors);
1028
1029    if update.op == UpdateOp::PlusPlus {
1030        Expression::IncrementExpr {
1031            operand: Box::new(operand),
1032            prefix: update.prefix,
1033        }
1034    } else {
1035        Expression::DecrementExpr {
1036            operand: Box::new(operand),
1037            prefix: update.prefix,
1038        }
1039    }
1040}
1041
1042fn parse_call_expression(
1043    call: &CallExpr,
1044    file: &str,
1045    errors: &mut Vec<Diagnostic>,
1046) -> Expression {
1047    let callee = match &call.callee {
1048        Callee::Expr(e) => parse_expression(e, file, errors),
1049        Callee::Super(_) => Expression::Identifier {
1050            name: "super".to_string(),
1051        },
1052        Callee::Import(_) => {
1053            errors.push(Diagnostic::error("Dynamic import is not supported", None));
1054            Expression::Identifier {
1055                name: "_error".to_string(),
1056            }
1057        }
1058    };
1059
1060    let args: Vec<Expression> = call
1061        .args
1062        .iter()
1063        .map(|arg| parse_expression(&arg.expr, file, errors))
1064        .collect();
1065
1066    Expression::CallExpr {
1067        callee: Box::new(callee),
1068        args,
1069    }
1070}
1071
1072fn parse_member_expression(
1073    member: &SwcMemberExpr,
1074    file: &str,
1075    errors: &mut Vec<Diagnostic>,
1076) -> Expression {
1077    let prop_name = match &member.prop {
1078        MemberProp::Ident(ident) => ident.sym.to_string(),
1079        MemberProp::Computed(comp) => {
1080            // Computed member access: obj[expr]
1081            let object = parse_expression(&member.obj, file, errors);
1082            let index = parse_expression(&comp.expr, file, errors);
1083            return Expression::IndexAccess {
1084                object: Box::new(object),
1085                index: Box::new(index),
1086            };
1087        }
1088        MemberProp::PrivateName(_priv_name) => {
1089            errors.push(Diagnostic::error("Private field access (#field) is not supported", None));
1090            "_private".to_string()
1091        }
1092    };
1093
1094    // this.x -> PropertyAccess
1095    if let Expr::This(_) = &*member.obj {
1096        return Expression::PropertyAccess {
1097            property: prop_name,
1098        };
1099    }
1100
1101    // General member access: obj.method
1102    let object = parse_expression(&member.obj, file, errors);
1103    Expression::MemberExpr {
1104        object: Box::new(object),
1105        property: prop_name,
1106    }
1107}
1108
1109// ---------------------------------------------------------------------------
1110// BigInt helpers
1111// ---------------------------------------------------------------------------
1112
1113/// Convert SWC BigInt to i64. SWC represents BigInt as a boxed `num_bigint::BigInt`.
1114fn bigint_to_i64(bigint_lit: &swc::BigInt) -> i64 {
1115    // SWC BigInt has a `value` field of type `Box<num_bigint::BigInt>`.
1116    // We convert via string representation to i64.
1117    use std::str::FromStr;
1118    let s = bigint_lit.value.to_string();
1119    i64::from_str(&s).unwrap_or(0)
1120}
1121
1122// ---------------------------------------------------------------------------
1123// Multi-format dispatch
1124// ---------------------------------------------------------------------------
1125
1126/// Parse a source string, automatically selecting the parser based on file extension.
1127///
1128/// Supported extensions:
1129/// - `.runar.sol` -> Solidity-like parser
1130/// - `.runar.move` -> Move-style parser
1131/// - `.runar.rs` -> Rust DSL parser
1132/// - `.runar.py` -> Python parser
1133/// - anything else (including `.runar.ts`) -> TypeScript parser (default)
1134pub fn parse_source(source: &str, file_name: Option<&str>) -> ParseResult {
1135    let name = file_name.unwrap_or("contract.ts");
1136    if name.ends_with(".runar.sol") {
1137        return super::parser_sol::parse_solidity(source, file_name);
1138    }
1139    if name.ends_with(".runar.move") {
1140        return super::parser_move::parse_move(source, file_name);
1141    }
1142    if name.ends_with(".runar.rs") {
1143        return super::parser_rustmacro::parse_rust_dsl(source, file_name);
1144    }
1145    if name.ends_with(".runar.py") {
1146        return super::parser_python::parse_python(source, file_name);
1147    }
1148    if name.ends_with(".runar.go") {
1149        return super::parser_gocontract::parse_go_contract(source, file_name);
1150    }
1151    if name.ends_with(".runar.rb") {
1152        return super::parser_ruby::parse_ruby(source, file_name);
1153    }
1154    // Default: TypeScript parser
1155    parse(source, file_name)
1156}
1157
1158// ---------------------------------------------------------------------------
1159// Tests
1160// ---------------------------------------------------------------------------
1161
1162#[cfg(test)]
1163mod tests {
1164    use super::*;
1165
1166    // -----------------------------------------------------------------------
1167    // Basic P2PKH contract
1168    // -----------------------------------------------------------------------
1169
1170    const P2PKH_SOURCE: &str = r#"
1171        import { SmartContract, assert, Sig, PubKey, Ripemd160, hash160 } from 'runar-lang';
1172
1173        class P2PKH extends SmartContract {
1174            readonly pubKeyHash: Ripemd160;
1175
1176            constructor(pubKeyHash: Ripemd160) {
1177                super(pubKeyHash);
1178            }
1179
1180            public unlock(sig: Sig, pubKey: PubKey) {
1181                assert(hash160(pubKey) === this.pubKeyHash);
1182                assert(checkSig(sig, pubKey));
1183            }
1184        }
1185    "#;
1186
1187    #[test]
1188    fn test_parse_p2pkh_contract_name() {
1189        let result = parse(P2PKH_SOURCE, Some("P2PKH.runar.ts"));
1190        assert!(result.errors.is_empty(), "unexpected errors: {:?}", result.errors);
1191        let contract = result.contract.expect("should produce a contract");
1192        assert_eq!(contract.name, "P2PKH");
1193    }
1194
1195    #[test]
1196    fn test_parse_p2pkh_parent_class() {
1197        let result = parse(P2PKH_SOURCE, Some("P2PKH.runar.ts"));
1198        let contract = result.contract.unwrap();
1199        assert_eq!(contract.parent_class, "SmartContract");
1200    }
1201
1202    #[test]
1203    fn test_parse_p2pkh_properties() {
1204        let result = parse(P2PKH_SOURCE, Some("P2PKH.runar.ts"));
1205        let contract = result.contract.unwrap();
1206        assert_eq!(contract.properties.len(), 1);
1207        assert_eq!(contract.properties[0].name, "pubKeyHash");
1208        assert!(contract.properties[0].readonly);
1209        assert!(matches!(
1210            &contract.properties[0].prop_type,
1211            TypeNode::Primitive(PrimitiveTypeName::Ripemd160)
1212        ));
1213    }
1214
1215    #[test]
1216    fn test_parse_p2pkh_constructor() {
1217        let result = parse(P2PKH_SOURCE, Some("P2PKH.runar.ts"));
1218        let contract = result.contract.unwrap();
1219        assert_eq!(contract.constructor.name, "constructor");
1220        assert_eq!(contract.constructor.params.len(), 1);
1221        assert_eq!(contract.constructor.params[0].name, "pubKeyHash");
1222    }
1223
1224    #[test]
1225    fn test_parse_p2pkh_methods() {
1226        let result = parse(P2PKH_SOURCE, Some("P2PKH.runar.ts"));
1227        let contract = result.contract.unwrap();
1228        assert_eq!(contract.methods.len(), 1);
1229        assert_eq!(contract.methods[0].name, "unlock");
1230        assert_eq!(contract.methods[0].visibility, Visibility::Public);
1231        assert_eq!(contract.methods[0].params.len(), 2);
1232        assert_eq!(contract.methods[0].params[0].name, "sig");
1233        assert_eq!(contract.methods[0].params[1].name, "pubKey");
1234    }
1235
1236    // -----------------------------------------------------------------------
1237    // Stateful Counter contract
1238    // -----------------------------------------------------------------------
1239
1240    const COUNTER_SOURCE: &str = r#"
1241        import { StatefulSmartContract, assert } from 'runar-lang';
1242
1243        class Counter extends StatefulSmartContract {
1244            count: bigint;
1245
1246            constructor(count: bigint) {
1247                super(count);
1248            }
1249
1250            public increment() {
1251                this.count++;
1252                assert(this.count > 0n);
1253            }
1254        }
1255    "#;
1256
1257    #[test]
1258    fn test_parse_counter_stateful() {
1259        let result = parse(COUNTER_SOURCE, Some("Counter.runar.ts"));
1260        assert!(result.errors.is_empty(), "unexpected errors: {:?}", result.errors);
1261        let contract = result.contract.expect("should produce a contract");
1262        assert_eq!(contract.name, "Counter");
1263        assert_eq!(contract.parent_class, "StatefulSmartContract");
1264    }
1265
1266    #[test]
1267    fn test_parse_counter_mutable_property() {
1268        let result = parse(COUNTER_SOURCE, Some("Counter.runar.ts"));
1269        let contract = result.contract.unwrap();
1270        assert_eq!(contract.properties.len(), 1);
1271        assert_eq!(contract.properties[0].name, "count");
1272        assert!(!contract.properties[0].readonly, "count should be mutable");
1273        assert!(matches!(
1274            &contract.properties[0].prop_type,
1275            TypeNode::Primitive(PrimitiveTypeName::Bigint)
1276        ));
1277    }
1278
1279    #[test]
1280    fn test_parse_counter_increment_method() {
1281        let result = parse(COUNTER_SOURCE, Some("Counter.runar.ts"));
1282        let contract = result.contract.unwrap();
1283        assert_eq!(contract.methods.len(), 1);
1284        assert_eq!(contract.methods[0].name, "increment");
1285        assert_eq!(contract.methods[0].visibility, Visibility::Public);
1286        assert!(contract.methods[0].params.is_empty());
1287        assert!(!contract.methods[0].body.is_empty(), "increment body should not be empty");
1288    }
1289
1290    // -----------------------------------------------------------------------
1291    // Multiple methods
1292    // -----------------------------------------------------------------------
1293
1294    #[test]
1295    fn test_parse_multiple_methods() {
1296        let source = r#"
1297            import { SmartContract, assert } from 'runar-lang';
1298
1299            class Multi extends SmartContract {
1300                readonly x: bigint;
1301
1302                constructor(x: bigint) {
1303                    super(x);
1304                }
1305
1306                public methodA(a: bigint) {
1307                    assert(a > 0n);
1308                }
1309
1310                public methodB(b: bigint) {
1311                    assert(b > 0n);
1312                }
1313
1314                private helper(v: bigint) {
1315                    assert(v > 0n);
1316                }
1317            }
1318        "#;
1319        let result = parse(source, Some("Multi.runar.ts"));
1320        assert!(result.errors.is_empty(), "unexpected errors: {:?}", result.errors);
1321        let contract = result.contract.unwrap();
1322        assert_eq!(contract.methods.len(), 3);
1323        assert_eq!(contract.methods[0].name, "methodA");
1324        assert_eq!(contract.methods[0].visibility, Visibility::Public);
1325        assert_eq!(contract.methods[1].name, "methodB");
1326        assert_eq!(contract.methods[1].visibility, Visibility::Public);
1327        assert_eq!(contract.methods[2].name, "helper");
1328        assert_eq!(contract.methods[2].visibility, Visibility::Private);
1329    }
1330
1331    // -----------------------------------------------------------------------
1332    // Property with initializer
1333    // -----------------------------------------------------------------------
1334
1335    #[test]
1336    fn test_parse_property_with_initializer() {
1337        let source = r#"
1338            import { StatefulSmartContract, assert } from 'runar-lang';
1339
1340            class WithInit extends StatefulSmartContract {
1341                count: bigint = 0n;
1342
1343                constructor() {
1344                    super();
1345                }
1346
1347                public check() {
1348                    assert(this.count === 0n);
1349                }
1350            }
1351        "#;
1352        let result = parse(source, Some("WithInit.runar.ts"));
1353        assert!(result.errors.is_empty(), "unexpected errors: {:?}", result.errors);
1354        let contract = result.contract.unwrap();
1355        assert_eq!(contract.properties.len(), 1);
1356        assert!(
1357            contract.properties[0].initializer.is_some(),
1358            "property should have an initializer"
1359        );
1360    }
1361
1362    // -----------------------------------------------------------------------
1363    // Error handling
1364    // -----------------------------------------------------------------------
1365
1366    #[test]
1367    fn test_parse_no_contract_class_error() {
1368        let source = r#"
1369            class NotAContract {
1370                doSomething() {}
1371            }
1372        "#;
1373        let result = parse(source, Some("bad.runar.ts"));
1374        assert!(result.contract.is_none(), "should not produce a contract");
1375        assert!(
1376            result.errors.iter().any(|e| e.message.contains("No class extending SmartContract")),
1377            "should report missing SmartContract error, got: {:?}",
1378            result.errors
1379        );
1380    }
1381
1382    #[test]
1383    fn test_parse_syntax_error() {
1384        let source = "class { this is not valid }}}}}";
1385        let result = parse(source, Some("bad.runar.ts"));
1386        assert!(
1387            !result.errors.is_empty(),
1388            "should report parse errors for invalid syntax"
1389        );
1390    }
1391
1392    #[test]
1393    fn test_parse_empty_source_error() {
1394        let source = "";
1395        let result = parse(source, Some("empty.runar.ts"));
1396        assert!(result.contract.is_none());
1397        assert!(
1398            result.errors.iter().any(|e| e.message.contains("No class extending SmartContract")),
1399            "empty source should report no contract found, got: {:?}",
1400            result.errors
1401        );
1402    }
1403
1404    // -----------------------------------------------------------------------
1405    // Expressions: binary, unary, ternary, member access
1406    // -----------------------------------------------------------------------
1407
1408    #[test]
1409    fn test_parse_binary_expressions() {
1410        let source = r#"
1411            import { SmartContract, assert } from 'runar-lang';
1412
1413            class BinOps extends SmartContract {
1414                readonly x: bigint;
1415
1416                constructor(x: bigint) {
1417                    super(x);
1418                }
1419
1420                public check(a: bigint, b: bigint) {
1421                    const sum = a + b;
1422                    const diff = a - b;
1423                    const prod = a * b;
1424                    assert(sum > 0n);
1425                }
1426            }
1427        "#;
1428        let result = parse(source, Some("BinOps.runar.ts"));
1429        assert!(result.errors.is_empty(), "unexpected errors: {:?}", result.errors);
1430        let contract = result.contract.unwrap();
1431        let body = &contract.methods[0].body;
1432        // Should have at least 3 variable declarations and 1 assert
1433        assert!(body.len() >= 4, "expected at least 4 statements, got {}", body.len());
1434    }
1435
1436    #[test]
1437    fn test_parse_ternary_expression() {
1438        let source = r#"
1439            import { SmartContract, assert } from 'runar-lang';
1440
1441            class Ternary extends SmartContract {
1442                readonly x: bigint;
1443
1444                constructor(x: bigint) {
1445                    super(x);
1446                }
1447
1448                public check(a: bigint) {
1449                    const result = a > 0n ? 1n : 0n;
1450                    assert(result === 1n);
1451                }
1452            }
1453        "#;
1454        let result = parse(source, Some("Ternary.runar.ts"));
1455        assert!(result.errors.is_empty(), "unexpected errors: {:?}", result.errors);
1456        let contract = result.contract.unwrap();
1457        // Find the ternary in the first variable declaration
1458        if let Statement::VariableDecl { init, .. } = &contract.methods[0].body[0] {
1459            assert!(
1460                matches!(init, Expression::TernaryExpr { .. }),
1461                "expected ternary expression, got {:?}",
1462                init
1463            );
1464        } else {
1465            panic!("expected VariableDecl as first statement");
1466        }
1467    }
1468
1469    // -----------------------------------------------------------------------
1470    // parse_source dispatch
1471    // -----------------------------------------------------------------------
1472
1473    #[test]
1474    fn test_parse_source_ts_dispatch() {
1475        // parse_source with .runar.ts should use the TS parser
1476        let result = parse_source(P2PKH_SOURCE, Some("P2PKH.runar.ts"));
1477        assert!(result.errors.is_empty(), "unexpected errors: {:?}", result.errors);
1478        let contract = result.contract.expect("should produce a contract via TS parser");
1479        assert_eq!(contract.name, "P2PKH");
1480    }
1481
1482    #[test]
1483    fn test_parse_source_default_dispatch() {
1484        // parse_source with no extension hint should default to TS parser
1485        let result = parse_source(P2PKH_SOURCE, None);
1486        assert!(result.errors.is_empty(), "unexpected errors: {:?}", result.errors);
1487        assert!(result.contract.is_some());
1488    }
1489
1490    // -----------------------------------------------------------------------
1491    // For loop
1492    // -----------------------------------------------------------------------
1493
1494    #[test]
1495    fn test_parse_for_loop() {
1496        let source = r#"
1497            import { SmartContract, assert } from 'runar-lang';
1498
1499            class Loop extends SmartContract {
1500                readonly x: bigint;
1501
1502                constructor(x: bigint) {
1503                    super(x);
1504                }
1505
1506                public check() {
1507                    let sum = 0n;
1508                    for (let i = 0n; i < 10n; i++) {
1509                        sum += i;
1510                    }
1511                    assert(sum === 45n);
1512                }
1513            }
1514        "#;
1515        let result = parse(source, Some("Loop.runar.ts"));
1516        assert!(result.errors.is_empty(), "unexpected errors: {:?}", result.errors);
1517        let contract = result.contract.unwrap();
1518        let body = &contract.methods[0].body;
1519        // Should contain a ForStatement
1520        let has_for = body.iter().any(|s| matches!(s, Statement::ForStatement { .. }));
1521        assert!(has_for, "should contain a ForStatement, got: {:?}", body);
1522    }
1523
1524    // -----------------------------------------------------------------------
1525    // If-else
1526    // -----------------------------------------------------------------------
1527
1528    #[test]
1529    fn test_parse_if_else() {
1530        let source = r#"
1531            import { SmartContract, assert } from 'runar-lang';
1532
1533            class IfElse extends SmartContract {
1534                readonly x: bigint;
1535
1536                constructor(x: bigint) {
1537                    super(x);
1538                }
1539
1540                public check(a: bigint) {
1541                    if (a > 0n) {
1542                        assert(a > 0n);
1543                    } else {
1544                        assert(a === 0n);
1545                    }
1546                }
1547            }
1548        "#;
1549        let result = parse(source, Some("IfElse.runar.ts"));
1550        assert!(result.errors.is_empty(), "unexpected errors: {:?}", result.errors);
1551        let contract = result.contract.unwrap();
1552        let body = &contract.methods[0].body;
1553        let has_if = body.iter().any(|s| matches!(s, Statement::IfStatement { .. }));
1554        assert!(has_if, "should contain an IfStatement");
1555    }
1556
1557    // -----------------------------------------------------------------------
1558    // Exported contract class
1559    // -----------------------------------------------------------------------
1560
1561    #[test]
1562    fn test_parse_exported_contract() {
1563        let source = r#"
1564            import { SmartContract, assert } from 'runar-lang';
1565
1566            export class Exported extends SmartContract {
1567                readonly val: bigint;
1568
1569                constructor(val: bigint) {
1570                    super(val);
1571                }
1572
1573                public check() {
1574                    assert(this.val > 0n);
1575                }
1576            }
1577        "#;
1578        let result = parse(source, Some("Exported.runar.ts"));
1579        assert!(result.errors.is_empty(), "unexpected errors: {:?}", result.errors);
1580        let contract = result.contract.expect("exported class should be found");
1581        assert_eq!(contract.name, "Exported");
1582    }
1583
1584    // -----------------------------------------------------------------------
1585    // Type parsing
1586    // -----------------------------------------------------------------------
1587
1588    #[test]
1589    fn test_parse_various_param_types() {
1590        let source = r#"
1591            import { SmartContract, assert, PubKey, Sig, ByteString, Sha256 } from 'runar-lang';
1592
1593            class TypeTest extends SmartContract {
1594                readonly h: Sha256;
1595
1596                constructor(h: Sha256) {
1597                    super(h);
1598                }
1599
1600                public check(sig: Sig, pubKey: PubKey, data: ByteString, flag: boolean) {
1601                    assert(flag);
1602                }
1603            }
1604        "#;
1605        let result = parse(source, Some("TypeTest.runar.ts"));
1606        assert!(result.errors.is_empty(), "unexpected errors: {:?}", result.errors);
1607        let contract = result.contract.unwrap();
1608        let params = &contract.methods[0].params;
1609        assert_eq!(params.len(), 4);
1610        assert!(matches!(&params[0].param_type, TypeNode::Primitive(PrimitiveTypeName::Sig)));
1611        assert!(matches!(&params[1].param_type, TypeNode::Primitive(PrimitiveTypeName::PubKey)));
1612        assert!(matches!(&params[2].param_type, TypeNode::Primitive(PrimitiveTypeName::ByteString)));
1613        assert!(matches!(&params[3].param_type, TypeNode::Primitive(PrimitiveTypeName::Boolean)));
1614    }
1615}