Skip to main content

runar_compiler_rust/frontend/
validator.rs

1//! Pass 2: Validate
2//!
3//! Validates the Rúnar AST against the language subset constraints.
4//! This pass does NOT modify the AST; it only reports errors and warnings.
5
6use std::collections::{HashMap, HashSet};
7
8use super::ast::*;
9
10// ---------------------------------------------------------------------------
11// Public API
12// ---------------------------------------------------------------------------
13
14/// Result of validation.
15pub struct ValidationResult {
16    pub errors: Vec<String>,
17    pub warnings: Vec<String>,
18}
19
20/// Validate a parsed Rúnar AST against the language subset constraints.
21pub fn validate(contract: &ContractNode) -> ValidationResult {
22    let mut errors = Vec::new();
23    let mut warnings = Vec::new();
24
25    validate_properties(contract, &mut errors, &mut warnings);
26    validate_constructor(contract, &mut errors);
27    validate_methods(contract, &mut errors, &mut warnings);
28    check_no_recursion(contract, &mut errors);
29
30    ValidationResult { errors, warnings }
31}
32
33// ---------------------------------------------------------------------------
34// Valid primitive types for properties
35// ---------------------------------------------------------------------------
36
37fn is_valid_property_primitive(name: &PrimitiveTypeName) -> bool {
38    match name {
39        PrimitiveTypeName::Bigint
40        | PrimitiveTypeName::Boolean
41        | PrimitiveTypeName::ByteString
42        | PrimitiveTypeName::PubKey
43        | PrimitiveTypeName::Sig
44        | PrimitiveTypeName::Sha256
45        | PrimitiveTypeName::Ripemd160
46        | PrimitiveTypeName::Addr
47        | PrimitiveTypeName::SigHashPreimage
48        | PrimitiveTypeName::RabinSig
49        | PrimitiveTypeName::RabinPubKey
50        | PrimitiveTypeName::Point => true,
51        PrimitiveTypeName::Void => false,
52    }
53}
54
55// ---------------------------------------------------------------------------
56// Property validation
57// ---------------------------------------------------------------------------
58
59fn validate_properties(contract: &ContractNode, errors: &mut Vec<String>, warnings: &mut Vec<String>) {
60    for prop in &contract.properties {
61        validate_property_type(&prop.prop_type, errors);
62
63        // V27: Error when any property is named `txPreimage`
64        if prop.name == "txPreimage" {
65            errors.push(
66                "'txPreimage' is a reserved implicit parameter name and must not be used as a property name".to_string()
67            );
68        }
69    }
70
71    // SmartContract requires all properties to be readonly
72    if contract.parent_class == "SmartContract" {
73        for prop in &contract.properties {
74            if !prop.readonly {
75                errors.push(format!(
76                    "property '{}' in SmartContract must be readonly. Use StatefulSmartContract for mutable state.",
77                    prop.name
78                ));
79            }
80        }
81    }
82
83    // V26: Warn when a StatefulSmartContract has no mutable (non-readonly) properties
84    if contract.parent_class == "StatefulSmartContract" {
85        let has_mutable = contract.properties.iter().any(|p| !p.readonly);
86        if !has_mutable {
87            warnings.push(
88                "StatefulSmartContract has no mutable properties; consider using SmartContract instead".to_string()
89            );
90        }
91    }
92}
93
94fn validate_property_type(type_node: &TypeNode, errors: &mut Vec<String>) {
95    match type_node {
96        TypeNode::Primitive(name) => {
97            if !is_valid_property_primitive(name) {
98                errors.push(format!("Property type '{}' is not valid", name.as_str()));
99            }
100        }
101        TypeNode::FixedArray { element, length } => {
102            if *length == 0 {
103                errors.push("FixedArray length must be a positive integer".to_string());
104            }
105            validate_property_type(element, errors);
106        }
107        TypeNode::Custom(name) => {
108            errors.push(format!(
109                "Unsupported type '{}' in property declaration. Use one of: bigint, boolean, ByteString, PubKey, Sig, Sha256, Ripemd160, Addr, SigHashPreimage, RabinSig, RabinPubKey, or FixedArray<T, N>",
110                name
111            ));
112        }
113    }
114}
115
116// ---------------------------------------------------------------------------
117// Constructor validation
118// ---------------------------------------------------------------------------
119
120fn validate_constructor(contract: &ContractNode, errors: &mut Vec<String>) {
121    let ctor = &contract.constructor;
122    let prop_names: HashSet<String> = contract.properties.iter().map(|p| p.name.clone()).collect();
123
124    // Check that constructor has a super() call as first statement
125    if ctor.body.is_empty() {
126        errors.push("Constructor must call super() as its first statement".to_string());
127        return;
128    }
129
130    if !is_super_call(&ctor.body[0]) {
131        errors.push("Constructor must call super() as its first statement".to_string());
132    }
133
134    // Check that all properties are assigned in constructor
135    let mut assigned_props = HashSet::new();
136    for stmt in &ctor.body {
137        if let Statement::Assignment { target, .. } = stmt {
138            if let Expression::PropertyAccess { property } = target {
139                assigned_props.insert(property.clone());
140            }
141        }
142    }
143
144    // Properties with initializers don't need constructor assignments
145    let props_with_init: HashSet<String> = contract
146        .properties
147        .iter()
148        .filter(|p| p.initializer.is_some())
149        .map(|p| p.name.clone())
150        .collect();
151
152    for prop_name in &prop_names {
153        if !assigned_props.contains(prop_name) && !props_with_init.contains(prop_name) {
154            errors.push(format!(
155                "Property '{}' must be assigned in the constructor",
156                prop_name
157            ));
158        }
159    }
160
161    // Validate constructor params have type annotations
162    for param in &ctor.params {
163        if let TypeNode::Custom(ref name) = param.param_type {
164            if name == "unknown" {
165                errors.push(format!(
166                    "Constructor parameter '{}' must have a type annotation",
167                    param.name
168                ));
169            }
170        }
171    }
172
173    // Validate statements in constructor body
174    for stmt in &ctor.body {
175        validate_statement(stmt, errors);
176    }
177}
178
179fn is_super_call(stmt: &Statement) -> bool {
180    if let Statement::ExpressionStatement { expression, .. } = stmt {
181        if let Expression::CallExpr { callee, .. } = expression {
182            if let Expression::Identifier { name } = callee.as_ref() {
183                return name == "super";
184            }
185        }
186    }
187    false
188}
189
190// ---------------------------------------------------------------------------
191// Method validation
192// ---------------------------------------------------------------------------
193
194fn validate_methods(contract: &ContractNode, errors: &mut Vec<String>, warnings: &mut Vec<String>) {
195    for method in &contract.methods {
196        validate_method(method, contract, errors);
197
198        // V24, V25: Warn when StatefulSmartContract public method calls checkPreimage or getStateScript explicitly
199        if contract.parent_class == "StatefulSmartContract" && method.visibility == Visibility::Public {
200            warn_manual_preimage_usage(method, warnings);
201        }
202    }
203}
204
205fn validate_method(method: &MethodNode, contract: &ContractNode, errors: &mut Vec<String>) {
206    // All params must have type annotations
207    for param in &method.params {
208        if let TypeNode::Custom(ref name) = param.param_type {
209            if name == "unknown" {
210                errors.push(format!(
211                    "Parameter '{}' in method '{}' must have a type annotation",
212                    param.name, method.name
213                ));
214            }
215        }
216    }
217
218    // Public methods must end with an assert() call (unless StatefulSmartContract,
219    // where the compiler auto-injects the final assert)
220    if method.visibility == Visibility::Public && contract.parent_class == "SmartContract" {
221        if !ends_with_assert(&method.body) {
222            errors.push(format!(
223                "Public method '{}' must end with an assert() call",
224                method.name
225            ));
226        }
227    }
228
229    // Validate all statements in method body
230    for stmt in &method.body {
231        validate_statement(stmt, errors);
232    }
233}
234
235fn ends_with_assert(body: &[Statement]) -> bool {
236    if body.is_empty() {
237        return false;
238    }
239
240    let last = &body[body.len() - 1];
241
242    // Direct assert() call as expression statement
243    if let Statement::ExpressionStatement { expression, .. } = last {
244        if is_assert_call(expression) {
245            return true;
246        }
247    }
248
249    // If/else where both branches end with assert
250    if let Statement::IfStatement {
251        then_branch,
252        else_branch,
253        ..
254    } = last
255    {
256        let then_ends = ends_with_assert(then_branch);
257        let else_ends = else_branch
258            .as_ref()
259            .map_or(false, |e| ends_with_assert(e));
260        return then_ends && else_ends;
261    }
262
263    false
264}
265
266fn is_assert_call(expr: &Expression) -> bool {
267    if let Expression::CallExpr { callee, .. } = expr {
268        if let Expression::Identifier { name } = callee.as_ref() {
269            return name == "assert";
270        }
271    }
272    false
273}
274
275// ---------------------------------------------------------------------------
276// Statement validation
277// ---------------------------------------------------------------------------
278
279fn validate_statement(stmt: &Statement, errors: &mut Vec<String>) {
280    match stmt {
281        Statement::VariableDecl { init, .. } => {
282            validate_expression(init, errors);
283        }
284        Statement::Assignment { target, value, .. } => {
285            validate_expression(target, errors);
286            validate_expression(value, errors);
287        }
288        Statement::IfStatement {
289            condition,
290            then_branch,
291            else_branch,
292            ..
293        } => {
294            validate_expression(condition, errors);
295            for s in then_branch {
296                validate_statement(s, errors);
297            }
298            if let Some(else_stmts) = else_branch {
299                for s in else_stmts {
300                    validate_statement(s, errors);
301                }
302            }
303        }
304        Statement::ForStatement {
305            condition,
306            init,
307            body,
308            ..
309        } => {
310            validate_expression(condition, errors);
311
312            // Check that the loop bound is a compile-time constant
313            if let Expression::BinaryExpr { right, .. } = condition {
314                if !is_compile_time_constant(right) {
315                    errors.push(
316                        "For loop bound must be a compile-time constant (literal or const variable)"
317                            .to_string(),
318                    );
319                }
320            }
321
322            // Validate init
323            if let Statement::VariableDecl { init: init_expr, .. } = init.as_ref() {
324                validate_expression(init_expr, errors);
325            }
326
327            // Validate body
328            for s in body {
329                validate_statement(s, errors);
330            }
331        }
332        Statement::ExpressionStatement { expression, .. } => {
333            validate_expression(expression, errors);
334        }
335        Statement::ReturnStatement { value, .. } => {
336            if let Some(v) = value {
337                validate_expression(v, errors);
338            }
339        }
340    }
341}
342
343fn is_compile_time_constant(expr: &Expression) -> bool {
344    match expr {
345        Expression::BigIntLiteral { .. } => true,
346        Expression::BoolLiteral { .. } => true,
347        Expression::Identifier { .. } => true, // Could be a const
348        Expression::UnaryExpr { op, operand } if *op == UnaryOp::Neg => {
349            is_compile_time_constant(operand)
350        }
351        _ => false,
352    }
353}
354
355// ---------------------------------------------------------------------------
356// Expression validation
357// ---------------------------------------------------------------------------
358
359fn validate_expression(expr: &Expression, errors: &mut Vec<String>) {
360    match expr {
361        Expression::BinaryExpr { left, right, .. } => {
362            validate_expression(left, errors);
363            validate_expression(right, errors);
364        }
365        Expression::UnaryExpr { operand, .. } => {
366            validate_expression(operand, errors);
367        }
368        Expression::CallExpr { callee, args, .. } => {
369            validate_expression(callee, errors);
370            for arg in args {
371                validate_expression(arg, errors);
372            }
373        }
374        Expression::MemberExpr { object, .. } => {
375            validate_expression(object, errors);
376        }
377        Expression::TernaryExpr {
378            condition,
379            consequent,
380            alternate,
381        } => {
382            validate_expression(condition, errors);
383            validate_expression(consequent, errors);
384            validate_expression(alternate, errors);
385        }
386        Expression::IndexAccess { object, index } => {
387            validate_expression(object, errors);
388            validate_expression(index, errors);
389        }
390        Expression::IncrementExpr { operand, .. } | Expression::DecrementExpr { operand, .. } => {
391            validate_expression(operand, errors);
392        }
393        // Leaf nodes -- nothing to validate
394        Expression::Identifier { .. }
395        | Expression::BigIntLiteral { .. }
396        | Expression::BoolLiteral { .. }
397        | Expression::ByteStringLiteral { .. }
398        | Expression::PropertyAccess { .. } => {}
399    }
400}
401
402// ---------------------------------------------------------------------------
403// Recursion detection
404// ---------------------------------------------------------------------------
405
406fn check_no_recursion(contract: &ContractNode, errors: &mut Vec<String>) {
407    // Build call graph: method name -> set of methods it calls
408    let mut call_graph: HashMap<String, HashSet<String>> = HashMap::new();
409    let mut method_names: HashSet<String> = HashSet::new();
410
411    for method in &contract.methods {
412        method_names.insert(method.name.clone());
413        let mut calls = HashSet::new();
414        collect_method_calls(&method.body, &mut calls);
415        call_graph.insert(method.name.clone(), calls);
416    }
417
418    // Also add constructor
419    {
420        let mut calls = HashSet::new();
421        collect_method_calls(&contract.constructor.body, &mut calls);
422        call_graph.insert("constructor".to_string(), calls);
423    }
424
425    // Check for cycles using DFS
426    for method in &contract.methods {
427        let mut visited = HashSet::new();
428        let mut stack = HashSet::new();
429
430        if has_cycle(
431            &method.name,
432            &call_graph,
433            &method_names,
434            &mut visited,
435            &mut stack,
436        ) {
437            errors.push(format!(
438                "Recursion detected: method '{}' calls itself directly or indirectly. Recursion is not allowed in Rúnar contracts.",
439                method.name
440            ));
441        }
442    }
443}
444
445fn collect_method_calls(stmts: &[Statement], calls: &mut HashSet<String>) {
446    for stmt in stmts {
447        collect_method_calls_in_statement(stmt, calls);
448    }
449}
450
451fn collect_method_calls_in_statement(stmt: &Statement, calls: &mut HashSet<String>) {
452    match stmt {
453        Statement::ExpressionStatement { expression, .. } => {
454            collect_method_calls_in_expr(expression, calls);
455        }
456        Statement::VariableDecl { init, .. } => {
457            collect_method_calls_in_expr(init, calls);
458        }
459        Statement::Assignment { target, value, .. } => {
460            collect_method_calls_in_expr(target, calls);
461            collect_method_calls_in_expr(value, calls);
462        }
463        Statement::IfStatement {
464            condition,
465            then_branch,
466            else_branch,
467            ..
468        } => {
469            collect_method_calls_in_expr(condition, calls);
470            collect_method_calls(then_branch, calls);
471            if let Some(else_stmts) = else_branch {
472                collect_method_calls(else_stmts, calls);
473            }
474        }
475        Statement::ForStatement {
476            condition, body, ..
477        } => {
478            collect_method_calls_in_expr(condition, calls);
479            collect_method_calls(body, calls);
480        }
481        Statement::ReturnStatement { value, .. } => {
482            if let Some(v) = value {
483                collect_method_calls_in_expr(v, calls);
484            }
485        }
486    }
487}
488
489fn collect_method_calls_in_expr(expr: &Expression, calls: &mut HashSet<String>) {
490    match expr {
491        Expression::CallExpr { callee, args, .. } => {
492            // Check if callee is `this.methodName` (PropertyAccess variant)
493            if let Expression::PropertyAccess { property } = callee.as_ref() {
494                calls.insert(property.clone());
495            }
496            // Also check `this.method` via MemberExpr
497            if let Expression::MemberExpr { object, property } = callee.as_ref() {
498                if let Expression::Identifier { name } = object.as_ref() {
499                    if name == "this" {
500                        calls.insert(property.clone());
501                    }
502                }
503            }
504            collect_method_calls_in_expr(callee, calls);
505            for arg in args {
506                collect_method_calls_in_expr(arg, calls);
507            }
508        }
509        Expression::BinaryExpr { left, right, .. } => {
510            collect_method_calls_in_expr(left, calls);
511            collect_method_calls_in_expr(right, calls);
512        }
513        Expression::UnaryExpr { operand, .. } => {
514            collect_method_calls_in_expr(operand, calls);
515        }
516        Expression::MemberExpr { object, .. } => {
517            collect_method_calls_in_expr(object, calls);
518        }
519        Expression::TernaryExpr {
520            condition,
521            consequent,
522            alternate,
523        } => {
524            collect_method_calls_in_expr(condition, calls);
525            collect_method_calls_in_expr(consequent, calls);
526            collect_method_calls_in_expr(alternate, calls);
527        }
528        Expression::IndexAccess { object, index } => {
529            collect_method_calls_in_expr(object, calls);
530            collect_method_calls_in_expr(index, calls);
531        }
532        Expression::IncrementExpr { operand, .. } | Expression::DecrementExpr { operand, .. } => {
533            collect_method_calls_in_expr(operand, calls);
534        }
535        // Leaf nodes
536        _ => {}
537    }
538}
539
540fn has_cycle(
541    method_name: &str,
542    call_graph: &HashMap<String, HashSet<String>>,
543    method_names: &HashSet<String>,
544    visited: &mut HashSet<String>,
545    stack: &mut HashSet<String>,
546) -> bool {
547    if stack.contains(method_name) {
548        return true;
549    }
550    if visited.contains(method_name) {
551        return false;
552    }
553
554    visited.insert(method_name.to_string());
555    stack.insert(method_name.to_string());
556
557    if let Some(calls) = call_graph.get(method_name) {
558        for callee in calls {
559            if method_names.contains(callee) {
560                if has_cycle(callee, call_graph, method_names, visited, stack) {
561                    return true;
562                }
563            }
564        }
565    }
566
567    stack.remove(method_name);
568    false
569}
570
571// ---------------------------------------------------------------------------
572// V24, V25: Warn about manual use of checkPreimage / getStateScript in
573// StatefulSmartContract public methods.
574// ---------------------------------------------------------------------------
575
576fn warn_manual_preimage_usage(method: &MethodNode, warnings: &mut Vec<String>) {
577    walk_expressions_in_body(&method.body, &mut |expr| {
578        // V24: Detect manual checkPreimage(...)
579        if let Expression::CallExpr { callee, .. } = expr {
580            if let Expression::Identifier { name } = callee.as_ref() {
581                if name == "checkPreimage" {
582                    warnings.push(format!(
583                        "StatefulSmartContract auto-injects checkPreimage(); calling it manually in '{}' will cause a duplicate verification",
584                        method.name
585                    ));
586                }
587            }
588            // V25: Detect manual this.getStateScript()
589            if let Expression::PropertyAccess { property } = callee.as_ref() {
590                if property == "getStateScript" {
591                    warnings.push(format!(
592                        "StatefulSmartContract auto-injects state continuation; calling getStateScript() manually in '{}' is redundant",
593                        method.name
594                    ));
595                }
596            }
597        }
598    });
599}
600
601fn walk_expressions_in_body(stmts: &[Statement], visitor: &mut impl FnMut(&Expression)) {
602    for stmt in stmts {
603        walk_expressions_in_statement(stmt, visitor);
604    }
605}
606
607fn walk_expressions_in_statement(stmt: &Statement, visitor: &mut impl FnMut(&Expression)) {
608    match stmt {
609        Statement::ExpressionStatement { expression, .. } => {
610            walk_expression(expression, visitor);
611        }
612        Statement::VariableDecl { init, .. } => {
613            walk_expression(init, visitor);
614        }
615        Statement::Assignment { target, value, .. } => {
616            walk_expression(target, visitor);
617            walk_expression(value, visitor);
618        }
619        Statement::IfStatement {
620            condition,
621            then_branch,
622            else_branch,
623            ..
624        } => {
625            walk_expression(condition, visitor);
626            walk_expressions_in_body(then_branch, visitor);
627            if let Some(else_stmts) = else_branch {
628                walk_expressions_in_body(else_stmts, visitor);
629            }
630        }
631        Statement::ForStatement {
632            condition, body, ..
633        } => {
634            walk_expression(condition, visitor);
635            walk_expressions_in_body(body, visitor);
636        }
637        Statement::ReturnStatement { value, .. } => {
638            if let Some(v) = value {
639                walk_expression(v, visitor);
640            }
641        }
642    }
643}
644
645fn walk_expression(expr: &Expression, visitor: &mut impl FnMut(&Expression)) {
646    visitor(expr);
647    match expr {
648        Expression::CallExpr { callee, args } => {
649            walk_expression(callee, visitor);
650            for arg in args {
651                walk_expression(arg, visitor);
652            }
653        }
654        Expression::BinaryExpr { left, right, .. } => {
655            walk_expression(left, visitor);
656            walk_expression(right, visitor);
657        }
658        Expression::UnaryExpr { operand, .. } => {
659            walk_expression(operand, visitor);
660        }
661        Expression::TernaryExpr {
662            condition,
663            consequent,
664            alternate,
665        } => {
666            walk_expression(condition, visitor);
667            walk_expression(consequent, visitor);
668            walk_expression(alternate, visitor);
669        }
670        Expression::MemberExpr { object, .. } => {
671            walk_expression(object, visitor);
672        }
673        Expression::IndexAccess { object, index } => {
674            walk_expression(object, visitor);
675            walk_expression(index, visitor);
676        }
677        Expression::IncrementExpr { operand, .. } | Expression::DecrementExpr { operand, .. } => {
678            walk_expression(operand, visitor);
679        }
680        _ => {}
681    }
682}
683
684// ---------------------------------------------------------------------------
685// Tests
686// ---------------------------------------------------------------------------
687
688#[cfg(test)]
689mod tests {
690    use super::*;
691    use crate::frontend::parser::parse_source;
692
693    /// Helper: parse a TypeScript source string and return the ContractNode.
694    fn parse_contract(source: &str) -> ContractNode {
695        let result = parse_source(source, Some("test.runar.ts"));
696        assert!(
697            result.errors.is_empty(),
698            "parse errors: {:?}",
699            result.errors
700        );
701        result.contract.expect("expected a contract from parse")
702    }
703
704    #[test]
705    fn test_valid_p2pkh_passes_validation() {
706        let source = r#"
707import { SmartContract, Addr, PubKey, Sig } from 'runar-lang';
708
709class P2PKH extends SmartContract {
710    readonly pubKeyHash: Addr;
711
712    constructor(pubKeyHash: Addr) {
713        super(pubKeyHash);
714        this.pubKeyHash = pubKeyHash;
715    }
716
717    public unlock(sig: Sig, pubKey: PubKey) {
718        assert(hash160(pubKey) === this.pubKeyHash);
719        assert(checkSig(sig, pubKey));
720    }
721}
722"#;
723        let contract = parse_contract(source);
724        let result = validate(&contract);
725        assert!(
726            result.errors.is_empty(),
727            "expected no validation errors, got: {:?}",
728            result.errors
729        );
730    }
731
732    #[test]
733    fn test_missing_super_in_constructor_produces_error() {
734        let source = r#"
735import { SmartContract } from 'runar-lang';
736
737class Bad extends SmartContract {
738    readonly x: bigint;
739
740    constructor(x: bigint) {
741        this.x = x;
742    }
743
744    public check(v: bigint) {
745        assert(v === this.x);
746    }
747}
748"#;
749        let contract = parse_contract(source);
750        let result = validate(&contract);
751        assert!(
752            !result.errors.is_empty(),
753            "expected validation errors for missing super()"
754        );
755        let has_super_error = result
756            .errors
757            .iter()
758            .any(|e| e.to_lowercase().contains("super"));
759        assert!(
760            has_super_error,
761            "expected error about super(), got: {:?}",
762            result.errors
763        );
764    }
765
766    #[test]
767    fn test_public_method_not_ending_with_assert_produces_error() {
768        let source = r#"
769import { SmartContract } from 'runar-lang';
770
771class NoAssert extends SmartContract {
772    readonly x: bigint;
773
774    constructor(x: bigint) {
775        super(x);
776        this.x = x;
777    }
778
779    public check(v: bigint) {
780        const sum = v + this.x;
781    }
782}
783"#;
784        let contract = parse_contract(source);
785        let result = validate(&contract);
786        assert!(
787            !result.errors.is_empty(),
788            "expected validation errors for missing assert at end of public method"
789        );
790        let has_assert_error = result
791            .errors
792            .iter()
793            .any(|e| e.to_lowercase().contains("assert"));
794        assert!(
795            has_assert_error,
796            "expected error about missing assert(), got: {:?}",
797            result.errors
798        );
799    }
800
801    #[test]
802    fn test_direct_recursion_produces_error() {
803        let source = r#"
804import { SmartContract } from 'runar-lang';
805
806class Recursive extends SmartContract {
807    readonly x: bigint;
808
809    constructor(x: bigint) {
810        super(x);
811        this.x = x;
812    }
813
814    public check(v: bigint) {
815        this.check(v);
816        assert(v === this.x);
817    }
818}
819"#;
820        let contract = parse_contract(source);
821        let result = validate(&contract);
822        assert!(
823            !result.errors.is_empty(),
824            "expected validation errors for recursion"
825        );
826        let has_recursion_error = result
827            .errors
828            .iter()
829            .any(|e| e.to_lowercase().contains("recursion") || e.to_lowercase().contains("recursive"));
830        assert!(
831            has_recursion_error,
832            "expected error about recursion, got: {:?}",
833            result.errors
834        );
835    }
836
837    #[test]
838    fn test_stateful_contract_passes_validation() {
839        // StatefulSmartContract public methods don't need to end with assert
840        // because the compiler auto-injects the final assert.
841        let source = r#"
842import { StatefulSmartContract } from 'runar-lang';
843
844class Counter extends StatefulSmartContract {
845    count: bigint;
846
847    constructor(count: bigint) {
848        super(count);
849        this.count = count;
850    }
851
852    public increment() {
853        this.count++;
854    }
855}
856"#;
857        let contract = parse_contract(source);
858        let result = validate(&contract);
859        assert!(
860            result.errors.is_empty(),
861            "expected no validation errors for stateful contract, got: {:?}",
862            result.errors
863        );
864    }
865
866    /// Alias mirroring the name used in Go/Python test suites.
867    #[test]
868    fn test_constructor_missing_super_fails() {
869        let source = r#"
870import { SmartContract, Addr, PubKey, Sig } from 'runar-lang';
871
872class P2PKH extends SmartContract {
873    readonly pubKeyHash: Addr;
874
875    constructor(pubKeyHash: Addr) {
876        this.pubKeyHash = pubKeyHash;
877    }
878
879    public unlock(sig: Sig, pubKey: PubKey) {
880        assert(hash160(pubKey) === this.pubKeyHash);
881        assert(checkSig(sig, pubKey));
882    }
883}
884"#;
885        let contract = parse_contract(source);
886        let result = validate(&contract);
887        assert!(
888            !result.errors.is_empty(),
889            "expected validation errors for missing super()"
890        );
891        assert!(
892            result.errors.iter().any(|e| e.to_lowercase().contains("super")),
893            "expected error about super(), got: {:?}",
894            result.errors
895        );
896    }
897
898    /// Alias mirroring the name used in Go/Python test suites.
899    #[test]
900    fn test_public_method_missing_final_assert_fails() {
901        let source = r#"
902import { SmartContract } from 'runar-lang';
903
904class P2PKH extends SmartContract {
905    readonly x: bigint;
906
907    constructor(x: bigint) {
908        super(x);
909        this.x = x;
910    }
911
912    public unlock(val: bigint): void { const y = val + 1n; }
913}
914"#;
915        let contract = parse_contract(source);
916        let result = validate(&contract);
917        assert!(
918            !result.errors.is_empty(),
919            "expected validation errors for missing assert at end of public method"
920        );
921        assert!(
922            result.errors.iter().any(|e| e.to_lowercase().contains("assert")),
923            "expected error about missing assert(), got: {:?}",
924            result.errors
925        );
926    }
927
928    /// Alias mirroring the name used in Go/Python test suites.
929    #[test]
930    fn test_direct_recursion_fails() {
931        let source = r#"
932import { SmartContract } from 'runar-lang';
933
934class Rec extends SmartContract {
935    readonly x: bigint;
936
937    constructor(x: bigint) {
938        super(x);
939        this.x = x;
940    }
941
942    public recurse(v: bigint) {
943        this.recurse(v);
944        assert(v === this.x);
945    }
946}
947"#;
948        let contract = parse_contract(source);
949        let result = validate(&contract);
950        assert!(
951            !result.errors.is_empty(),
952            "expected validation errors for direct recursion"
953        );
954        assert!(
955            result
956                .errors
957                .iter()
958                .any(|e| e.to_lowercase().contains("recursion") || e.to_lowercase().contains("recursive")),
959            "expected error about recursion, got: {:?}",
960            result.errors
961        );
962    }
963}