1use std::collections::{HashMap, HashSet};
7
8use super::ast::*;
9use super::diagnostic::Diagnostic;
10
11pub struct ValidationResult {
17 pub errors: Vec<Diagnostic>,
18 pub warnings: Vec<Diagnostic>,
19}
20
21impl ValidationResult {
22 pub fn error_strings(&self) -> Vec<String> {
24 self.errors.iter().map(|d| d.format_message()).collect()
25 }
26 pub fn warning_strings(&self) -> Vec<String> {
28 self.warnings.iter().map(|d| d.format_message()).collect()
29 }
30}
31
32pub 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
45fn 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
67fn 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 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 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 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
128fn 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 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 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 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 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 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
202fn 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 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 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 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 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 if let Statement::ExpressionStatement { expression, .. } = last {
256 if is_assert_call(expression) {
257 return true;
258 }
259 }
260
261 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
287fn 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 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 if let Statement::VariableDecl { init: init_expr, .. } = init.as_ref() {
336 validate_expression(init_expr, errors);
337 }
338
339 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, Expression::UnaryExpr { op, operand } if *op == UnaryOp::Neg => {
361 is_compile_time_constant(operand)
362 }
363 _ => false,
364 }
365}
366
367fn 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 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 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
440fn check_no_recursion(contract: &ContractNode, errors: &mut Vec<Diagnostic>) {
445 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 {
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 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 if let Expression::PropertyAccess { property } = callee.as_ref() {
532 calls.insert(property.clone());
533 }
534 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 _ => {}
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
609fn 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 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 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#[cfg(test)]
728mod tests {
729 use super::*;
730 use crate::frontend::parser::parse_source;
731
732 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 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 #[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 #[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 #[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}