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