1use std::collections::{HashMap, HashSet};
7
8use super::ast::*;
9
10pub struct ValidationResult {
16 pub errors: Vec<String>,
17 pub warnings: Vec<String>,
18}
19
20pub 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
33fn 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
55fn 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 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 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 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
116fn 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 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 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 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 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 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
190fn 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 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 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 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 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 if let Statement::ExpressionStatement { expression, .. } = last {
244 if is_assert_call(expression) {
245 return true;
246 }
247 }
248
249 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
275fn 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 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 if let Statement::VariableDecl { init: init_expr, .. } = init.as_ref() {
324 validate_expression(init_expr, errors);
325 }
326
327 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, Expression::UnaryExpr { op, operand } if *op == UnaryOp::Neg => {
349 is_compile_time_constant(operand)
350 }
351 _ => false,
352 }
353}
354
355fn 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 Expression::Identifier { .. }
395 | Expression::BigIntLiteral { .. }
396 | Expression::BoolLiteral { .. }
397 | Expression::ByteStringLiteral { .. }
398 | Expression::PropertyAccess { .. } => {}
399 }
400}
401
402fn check_no_recursion(contract: &ContractNode, errors: &mut Vec<String>) {
407 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 {
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 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 if let Expression::PropertyAccess { property } = callee.as_ref() {
494 calls.insert(property.clone());
495 }
496 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 _ => {}
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
571fn warn_manual_preimage_usage(method: &MethodNode, warnings: &mut Vec<String>) {
577 walk_expressions_in_body(&method.body, &mut |expr| {
578 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 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#[cfg(test)]
689mod tests {
690 use super::*;
691 use crate::frontend::parser::parse_source;
692
693 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 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 #[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 #[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 #[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}