Skip to main content

runar_compiler_rust/frontend/
parser_ruby.rs

1//! Ruby parser for Rúnar contracts (.runar.rb).
2//!
3//! Parses Ruby-style contract definitions using a hand-written tokenizer
4//! and recursive descent parser. Produces the same AST as the TypeScript
5//! and Python parsers.
6//!
7//! ## Expected format
8//!
9//! ```ruby
10//! require 'runar'
11//!
12//! class P2PKH < Runar::SmartContract
13//!   prop :pub_key_hash, Addr
14//!
15//!   def initialize(pub_key_hash)
16//!     super(pub_key_hash)
17//!     @pub_key_hash = pub_key_hash
18//!   end
19//!
20//!   runar_public sig: Sig, pub_key: PubKey
21//!   def unlock(sig, pub_key)
22//!     assert hash160(pub_key) == @pub_key_hash
23//!     assert check_sig(sig, pub_key)
24//!   end
25//! end
26//! ```
27//!
28//! Key mappings:
29//! - `class Foo < Runar::SmartContract` -> contract
30//! - `runar_public` -> Visibility::Public for the next method
31//! - `params` -> pending param types for a private method
32//! - `prop :name, Type` -> PropertyNode
33//! - `@ivar` -> PropertyAccess (like `this.prop`)
34//! - `assert expr` -> assert(expr)
35//! - `**` -> pow() call
36//! - `and`/`or`/`not` -> And/Or/Not operators
37//! - `==`/`!=` -> StrictEq/StrictNe
38//! - `for i in start...end` -> ForStatement (exclusive)
39//! - `for i in start..end` -> ForStatement (inclusive)
40//! - `unless` -> if with negated condition
41//! - snake_case identifiers -> camelCase in AST
42
43use super::ast::{
44    BinaryOp, ContractNode, Expression, MethodNode, ParamNode, PrimitiveTypeName, PropertyNode,
45    SourceLocation, Statement, TypeNode, UnaryOp, Visibility,
46};
47use super::parser::ParseResult;
48use std::collections::HashMap;
49
50// ---------------------------------------------------------------------------
51// Public API
52// ---------------------------------------------------------------------------
53
54/// Parse a Ruby-format Rúnar contract source.
55pub fn parse_ruby(source: &str, file_name: Option<&str>) -> ParseResult {
56    use super::diagnostic::Diagnostic;
57
58    let file = file_name.unwrap_or("contract.runar.rb");
59    let mut str_errors: Vec<String> = Vec::new();
60
61    let tokens = tokenize(source);
62    let mut parser = RbParser::new(tokens, file, &mut str_errors);
63
64    let contract = parser.parse_contract();
65
66    let errors = str_errors
67        .into_iter()
68        .map(|msg| Diagnostic::error(msg, None))
69        .collect();
70
71    ParseResult { contract, errors }
72}
73
74// ---------------------------------------------------------------------------
75// Name conversion helpers
76// ---------------------------------------------------------------------------
77
78/// Convert snake_case to camelCase. Single words pass through unchanged.
79fn snake_to_camel(name: &str) -> String {
80    // Strip leading underscores so `_require_owner` becomes `requireOwner` not `RequireOwner`.
81    let stripped = name.trim_start_matches('_');
82    if stripped.is_empty() {
83        return name.to_string();
84    }
85    let mut result = String::new();
86    let mut capitalize_next = false;
87
88    for ch in stripped.chars() {
89        if ch == '_' {
90            capitalize_next = true;
91        } else if capitalize_next {
92            result.push(ch.to_ascii_uppercase());
93            capitalize_next = false;
94        } else {
95            result.push(ch);
96        }
97    }
98
99    result
100}
101
102/// Map Ruby builtin function names to Rúnar AST callee names.
103fn map_builtin_name(name: &str) -> String {
104    // Exact-match special cases (names that don't follow simple snake_case -> camelCase)
105    match name {
106        "verify_wots" => return "verifyWOTS".to_string(),
107        "verify_slh_dsa_sha2_128s" => return "verifySLHDSA_SHA2_128s".to_string(),
108        "verify_slh_dsa_sha2_128f" => return "verifySLHDSA_SHA2_128f".to_string(),
109        "verify_slh_dsa_sha2_192s" => return "verifySLHDSA_SHA2_192s".to_string(),
110        "verify_slh_dsa_sha2_192f" => return "verifySLHDSA_SHA2_192f".to_string(),
111        "verify_slh_dsa_sha2_256s" => return "verifySLHDSA_SHA2_256s".to_string(),
112        "verify_slh_dsa_sha2_256f" => return "verifySLHDSA_SHA2_256f".to_string(),
113        "verify_rabin_sig" => return "verifyRabinSig".to_string(),
114        "check_sig" => return "checkSig".to_string(),
115        "check_multi_sig" => return "checkMultiSig".to_string(),
116        "check_preimage" => return "checkPreimage".to_string(),
117        "hash160" => return "hash160".to_string(),
118        "hash256" => return "hash256".to_string(),
119        "sha256" => return "sha256".to_string(),
120        "ripemd160" => return "ripemd160".to_string(),
121        "num2bin" => return "num2bin".to_string(),
122        "reverse_bytes" => return "reverseBytes".to_string(),
123        "extract_locktime" => return "extractLocktime".to_string(),
124        "extract_output_hash" => return "extractOutputHash".to_string(),
125        "extract_amount" => return "extractAmount".to_string(),
126        "extract_version" => return "extractVersion".to_string(),
127        "extract_sequence" => return "extractSequence".to_string(),
128        "ec_add" => return "ecAdd".to_string(),
129        "ec_mul" => return "ecMul".to_string(),
130        "ec_mul_gen" => return "ecMulGen".to_string(),
131        "ec_negate" => return "ecNegate".to_string(),
132        "ec_on_curve" => return "ecOnCurve".to_string(),
133        "ec_mod_reduce" => return "ecModReduce".to_string(),
134        "ec_encode_compressed" => return "ecEncodeCompressed".to_string(),
135        "ec_make_point" => return "ecMakePoint".to_string(),
136        "ec_point_x" => return "ecPointX".to_string(),
137        "ec_point_y" => return "ecPointY".to_string(),
138        "mul_div" => return "mulDiv".to_string(),
139        "percent_of" => return "percentOf".to_string(),
140        "safe_div" => return "safediv".to_string(),
141        "safe_mod" => return "safemod".to_string(),
142        "div_mod" => return "divmod".to_string(),
143        "bin2num" => return "bin2num".to_string(),
144        "add_output" => return "addOutput".to_string(),
145        "add_raw_output" => return "addRawOutput".to_string(),
146        "get_state_script" => return "getStateScript".to_string(),
147        // SHA-256 partial verification
148        "sha256_compress" => return "sha256Compress".to_string(),
149        "sha256_finalize" => return "sha256Finalize".to_string(),
150        // Transaction intrinsics
151        "extract_nsequence" => return "extractNSequence".to_string(),
152        "extract_hash_prevouts" => return "extractHashPrevouts".to_string(),
153        "extract_hash_sequence" => return "extractHashSequence".to_string(),
154        "extract_outpoint" => return "extractOutpoint".to_string(),
155        "extract_script_code" => return "extractScriptCode".to_string(),
156        "extract_input_index" => return "extractInputIndex".to_string(),
157        "extract_sig_hash_type" => return "extractSigHashType".to_string(),
158        "extract_outputs" => return "extractOutputs".to_string(),
159        // EC constants
160        "EC_P" => return "EC_P".to_string(),
161        "EC_N" => return "EC_N".to_string(),
162        "EC_G" => return "EC_G".to_string(),
163        _ => {}
164    }
165
166    // Names that pass through unchanged
167    match name {
168        "bool" | "abs" | "min" | "max" | "len" | "pow" | "cat" | "within" | "safediv"
169        | "safemod" | "clamp" | "sign" | "sqrt" | "gcd" | "divmod" | "log2" | "substr" => {
170            return name.to_string();
171        }
172        _ => {}
173    }
174
175    // Default: snake_case -> camelCase
176    snake_to_camel(name)
177}
178
179/// Map Ruby type names to Rúnar AST types.
180fn map_rb_type(name: &str) -> &str {
181    match name {
182        "Bigint" | "Integer" | "Int" => "bigint",
183        "Boolean" => "boolean",
184        "ByteString" => "ByteString",
185        "PubKey" => "PubKey",
186        "Sig" => "Sig",
187        "Addr" => "Addr",
188        "Sha256" => "Sha256",
189        "Ripemd160" => "Ripemd160",
190        "SigHashPreimage" => "SigHashPreimage",
191        "RabinSig" => "RabinSig",
192        "RabinPubKey" => "RabinPubKey",
193        "Point" => "Point",
194        _ => name,
195    }
196}
197
198// ---------------------------------------------------------------------------
199// Tokeniser
200// ---------------------------------------------------------------------------
201
202#[derive(Debug, Clone, PartialEq)]
203enum Token {
204    // Keywords
205    Class,
206    Def,
207    If,
208    Elsif,
209    Else,
210    Unless,
211    For,
212    In,
213    End,
214    Return,
215    TrueLit,
216    FalseLit,
217    NilLit,
218    And,
219    Or,
220    Not,
221    Super,
222    Require,
223    Assert,
224    Do,
225
226    // Identifiers and literals
227    Ident(String),
228    NumberLit(i64),
229    HexStringLit(String),  // single-quoted strings
230    StringLit(String),      // double-quoted strings
231    Symbol(String),         // :name
232    Ivar(String),           // @name
233
234    // Operators
235    Plus,
236    Minus,
237    Star,
238    Slash,
239    Percent,
240    DoubleStar,   // **
241    EqEq,         // ==
242    NotEq,        // !=
243    Lt,
244    Le,
245    Gt,
246    Ge,
247    LShift,       // <<
248    RShift,       // >>
249    AndAnd,       // &&
250    OrOr,         // ||
251    BitAnd,       // &
252    BitOr,        // |
253    BitXor,       // ^
254    Tilde,        // ~
255    Bang,         // !
256    Eq,           // =
257    PlusEq,       // +=
258    MinusEq,      // -=
259    StarEq,       // *=
260    SlashEq,      // /=
261    PercentEq,    // %=
262    DotDotDot,    // ... (exclusive range)
263    DotDot,       // ..  (inclusive range)
264    Question,     // ?
265    ColonColon,   // ::
266
267    // Delimiters
268    LParen,
269    RParen,
270    LBracket,
271    RBracket,
272    Colon,
273    Comma,
274    Dot,
275
276    // Line separator
277    Newline,
278
279    // Special
280    Eof,
281}
282
283fn tokenize(source: &str) -> Vec<Token> {
284    let mut tokens = Vec::new();
285    let lines: Vec<&str> = source.split('\n').collect();
286    let mut paren_depth: usize = 0;
287
288    for raw_line in &lines {
289        // Strip trailing \r
290        let line = if raw_line.ends_with('\r') {
291            &raw_line[..raw_line.len() - 1]
292        } else {
293            raw_line
294        };
295
296        // Skip blank lines and comment-only lines
297        let stripped = line.trim_start();
298        if stripped.is_empty() || stripped.starts_with('#') {
299            continue;
300        }
301
302        let chars: Vec<char> = line.chars().collect();
303        let start_offset = chars.len() - stripped.len();
304        let mut pos = start_offset;
305
306        while pos < chars.len() {
307            let ch = chars[pos];
308
309            // Whitespace within line
310            if ch == ' ' || ch == '\t' {
311                pos += 1;
312                continue;
313            }
314
315            // Comment
316            if ch == '#' {
317                break; // rest of line is comment
318            }
319
320            // Instance variable: @name
321            if ch == '@' {
322                pos += 1;
323                let start = pos;
324                while pos < chars.len()
325                    && (chars[pos].is_ascii_alphanumeric() || chars[pos] == '_')
326                {
327                    pos += 1;
328                }
329                if pos > start {
330                    let name: String = chars[start..pos].iter().collect();
331                    tokens.push(Token::Ivar(name));
332                } else {
333                    // bare @ — unlikely but handle gracefully
334                    tokens.push(Token::Ident("@".to_string()));
335                }
336                continue;
337            }
338
339            // Three-dot range operator: ...
340            if ch == '.'
341                && pos + 2 < chars.len()
342                && chars[pos + 1] == '.'
343                && chars[pos + 2] == '.'
344            {
345                tokens.push(Token::DotDotDot);
346                pos += 3;
347                continue;
348            }
349
350            // Two-dot range operator: ..
351            if ch == '.' && pos + 1 < chars.len() && chars[pos + 1] == '.' {
352                tokens.push(Token::DotDot);
353                pos += 2;
354                continue;
355            }
356
357            // Two-char operators: ** :: == != <= >= << >> && || += -= *= /= %=
358            if ch == '*' && pos + 1 < chars.len() && chars[pos + 1] == '*' {
359                tokens.push(Token::DoubleStar);
360                pos += 2;
361                continue;
362            }
363            if ch == ':' && pos + 1 < chars.len() && chars[pos + 1] == ':' {
364                tokens.push(Token::ColonColon);
365                pos += 2;
366                continue;
367            }
368            if ch == '=' && pos + 1 < chars.len() && chars[pos + 1] == '=' {
369                tokens.push(Token::EqEq);
370                pos += 2;
371                continue;
372            }
373            if ch == '!' && pos + 1 < chars.len() && chars[pos + 1] == '=' {
374                tokens.push(Token::NotEq);
375                pos += 2;
376                continue;
377            }
378            if ch == '<' && pos + 1 < chars.len() && chars[pos + 1] == '=' {
379                tokens.push(Token::Le);
380                pos += 2;
381                continue;
382            }
383            if ch == '>' && pos + 1 < chars.len() && chars[pos + 1] == '=' {
384                tokens.push(Token::Ge);
385                pos += 2;
386                continue;
387            }
388            if ch == '<' && pos + 1 < chars.len() && chars[pos + 1] == '<' {
389                tokens.push(Token::LShift);
390                pos += 2;
391                continue;
392            }
393            if ch == '>' && pos + 1 < chars.len() && chars[pos + 1] == '>' {
394                tokens.push(Token::RShift);
395                pos += 2;
396                continue;
397            }
398            if ch == '&' && pos + 1 < chars.len() && chars[pos + 1] == '&' {
399                tokens.push(Token::AndAnd);
400                pos += 2;
401                continue;
402            }
403            if ch == '|' && pos + 1 < chars.len() && chars[pos + 1] == '|' {
404                tokens.push(Token::OrOr);
405                pos += 2;
406                continue;
407            }
408            if ch == '+' && pos + 1 < chars.len() && chars[pos + 1] == '=' {
409                tokens.push(Token::PlusEq);
410                pos += 2;
411                continue;
412            }
413            if ch == '-' && pos + 1 < chars.len() && chars[pos + 1] == '=' {
414                tokens.push(Token::MinusEq);
415                pos += 2;
416                continue;
417            }
418            if ch == '*' && pos + 1 < chars.len() && chars[pos + 1] == '=' {
419                tokens.push(Token::StarEq);
420                pos += 2;
421                continue;
422            }
423            if ch == '/' && pos + 1 < chars.len() && chars[pos + 1] == '=' {
424                tokens.push(Token::SlashEq);
425                pos += 2;
426                continue;
427            }
428            if ch == '%' && pos + 1 < chars.len() && chars[pos + 1] == '=' {
429                tokens.push(Token::PercentEq);
430                pos += 2;
431                continue;
432            }
433
434            // Parentheses (track depth for multi-line expressions)
435            if ch == '(' {
436                paren_depth += 1;
437                tokens.push(Token::LParen);
438                pos += 1;
439                continue;
440            }
441            if ch == ')' {
442                if paren_depth > 0 {
443                    paren_depth -= 1;
444                }
445                tokens.push(Token::RParen);
446                pos += 1;
447                continue;
448            }
449            if ch == '[' {
450                paren_depth += 1;
451                tokens.push(Token::LBracket);
452                pos += 1;
453                continue;
454            }
455            if ch == ']' {
456                if paren_depth > 0 {
457                    paren_depth -= 1;
458                }
459                tokens.push(Token::RBracket);
460                pos += 1;
461                continue;
462            }
463
464            // Symbol literal: :name (but not ::)
465            if ch == ':'
466                && pos + 1 < chars.len()
467                && (chars[pos + 1].is_ascii_alphabetic() || chars[pos + 1] == '_')
468            {
469                pos += 1; // skip ':'
470                let start = pos;
471                while pos < chars.len()
472                    && (chars[pos].is_ascii_alphanumeric() || chars[pos] == '_')
473                {
474                    pos += 1;
475                }
476                let name: String = chars[start..pos].iter().collect();
477                tokens.push(Token::Symbol(name));
478                continue;
479            }
480
481            // Single-char operators & delimiters
482            match ch {
483                '+' => {
484                    tokens.push(Token::Plus);
485                    pos += 1;
486                    continue;
487                }
488                '-' => {
489                    tokens.push(Token::Minus);
490                    pos += 1;
491                    continue;
492                }
493                '*' => {
494                    tokens.push(Token::Star);
495                    pos += 1;
496                    continue;
497                }
498                '/' => {
499                    tokens.push(Token::Slash);
500                    pos += 1;
501                    continue;
502                }
503                '%' => {
504                    tokens.push(Token::Percent);
505                    pos += 1;
506                    continue;
507                }
508                '<' => {
509                    tokens.push(Token::Lt);
510                    pos += 1;
511                    continue;
512                }
513                '>' => {
514                    tokens.push(Token::Gt);
515                    pos += 1;
516                    continue;
517                }
518                '!' => {
519                    tokens.push(Token::Bang);
520                    pos += 1;
521                    continue;
522                }
523                '~' => {
524                    tokens.push(Token::Tilde);
525                    pos += 1;
526                    continue;
527                }
528                '&' => {
529                    tokens.push(Token::BitAnd);
530                    pos += 1;
531                    continue;
532                }
533                '|' => {
534                    tokens.push(Token::BitOr);
535                    pos += 1;
536                    continue;
537                }
538                '^' => {
539                    tokens.push(Token::BitXor);
540                    pos += 1;
541                    continue;
542                }
543                '=' => {
544                    tokens.push(Token::Eq);
545                    pos += 1;
546                    continue;
547                }
548                ':' => {
549                    tokens.push(Token::Colon);
550                    pos += 1;
551                    continue;
552                }
553                ',' => {
554                    tokens.push(Token::Comma);
555                    pos += 1;
556                    continue;
557                }
558                '.' => {
559                    tokens.push(Token::Dot);
560                    pos += 1;
561                    continue;
562                }
563                '?' => {
564                    tokens.push(Token::Question);
565                    pos += 1;
566                    continue;
567                }
568                _ => {}
569            }
570
571            // Single-quoted string literals: hex ByteStrings
572            if ch == '\'' {
573                pos += 1;
574                let mut val = String::new();
575                while pos < chars.len() && chars[pos] != '\'' {
576                    if chars[pos] == '\\' && pos + 1 < chars.len() {
577                        pos += 1; // skip backslash
578                        val.push(chars[pos]);
579                        pos += 1;
580                    } else {
581                        val.push(chars[pos]);
582                        pos += 1;
583                    }
584                }
585                if pos < chars.len() {
586                    pos += 1; // skip closing quote
587                }
588                tokens.push(Token::HexStringLit(val));
589                continue;
590            }
591
592            // Double-quoted string literals
593            if ch == '"' {
594                pos += 1;
595                let mut val = String::new();
596                while pos < chars.len() && chars[pos] != '"' {
597                    if chars[pos] == '\\' && pos + 1 < chars.len() {
598                        pos += 1; // skip backslash
599                        val.push(chars[pos]);
600                        pos += 1;
601                    } else {
602                        val.push(chars[pos]);
603                        pos += 1;
604                    }
605                }
606                if pos < chars.len() {
607                    pos += 1; // skip closing quote
608                }
609                tokens.push(Token::StringLit(val));
610                continue;
611            }
612
613            // Numbers (decimal and hex)
614            if ch.is_ascii_digit() {
615                let mut num_str = String::new();
616                if ch == '0'
617                    && pos + 1 < chars.len()
618                    && (chars[pos + 1] == 'x' || chars[pos + 1] == 'X')
619                {
620                    // Hex number
621                    num_str.push_str("0x");
622                    pos += 2;
623                    while pos < chars.len()
624                        && (chars[pos].is_ascii_hexdigit() || chars[pos] == '_')
625                    {
626                        if chars[pos] != '_' {
627                            num_str.push(chars[pos]);
628                        }
629                        pos += 1;
630                    }
631                } else {
632                    while pos < chars.len()
633                        && (chars[pos].is_ascii_digit() || chars[pos] == '_')
634                    {
635                        if chars[pos] != '_' {
636                            num_str.push(chars[pos]);
637                        }
638                        pos += 1;
639                    }
640                }
641                let val = if num_str.starts_with("0x") || num_str.starts_with("0X") {
642                    i64::from_str_radix(&num_str[2..], 16).unwrap_or(0)
643                } else {
644                    num_str.parse::<i64>().unwrap_or(0)
645                };
646                tokens.push(Token::NumberLit(val));
647                continue;
648            }
649
650            // Identifiers and keywords
651            if ch.is_ascii_alphabetic() || ch == '_' {
652                let start = pos;
653                while pos < chars.len()
654                    && (chars[pos].is_ascii_alphanumeric() || chars[pos] == '_')
655                {
656                    pos += 1;
657                }
658                // Check for trailing ? or ! (Ruby method convention)
659                if pos < chars.len() && (chars[pos] == '?' || chars[pos] == '!') {
660                    pos += 1;
661                }
662                let word: String = chars[start..pos].iter().collect();
663                let tok = match word.as_str() {
664                    "class" => Token::Class,
665                    "def" => Token::Def,
666                    "if" => Token::If,
667                    "elsif" => Token::Elsif,
668                    "else" => Token::Else,
669                    "unless" => Token::Unless,
670                    "for" => Token::For,
671                    "in" => Token::In,
672                    "end" => Token::End,
673                    "return" => Token::Return,
674                    "true" => Token::TrueLit,
675                    "false" => Token::FalseLit,
676                    "nil" => Token::NilLit,
677                    "and" => Token::And,
678                    "or" => Token::Or,
679                    "not" => Token::Not,
680                    "super" => Token::Super,
681                    "require" => Token::Require,
682                    "assert" => Token::Assert,
683                    "do" => Token::Do,
684                    _ => Token::Ident(word),
685                };
686                tokens.push(tok);
687                continue;
688            }
689
690            // Skip unrecognised characters
691            pos += 1;
692        }
693
694        // Emit NEWLINE at end of significant line (only if not inside parens)
695        if paren_depth == 0 {
696            tokens.push(Token::Newline);
697        }
698    }
699
700    tokens.push(Token::Eof);
701    tokens
702}
703
704// ---------------------------------------------------------------------------
705// Parser
706// ---------------------------------------------------------------------------
707
708struct RbParser<'a> {
709    tokens: Vec<Token>,
710    pos: usize,
711    file: &'a str,
712    errors: &'a mut Vec<String>,
713    /// Track locally declared variables per method scope to distinguish decl from assignment.
714    declared_locals: std::collections::HashSet<String>,
715}
716
717impl<'a> RbParser<'a> {
718    fn new(tokens: Vec<Token>, file: &'a str, errors: &'a mut Vec<String>) -> Self {
719        Self {
720            tokens,
721            pos: 0,
722            file,
723            errors,
724            declared_locals: std::collections::HashSet::new(),
725        }
726    }
727
728    fn peek(&self) -> &Token {
729        self.tokens.get(self.pos).unwrap_or(&Token::Eof)
730    }
731
732    fn advance(&mut self) -> Token {
733        let t = self.tokens.get(self.pos).cloned().unwrap_or(Token::Eof);
734        self.pos += 1;
735        t
736    }
737
738    fn expect(&mut self, expected: &Token) -> bool {
739        if std::mem::discriminant(self.peek()) == std::mem::discriminant(expected) {
740            self.advance();
741            true
742        } else {
743            self.errors.push(format!(
744                "Expected {:?}, got {:?}",
745                expected,
746                self.peek()
747            ));
748            false
749        }
750    }
751
752    fn match_tok(&mut self, expected: &Token) -> bool {
753        if std::mem::discriminant(self.peek()) == std::mem::discriminant(expected) {
754            self.advance();
755            true
756        } else {
757            false
758        }
759    }
760
761    fn expect_ident(&mut self) -> String {
762        match self.advance() {
763            Token::Ident(name) => name,
764            other => {
765                self.errors
766                    .push(format!("Expected identifier, got {:?}", other));
767                "_error".to_string()
768            }
769        }
770    }
771
772    /// Check if the current token is an identifier with a specific name.
773    fn check_ident(&self, name: &str) -> bool {
774        matches!(self.peek(), Token::Ident(ref n) if n == name)
775    }
776
777    fn loc(&self) -> SourceLocation {
778        SourceLocation {
779            file: self.file.to_string(),
780            line: 1,
781            column: 0,
782        }
783    }
784
785    /// Skip NEWLINE tokens.
786    fn skip_newlines(&mut self) {
787        while *self.peek() == Token::Newline {
788            self.advance();
789        }
790    }
791
792    // -----------------------------------------------------------------------
793    // Top-level parsing
794    // -----------------------------------------------------------------------
795
796    fn parse_contract(&mut self) -> Option<ContractNode> {
797        self.skip_newlines();
798
799        // Skip `require 'runar'` lines
800        while *self.peek() == Token::Require {
801            self.parse_require_line();
802            self.skip_newlines();
803        }
804
805        self.skip_newlines();
806
807        if *self.peek() != Token::Class {
808            self.errors
809                .push("Expected 'class' declaration".to_string());
810            return None;
811        }
812
813        self.advance(); // consume 'class'
814        let contract_name = self.expect_ident();
815
816        // Expect `< Runar::SmartContract` or `< SmartContract`
817        self.expect(&Token::Lt);
818
819        // Parse parent class: could be `Runar::SmartContract` or just `SmartContract`
820        let first_part = self.expect_ident();
821        let parent_class = if *self.peek() == Token::ColonColon {
822            self.advance(); // '::'
823            self.expect_ident()
824        } else {
825            first_part
826        };
827
828        self.skip_newlines();
829
830        if parent_class != "SmartContract" && parent_class != "StatefulSmartContract" {
831            self.errors.push(format!(
832                "Unknown parent class: {}",
833                parent_class
834            ));
835            return None;
836        }
837
838        let mut properties = Vec::new();
839        let mut methods = Vec::new();
840        let mut constructor: Option<MethodNode> = None;
841
842        // Pending visibility/param types for the next method
843        let mut pending_visibility: Option<Visibility> = None;
844        let mut pending_param_types: Option<HashMap<String, TypeNode>> = None;
845
846        while *self.peek() != Token::End && *self.peek() != Token::Eof {
847            self.skip_newlines();
848            if *self.peek() == Token::End || *self.peek() == Token::Eof {
849                break;
850            }
851
852            // `prop :name, Type [, readonly: true]`
853            if self.check_ident("prop") {
854                if let Some(prop) = self.parse_prop(&parent_class) {
855                    properties.push(prop);
856                }
857                self.skip_newlines();
858                continue;
859            }
860
861            // `runar_public [key: Type, ...]`
862            if self.check_ident("runar_public") {
863                self.advance(); // 'runar_public'
864                pending_visibility = Some(Visibility::Public);
865                pending_param_types = self.parse_optional_param_types();
866                self.skip_newlines();
867                continue;
868            }
869
870            // `params key: Type, ...`
871            if self.check_ident("params") {
872                self.advance(); // 'params'
873                pending_param_types = self.parse_optional_param_types();
874                self.skip_newlines();
875                continue;
876            }
877
878            // Method definition
879            if *self.peek() == Token::Def {
880                let method = self.parse_method_def(
881                    &pending_visibility,
882                    &pending_param_types,
883                );
884                if method.name == "constructor" {
885                    constructor = Some(method);
886                } else {
887                    methods.push(method);
888                }
889                pending_visibility = None;
890                pending_param_types = None;
891                self.skip_newlines();
892                continue;
893            }
894
895            // Skip unknown tokens
896            self.advance();
897        }
898
899        self.match_tok(&Token::End); // end of class
900
901        // Auto-generate constructor if not provided
902        let mut constructor =
903            constructor.unwrap_or_else(|| build_constructor(&properties, self.file));
904
905        // Back-fill constructor param types from prop declarations.
906        // In Ruby, `def initialize(pub_key_hash)` has no type annotations —
907        // we infer them from the matching `prop :pub_key_hash, Addr` declarations.
908        for param in &mut constructor.params {
909            if matches!(&param.param_type, TypeNode::Custom(n) if n == "unknown") {
910                if let Some(prop) = properties.iter().find(|p| p.name == param.name) {
911                    param.param_type = prop.prop_type.clone();
912                }
913            }
914        }
915
916        // Convert bare calls to declared methods and intrinsics into this.method() calls.
917        // In Ruby, bare calls like `add_output(...)` are equivalent to
918        // `self.add_output(...)` / `this.addOutput(...)`.
919        let mut method_names: std::collections::HashSet<String> =
920            methods.iter().map(|m| m.name.clone()).collect();
921        // Intrinsic methods that must also be rewritten to property_access style.
922        method_names.insert("addOutput".to_string());
923        method_names.insert("addRawOutput".to_string());
924        method_names.insert("getStateScript".to_string());
925        for method in &mut methods {
926            rewrite_bare_method_calls(&mut method.body, &method_names);
927        }
928
929        // Implicit return conversion for private methods.
930        //
931        // Ruby methods implicitly return the value of their last expression.
932        // Private helper methods that return computed values (e.g. a fee
933        // calculation) rely on this — without conversion, the calling method
934        // would receive no return value and the type checker would reject
935        // the call.  We detect ExpressionStatement as the final statement
936        // and promote it to a ReturnStatement so the AST has an explicit
937        // return node for ANF lowering and type checking to consume.
938        for method in &mut methods {
939            if method.visibility == Visibility::Private && !method.body.is_empty() {
940                let last_idx = method.body.len() - 1;
941                if let Statement::ExpressionStatement { expression, source_location } = &method.body[last_idx] {
942                    let ret = Statement::ReturnStatement {
943                        value: Some(expression.clone()),
944                        source_location: source_location.clone(),
945                    };
946                    method.body[last_idx] = ret;
947                }
948            }
949        }
950
951        Some(ContractNode {
952            name: contract_name,
953            parent_class,
954            properties,
955            constructor,
956            methods,
957            source_file: self.file.to_string(),
958        })
959    }
960
961    fn parse_require_line(&mut self) {
962        self.advance(); // 'require'
963        // consume the rest of the line
964        while *self.peek() != Token::Newline && *self.peek() != Token::Eof {
965            self.advance();
966        }
967        self.skip_newlines();
968    }
969
970    /// Parse optional key: Type pairs after `runar_public` or `params`.
971    /// Returns None if there are no pairs (just a bare keyword).
972    fn parse_optional_param_types(&mut self) -> Option<HashMap<String, TypeNode>> {
973        // If the next token is NEWLINE or eof or def, there are no param types
974        if *self.peek() == Token::Newline
975            || *self.peek() == Token::Eof
976            || *self.peek() == Token::Def
977        {
978            return None;
979        }
980
981        let mut param_types = HashMap::new();
982
983        // Parse key: Type pairs
984        while *self.peek() != Token::Newline && *self.peek() != Token::Eof {
985            // Expect ident (param name)
986            let raw_name = match self.advance() {
987                Token::Ident(name) => name,
988                _ => break,
989            };
990
991            // Expect ':'
992            if !self.expect(&Token::Colon) {
993                break;
994            }
995
996            // Parse type
997            let type_node = self.parse_type();
998
999            param_types.insert(raw_name, type_node);
1000
1001            // Optional comma
1002            if !self.match_tok(&Token::Comma) {
1003                break;
1004            }
1005        }
1006
1007        if param_types.is_empty() {
1008            None
1009        } else {
1010            Some(param_types)
1011        }
1012    }
1013
1014    // -----------------------------------------------------------------------
1015    // Properties
1016    // -----------------------------------------------------------------------
1017
1018    fn parse_prop(&mut self, parent_class: &str) -> Option<PropertyNode> {
1019        self.advance(); // 'prop'
1020
1021        // Expect symbol :name
1022        let raw_name = match self.peek().clone() {
1023            Token::Symbol(name) => {
1024                self.advance();
1025                name
1026            }
1027            _ => {
1028                self.errors.push(format!(
1029                    "Expected symbol after 'prop', got {:?}",
1030                    self.peek()
1031                ));
1032                // Skip to end of line
1033                while *self.peek() != Token::Newline && *self.peek() != Token::Eof {
1034                    self.advance();
1035                }
1036                return None;
1037            }
1038        };
1039
1040        self.expect(&Token::Comma);
1041
1042        // Parse type
1043        let type_node = self.parse_type();
1044
1045        // Check for optional trailing options: readonly: true/false, default: <literal>
1046        // Multiple options are comma-separated and may appear in any order.
1047        let mut is_readonly = false;
1048        let mut initializer: Option<Expression> = None;
1049        while *self.peek() == Token::Comma {
1050            self.advance(); // ','
1051            if self.check_ident("readonly") {
1052                self.advance(); // 'readonly'
1053                self.expect(&Token::Colon);
1054                if *self.peek() == Token::TrueLit {
1055                    self.advance();
1056                    is_readonly = true;
1057                } else if *self.peek() == Token::FalseLit {
1058                    self.advance();
1059                    is_readonly = false;
1060                }
1061            } else if self.check_ident("default") {
1062                self.advance(); // 'default'
1063                self.expect(&Token::Colon);
1064                initializer = Some(self.parse_unary());
1065            } else {
1066                // Unknown trailing option -- stop parsing options
1067                break;
1068            }
1069        }
1070
1071        // In stateless contracts, all properties are readonly
1072        if parent_class == "SmartContract" {
1073            is_readonly = true;
1074        }
1075
1076        // Skip rest of line
1077        while *self.peek() != Token::Newline
1078            && *self.peek() != Token::Eof
1079            && *self.peek() != Token::End
1080        {
1081            self.advance();
1082        }
1083
1084        Some(PropertyNode {
1085            name: snake_to_camel(&raw_name),
1086            prop_type: type_node,
1087            readonly: is_readonly,
1088            initializer,
1089            source_location: self.loc(),
1090        })
1091    }
1092
1093    // -----------------------------------------------------------------------
1094    // Types
1095    // -----------------------------------------------------------------------
1096
1097    fn parse_type(&mut self) -> TypeNode {
1098        let raw_name = self.expect_ident();
1099
1100        // Check for FixedArray[T, N]
1101        if raw_name == "FixedArray" && *self.peek() == Token::LBracket {
1102            self.advance(); // '['
1103            let element = self.parse_type();
1104            self.expect(&Token::Comma);
1105            let length = match self.advance() {
1106                Token::NumberLit(n) => n as usize,
1107                _ => {
1108                    self.errors
1109                        .push("FixedArray requires numeric length".to_string());
1110                    0
1111                }
1112            };
1113            self.expect(&Token::RBracket);
1114            return TypeNode::FixedArray {
1115                element: Box::new(element),
1116                length,
1117            };
1118        }
1119
1120        let mapped = map_rb_type(&raw_name);
1121        if let Some(prim) = PrimitiveTypeName::from_str(mapped) {
1122            TypeNode::Primitive(prim)
1123        } else {
1124            TypeNode::Custom(mapped.to_string())
1125        }
1126    }
1127
1128    // -----------------------------------------------------------------------
1129    // Method definitions
1130    // -----------------------------------------------------------------------
1131
1132    fn parse_method_def(
1133        &mut self,
1134        pending_visibility: &Option<Visibility>,
1135        pending_param_types: &Option<HashMap<String, TypeNode>>,
1136    ) -> MethodNode {
1137        self.expect(&Token::Def);
1138
1139        let raw_name = match self.advance() {
1140            Token::Ident(name) => name,
1141            other => {
1142                self.errors
1143                    .push(format!("Expected method name, got {:?}", other));
1144                "_error".to_string()
1145            }
1146        };
1147
1148        // Reset local variable tracking for this method scope
1149        self.declared_locals.clear();
1150
1151        // Parse parameters (optional parentheses for no-arg methods)
1152        let params = if *self.peek() == Token::LParen {
1153            self.advance(); // '('
1154            let p = self.parse_params(pending_param_types);
1155            self.expect(&Token::RParen);
1156            p
1157        } else {
1158            Vec::new()
1159        };
1160
1161        self.skip_newlines();
1162
1163        // Parse body until 'end'
1164        let body = self.parse_statements();
1165
1166        self.expect(&Token::End);
1167
1168        // Determine if this is the constructor
1169        if raw_name == "initialize" {
1170            return MethodNode {
1171                name: "constructor".to_string(),
1172                params,
1173                body,
1174                visibility: Visibility::Public,
1175                source_location: self.loc(),
1176            };
1177        }
1178
1179        let is_public = pending_visibility.as_ref() == Some(&Visibility::Public);
1180        let method_name = snake_to_camel(&raw_name);
1181
1182        MethodNode {
1183            name: method_name,
1184            params,
1185            body,
1186            visibility: if is_public {
1187                Visibility::Public
1188            } else {
1189                Visibility::Private
1190            },
1191            source_location: self.loc(),
1192        }
1193    }
1194
1195    fn parse_params(
1196        &mut self,
1197        param_types: &Option<HashMap<String, TypeNode>>,
1198    ) -> Vec<ParamNode> {
1199        let mut params = Vec::new();
1200
1201        while *self.peek() != Token::RParen && *self.peek() != Token::Eof {
1202            let raw_name = self.expect_ident();
1203            let camel_name = snake_to_camel(&raw_name);
1204
1205            // Look up the type from the preceding runar_public/params declaration
1206            let param_type = if let Some(ref types) = param_types {
1207                types
1208                    .get(&raw_name)
1209                    .cloned()
1210                    .unwrap_or_else(|| TypeNode::Custom("unknown".to_string()))
1211            } else {
1212                TypeNode::Custom("unknown".to_string())
1213            };
1214
1215            params.push(ParamNode {
1216                name: camel_name,
1217                param_type,
1218            });
1219
1220            if !self.match_tok(&Token::Comma) {
1221                break;
1222            }
1223        }
1224
1225        params
1226    }
1227
1228    // -----------------------------------------------------------------------
1229    // Statements
1230    // -----------------------------------------------------------------------
1231
1232    fn parse_statements(&mut self) -> Vec<Statement> {
1233        let mut stmts = Vec::new();
1234
1235        while *self.peek() != Token::End
1236            && *self.peek() != Token::Elsif
1237            && *self.peek() != Token::Else
1238            && *self.peek() != Token::Eof
1239        {
1240            self.skip_newlines();
1241            if *self.peek() == Token::End
1242                || *self.peek() == Token::Elsif
1243                || *self.peek() == Token::Else
1244                || *self.peek() == Token::Eof
1245            {
1246                break;
1247            }
1248
1249            if let Some(stmt) = self.parse_statement() {
1250                stmts.push(stmt);
1251            }
1252            self.skip_newlines();
1253        }
1254
1255        stmts
1256    }
1257
1258    fn parse_statement(&mut self) -> Option<Statement> {
1259        match self.peek().clone() {
1260            // assert statement: assert expr
1261            Token::Assert => Some(self.parse_assert_statement()),
1262
1263            // if statement
1264            Token::If => Some(self.parse_if_statement()),
1265
1266            // unless statement (maps to if with negated condition)
1267            Token::Unless => Some(self.parse_unless_statement()),
1268
1269            // for statement
1270            Token::For => Some(self.parse_for_statement()),
1271
1272            // return statement
1273            Token::Return => Some(self.parse_return_statement()),
1274
1275            // super(args...) — constructor super call
1276            Token::Super => Some(self.parse_super_call()),
1277
1278            // Instance variable: @var = expr, @var += expr
1279            Token::Ivar(_) => Some(self.parse_ivar_statement()),
1280
1281            // Variable declaration or expression statement
1282            Token::Ident(_) => Some(self.parse_ident_statement()),
1283
1284            _ => {
1285                self.advance();
1286                None
1287            }
1288        }
1289    }
1290
1291    fn parse_assert_statement(&mut self) -> Statement {
1292        self.advance(); // consume 'assert'
1293        let expr = self.parse_expression();
1294        Statement::ExpressionStatement {
1295            expression: Expression::CallExpr {
1296                callee: Box::new(Expression::Identifier {
1297                    name: "assert".to_string(),
1298                }),
1299                args: vec![expr],
1300            },
1301            source_location: self.loc(),
1302        }
1303    }
1304
1305    fn parse_if_statement(&mut self) -> Statement {
1306        self.advance(); // consume 'if'
1307        let condition = self.parse_expression();
1308        // Optional newline after condition
1309        self.match_tok(&Token::Newline);
1310        self.skip_newlines();
1311
1312        let then_branch = self.parse_statements();
1313
1314        let else_branch = if *self.peek() == Token::Elsif {
1315            // elsif -> else { if ... }
1316            Some(vec![self.parse_elsif_statement()])
1317        } else if *self.peek() == Token::Else {
1318            self.advance(); // 'else'
1319            self.skip_newlines();
1320            let stmts = self.parse_statements();
1321            Some(stmts)
1322        } else {
1323            None
1324        };
1325
1326        self.expect(&Token::End);
1327
1328        Statement::IfStatement {
1329            condition,
1330            then_branch,
1331            else_branch,
1332            source_location: self.loc(),
1333        }
1334    }
1335
1336    fn parse_elsif_statement(&mut self) -> Statement {
1337        self.advance(); // consume 'elsif'
1338        let condition = self.parse_expression();
1339        self.skip_newlines();
1340
1341        let then_branch = self.parse_statements();
1342
1343        let else_branch = if *self.peek() == Token::Elsif {
1344            Some(vec![self.parse_elsif_statement()])
1345        } else if *self.peek() == Token::Else {
1346            self.advance(); // 'else'
1347            self.skip_newlines();
1348            let stmts = self.parse_statements();
1349            Some(stmts)
1350        } else {
1351            None
1352        };
1353
1354        // Note: the outer `end` is consumed by the parent parse_if_statement.
1355        // elsif branches do not consume their own `end`.
1356
1357        Statement::IfStatement {
1358            condition,
1359            then_branch,
1360            else_branch,
1361            source_location: self.loc(),
1362        }
1363    }
1364
1365    fn parse_unless_statement(&mut self) -> Statement {
1366        self.advance(); // consume 'unless'
1367        let raw_condition = self.parse_expression();
1368        self.skip_newlines();
1369
1370        let body = self.parse_statements();
1371
1372        self.expect(&Token::End);
1373
1374        // Unless is if with negated condition
1375        let condition = Expression::UnaryExpr {
1376            op: UnaryOp::Not,
1377            operand: Box::new(raw_condition),
1378        };
1379
1380        Statement::IfStatement {
1381            condition,
1382            then_branch: body,
1383            else_branch: None,
1384            source_location: self.loc(),
1385        }
1386    }
1387
1388    fn parse_for_statement(&mut self) -> Statement {
1389        self.advance(); // consume 'for'
1390
1391        let raw_var = self.expect_ident();
1392        let var_name = snake_to_camel(&raw_var);
1393
1394        self.expect(&Token::In);
1395
1396        // Parse start expression
1397        let start_expr = self.parse_expression();
1398
1399        // Expect range operator: .. (inclusive) or ... (exclusive)
1400        let is_exclusive = if *self.peek() == Token::DotDotDot {
1401            self.advance();
1402            true
1403        } else if *self.peek() == Token::DotDot {
1404            self.advance();
1405            false
1406        } else {
1407            self.errors
1408                .push("Expected range operator '..' or '...' in for loop".to_string());
1409            true // default to exclusive
1410        };
1411
1412        let end_expr = self.parse_expression();
1413
1414        // Optional 'do' keyword
1415        self.match_tok(&Token::Do);
1416        self.skip_newlines();
1417
1418        let body = self.parse_statements();
1419        self.expect(&Token::End);
1420
1421        // Construct a C-style for loop AST node
1422        let init = Statement::VariableDecl {
1423            name: var_name.clone(),
1424            var_type: Some(TypeNode::Primitive(PrimitiveTypeName::Bigint)),
1425            mutable: true,
1426            init: start_expr,
1427            source_location: self.loc(),
1428        };
1429
1430        let cmp_op = if is_exclusive {
1431            BinaryOp::Lt
1432        } else {
1433            BinaryOp::Le
1434        };
1435
1436        let condition = Expression::BinaryExpr {
1437            op: cmp_op,
1438            left: Box::new(Expression::Identifier {
1439                name: var_name.clone(),
1440            }),
1441            right: Box::new(end_expr),
1442        };
1443
1444        let update = Statement::ExpressionStatement {
1445            expression: Expression::IncrementExpr {
1446                operand: Box::new(Expression::Identifier { name: var_name }),
1447                prefix: false,
1448            },
1449            source_location: self.loc(),
1450        };
1451
1452        Statement::ForStatement {
1453            init: Box::new(init),
1454            condition,
1455            update: Box::new(update),
1456            body,
1457            source_location: self.loc(),
1458        }
1459    }
1460
1461    fn parse_return_statement(&mut self) -> Statement {
1462        self.advance(); // consume 'return'
1463        let value = if *self.peek() != Token::Newline
1464            && *self.peek() != Token::End
1465            && *self.peek() != Token::Eof
1466        {
1467            Some(self.parse_expression())
1468        } else {
1469            None
1470        };
1471        Statement::ReturnStatement {
1472            value,
1473            source_location: self.loc(),
1474        }
1475    }
1476
1477    fn parse_super_call(&mut self) -> Statement {
1478        // super(args...) in Ruby constructor
1479        self.advance(); // 'super'
1480        self.expect(&Token::LParen);
1481        let mut args = Vec::new();
1482        while *self.peek() != Token::RParen && *self.peek() != Token::Eof {
1483            args.push(self.parse_expression());
1484            if !self.match_tok(&Token::Comma) {
1485                break;
1486            }
1487        }
1488        self.expect(&Token::RParen);
1489
1490        Statement::ExpressionStatement {
1491            expression: Expression::CallExpr {
1492                callee: Box::new(Expression::Identifier {
1493                    name: "super".to_string(),
1494                }),
1495                args,
1496            },
1497            source_location: self.loc(),
1498        }
1499    }
1500
1501    fn parse_ivar_statement(&mut self) -> Statement {
1502        // @var = expr or @var += expr or @var as expression
1503        let raw_name = match self.advance() {
1504            Token::Ivar(name) => name,
1505            _ => "_error".to_string(),
1506        };
1507        let prop_name = snake_to_camel(&raw_name);
1508        let target = Expression::PropertyAccess {
1509            property: prop_name,
1510        };
1511
1512        // Simple assignment: @var = expr
1513        if self.match_tok(&Token::Eq) {
1514            let value = self.parse_expression();
1515            return Statement::Assignment {
1516                target,
1517                value,
1518                source_location: self.loc(),
1519            };
1520        }
1521
1522        // Compound assignments
1523        let compound_op = match self.peek() {
1524            Token::PlusEq => Some(BinaryOp::Add),
1525            Token::MinusEq => Some(BinaryOp::Sub),
1526            Token::StarEq => Some(BinaryOp::Mul),
1527            Token::SlashEq => Some(BinaryOp::Div),
1528            Token::PercentEq => Some(BinaryOp::Mod),
1529            _ => None,
1530        };
1531
1532        if let Some(bin_op) = compound_op {
1533            self.advance();
1534            let rhs = self.parse_expression();
1535            let value = Expression::BinaryExpr {
1536                op: bin_op,
1537                left: Box::new(target.clone()),
1538                right: Box::new(rhs),
1539            };
1540            return Statement::Assignment {
1541                target,
1542                value,
1543                source_location: self.loc(),
1544            };
1545        }
1546
1547        // Expression statement (e.g. @var.method(...))
1548        let expr = self.parse_postfix_from(target);
1549
1550        Statement::ExpressionStatement {
1551            expression: expr,
1552            source_location: self.loc(),
1553        }
1554    }
1555
1556    fn parse_ident_statement(&mut self) -> Statement {
1557        let raw_name = match self.peek().clone() {
1558            Token::Ident(ref name) => name.clone(),
1559            _ => "_error".to_string(),
1560        };
1561
1562        // Check for simple name = expr pattern (variable declaration or assignment)
1563        if self
1564            .tokens
1565            .get(self.pos + 1)
1566            .map_or(false, |t| *t == Token::Eq)
1567        {
1568            self.advance(); // consume ident
1569            self.advance(); // consume '='
1570            let value = self.parse_expression();
1571            let camel_name = snake_to_camel(&raw_name);
1572
1573            if self.declared_locals.contains(&camel_name) {
1574                // Already declared: this is an assignment
1575                return Statement::Assignment {
1576                    target: Expression::Identifier { name: camel_name },
1577                    value,
1578                    source_location: self.loc(),
1579                };
1580            } else {
1581                // First assignment: variable declaration
1582                self.declared_locals.insert(camel_name.clone());
1583                return Statement::VariableDecl {
1584                    name: camel_name,
1585                    var_type: None,
1586                    mutable: true,
1587                    init: value,
1588                    source_location: self.loc(),
1589                };
1590            }
1591        }
1592
1593        // Parse as expression first
1594        let expr = self.parse_expression();
1595
1596        // Simple assignment (e.g. a.b = expr)
1597        if self.match_tok(&Token::Eq) {
1598            let value = self.parse_expression();
1599            return Statement::Assignment {
1600                target: expr,
1601                value,
1602                source_location: self.loc(),
1603            };
1604        }
1605
1606        // Compound assignments
1607        let compound_op = match self.peek() {
1608            Token::PlusEq => Some(BinaryOp::Add),
1609            Token::MinusEq => Some(BinaryOp::Sub),
1610            Token::StarEq => Some(BinaryOp::Mul),
1611            Token::SlashEq => Some(BinaryOp::Div),
1612            Token::PercentEq => Some(BinaryOp::Mod),
1613            _ => None,
1614        };
1615
1616        if let Some(bin_op) = compound_op {
1617            self.advance();
1618            let rhs = self.parse_expression();
1619            let value = Expression::BinaryExpr {
1620                op: bin_op,
1621                left: Box::new(expr.clone()),
1622                right: Box::new(rhs),
1623            };
1624            return Statement::Assignment {
1625                target: expr,
1626                value,
1627                source_location: self.loc(),
1628            };
1629        }
1630
1631        // Expression statement
1632        Statement::ExpressionStatement {
1633            expression: expr,
1634            source_location: self.loc(),
1635        }
1636    }
1637
1638    // -----------------------------------------------------------------------
1639    // Expressions (precedence climbing)
1640    // -----------------------------------------------------------------------
1641
1642    fn parse_expression(&mut self) -> Expression {
1643        self.parse_ternary()
1644    }
1645
1646    /// Ruby ternary: `condition ? consequent : alternate`
1647    fn parse_ternary(&mut self) -> Expression {
1648        let expr = self.parse_or();
1649
1650        if *self.peek() == Token::Question {
1651            self.advance(); // '?'
1652            let consequent = self.parse_expression();
1653            self.expect(&Token::Colon);
1654            let alternate = self.parse_expression();
1655            Expression::TernaryExpr {
1656                condition: Box::new(expr),
1657                consequent: Box::new(consequent),
1658                alternate: Box::new(alternate),
1659            }
1660        } else {
1661            expr
1662        }
1663    }
1664
1665    fn parse_or(&mut self) -> Expression {
1666        let mut left = self.parse_and();
1667        while *self.peek() == Token::Or || *self.peek() == Token::OrOr {
1668            self.advance();
1669            let right = self.parse_and();
1670            left = Expression::BinaryExpr {
1671                op: BinaryOp::Or,
1672                left: Box::new(left),
1673                right: Box::new(right),
1674            };
1675        }
1676        left
1677    }
1678
1679    fn parse_and(&mut self) -> Expression {
1680        let mut left = self.parse_not();
1681        while *self.peek() == Token::And || *self.peek() == Token::AndAnd {
1682            self.advance();
1683            let right = self.parse_not();
1684            left = Expression::BinaryExpr {
1685                op: BinaryOp::And,
1686                left: Box::new(left),
1687                right: Box::new(right),
1688            };
1689        }
1690        left
1691    }
1692
1693    fn parse_not(&mut self) -> Expression {
1694        if *self.peek() == Token::Not || *self.peek() == Token::Bang {
1695            self.advance();
1696            let operand = self.parse_not();
1697            Expression::UnaryExpr {
1698                op: UnaryOp::Not,
1699                operand: Box::new(operand),
1700            }
1701        } else {
1702            self.parse_bit_or()
1703        }
1704    }
1705
1706    fn parse_bit_or(&mut self) -> Expression {
1707        let mut left = self.parse_bit_xor();
1708        while *self.peek() == Token::BitOr {
1709            self.advance();
1710            let right = self.parse_bit_xor();
1711            left = Expression::BinaryExpr {
1712                op: BinaryOp::BitOr,
1713                left: Box::new(left),
1714                right: Box::new(right),
1715            };
1716        }
1717        left
1718    }
1719
1720    fn parse_bit_xor(&mut self) -> Expression {
1721        let mut left = self.parse_bit_and();
1722        while *self.peek() == Token::BitXor {
1723            self.advance();
1724            let right = self.parse_bit_and();
1725            left = Expression::BinaryExpr {
1726                op: BinaryOp::BitXor,
1727                left: Box::new(left),
1728                right: Box::new(right),
1729            };
1730        }
1731        left
1732    }
1733
1734    fn parse_bit_and(&mut self) -> Expression {
1735        let mut left = self.parse_equality();
1736        while *self.peek() == Token::BitAnd {
1737            self.advance();
1738            let right = self.parse_equality();
1739            left = Expression::BinaryExpr {
1740                op: BinaryOp::BitAnd,
1741                left: Box::new(left),
1742                right: Box::new(right),
1743            };
1744        }
1745        left
1746    }
1747
1748    fn parse_equality(&mut self) -> Expression {
1749        let mut left = self.parse_comparison();
1750        loop {
1751            match self.peek() {
1752                Token::EqEq => {
1753                    self.advance();
1754                    let right = self.parse_comparison();
1755                    // == maps to ===
1756                    left = Expression::BinaryExpr {
1757                        op: BinaryOp::StrictEq,
1758                        left: Box::new(left),
1759                        right: Box::new(right),
1760                    };
1761                }
1762                Token::NotEq => {
1763                    self.advance();
1764                    let right = self.parse_comparison();
1765                    // != maps to !==
1766                    left = Expression::BinaryExpr {
1767                        op: BinaryOp::StrictNe,
1768                        left: Box::new(left),
1769                        right: Box::new(right),
1770                    };
1771                }
1772                _ => break,
1773            }
1774        }
1775        left
1776    }
1777
1778    fn parse_comparison(&mut self) -> Expression {
1779        let mut left = self.parse_shift();
1780        loop {
1781            match self.peek() {
1782                Token::Lt => {
1783                    self.advance();
1784                    let right = self.parse_shift();
1785                    left = Expression::BinaryExpr {
1786                        op: BinaryOp::Lt,
1787                        left: Box::new(left),
1788                        right: Box::new(right),
1789                    };
1790                }
1791                Token::Le => {
1792                    self.advance();
1793                    let right = self.parse_shift();
1794                    left = Expression::BinaryExpr {
1795                        op: BinaryOp::Le,
1796                        left: Box::new(left),
1797                        right: Box::new(right),
1798                    };
1799                }
1800                Token::Gt => {
1801                    self.advance();
1802                    let right = self.parse_shift();
1803                    left = Expression::BinaryExpr {
1804                        op: BinaryOp::Gt,
1805                        left: Box::new(left),
1806                        right: Box::new(right),
1807                    };
1808                }
1809                Token::Ge => {
1810                    self.advance();
1811                    let right = self.parse_shift();
1812                    left = Expression::BinaryExpr {
1813                        op: BinaryOp::Ge,
1814                        left: Box::new(left),
1815                        right: Box::new(right),
1816                    };
1817                }
1818                _ => break,
1819            }
1820        }
1821        left
1822    }
1823
1824    fn parse_shift(&mut self) -> Expression {
1825        let mut left = self.parse_additive();
1826        loop {
1827            match self.peek() {
1828                Token::LShift => {
1829                    self.advance();
1830                    let right = self.parse_additive();
1831                    left = Expression::BinaryExpr {
1832                        op: BinaryOp::Shl,
1833                        left: Box::new(left),
1834                        right: Box::new(right),
1835                    };
1836                }
1837                Token::RShift => {
1838                    self.advance();
1839                    let right = self.parse_additive();
1840                    left = Expression::BinaryExpr {
1841                        op: BinaryOp::Shr,
1842                        left: Box::new(left),
1843                        right: Box::new(right),
1844                    };
1845                }
1846                _ => break,
1847            }
1848        }
1849        left
1850    }
1851
1852    fn parse_additive(&mut self) -> Expression {
1853        let mut left = self.parse_multiplicative();
1854        loop {
1855            match self.peek() {
1856                Token::Plus => {
1857                    self.advance();
1858                    let right = self.parse_multiplicative();
1859                    left = Expression::BinaryExpr {
1860                        op: BinaryOp::Add,
1861                        left: Box::new(left),
1862                        right: Box::new(right),
1863                    };
1864                }
1865                Token::Minus => {
1866                    self.advance();
1867                    let right = self.parse_multiplicative();
1868                    left = Expression::BinaryExpr {
1869                        op: BinaryOp::Sub,
1870                        left: Box::new(left),
1871                        right: Box::new(right),
1872                    };
1873                }
1874                _ => break,
1875            }
1876        }
1877        left
1878    }
1879
1880    fn parse_multiplicative(&mut self) -> Expression {
1881        let mut left = self.parse_unary();
1882        loop {
1883            match self.peek() {
1884                Token::Star => {
1885                    self.advance();
1886                    let right = self.parse_unary();
1887                    left = Expression::BinaryExpr {
1888                        op: BinaryOp::Mul,
1889                        left: Box::new(left),
1890                        right: Box::new(right),
1891                    };
1892                }
1893                Token::Slash => {
1894                    self.advance();
1895                    let right = self.parse_unary();
1896                    left = Expression::BinaryExpr {
1897                        op: BinaryOp::Div,
1898                        left: Box::new(left),
1899                        right: Box::new(right),
1900                    };
1901                }
1902                Token::Percent => {
1903                    self.advance();
1904                    let right = self.parse_unary();
1905                    left = Expression::BinaryExpr {
1906                        op: BinaryOp::Mod,
1907                        left: Box::new(left),
1908                        right: Box::new(right),
1909                    };
1910                }
1911                _ => break,
1912            }
1913        }
1914        left
1915    }
1916
1917    fn parse_unary(&mut self) -> Expression {
1918        match self.peek() {
1919            Token::Minus => {
1920                self.advance();
1921                let operand = self.parse_unary();
1922                Expression::UnaryExpr {
1923                    op: UnaryOp::Neg,
1924                    operand: Box::new(operand),
1925                }
1926            }
1927            Token::Tilde => {
1928                self.advance();
1929                let operand = self.parse_unary();
1930                Expression::UnaryExpr {
1931                    op: UnaryOp::BitNot,
1932                    operand: Box::new(operand),
1933                }
1934            }
1935            Token::Bang => {
1936                self.advance();
1937                let operand = self.parse_unary();
1938                Expression::UnaryExpr {
1939                    op: UnaryOp::Not,
1940                    operand: Box::new(operand),
1941                }
1942            }
1943            _ => self.parse_power(),
1944        }
1945    }
1946
1947    /// `**` is right-associative and maps to pow() call.
1948    fn parse_power(&mut self) -> Expression {
1949        let base = self.parse_postfix();
1950
1951        if *self.peek() == Token::DoubleStar {
1952            self.advance();
1953            let exp = self.parse_power(); // right-recursive for right-associativity
1954            Expression::CallExpr {
1955                callee: Box::new(Expression::Identifier {
1956                    name: "pow".to_string(),
1957                }),
1958                args: vec![base, exp],
1959            }
1960        } else {
1961            base
1962        }
1963    }
1964
1965    fn parse_postfix(&mut self) -> Expression {
1966        let expr = self.parse_primary();
1967        self.parse_postfix_from(expr)
1968    }
1969
1970    /// Parse postfix operations (method calls, property access, indexing) from a given expression.
1971    fn parse_postfix_from(&mut self, mut expr: Expression) -> Expression {
1972        loop {
1973            match self.peek().clone() {
1974                Token::Dot => {
1975                    self.advance(); // '.'
1976                    let raw_prop = self.expect_ident();
1977                    let prop = map_builtin_name(&raw_prop);
1978
1979                    // Check if it's a method call
1980                    if *self.peek() == Token::LParen {
1981                        let args = self.parse_call_args();
1982                        // Handle property_access.method(...)
1983                        if matches!(&expr, Expression::Identifier { name } if name == "this") {
1984                            expr = Expression::CallExpr {
1985                                callee: Box::new(Expression::MemberExpr {
1986                                    object: Box::new(Expression::Identifier {
1987                                        name: "this".to_string(),
1988                                    }),
1989                                    property: prop,
1990                                }),
1991                                args,
1992                            };
1993                        } else {
1994                            expr = Expression::CallExpr {
1995                                callee: Box::new(Expression::MemberExpr {
1996                                    object: Box::new(expr),
1997                                    property: prop,
1998                                }),
1999                                args,
2000                            };
2001                        }
2002                    } else {
2003                        // Property access
2004                        if matches!(&expr, Expression::Identifier { name } if name == "this") {
2005                            expr = Expression::PropertyAccess { property: prop };
2006                        } else {
2007                            expr = Expression::MemberExpr {
2008                                object: Box::new(expr),
2009                                property: prop,
2010                            };
2011                        }
2012                    }
2013                }
2014                Token::LParen => {
2015                    let args = self.parse_call_args();
2016                    expr = Expression::CallExpr {
2017                        callee: Box::new(expr),
2018                        args,
2019                    };
2020                }
2021                Token::LBracket => {
2022                    self.advance();
2023                    let index = self.parse_expression();
2024                    self.expect(&Token::RBracket);
2025                    expr = Expression::IndexAccess {
2026                        object: Box::new(expr),
2027                        index: Box::new(index),
2028                    };
2029                }
2030                _ => break,
2031            }
2032        }
2033
2034        expr
2035    }
2036
2037    fn parse_primary(&mut self) -> Expression {
2038        match self.advance() {
2039            Token::NumberLit(v) => Expression::BigIntLiteral { value: v },
2040            Token::TrueLit => Expression::BoolLiteral { value: true },
2041            Token::FalseLit => Expression::BoolLiteral { value: false },
2042            Token::NilLit => Expression::BigIntLiteral { value: 0 },
2043            Token::HexStringLit(v) => Expression::ByteStringLiteral { value: v },
2044            Token::StringLit(v) => Expression::ByteStringLiteral { value: v },
2045            Token::Ivar(name) => {
2046                // @var -> property access
2047                let prop_name = snake_to_camel(&name);
2048                Expression::PropertyAccess {
2049                    property: prop_name,
2050                }
2051            }
2052            Token::Ident(name) => {
2053                let mapped = map_builtin_name(&name);
2054                Expression::Identifier { name: mapped }
2055            }
2056            Token::Assert => {
2057                // assert used as expression (unusual but handle it)
2058                Expression::Identifier {
2059                    name: "assert".to_string(),
2060                }
2061            }
2062            Token::Super => {
2063                Expression::Identifier {
2064                    name: "super".to_string(),
2065                }
2066            }
2067            Token::LParen => {
2068                let expr = self.parse_expression();
2069                self.expect(&Token::RParen);
2070                expr
2071            }
2072            Token::LBracket => {
2073                // Array literal: [elem, ...]
2074                let mut elements = Vec::new();
2075                while *self.peek() != Token::RBracket && *self.peek() != Token::Eof {
2076                    elements.push(self.parse_expression());
2077                    if !self.match_tok(&Token::Comma) {
2078                        break;
2079                    }
2080                }
2081                self.expect(&Token::RBracket);
2082                Expression::ArrayLiteral { elements }
2083            }
2084            other => {
2085                self.errors
2086                    .push(format!("Unexpected token in expression: {:?}", other));
2087                Expression::BigIntLiteral { value: 0 }
2088            }
2089        }
2090    }
2091
2092    fn parse_call_args(&mut self) -> Vec<Expression> {
2093        self.expect(&Token::LParen);
2094        let mut args = Vec::new();
2095        while *self.peek() != Token::RParen && *self.peek() != Token::Eof {
2096            args.push(self.parse_expression());
2097            if !self.match_tok(&Token::Comma) {
2098                break;
2099            }
2100        }
2101        self.expect(&Token::RParen);
2102        args
2103    }
2104}
2105
2106// ---------------------------------------------------------------------------
2107// Constructor builder
2108// ---------------------------------------------------------------------------
2109
2110fn build_constructor(properties: &[PropertyNode], file: &str) -> MethodNode {
2111    // Properties with initializers do not need constructor parameters.
2112    let required_props: Vec<&PropertyNode> =
2113        properties.iter().filter(|p| p.initializer.is_none()).collect();
2114
2115    let params: Vec<ParamNode> = required_props
2116        .iter()
2117        .map(|p| ParamNode {
2118            name: p.name.clone(),
2119            param_type: p.prop_type.clone(),
2120        })
2121        .collect();
2122
2123    let mut body: Vec<Statement> = Vec::new();
2124
2125    // super(...) call
2126    let super_args: Vec<Expression> = required_props
2127        .iter()
2128        .map(|p| Expression::Identifier {
2129            name: p.name.clone(),
2130        })
2131        .collect();
2132    body.push(Statement::ExpressionStatement {
2133        expression: Expression::CallExpr {
2134            callee: Box::new(Expression::Identifier {
2135                name: "super".to_string(),
2136            }),
2137            args: super_args,
2138        },
2139        source_location: SourceLocation {
2140            file: file.to_string(),
2141            line: 1,
2142            column: 0,
2143        },
2144    });
2145
2146    // this.x = x for each required property
2147    for p in &required_props {
2148        body.push(Statement::Assignment {
2149            target: Expression::PropertyAccess {
2150                property: p.name.clone(),
2151            },
2152            value: Expression::Identifier {
2153                name: p.name.clone(),
2154            },
2155            source_location: SourceLocation {
2156                file: file.to_string(),
2157                line: 1,
2158                column: 0,
2159            },
2160        });
2161    }
2162
2163    MethodNode {
2164        name: "constructor".to_string(),
2165        params,
2166        body,
2167        visibility: Visibility::Public,
2168        source_location: SourceLocation {
2169            file: file.to_string(),
2170            line: 1,
2171            column: 0,
2172        },
2173    }
2174}
2175
2176// ---------------------------------------------------------------------------
2177// Bare method call rewriter
2178// ---------------------------------------------------------------------------
2179
2180/// Rewrite bare function calls to declared contract methods as `this.method()` calls.
2181/// In Ruby, `compute_threshold(a, b)` is equivalent to `self.compute_threshold(a, b)`.
2182fn rewrite_bare_method_calls(stmts: &mut [Statement], method_names: &std::collections::HashSet<String>) {
2183    for stmt in stmts.iter_mut() {
2184        rewrite_stmt(stmt, method_names);
2185    }
2186}
2187
2188fn rewrite_expr(expr: &mut Expression, method_names: &std::collections::HashSet<String>) {
2189    match expr {
2190        Expression::CallExpr { callee, args } => {
2191            for arg in args.iter_mut() {
2192                rewrite_expr(arg, method_names);
2193            }
2194            if let Expression::Identifier { name } = callee.as_ref() {
2195                if method_names.contains(name) {
2196                    *callee = Box::new(Expression::PropertyAccess { property: name.clone() });
2197                }
2198            } else {
2199                rewrite_expr(callee.as_mut(), method_names);
2200            }
2201        }
2202        Expression::BinaryExpr { left, right, .. } => {
2203            rewrite_expr(left.as_mut(), method_names);
2204            rewrite_expr(right.as_mut(), method_names);
2205        }
2206        Expression::UnaryExpr { operand, .. } => {
2207            rewrite_expr(operand.as_mut(), method_names);
2208        }
2209        Expression::TernaryExpr { condition, consequent, alternate } => {
2210            rewrite_expr(condition.as_mut(), method_names);
2211            rewrite_expr(consequent.as_mut(), method_names);
2212            rewrite_expr(alternate.as_mut(), method_names);
2213        }
2214        _ => {}
2215    }
2216}
2217
2218fn rewrite_stmt(stmt: &mut Statement, method_names: &std::collections::HashSet<String>) {
2219    match stmt {
2220        Statement::ExpressionStatement { expression, .. } => {
2221            rewrite_expr(expression, method_names);
2222        }
2223        Statement::VariableDecl { init, .. } => {
2224            rewrite_expr(init, method_names);
2225        }
2226        Statement::Assignment { value, .. } => {
2227            rewrite_expr(value, method_names);
2228        }
2229        Statement::ReturnStatement { value, .. } => {
2230            if let Some(v) = value {
2231                rewrite_expr(v, method_names);
2232            }
2233        }
2234        Statement::IfStatement { condition, then_branch, else_branch, .. } => {
2235            rewrite_expr(condition, method_names);
2236            rewrite_bare_method_calls(then_branch, method_names);
2237            if let Some(else_stmts) = else_branch {
2238                rewrite_bare_method_calls(else_stmts, method_names);
2239            }
2240        }
2241        Statement::ForStatement { body, .. } => {
2242            rewrite_bare_method_calls(body, method_names);
2243        }
2244    }
2245}
2246
2247// ---------------------------------------------------------------------------
2248// Tests
2249// ---------------------------------------------------------------------------
2250
2251#[cfg(test)]
2252mod tests {
2253    use super::*;
2254
2255    #[test]
2256    fn test_snake_to_camel() {
2257        assert_eq!(snake_to_camel("hello_world"), "helloWorld");
2258        assert_eq!(snake_to_camel("check_sig"), "checkSig");
2259        assert_eq!(snake_to_camel("already"), "already");
2260        assert_eq!(snake_to_camel("a_b_c"), "aBC");
2261        assert_eq!(snake_to_camel("pub_key_hash"), "pubKeyHash");
2262    }
2263
2264    #[test]
2265    fn test_builtin_name_mapping() {
2266        assert_eq!(map_builtin_name("check_sig"), "checkSig");
2267        assert_eq!(map_builtin_name("hash160"), "hash160");
2268        assert_eq!(map_builtin_name("verify_wots"), "verifyWOTS");
2269        assert_eq!(
2270            map_builtin_name("verify_slh_dsa_sha2_128s"),
2271            "verifySLHDSA_SHA2_128s"
2272        );
2273        assert_eq!(map_builtin_name("ec_add"), "ecAdd");
2274        assert_eq!(map_builtin_name("add_output"), "addOutput");
2275        assert_eq!(map_builtin_name("abs"), "abs");
2276        assert_eq!(map_builtin_name("some_func"), "someFunc");
2277    }
2278
2279    #[test]
2280    fn test_parse_simple_ruby_contract() {
2281        let source = r#"
2282require 'runar'
2283
2284class P2PKH < Runar::SmartContract
2285  prop :pub_key_hash, Addr
2286
2287  def initialize(pub_key_hash)
2288    super(pub_key_hash)
2289    @pub_key_hash = pub_key_hash
2290  end
2291
2292  runar_public sig: Sig, pub_key: PubKey
2293  def unlock(sig, pub_key)
2294    assert hash160(pub_key) == @pub_key_hash
2295    assert check_sig(sig, pub_key)
2296  end
2297end
2298"#;
2299
2300        let result = parse_ruby(source, Some("P2PKH.runar.rb"));
2301        assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
2302        let contract = result.contract.unwrap();
2303        assert_eq!(contract.name, "P2PKH");
2304        assert_eq!(contract.parent_class, "SmartContract");
2305        assert_eq!(contract.properties.len(), 1);
2306        assert_eq!(contract.properties[0].name, "pubKeyHash");
2307        assert!(contract.properties[0].readonly);
2308        assert_eq!(contract.methods.len(), 1);
2309        assert_eq!(contract.methods[0].name, "unlock");
2310        assert_eq!(contract.methods[0].visibility, Visibility::Public);
2311        assert_eq!(contract.methods[0].params.len(), 2);
2312        assert_eq!(contract.methods[0].params[0].name, "sig");
2313        assert_eq!(contract.methods[0].params[1].name, "pubKey");
2314    }
2315
2316    #[test]
2317    fn test_parse_stateful_ruby_contract() {
2318        let source = r#"
2319require 'runar'
2320
2321class Counter < Runar::StatefulSmartContract
2322  prop :count, Bigint
2323
2324  def initialize(count)
2325    super(count)
2326    @count = count
2327  end
2328
2329  runar_public
2330  def increment
2331    @count += 1
2332  end
2333
2334  runar_public
2335  def decrement
2336    assert @count > 0
2337    @count -= 1
2338  end
2339end
2340"#;
2341
2342        let result = parse_ruby(source, Some("Counter.runar.rb"));
2343        assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
2344        let contract = result.contract.unwrap();
2345        assert_eq!(contract.name, "Counter");
2346        assert_eq!(contract.parent_class, "StatefulSmartContract");
2347        assert_eq!(contract.properties.len(), 1);
2348        assert_eq!(contract.properties[0].name, "count");
2349        assert!(!contract.properties[0].readonly);
2350        assert_eq!(contract.methods.len(), 2);
2351        assert_eq!(contract.methods[0].name, "increment");
2352        assert_eq!(contract.methods[0].visibility, Visibility::Public);
2353        assert_eq!(contract.methods[1].name, "decrement");
2354        assert_eq!(contract.methods[1].visibility, Visibility::Public);
2355    }
2356
2357    #[test]
2358    fn test_parse_readonly_property() {
2359        let source = r#"
2360require 'runar'
2361
2362class Token < Runar::StatefulSmartContract
2363  prop :owner, PubKey
2364  prop :balance, Bigint
2365  prop :token_id, ByteString, readonly: true
2366
2367  def initialize(owner, balance, token_id)
2368    super(owner, balance, token_id)
2369    @owner = owner
2370    @balance = balance
2371    @token_id = token_id
2372  end
2373
2374  runar_public sig: Sig, to: PubKey, amount: Bigint, output_satoshis: Bigint
2375  def transfer(sig, to, amount, output_satoshis)
2376    assert check_sig(sig, @owner)
2377    assert amount > 0
2378    assert amount <= @balance
2379    add_output(output_satoshis, to, amount)
2380    add_output(output_satoshis, @owner, @balance - amount)
2381  end
2382end
2383"#;
2384
2385        let result = parse_ruby(source, Some("Token.runar.rb"));
2386        assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
2387        let contract = result.contract.unwrap();
2388        assert_eq!(contract.name, "Token");
2389        assert_eq!(contract.properties.len(), 3);
2390        assert!(!contract.properties[0].readonly); // owner
2391        assert!(!contract.properties[1].readonly); // balance
2392        assert!(contract.properties[2].readonly);  // token_id
2393        assert_eq!(contract.methods.len(), 1);
2394        assert_eq!(contract.methods[0].name, "transfer");
2395        assert_eq!(contract.methods[0].params.len(), 4);
2396    }
2397
2398    #[test]
2399    fn test_parse_for_loop_exclusive() {
2400        let source = r#"
2401require 'runar'
2402
2403class BoundedLoop < Runar::SmartContract
2404  prop :expected_sum, Bigint
2405
2406  runar_public n: Bigint
2407  def verify(n)
2408    total = 0
2409    for i in 0...n do
2410      total += i
2411    end
2412    assert total == @expected_sum
2413  end
2414end
2415"#;
2416
2417        let result = parse_ruby(source, Some("BoundedLoop.runar.rb"));
2418        assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
2419        let contract = result.contract.unwrap();
2420        assert_eq!(contract.methods.len(), 1);
2421        // The for loop should be present in the body
2422        let body = &contract.methods[0].body;
2423        assert!(body.len() >= 3); // total = 0, for loop, assert
2424    }
2425
2426    #[test]
2427    fn test_parse_for_loop_inclusive() {
2428        let source = r#"
2429require 'runar'
2430
2431class InclusiveLoop < Runar::SmartContract
2432  prop :expected_sum, Bigint
2433
2434  runar_public n: Bigint
2435  def verify(n)
2436    total = 0
2437    for i in 0..n
2438      total += i
2439    end
2440    assert total == @expected_sum
2441  end
2442end
2443"#;
2444
2445        let result = parse_ruby(source, Some("InclusiveLoop.runar.rb"));
2446        assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
2447        let contract = result.contract.unwrap();
2448        assert_eq!(contract.methods.len(), 1);
2449    }
2450
2451    #[test]
2452    fn test_parse_if_elsif_else() {
2453        let source = r#"
2454require 'runar'
2455
2456class Branching < Runar::SmartContract
2457  prop :value, Bigint
2458
2459  runar_public x: Bigint
2460  def check(x)
2461    if x > 10
2462      assert @value == 1
2463    elsif x > 5
2464      assert @value == 2
2465    else
2466      assert @value == 3
2467    end
2468  end
2469end
2470"#;
2471
2472        let result = parse_ruby(source, Some("Branching.runar.rb"));
2473        assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
2474        let contract = result.contract.unwrap();
2475        assert_eq!(contract.methods.len(), 1);
2476    }
2477
2478    #[test]
2479    fn test_parse_unless_statement() {
2480        let source = r#"
2481require 'runar'
2482
2483class Checker < Runar::SmartContract
2484  prop :value, Bigint
2485
2486  runar_public
2487  def check
2488    unless @value == 0
2489      assert @value > 0
2490    end
2491  end
2492end
2493"#;
2494
2495        let result = parse_ruby(source, Some("Checker.runar.rb"));
2496        assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
2497        let contract = result.contract.unwrap();
2498        assert_eq!(contract.methods.len(), 1);
2499    }
2500
2501    #[test]
2502    fn test_parse_power_operator() {
2503        let source = r#"
2504require 'runar'
2505
2506class Power < Runar::SmartContract
2507  prop :expected, Bigint
2508
2509  runar_public x: Bigint, y: Bigint
2510  def verify(x, y)
2511    assert x ** y == @expected
2512  end
2513end
2514"#;
2515
2516        let result = parse_ruby(source, Some("Power.runar.rb"));
2517        assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
2518    }
2519
2520    #[test]
2521    fn test_parse_auto_generated_constructor() {
2522        let source = r#"
2523require 'runar'
2524
2525class Simple < Runar::SmartContract
2526  prop :value, Bigint
2527
2528  runar_public x: Bigint
2529  def verify(x)
2530    assert x == @value
2531  end
2532end
2533"#;
2534
2535        let result = parse_ruby(source, Some("Simple.runar.rb"));
2536        assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
2537        let contract = result.contract.unwrap();
2538        // Constructor should be auto-generated
2539        assert_eq!(contract.constructor.name, "constructor");
2540        assert_eq!(contract.constructor.params.len(), 1);
2541        assert_eq!(contract.constructor.params[0].name, "value");
2542    }
2543
2544    #[test]
2545    fn test_parse_parent_class_without_runar_prefix() {
2546        let source = r#"
2547require 'runar'
2548
2549class Simple < SmartContract
2550  prop :value, Bigint
2551
2552  runar_public x: Bigint
2553  def verify(x)
2554    assert x == @value
2555  end
2556end
2557"#;
2558
2559        let result = parse_ruby(source, Some("Simple.runar.rb"));
2560        assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
2561        let contract = result.contract.unwrap();
2562        assert_eq!(contract.parent_class, "SmartContract");
2563    }
2564
2565    #[test]
2566    fn test_parse_hex_string_literal() {
2567        let source = r#"
2568require 'runar'
2569
2570class HexTest < Runar::SmartContract
2571  prop :data, ByteString
2572
2573  runar_public
2574  def verify
2575    assert @data == 'deadbeef'
2576  end
2577end
2578"#;
2579
2580        let result = parse_ruby(source, Some("HexTest.runar.rb"));
2581        assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
2582    }
2583
2584    #[test]
2585    fn test_parse_private_method_with_params() {
2586        let source = r#"
2587require 'runar'
2588
2589class WithHelper < Runar::SmartContract
2590  prop :value, Bigint
2591
2592  params x: Bigint
2593  def helper(x)
2594    return x + 1
2595  end
2596
2597  runar_public n: Bigint
2598  def verify(n)
2599    result = helper(n)
2600    assert result == @value
2601  end
2602end
2603"#;
2604
2605        let result = parse_ruby(source, Some("WithHelper.runar.rb"));
2606        assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
2607        let contract = result.contract.unwrap();
2608        assert_eq!(contract.methods.len(), 2);
2609        assert_eq!(contract.methods[0].name, "helper");
2610        assert_eq!(contract.methods[0].visibility, Visibility::Private);
2611        assert_eq!(contract.methods[1].name, "verify");
2612        assert_eq!(contract.methods[1].visibility, Visibility::Public);
2613    }
2614}