Skip to main content

runar_compiler_rust/frontend/
typecheck.rs

1//! Pass 3: Type-Check
2//!
3//! Type-checks the Rúnar AST. Builds type environments from properties,
4//! constructor parameters, and method parameters, then verifies all
5//! expressions have consistent types.
6
7use std::collections::{HashMap, HashSet};
8
9use super::ast::*;
10use super::diagnostic::Diagnostic;
11
12// ---------------------------------------------------------------------------
13// Public API
14// ---------------------------------------------------------------------------
15
16/// Result of type checking.
17pub struct TypeCheckResult {
18    pub errors: Vec<Diagnostic>,
19}
20
21impl TypeCheckResult {
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}
27
28/// Type-check a Rúnar AST. Returns any type errors found.
29pub 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
41// ---------------------------------------------------------------------------
42// Type representation
43// ---------------------------------------------------------------------------
44
45/// Internal type representation (simplified string-based).
46type TType = String;
47
48const VOID: &str = "void";
49const BIGINT: &str = "bigint";
50const BOOLEAN: &str = "boolean";
51const BYTESTRING: &str = "ByteString";
52
53// ---------------------------------------------------------------------------
54// Built-in function signatures
55// ---------------------------------------------------------------------------
56
57struct 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    // Preimage extractors
127    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
143// ---------------------------------------------------------------------------
144// Subtyping
145// ---------------------------------------------------------------------------
146
147/// ByteString subtypes -- types represented as byte strings on the stack.
148fn is_bytestring_subtype(t: &str) -> bool {
149    matches!(
150        t,
151        "ByteString" | "PubKey" | "Sig" | "Sha256" | "Ripemd160" | "Addr" | "SigHashPreimage" | "Point"
152    )
153}
154
155/// Bigint subtypes -- types represented as integers on the stack.
156fn 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    // ByteString subtypes
166    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    // Both in the ByteString family -> compatible (e.g. Addr and Ripemd160)
174    if is_bytestring_subtype(actual) && is_bytestring_subtype(expected) {
175        return true;
176    }
177
178    // bigint subtypes
179    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    // Both in the bigint family -> compatible
187    if is_bigint_subtype(actual) && is_bigint_subtype(expected) {
188        return true;
189    }
190
191    // Array subtyping
192    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
206// ---------------------------------------------------------------------------
207// Type environment
208// ---------------------------------------------------------------------------
209
210struct 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
245// ---------------------------------------------------------------------------
246// Type checker
247// ---------------------------------------------------------------------------
248
249/// Types whose values can be consumed at most once.
250fn is_affine_type(t: &str) -> bool {
251    matches!(t, "Sig" | "SigHashPreimage")
252}
253
254/// Maps consuming function names to the parameter indices that consume
255/// affine values.
256fn 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        // For StatefulSmartContract, add the implicit txPreimage property
283        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    /// Push a type-check error using the current method's source location.
314    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        // Set current method location for diagnostics
323        self.current_method_loc = Some(ctor.source_location.clone());
324
325        // Reset affine tracking for this scope
326        self.consumed_values.clear();
327
328        // Add constructor params to env
329        for param in &ctor.params {
330            env.define(&param.name, type_node_to_ttype(&param.param_type));
331        }
332
333        // Add properties to env
334        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        // Set current method location for diagnostics
345        self.current_method_loc = Some(method.source_location.clone());
346
347        // Reset affine tracking for this method
348        self.consumed_values.clear();
349
350        // Add method params to env
351        for param in &method.params {
352            env.define(&param.name, type_node_to_ttype(&param.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    /// Infer the type of an expression.
453    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                // Check if it's a builtin function name
477                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                    // Check if it's a property
501                    if let Some(t) = self.prop_types.get(property) {
502                        return t.clone();
503                    }
504                    // Check if it's a method
505                    if self.method_sigs.contains_key(property) {
506                        return "<method>".to_string();
507                    }
508                    // Special: getStateScript
509                    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                // SigHash.ALL, SigHash.FORKID, etc.
521                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                // Infer element type from the first element; treat as element-type array.
605                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            // Add: bigint x bigint -> bigint, or ByteString x ByteString -> ByteString (OP_CAT)
627            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            // Arithmetic: bigint x bigint -> bigint
649            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            // Comparison: bigint x bigint -> boolean
668            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            // Equality: T x T -> boolean
687            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            // Logical: boolean x boolean -> boolean
704            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            // Bitwise &, |, ^: bigint x bigint -> bigint, or ByteString x ByteString -> ByteString
723            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            // Shift: bigint x bigint -> bigint
745            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        // super() call in constructor
814        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        // Direct builtin call: assert(...), checkSig(...), sha256(...), etc.
824        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            // Check if it's a known contract method
832            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, &param_strs, &return_type, args, env);
835            }
836
837            // Check if it's a local variable
838            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        // this.method(...) via PropertyAccess
856        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            // Check contract method signatures
872            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, &param_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        // member_expr call: obj.method(...)
888        if let Expression::MemberExpr { object, property } = callee {
889            // .clone() is a Rust idiom — allow it as a no-op
890            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                        &param_strs,
908                        &return_type,
909                        args,
910                        env,
911                    );
912                }
913            }
914
915            // Not this.method — reject (e.g. std::process::exit)
916            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        // Fallback — unknown callee shape
931        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        // Special case: assert can take 1 or 2 args
950        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        // Special case: checkSig — strict type enforcement
973        // arg0 must be Sig (not just any ByteString subtype)
974        // arg1 must be PubKey (not just any ByteString subtype)
975        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                // arg0 must be Sig specifically (or a subtype of Sig — currently Sig has no subtypes)
985                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                // arg1 must be PubKey specifically (or a subtype of PubKey)
995                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            // Infer remaining args if overprovided
1003            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        // Special case: checkMultiSig
1011        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        // Standard argument count check
1026        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        // Infer remaining args even if count mismatches
1052        for i in count..args.len() {
1053            self.infer_expr_type(&args[i], env);
1054        }
1055
1056        // Affine type enforcement
1057        self.check_affine_consumption(func_name, args, env);
1058
1059        return_type.to_string()
1060    }
1061
1062    /// Check affine type constraints: Sig and SigHashPreimage values may
1063    /// only be consumed once by a consuming function.
1064    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 &param_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
1102// ---------------------------------------------------------------------------
1103// Helpers
1104// ---------------------------------------------------------------------------
1105
1106// ---------------------------------------------------------------------------
1107// Private method return type inference
1108// ---------------------------------------------------------------------------
1109
1110/// Infer a private method's return type by walking all return statements
1111/// and inferring the type of their expressions. Returns "void" if no
1112/// return statements with values are found.
1113fn 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    // Check if all are in the bigint family
1127    if return_types.iter().all(|t| is_bigint_subtype(t)) {
1128        return BIGINT.to_string();
1129    }
1130
1131    // Check if all are in the ByteString family
1132    if return_types.iter().all(|t| is_bytestring_subtype(t)) {
1133        return BYTESTRING.to_string();
1134    }
1135
1136    // Check if all are boolean
1137    if return_types.iter().all(|t| t == BOOLEAN) {
1138        return BOOLEAN.to_string();
1139    }
1140
1141    // Mixed types -- return the first as a best effort
1142    first.clone()
1143}
1144
1145/// Recursively collect inferred types from return statements.
1146fn 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
1174/// Lightweight static expression type inference without a type environment.
1175/// Used for inferring return types of private methods before the full
1176/// type-check pass runs.
1177fn 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// ---------------------------------------------------------------------------
1250// Tests
1251// ---------------------------------------------------------------------------
1252
1253#[cfg(test)]
1254mod tests {
1255    use super::*;
1256    use crate::frontend::parser::parse_source;
1257    use crate::frontend::validator;
1258
1259    /// Helper: parse and validate a TypeScript source string, then return the ContractNode.
1260    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    // -----------------------------------------------------------------------
1440    // Test: Valid arithmetic contract passes type check
1441    // -----------------------------------------------------------------------
1442
1443    #[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    // -----------------------------------------------------------------------
1476    // Test: Valid boolean logic contract passes type check
1477    // -----------------------------------------------------------------------
1478
1479    #[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    // -----------------------------------------------------------------------
1512    // Test: Subtype compatibility — PubKey where ByteString expected passes
1513    // -----------------------------------------------------------------------
1514
1515    #[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        // parse_and_validate calls validate() which may fail on Sha256 type
1534        // We test typecheck directly after a best-effort parse
1535        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                // PubKey should be assignable to ByteString (sha256's parameter type)
1540                // so there should be no error about PubKey argument type mismatch
1541                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        // If parse itself fails, the test is inconclusive — we skip rather than fail
1552    }
1553
1554    // -----------------------------------------------------------------------
1555    // Test: console.log call is rejected as unknown function
1556    // Mirrors Go TestTypeCheck_UnknownFunction_ConsoleLog
1557    // -----------------------------------------------------------------------
1558
1559    #[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        // If parse itself fails (e.g. because the parser rejects member calls on unknown objects),
1595        // that also counts as rejection — just not at the typecheck level
1596    }
1597}