1use std::collections::{HashMap, HashSet};
8
9use super::ast::*;
10use super::diagnostic::Diagnostic;
11
12pub struct TypeCheckResult {
18 pub errors: Vec<Diagnostic>,
19}
20
21impl TypeCheckResult {
22 pub fn error_strings(&self) -> Vec<String> {
24 self.errors.iter().map(|d| d.format_message()).collect()
25 }
26}
27
28pub fn typecheck(contract: &ContractNode) -> TypeCheckResult {
30 let mut errors: Vec<Diagnostic> = Vec::new();
31 let mut checker = TypeChecker::new(contract, &mut errors);
32
33 checker.check_constructor();
34 for method in &contract.methods {
35 checker.check_method(method);
36 }
37
38 TypeCheckResult { errors }
39}
40
41type TType = String;
47
48const VOID: &str = "void";
49const BIGINT: &str = "bigint";
50const BOOLEAN: &str = "boolean";
51const BYTESTRING: &str = "ByteString";
52
53struct FuncSig {
58 params: Vec<&'static str>,
59 return_type: &'static str,
60}
61
62fn builtin_functions() -> HashMap<&'static str, FuncSig> {
63 let mut m = HashMap::new();
64
65 m.insert("sha256", FuncSig { params: vec!["ByteString"], return_type: "Sha256" });
66 m.insert("ripemd160", FuncSig { params: vec!["ByteString"], return_type: "Ripemd160" });
67 m.insert("hash160", FuncSig { params: vec!["ByteString"], return_type: "Ripemd160" });
68 m.insert("hash256", FuncSig { params: vec!["ByteString"], return_type: "Sha256" });
69 m.insert("checkSig", FuncSig { params: vec!["Sig", "PubKey"], return_type: "boolean" });
70 m.insert("checkMultiSig", FuncSig { params: vec!["Sig[]", "PubKey[]"], return_type: "boolean" });
71 m.insert("assert", FuncSig { params: vec!["boolean"], return_type: "void" });
72 m.insert("len", FuncSig { params: vec!["ByteString"], return_type: "bigint" });
73 m.insert("cat", FuncSig { params: vec!["ByteString", "ByteString"], return_type: "ByteString" });
74 m.insert("substr", FuncSig { params: vec!["ByteString", "bigint", "bigint"], return_type: "ByteString" });
75 m.insert("num2bin", FuncSig { params: vec!["bigint", "bigint"], return_type: "ByteString" });
76 m.insert("bin2num", FuncSig { params: vec!["ByteString"], return_type: "bigint" });
77 m.insert("checkPreimage", FuncSig { params: vec!["SigHashPreimage"], return_type: "boolean" });
78 m.insert("verifyRabinSig", FuncSig { params: vec!["ByteString", "RabinSig", "ByteString", "RabinPubKey"], return_type: "boolean" });
79 m.insert("verifyWOTS", FuncSig { params: vec!["ByteString", "ByteString", "ByteString"], return_type: "boolean" });
80 m.insert("verifySLHDSA_SHA2_128s", FuncSig { params: vec!["ByteString", "ByteString", "ByteString"], return_type: "boolean" });
81 m.insert("verifySLHDSA_SHA2_128f", FuncSig { params: vec!["ByteString", "ByteString", "ByteString"], return_type: "boolean" });
82 m.insert("verifySLHDSA_SHA2_192s", FuncSig { params: vec!["ByteString", "ByteString", "ByteString"], return_type: "boolean" });
83 m.insert("verifySLHDSA_SHA2_192f", FuncSig { params: vec!["ByteString", "ByteString", "ByteString"], return_type: "boolean" });
84 m.insert("verifySLHDSA_SHA2_256s", FuncSig { params: vec!["ByteString", "ByteString", "ByteString"], return_type: "boolean" });
85 m.insert("verifySLHDSA_SHA2_256f", FuncSig { params: vec!["ByteString", "ByteString", "ByteString"], return_type: "boolean" });
86 m.insert("ecAdd", FuncSig { params: vec!["Point", "Point"], return_type: "Point" });
87 m.insert("ecMul", FuncSig { params: vec!["Point", "bigint"], return_type: "Point" });
88 m.insert("ecMulGen", FuncSig { params: vec!["bigint"], return_type: "Point" });
89 m.insert("ecNegate", FuncSig { params: vec!["Point"], return_type: "Point" });
90 m.insert("ecOnCurve", FuncSig { params: vec!["Point"], return_type: "boolean" });
91 m.insert("ecModReduce", FuncSig { params: vec!["bigint", "bigint"], return_type: "bigint" });
92 m.insert("ecEncodeCompressed", FuncSig { params: vec!["Point"], return_type: "ByteString" });
93 m.insert("ecMakePoint", FuncSig { params: vec!["bigint", "bigint"], return_type: "Point" });
94 m.insert("ecPointX", FuncSig { params: vec!["Point"], return_type: "bigint" });
95 m.insert("ecPointY", FuncSig { params: vec!["Point"], return_type: "bigint" });
96 m.insert("abs", FuncSig { params: vec!["bigint"], return_type: "bigint" });
97 m.insert("min", FuncSig { params: vec!["bigint", "bigint"], return_type: "bigint" });
98 m.insert("max", FuncSig { params: vec!["bigint", "bigint"], return_type: "bigint" });
99 m.insert("within", FuncSig { params: vec!["bigint", "bigint", "bigint"], return_type: "boolean" });
100 m.insert("reverseBytes", FuncSig { params: vec!["ByteString"], return_type: "ByteString" });
101 m.insert("left", FuncSig { params: vec!["ByteString", "bigint"], return_type: "ByteString" });
102 m.insert("right", FuncSig { params: vec!["ByteString", "bigint"], return_type: "ByteString" });
103 m.insert("int2str", FuncSig { params: vec!["bigint", "bigint"], return_type: "ByteString" });
104 m.insert("toByteString", FuncSig { params: vec!["ByteString"], return_type: "ByteString" });
105 m.insert("exit", FuncSig { params: vec!["boolean"], return_type: "void" });
106 m.insert("pack", FuncSig { params: vec!["bigint"], return_type: "ByteString" });
107 m.insert("unpack", FuncSig { params: vec!["ByteString"], return_type: "bigint" });
108 m.insert("safediv", FuncSig { params: vec!["bigint", "bigint"], return_type: "bigint" });
109 m.insert("safemod", FuncSig { params: vec!["bigint", "bigint"], return_type: "bigint" });
110 m.insert("clamp", FuncSig { params: vec!["bigint", "bigint", "bigint"], return_type: "bigint" });
111 m.insert("sign", FuncSig { params: vec!["bigint"], return_type: "bigint" });
112 m.insert("pow", FuncSig { params: vec!["bigint", "bigint"], return_type: "bigint" });
113 m.insert("mulDiv", FuncSig { params: vec!["bigint", "bigint", "bigint"], return_type: "bigint" });
114 m.insert("percentOf", FuncSig { params: vec!["bigint", "bigint"], return_type: "bigint" });
115 m.insert("sqrt", FuncSig { params: vec!["bigint"], return_type: "bigint" });
116 m.insert("gcd", FuncSig { params: vec!["bigint", "bigint"], return_type: "bigint" });
117 m.insert("divmod", FuncSig { params: vec!["bigint", "bigint"], return_type: "bigint" });
118 m.insert("log2", FuncSig { params: vec!["bigint"], return_type: "bigint" });
119 m.insert("bool", FuncSig { params: vec!["bigint"], return_type: "boolean" });
120 m.insert("split", FuncSig { params: vec!["ByteString", "bigint"], return_type: "ByteString" });
121 m.insert("sha256Compress", FuncSig { params: vec!["ByteString", "ByteString"], return_type: "ByteString" });
122 m.insert("sha256Finalize", FuncSig { params: vec!["ByteString", "ByteString", "bigint"], return_type: "ByteString" });
123 m.insert("blake3Compress", FuncSig { params: vec!["ByteString", "ByteString"], return_type: "ByteString" });
124 m.insert("blake3Hash", FuncSig { params: vec!["ByteString"], return_type: "ByteString" });
125
126 m.insert("extractVersion", FuncSig { params: vec!["SigHashPreimage"], return_type: "bigint" });
128 m.insert("extractHashPrevouts", FuncSig { params: vec!["SigHashPreimage"], return_type: "Sha256" });
129 m.insert("extractHashSequence", FuncSig { params: vec!["SigHashPreimage"], return_type: "Sha256" });
130 m.insert("extractOutpoint", FuncSig { params: vec!["SigHashPreimage"], return_type: "ByteString" });
131 m.insert("extractInputIndex", FuncSig { params: vec!["SigHashPreimage"], return_type: "bigint" });
132 m.insert("extractScriptCode", FuncSig { params: vec!["SigHashPreimage"], return_type: "ByteString" });
133 m.insert("extractAmount", FuncSig { params: vec!["SigHashPreimage"], return_type: "bigint" });
134 m.insert("extractSequence", FuncSig { params: vec!["SigHashPreimage"], return_type: "bigint" });
135 m.insert("extractOutputHash", FuncSig { params: vec!["SigHashPreimage"], return_type: "Sha256" });
136 m.insert("extractOutputs", FuncSig { params: vec!["SigHashPreimage"], return_type: "Sha256" });
137 m.insert("extractLocktime", FuncSig { params: vec!["SigHashPreimage"], return_type: "bigint" });
138 m.insert("extractSigHashType", FuncSig { params: vec!["SigHashPreimage"], return_type: "bigint" });
139
140 m
141}
142
143fn is_bytestring_subtype(t: &str) -> bool {
149 matches!(
150 t,
151 "ByteString" | "PubKey" | "Sig" | "Sha256" | "Ripemd160" | "Addr" | "SigHashPreimage" | "Point"
152 )
153}
154
155fn is_bigint_subtype(t: &str) -> bool {
157 matches!(t, "bigint" | "RabinSig" | "RabinPubKey")
158}
159
160fn is_subtype(actual: &str, expected: &str) -> bool {
161 if actual == expected {
162 return true;
163 }
164
165 if expected == "ByteString" && is_bytestring_subtype(actual) {
167 return true;
168 }
169 if actual == "ByteString" && is_bytestring_subtype(expected) {
170 return true;
171 }
172
173 if is_bytestring_subtype(actual) && is_bytestring_subtype(expected) {
175 return true;
176 }
177
178 if expected == "bigint" && is_bigint_subtype(actual) {
180 return true;
181 }
182 if actual == "bigint" && is_bigint_subtype(expected) {
183 return true;
184 }
185
186 if is_bigint_subtype(actual) && is_bigint_subtype(expected) {
188 return true;
189 }
190
191 if expected.ends_with("[]") && actual.ends_with("[]") {
193 return is_subtype(
194 &actual[..actual.len() - 2],
195 &expected[..expected.len() - 2],
196 );
197 }
198
199 false
200}
201
202fn is_bigint_family(t: &str) -> bool {
203 is_bigint_subtype(t)
204}
205
206struct TypeEnv {
211 scopes: Vec<HashMap<String, TType>>,
212}
213
214impl TypeEnv {
215 fn new() -> Self {
216 TypeEnv {
217 scopes: vec![HashMap::new()],
218 }
219 }
220
221 fn push_scope(&mut self) {
222 self.scopes.push(HashMap::new());
223 }
224
225 fn pop_scope(&mut self) {
226 self.scopes.pop();
227 }
228
229 fn define(&mut self, name: &str, t: TType) {
230 if let Some(top) = self.scopes.last_mut() {
231 top.insert(name.to_string(), t);
232 }
233 }
234
235 fn lookup(&self, name: &str) -> Option<&TType> {
236 for scope in self.scopes.iter().rev() {
237 if let Some(t) = scope.get(name) {
238 return Some(t);
239 }
240 }
241 None
242 }
243}
244
245fn is_affine_type(t: &str) -> bool {
251 matches!(t, "Sig" | "SigHashPreimage")
252}
253
254fn consuming_param_indices(func_name: &str) -> Option<&'static [usize]> {
257 match func_name {
258 "checkSig" => Some(&[0]),
259 "checkMultiSig" => Some(&[0]),
260 "checkPreimage" => Some(&[0]),
261 _ => None,
262 }
263}
264
265struct TypeChecker<'a> {
266 contract: &'a ContractNode,
267 errors: &'a mut Vec<Diagnostic>,
268 prop_types: HashMap<String, TType>,
269 method_sigs: HashMap<String, (Vec<TType>, TType)>,
270 builtins: HashMap<&'static str, FuncSig>,
271 consumed_values: HashSet<String>,
272 current_method_loc: Option<SourceLocation>,
273}
274
275impl<'a> TypeChecker<'a> {
276 fn new(contract: &'a ContractNode, errors: &'a mut Vec<Diagnostic>) -> Self {
277 let mut prop_types = HashMap::new();
278 for prop in &contract.properties {
279 prop_types.insert(prop.name.clone(), type_node_to_ttype(&prop.prop_type));
280 }
281
282 if contract.parent_class == "StatefulSmartContract" {
284 prop_types.insert("txPreimage".to_string(), "SigHashPreimage".to_string());
285 }
286
287 let mut method_sigs = HashMap::new();
288 for method in &contract.methods {
289 let params: Vec<TType> = method
290 .params
291 .iter()
292 .map(|p| type_node_to_ttype(&p.param_type))
293 .collect();
294 let return_type = if method.visibility == Visibility::Public {
295 VOID.to_string()
296 } else {
297 infer_method_return_type(method)
298 };
299 method_sigs.insert(method.name.clone(), (params, return_type));
300 }
301
302 TypeChecker {
303 contract,
304 errors,
305 prop_types,
306 method_sigs,
307 builtins: builtin_functions(),
308 consumed_values: HashSet::new(),
309 current_method_loc: None,
310 }
311 }
312
313 fn add_error(&mut self, msg: impl Into<String>) {
315 self.errors.push(Diagnostic::error(msg, self.current_method_loc.clone()));
316 }
317
318 fn check_constructor(&mut self) {
319 let ctor = &self.contract.constructor;
320 let mut env = TypeEnv::new();
321
322 self.current_method_loc = Some(ctor.source_location.clone());
324
325 self.consumed_values.clear();
327
328 for param in &ctor.params {
330 env.define(¶m.name, type_node_to_ttype(¶m.param_type));
331 }
332
333 for prop in &self.contract.properties {
335 env.define(&prop.name, type_node_to_ttype(&prop.prop_type));
336 }
337
338 self.check_statements(&ctor.body, &mut env);
339 }
340
341 fn check_method(&mut self, method: &MethodNode) {
342 let mut env = TypeEnv::new();
343
344 self.current_method_loc = Some(method.source_location.clone());
346
347 self.consumed_values.clear();
349
350 for param in &method.params {
352 env.define(¶m.name, type_node_to_ttype(¶m.param_type));
353 }
354
355 self.check_statements(&method.body, &mut env);
356 }
357
358 fn check_statements(&mut self, stmts: &[Statement], env: &mut TypeEnv) {
359 for stmt in stmts {
360 self.check_statement(stmt, env);
361 }
362 }
363
364 fn check_statement(&mut self, stmt: &Statement, env: &mut TypeEnv) {
365 match stmt {
366 Statement::VariableDecl {
367 name,
368 var_type,
369 init,
370 ..
371 } => {
372 let init_type = self.infer_expr_type(init, env);
373 if let Some(declared) = var_type {
374 let declared_type = type_node_to_ttype(declared);
375 if !is_subtype(&init_type, &declared_type) {
376 self.add_error(format!(
377 "Type '{}' is not assignable to type '{}'",
378 init_type, declared_type
379 ));
380 }
381 env.define(name, declared_type);
382 } else {
383 env.define(name, init_type);
384 }
385 }
386
387 Statement::Assignment { target, value, .. } => {
388 let target_type = self.infer_expr_type(target, env);
389 let value_type = self.infer_expr_type(value, env);
390 if !is_subtype(&value_type, &target_type) {
391 self.add_error(format!(
392 "Type '{}' is not assignable to type '{}'",
393 value_type, target_type
394 ));
395 }
396 }
397
398 Statement::IfStatement {
399 condition,
400 then_branch,
401 else_branch,
402 ..
403 } => {
404 let cond_type = self.infer_expr_type(condition, env);
405 if cond_type != BOOLEAN {
406 self.add_error(format!(
407 "If condition must be boolean, got '{}'",
408 cond_type
409 ));
410 }
411 env.push_scope();
412 self.check_statements(then_branch, env);
413 env.pop_scope();
414 if let Some(else_stmts) = else_branch {
415 env.push_scope();
416 self.check_statements(else_stmts, env);
417 env.pop_scope();
418 }
419 }
420
421 Statement::ForStatement {
422 init,
423 condition,
424 body,
425 ..
426 } => {
427 env.push_scope();
428 self.check_statement(init, env);
429 let cond_type = self.infer_expr_type(condition, env);
430 if cond_type != BOOLEAN {
431 self.add_error(format!(
432 "For loop condition must be boolean, got '{}'",
433 cond_type
434 ));
435 }
436 self.check_statements(body, env);
437 env.pop_scope();
438 }
439
440 Statement::ExpressionStatement { expression, .. } => {
441 self.infer_expr_type(expression, env);
442 }
443
444 Statement::ReturnStatement { value, .. } => {
445 if let Some(v) = value {
446 self.infer_expr_type(v, env);
447 }
448 }
449 }
450 }
451
452 fn infer_expr_type(&mut self, expr: &Expression, env: &mut TypeEnv) -> TType {
454 match expr {
455 Expression::BigIntLiteral { .. } => BIGINT.to_string(),
456
457 Expression::BoolLiteral { .. } => BOOLEAN.to_string(),
458
459 Expression::ByteStringLiteral { .. } => BYTESTRING.to_string(),
460
461 Expression::Identifier { name } => {
462 if name == "this" {
463 return "<this>".to_string();
464 }
465 if name == "super" {
466 return "<super>".to_string();
467 }
468 if name == "true" || name == "false" {
469 return BOOLEAN.to_string();
470 }
471
472 if let Some(t) = env.lookup(name) {
473 return t.clone();
474 }
475
476 if self.builtins.contains_key(name.as_str()) {
478 return "<builtin>".to_string();
479 }
480
481 "<unknown>".to_string()
482 }
483
484 Expression::PropertyAccess { property } => {
485 if let Some(t) = self.prop_types.get(property) {
486 return t.clone();
487 }
488
489 self.add_error(format!(
490 "Property '{}' does not exist on the contract",
491 property
492 ));
493 "<unknown>".to_string()
494 }
495
496 Expression::MemberExpr { object, property } => {
497 let obj_type = self.infer_expr_type(object, env);
498
499 if obj_type == "<this>" {
500 if let Some(t) = self.prop_types.get(property) {
502 return t.clone();
503 }
504 if self.method_sigs.contains_key(property) {
506 return "<method>".to_string();
507 }
508 if property == "getStateScript" {
510 return "<method>".to_string();
511 }
512
513 self.add_error(format!(
514 "Property or method '{}' does not exist on the contract",
515 property
516 ));
517 return "<unknown>".to_string();
518 }
519
520 if let Expression::Identifier { name } = object.as_ref() {
522 if name == "SigHash" {
523 return BIGINT.to_string();
524 }
525 }
526
527 "<unknown>".to_string()
528 }
529
530 Expression::BinaryExpr { op, left, right } => {
531 self.check_binary_expr(op, left, right, env)
532 }
533
534 Expression::UnaryExpr { op, operand } => self.check_unary_expr(op, operand, env),
535
536 Expression::CallExpr { callee, args } => self.check_call_expr(callee, args, env),
537
538 Expression::TernaryExpr {
539 condition,
540 consequent,
541 alternate,
542 } => {
543 let cond_type = self.infer_expr_type(condition, env);
544 if cond_type != BOOLEAN {
545 self.add_error(format!(
546 "Ternary condition must be boolean, got '{}'",
547 cond_type
548 ));
549 }
550 let cons_type = self.infer_expr_type(consequent, env);
551 let alt_type = self.infer_expr_type(alternate, env);
552
553 if cons_type != alt_type {
554 if is_subtype(&alt_type, &cons_type) {
555 return cons_type;
556 }
557 if is_subtype(&cons_type, &alt_type) {
558 return alt_type;
559 }
560 self.add_error(format!(
561 "Ternary branches have incompatible types: '{}' and '{}'",
562 cons_type, alt_type
563 ));
564 }
565 cons_type
566 }
567
568 Expression::IndexAccess { object, index } => {
569 let obj_type = self.infer_expr_type(object, env);
570 let index_type = self.infer_expr_type(index, env);
571
572 if !is_bigint_family(&index_type) {
573 self.add_error(format!(
574 "Array index must be bigint, got '{}'",
575 index_type
576 ));
577 }
578
579 if obj_type.ends_with("[]") {
580 return obj_type[..obj_type.len() - 2].to_string();
581 }
582
583 "<unknown>".to_string()
584 }
585
586 Expression::IncrementExpr { operand, .. }
587 | Expression::DecrementExpr { operand, .. } => {
588 let operand_type = self.infer_expr_type(operand, env);
589 if !is_bigint_family(&operand_type) {
590 let op_str = if matches!(expr, Expression::IncrementExpr { .. }) {
591 "++"
592 } else {
593 "--"
594 };
595 self.add_error(format!(
596 "{} operator requires bigint, got '{}'",
597 op_str, operand_type
598 ));
599 }
600 BIGINT.to_string()
601 }
602
603 Expression::ArrayLiteral { elements } => {
604 if let Some(first) = elements.first() {
606 let elem_type = self.infer_expr_type(first, env);
607 format!("{}[]", elem_type)
608 } else {
609 "<unknown>[]".to_string()
610 }
611 }
612 }
613 }
614
615 fn check_binary_expr(
616 &mut self,
617 op: &BinaryOp,
618 left: &Expression,
619 right: &Expression,
620 env: &mut TypeEnv,
621 ) -> TType {
622 let left_type = self.infer_expr_type(left, env);
623 let right_type = self.infer_expr_type(right, env);
624
625 match op {
626 BinaryOp::Add => {
628 if is_bytestring_subtype(&left_type) && is_bytestring_subtype(&right_type) {
629 return BYTESTRING.to_string();
630 }
631 if !is_bigint_family(&left_type) {
632 self.add_error(format!(
633 "Left operand of '{}' must be bigint or ByteString, got '{}'",
634 op.as_str(),
635 left_type
636 ));
637 }
638 if !is_bigint_family(&right_type) {
639 self.add_error(format!(
640 "Right operand of '{}' must be bigint or ByteString, got '{}'",
641 op.as_str(),
642 right_type
643 ));
644 }
645 BIGINT.to_string()
646 }
647
648 BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => {
650 if !is_bigint_family(&left_type) {
651 self.add_error(format!(
652 "Left operand of '{}' must be bigint, got '{}'",
653 op.as_str(),
654 left_type
655 ));
656 }
657 if !is_bigint_family(&right_type) {
658 self.add_error(format!(
659 "Right operand of '{}' must be bigint, got '{}'",
660 op.as_str(),
661 right_type
662 ));
663 }
664 BIGINT.to_string()
665 }
666
667 BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge => {
669 if !is_bigint_family(&left_type) {
670 self.add_error(format!(
671 "Left operand of '{}' must be bigint, got '{}'",
672 op.as_str(),
673 left_type
674 ));
675 }
676 if !is_bigint_family(&right_type) {
677 self.add_error(format!(
678 "Right operand of '{}' must be bigint, got '{}'",
679 op.as_str(),
680 right_type
681 ));
682 }
683 BOOLEAN.to_string()
684 }
685
686 BinaryOp::StrictEq | BinaryOp::StrictNe => {
688 if !is_subtype(&left_type, &right_type)
689 && !is_subtype(&right_type, &left_type)
690 {
691 if left_type != "<unknown>" && right_type != "<unknown>" {
692 self.add_error(format!(
693 "Cannot compare '{}' and '{}' with '{}'",
694 left_type,
695 right_type,
696 op.as_str()
697 ));
698 }
699 }
700 BOOLEAN.to_string()
701 }
702
703 BinaryOp::And | BinaryOp::Or => {
705 if left_type != BOOLEAN && left_type != "<unknown>" {
706 self.add_error(format!(
707 "Left operand of '{}' must be boolean, got '{}'",
708 op.as_str(),
709 left_type
710 ));
711 }
712 if right_type != BOOLEAN && right_type != "<unknown>" {
713 self.add_error(format!(
714 "Right operand of '{}' must be boolean, got '{}'",
715 op.as_str(),
716 right_type
717 ));
718 }
719 BOOLEAN.to_string()
720 }
721
722 BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor => {
724 if is_bytestring_subtype(&left_type) && is_bytestring_subtype(&right_type) {
725 return BYTESTRING.to_string();
726 }
727 if !is_bigint_family(&left_type) {
728 self.add_error(format!(
729 "Left operand of '{}' must be bigint or ByteString, got '{}'",
730 op.as_str(),
731 left_type
732 ));
733 }
734 if !is_bigint_family(&right_type) {
735 self.add_error(format!(
736 "Right operand of '{}' must be bigint or ByteString, got '{}'",
737 op.as_str(),
738 right_type
739 ));
740 }
741 BIGINT.to_string()
742 }
743
744 BinaryOp::Shl | BinaryOp::Shr => {
746 if !is_bigint_family(&left_type) {
747 self.add_error(format!(
748 "Left operand of '{}' must be bigint, got '{}'",
749 op.as_str(),
750 left_type
751 ));
752 }
753 if !is_bigint_family(&right_type) {
754 self.add_error(format!(
755 "Right operand of '{}' must be bigint, got '{}'",
756 op.as_str(),
757 right_type
758 ));
759 }
760 BIGINT.to_string()
761 }
762 }
763 }
764
765 fn check_unary_expr(
766 &mut self,
767 op: &UnaryOp,
768 operand: &Expression,
769 env: &mut TypeEnv,
770 ) -> TType {
771 let operand_type = self.infer_expr_type(operand, env);
772
773 match op {
774 UnaryOp::Not => {
775 if operand_type != BOOLEAN && operand_type != "<unknown>" {
776 self.add_error(format!(
777 "Operand of '!' must be boolean, got '{}'",
778 operand_type
779 ));
780 }
781 BOOLEAN.to_string()
782 }
783 UnaryOp::Neg => {
784 if !is_bigint_family(&operand_type) {
785 self.add_error(format!(
786 "Operand of unary '-' must be bigint, got '{}'",
787 operand_type
788 ));
789 }
790 BIGINT.to_string()
791 }
792 UnaryOp::BitNot => {
793 if is_bytestring_subtype(&operand_type) {
794 return BYTESTRING.to_string();
795 }
796 if !is_bigint_family(&operand_type) {
797 self.add_error(format!(
798 "Operand of '~' must be bigint or ByteString, got '{}'",
799 operand_type
800 ));
801 }
802 BIGINT.to_string()
803 }
804 }
805 }
806
807 fn check_call_expr(
808 &mut self,
809 callee: &Expression,
810 args: &[Expression],
811 env: &mut TypeEnv,
812 ) -> TType {
813 if let Expression::Identifier { name } = callee {
815 if name == "super" {
816 for arg in args {
817 self.infer_expr_type(arg, env);
818 }
819 return VOID.to_string();
820 }
821 }
822
823 if let Expression::Identifier { name } = callee {
825 if let Some(sig) = self.builtins.get(name.as_str()) {
826 let sig_params = sig.params.clone();
827 let sig_return_type = sig.return_type;
828 return self.check_call_args(name, &sig_params, sig_return_type, args, env);
829 }
830
831 if let Some((params, return_type)) = self.method_sigs.get(name).cloned() {
833 let param_strs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
834 return self.check_call_args(name, ¶m_strs, &return_type, args, env);
835 }
836
837 if env.lookup(name).is_some() {
839 for arg in args {
840 self.infer_expr_type(arg, env);
841 }
842 return "<unknown>".to_string();
843 }
844
845 self.add_error(format!(
846 "unknown function '{}' — only Rúnar built-in functions and contract methods are allowed",
847 name
848 ));
849 for arg in args {
850 self.infer_expr_type(arg, env);
851 }
852 return "<unknown>".to_string();
853 }
854
855 if let Expression::PropertyAccess { property } = callee {
857 if property == "getStateScript" {
858 if !args.is_empty() {
859 self.add_error("getStateScript() takes no arguments");
860 }
861 return BYTESTRING.to_string();
862 }
863
864 if property == "addOutput" || property == "addRawOutput" {
865 for arg in args {
866 self.infer_expr_type(arg, env);
867 }
868 return VOID.to_string();
869 }
870
871 if let Some((params, return_type)) = self.method_sigs.get(property).cloned() {
873 let param_strs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
874 return self.check_call_args(property, ¶m_strs, &return_type, args, env);
875 }
876
877 self.add_error(format!(
878 "unknown method 'self.{}' — only Rúnar built-in methods and contract methods are allowed",
879 property
880 ));
881 for arg in args {
882 self.infer_expr_type(arg, env);
883 }
884 return "<unknown>".to_string();
885 }
886
887 if let Expression::MemberExpr { object, property } = callee {
889 if property == "clone" {
891 return self.infer_expr_type(object, env);
892 }
893
894 let obj_type = self.infer_expr_type(object, env);
895
896 if obj_type == "<this>"
897 || matches!(object.as_ref(), Expression::Identifier { name } if name == "this")
898 {
899 if property == "getStateScript" {
900 return BYTESTRING.to_string();
901 }
902
903 if let Some((params, return_type)) = self.method_sigs.get(property).cloned() {
904 let param_strs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
905 return self.check_call_args(
906 property,
907 ¶m_strs,
908 &return_type,
909 args,
910 env,
911 );
912 }
913 }
914
915 let obj_name = match object.as_ref() {
917 Expression::Identifier { name } => name.clone(),
918 _ => "<expr>".to_string(),
919 };
920 self.add_error(format!(
921 "unknown function '{}.{}' — only Rúnar built-in functions and contract methods are allowed",
922 obj_name, property
923 ));
924 for arg in args {
925 self.infer_expr_type(arg, env);
926 }
927 return "<unknown>".to_string();
928 }
929
930 self.add_error(
932 "unsupported function call expression — only Rúnar built-in functions and contract methods are allowed"
933 );
934 self.infer_expr_type(callee, env);
935 for arg in args {
936 self.infer_expr_type(arg, env);
937 }
938 "<unknown>".to_string()
939 }
940
941 fn check_call_args(
942 &mut self,
943 func_name: &str,
944 sig_params: &[&str],
945 return_type: &str,
946 args: &[Expression],
947 env: &mut TypeEnv,
948 ) -> TType {
949 if func_name == "assert" {
951 if args.is_empty() || args.len() > 2 {
952 self.add_error(format!(
953 "assert() expects 1 or 2 arguments, got {}",
954 args.len()
955 ));
956 }
957 if !args.is_empty() {
958 let cond_type = self.infer_expr_type(&args[0], env);
959 if cond_type != BOOLEAN && cond_type != "<unknown>" {
960 self.add_error(format!(
961 "assert() condition must be boolean, got '{}'",
962 cond_type
963 ));
964 }
965 }
966 if args.len() >= 2 {
967 self.infer_expr_type(&args[1], env);
968 }
969 return return_type.to_string();
970 }
971
972 if func_name == "checkSig" {
976 if args.len() != 2 {
977 self.add_error(format!(
978 "checkSig() expects 2 argument(s), got {}",
979 args.len()
980 ));
981 }
982 if args.len() >= 1 {
983 let arg0_type = self.infer_expr_type(&args[0], env);
984 if arg0_type != "Sig" && arg0_type != "<unknown>" {
986 self.add_error(format!(
987 "Argument 1 of checkSig(): expected 'Sig', got '{}'",
988 arg0_type
989 ));
990 }
991 }
992 if args.len() >= 2 {
993 let arg1_type = self.infer_expr_type(&args[1], env);
994 if arg1_type != "PubKey" && arg1_type != "<unknown>" {
996 self.add_error(format!(
997 "Argument 2 of checkSig(): expected 'PubKey', got '{}'",
998 arg1_type
999 ));
1000 }
1001 }
1002 for i in 2..args.len() {
1004 self.infer_expr_type(&args[i], env);
1005 }
1006 self.check_affine_consumption(func_name, args, env);
1007 return return_type.to_string();
1008 }
1009
1010 if func_name == "checkMultiSig" {
1012 if args.len() != 2 {
1013 self.add_error(format!(
1014 "checkMultiSig() expects 2 arguments, got {}",
1015 args.len()
1016 ));
1017 }
1018 for arg in args {
1019 self.infer_expr_type(arg, env);
1020 }
1021 self.check_affine_consumption(func_name, args, env);
1022 return return_type.to_string();
1023 }
1024
1025 if args.len() != sig_params.len() {
1027 self.add_error(format!(
1028 "{}() expects {} argument(s), got {}",
1029 func_name,
1030 sig_params.len(),
1031 args.len()
1032 ));
1033 }
1034
1035 let count = args.len().min(sig_params.len());
1036 for i in 0..count {
1037 let arg_type = self.infer_expr_type(&args[i], env);
1038 let expected = sig_params[i];
1039
1040 if !is_subtype(&arg_type, expected) && arg_type != "<unknown>" {
1041 self.add_error(format!(
1042 "Argument {} of {}(): expected '{}', got '{}'",
1043 i + 1,
1044 func_name,
1045 expected,
1046 arg_type
1047 ));
1048 }
1049 }
1050
1051 for i in count..args.len() {
1053 self.infer_expr_type(&args[i], env);
1054 }
1055
1056 self.check_affine_consumption(func_name, args, env);
1058
1059 return_type.to_string()
1060 }
1061
1062 fn check_affine_consumption(
1065 &mut self,
1066 func_name: &str,
1067 args: &[Expression],
1068 env: &mut TypeEnv,
1069 ) {
1070 let indices = match consuming_param_indices(func_name) {
1071 Some(indices) => indices,
1072 None => return,
1073 };
1074
1075 for ¶m_index in indices {
1076 if param_index >= args.len() {
1077 continue;
1078 }
1079
1080 let arg = &args[param_index];
1081 if let Expression::Identifier { name } = arg {
1082 if let Some(arg_type) = env.lookup(name) {
1083 let arg_type = arg_type.clone();
1084 if !is_affine_type(&arg_type) {
1085 continue;
1086 }
1087
1088 if self.consumed_values.contains(name) {
1089 self.add_error(format!(
1090 "affine value '{}' has already been consumed",
1091 name
1092 ));
1093 } else {
1094 self.consumed_values.insert(name.clone());
1095 }
1096 }
1097 }
1098 }
1099 }
1100}
1101
1102fn infer_method_return_type(method: &MethodNode) -> TType {
1114 let return_types = collect_return_types(&method.body);
1115
1116 if return_types.is_empty() {
1117 return VOID.to_string();
1118 }
1119
1120 let first = &return_types[0];
1121 let all_same = return_types.iter().all(|t| t == first);
1122 if all_same {
1123 return first.clone();
1124 }
1125
1126 if return_types.iter().all(|t| is_bigint_subtype(t)) {
1128 return BIGINT.to_string();
1129 }
1130
1131 if return_types.iter().all(|t| is_bytestring_subtype(t)) {
1133 return BYTESTRING.to_string();
1134 }
1135
1136 if return_types.iter().all(|t| t == BOOLEAN) {
1138 return BOOLEAN.to_string();
1139 }
1140
1141 first.clone()
1143}
1144
1145fn collect_return_types(stmts: &[Statement]) -> Vec<TType> {
1147 let mut types = Vec::new();
1148 for stmt in stmts {
1149 match stmt {
1150 Statement::ReturnStatement { value, .. } => {
1151 if let Some(v) = value {
1152 types.push(infer_expr_type_static(v));
1153 }
1154 }
1155 Statement::IfStatement {
1156 then_branch,
1157 else_branch,
1158 ..
1159 } => {
1160 types.extend(collect_return_types(then_branch));
1161 if let Some(else_stmts) = else_branch {
1162 types.extend(collect_return_types(else_stmts));
1163 }
1164 }
1165 Statement::ForStatement { body, .. } => {
1166 types.extend(collect_return_types(body));
1167 }
1168 _ => {}
1169 }
1170 }
1171 types
1172}
1173
1174fn infer_expr_type_static(expr: &Expression) -> TType {
1178 match expr {
1179 Expression::BigIntLiteral { .. } => BIGINT.to_string(),
1180 Expression::BoolLiteral { .. } => BOOLEAN.to_string(),
1181 Expression::ByteStringLiteral { .. } => BYTESTRING.to_string(),
1182 Expression::Identifier { name } => {
1183 if name == "true" || name == "false" {
1184 BOOLEAN.to_string()
1185 } else {
1186 "<unknown>".to_string()
1187 }
1188 }
1189 Expression::BinaryExpr { op, .. } => match op {
1190 BinaryOp::Add
1191 | BinaryOp::Sub
1192 | BinaryOp::Mul
1193 | BinaryOp::Div
1194 | BinaryOp::Mod
1195 | BinaryOp::BitAnd
1196 | BinaryOp::BitOr
1197 | BinaryOp::BitXor
1198 | BinaryOp::Shl
1199 | BinaryOp::Shr => BIGINT.to_string(),
1200 _ => BOOLEAN.to_string(),
1201 },
1202 Expression::UnaryExpr { op, .. } => match op {
1203 UnaryOp::Not => BOOLEAN.to_string(),
1204 _ => BIGINT.to_string(),
1205 },
1206 Expression::CallExpr { callee, .. } => {
1207 let builtins = builtin_functions();
1208 if let Expression::Identifier { name } = callee.as_ref() {
1209 if let Some(sig) = builtins.get(name.as_str()) {
1210 return sig.return_type.to_string();
1211 }
1212 }
1213 if let Expression::PropertyAccess { property } = callee.as_ref() {
1214 if let Some(sig) = builtins.get(property.as_str()) {
1215 return sig.return_type.to_string();
1216 }
1217 }
1218 "<unknown>".to_string()
1219 }
1220 Expression::TernaryExpr {
1221 consequent,
1222 alternate,
1223 ..
1224 } => {
1225 let cons_type = infer_expr_type_static(consequent);
1226 if cons_type != "<unknown>" {
1227 cons_type
1228 } else {
1229 infer_expr_type_static(alternate)
1230 }
1231 }
1232 Expression::IncrementExpr { .. } | Expression::DecrementExpr { .. } => {
1233 BIGINT.to_string()
1234 }
1235 _ => "<unknown>".to_string(),
1236 }
1237}
1238
1239fn type_node_to_ttype(node: &TypeNode) -> TType {
1240 match node {
1241 TypeNode::Primitive(name) => name.as_str().to_string(),
1242 TypeNode::FixedArray { element, .. } => {
1243 format!("{}[]", type_node_to_ttype(element))
1244 }
1245 TypeNode::Custom(name) => name.clone(),
1246 }
1247}
1248
1249#[cfg(test)]
1254mod tests {
1255 use super::*;
1256 use crate::frontend::parser::parse_source;
1257 use crate::frontend::validator;
1258
1259 fn parse_and_validate(source: &str) -> ContractNode {
1261 let result = parse_source(source, Some("test.runar.ts"));
1262 assert!(
1263 result.errors.is_empty(),
1264 "parse errors: {:?}",
1265 result.errors
1266 );
1267 let contract = result.contract.expect("expected a contract from parse");
1268 let validation = validator::validate(&contract);
1269 assert!(
1270 validation.errors.is_empty(),
1271 "validation errors: {:?}",
1272 validation.errors
1273 );
1274 contract
1275 }
1276
1277 #[test]
1278 fn test_valid_p2pkh_passes_typecheck() {
1279 let source = r#"
1280import { SmartContract, Addr, PubKey, Sig } from 'runar-lang';
1281
1282class P2PKH extends SmartContract {
1283 readonly pubKeyHash: Addr;
1284
1285 constructor(pubKeyHash: Addr) {
1286 super(pubKeyHash);
1287 this.pubKeyHash = pubKeyHash;
1288 }
1289
1290 public unlock(sig: Sig, pubKey: PubKey) {
1291 assert(hash160(pubKey) === this.pubKeyHash);
1292 assert(checkSig(sig, pubKey));
1293 }
1294}
1295"#;
1296 let contract = parse_and_validate(source);
1297 let result = typecheck(&contract);
1298 assert!(
1299 result.errors.is_empty(),
1300 "expected no typecheck errors, got: {:?}",
1301 result.errors
1302 );
1303 }
1304
1305 #[test]
1306 fn test_unknown_function_call_produces_error() {
1307 let source = r#"
1308import { SmartContract } from 'runar-lang';
1309
1310class Bad extends SmartContract {
1311 readonly x: bigint;
1312
1313 constructor(x: bigint) {
1314 super(x);
1315 this.x = x;
1316 }
1317
1318 public check(v: bigint) {
1319 const y = Math.floor(v);
1320 assert(y === this.x);
1321 }
1322}
1323"#;
1324 let contract = parse_and_validate(source);
1325 let result = typecheck(&contract);
1326 assert!(
1327 !result.errors.is_empty(),
1328 "expected typecheck errors for unknown function Math.floor"
1329 );
1330 let has_unknown_error = result
1331 .errors
1332 .iter()
1333 .any(|e| e.message.to_lowercase().contains("unknown"));
1334 assert!(
1335 has_unknown_error,
1336 "expected error about unknown function, got: {:?}",
1337 result.errors
1338 );
1339 }
1340
1341 #[test]
1342 fn test_builtin_with_wrong_arg_count_produces_error() {
1343 let source = r#"
1344import { SmartContract, PubKey, Sig } from 'runar-lang';
1345
1346class Bad extends SmartContract {
1347 readonly x: bigint;
1348
1349 constructor(x: bigint) {
1350 super(x);
1351 this.x = x;
1352 }
1353
1354 public check(v: bigint) {
1355 assert(min(v));
1356 }
1357}
1358"#;
1359 let contract = parse_and_validate(source);
1360 let result = typecheck(&contract);
1361 assert!(
1362 !result.errors.is_empty(),
1363 "expected typecheck errors for wrong arg count"
1364 );
1365 let has_arg_count_error = result
1366 .errors
1367 .iter()
1368 .any(|e| e.message.contains("expects") && e.message.contains("argument"));
1369 assert!(
1370 has_arg_count_error,
1371 "expected error about wrong argument count, got: {:?}",
1372 result.errors
1373 );
1374 }
1375
1376 #[test]
1377 fn test_arithmetic_on_boolean_produces_error() {
1378 let source = r#"
1379import { SmartContract } from 'runar-lang';
1380
1381class Bad extends SmartContract {
1382 readonly x: bigint;
1383
1384 constructor(x: bigint) {
1385 super(x);
1386 this.x = x;
1387 }
1388
1389 public check(v: bigint, flag: boolean) {
1390 const sum = v + flag;
1391 assert(sum === this.x);
1392 }
1393}
1394"#;
1395 let contract = parse_and_validate(source);
1396 let result = typecheck(&contract);
1397 assert!(
1398 !result.errors.is_empty(),
1399 "expected typecheck errors for arithmetic on boolean"
1400 );
1401 let has_type_error = result
1402 .errors
1403 .iter()
1404 .any(|e| e.message.contains("bigint") || e.message.contains("boolean"));
1405 assert!(
1406 has_type_error,
1407 "expected type mismatch error, got: {:?}",
1408 result.errors
1409 );
1410 }
1411
1412 #[test]
1413 fn test_valid_stateful_contract_passes_typecheck() {
1414 let source = r#"
1415import { StatefulSmartContract } from 'runar-lang';
1416
1417class Counter extends StatefulSmartContract {
1418 count: bigint;
1419
1420 constructor(count: bigint) {
1421 super(count);
1422 this.count = count;
1423 }
1424
1425 public increment() {
1426 this.count++;
1427 }
1428}
1429"#;
1430 let contract = parse_and_validate(source);
1431 let result = typecheck(&contract);
1432 assert!(
1433 result.errors.is_empty(),
1434 "expected no typecheck errors for stateful contract, got: {:?}",
1435 result.errors
1436 );
1437 }
1438
1439 #[test]
1444 fn test_valid_arithmetic() {
1445 let source = r#"
1446import { SmartContract, assert } from 'runar-lang';
1447
1448class Arithmetic extends SmartContract {
1449 readonly target: bigint;
1450
1451 constructor(target: bigint) {
1452 super(target);
1453 this.target = target;
1454 }
1455
1456 public verify(a: bigint, b: bigint) {
1457 const sum: bigint = a + b;
1458 const diff: bigint = a - b;
1459 const prod: bigint = a * b;
1460 const quot: bigint = a / b;
1461 const result: bigint = sum + diff + prod + quot;
1462 assert(result === this.target);
1463 }
1464}
1465"#;
1466 let contract = parse_and_validate(source);
1467 let result = typecheck(&contract);
1468 assert!(
1469 result.errors.is_empty(),
1470 "expected no typecheck errors for Arithmetic, got: {:?}",
1471 result.errors
1472 );
1473 }
1474
1475 #[test]
1480 fn test_valid_boolean_logic() {
1481 let source = r#"
1482import { SmartContract, assert } from 'runar-lang';
1483
1484class BoolLogic extends SmartContract {
1485 readonly threshold: bigint;
1486
1487 constructor(threshold: bigint) {
1488 super(threshold);
1489 this.threshold = threshold;
1490 }
1491
1492 public verify(a: bigint, b: bigint, flag: boolean) {
1493 const aAbove: boolean = a > this.threshold;
1494 const bAbove: boolean = b > this.threshold;
1495 const bothAbove: boolean = aAbove && bAbove;
1496 const eitherAbove: boolean = aAbove || bAbove;
1497 const notFlag: boolean = !flag;
1498 assert(bothAbove || (eitherAbove && notFlag));
1499 }
1500}
1501"#;
1502 let contract = parse_and_validate(source);
1503 let result = typecheck(&contract);
1504 assert!(
1505 result.errors.is_empty(),
1506 "expected no typecheck errors for BoolLogic, got: {:?}",
1507 result.errors
1508 );
1509 }
1510
1511 #[test]
1516 fn test_subtype_compatibility() {
1517 let source = r#"
1518import { SmartContract, assert, PubKey, sha256 } from 'runar-lang';
1519
1520class HashCheck extends SmartContract {
1521 readonly expectedHash: Sha256;
1522
1523 constructor(expectedHash: Sha256) {
1524 super(expectedHash);
1525 this.expectedHash = expectedHash;
1526 }
1527
1528 public verify(pubKey: PubKey) {
1529 assert(sha256(pubKey) === this.expectedHash);
1530 }
1531}
1532"#;
1533 let result = crate::frontend::parser::parse_source(source, Some("test.runar.ts"));
1536 if result.errors.is_empty() {
1537 if let Some(contract) = result.contract {
1538 let tc_result = typecheck(&contract);
1539 let pubkey_arg_error = tc_result.errors.iter().any(|e| {
1542 e.message.contains("argument") && e.message.contains("PubKey")
1543 });
1544 assert!(
1545 !pubkey_arg_error,
1546 "PubKey should be assignable to ByteString, but got error about PubKey argument: {:?}",
1547 tc_result.errors
1548 );
1549 }
1550 }
1551 }
1553
1554 #[test]
1560 fn test_unknown_function_console_log() {
1561 let source = r#"
1562import { SmartContract } from 'runar-lang';
1563
1564class Bad extends SmartContract {
1565 constructor() {
1566 super();
1567 }
1568
1569 public check(val: bigint) {
1570 console.log(val);
1571 assert(val > 0n);
1572 }
1573}
1574"#;
1575 let result = crate::frontend::parser::parse_source(source, Some("test.runar.ts"));
1576 if result.errors.is_empty() {
1577 if let Some(contract) = result.contract {
1578 let tc_result = typecheck(&contract);
1579 assert!(
1580 !tc_result.errors.is_empty(),
1581 "expected typecheck errors for unknown function console.log"
1582 );
1583 let has_unknown_error = tc_result.errors.iter().any(|e| {
1584 e.message.to_lowercase().contains("unknown")
1585 || e.message.to_lowercase().contains("console")
1586 });
1587 assert!(
1588 has_unknown_error,
1589 "expected error about unknown function console.log, got: {:?}",
1590 tc_result.errors
1591 );
1592 }
1593 }
1594 }
1597}