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