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