Skip to main content

seqc/
ast.rs

1//! Abstract Syntax Tree for Seq
2//!
3//! Minimal AST sufficient for hello-world and basic programs.
4//! Will be extended as we add more language features.
5
6use crate::types::{Effect, StackType, Type};
7use std::path::PathBuf;
8
9/// Source location for error reporting and tooling
10#[derive(Debug, Clone, PartialEq)]
11pub struct SourceLocation {
12    pub file: PathBuf,
13    /// Start line (0-indexed for LSP compatibility)
14    pub start_line: usize,
15    /// End line (0-indexed, inclusive)
16    pub end_line: usize,
17}
18
19impl SourceLocation {
20    /// Create a new source location with just a single line (for backward compatibility)
21    pub fn new(file: PathBuf, line: usize) -> Self {
22        SourceLocation {
23            file,
24            start_line: line,
25            end_line: line,
26        }
27    }
28
29    /// Create a source location spanning multiple lines
30    pub fn span(file: PathBuf, start_line: usize, end_line: usize) -> Self {
31        debug_assert!(
32            start_line <= end_line,
33            "SourceLocation: start_line ({}) must be <= end_line ({})",
34            start_line,
35            end_line
36        );
37        SourceLocation {
38            file,
39            start_line,
40            end_line,
41        }
42    }
43}
44
45impl std::fmt::Display for SourceLocation {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        if self.start_line == self.end_line {
48            write!(f, "{}:{}", self.file.display(), self.start_line + 1)
49        } else {
50            write!(
51                f,
52                "{}:{}-{}",
53                self.file.display(),
54                self.start_line + 1,
55                self.end_line + 1
56            )
57        }
58    }
59}
60
61/// Include statement
62#[derive(Debug, Clone, PartialEq)]
63pub enum Include {
64    /// Standard library include: `include std:http`
65    Std(String),
66    /// Relative path include: `include "my-utils"`
67    Relative(String),
68    /// FFI library include: `include ffi:readline`
69    Ffi(String),
70}
71
72// ============================================================================
73//                     ALGEBRAIC DATA TYPES (ADTs)
74// ============================================================================
75
76/// A field in a union variant
77/// Example: `response-chan: Int`
78#[derive(Debug, Clone, PartialEq)]
79pub struct UnionField {
80    pub name: String,
81    pub type_name: String, // For now, just store the type name as string
82}
83
84/// A variant in a union type
85/// Example: `Get { response-chan: Int }`
86#[derive(Debug, Clone, PartialEq)]
87pub struct UnionVariant {
88    pub name: String,
89    pub fields: Vec<UnionField>,
90    pub source: Option<SourceLocation>,
91}
92
93/// A union type definition
94/// Example:
95/// ```seq
96/// union Message {
97///   Get { response-chan: Int }
98///   Increment { response-chan: Int }
99///   Report { op: Int, delta: Int, total: Int }
100/// }
101/// ```
102#[derive(Debug, Clone, PartialEq)]
103pub struct UnionDef {
104    pub name: String,
105    pub variants: Vec<UnionVariant>,
106    pub source: Option<SourceLocation>,
107}
108
109/// A pattern in a match expression
110/// For Phase 1: just the variant name (stack-based matching)
111/// Later phases will add field bindings: `Get { chan }`
112#[derive(Debug, Clone, PartialEq)]
113pub enum Pattern {
114    /// Match a variant by name, pushing all fields to stack
115    /// Example: `Get ->` pushes response-chan to stack
116    Variant(String),
117
118    /// Match a variant with named field bindings (Phase 5)
119    /// Example: `Get { chan } ->` binds chan to the response-chan field
120    VariantWithBindings { name: String, bindings: Vec<String> },
121}
122
123/// A single arm in a match expression
124#[derive(Debug, Clone, PartialEq)]
125pub struct MatchArm {
126    pub pattern: Pattern,
127    pub body: Vec<Statement>,
128    /// Source span for error reporting (points to variant name)
129    pub span: Option<Span>,
130}
131
132#[derive(Debug, Clone, PartialEq)]
133pub struct Program {
134    pub includes: Vec<Include>,
135    pub unions: Vec<UnionDef>,
136    pub words: Vec<WordDef>,
137}
138
139#[derive(Debug, Clone, PartialEq)]
140pub struct WordDef {
141    pub name: String,
142    /// Optional stack effect declaration
143    /// Example: ( ..a Int -- ..a Bool )
144    pub effect: Option<Effect>,
145    pub body: Vec<Statement>,
146    /// Source location for error reporting (collision detection)
147    pub source: Option<SourceLocation>,
148    /// Lint IDs that are allowed (suppressed) for this word
149    /// Set via `# seq:allow(lint-id)` annotation before the word definition
150    pub allowed_lints: Vec<String>,
151}
152
153/// Source span for a single token or expression
154#[derive(Debug, Clone, PartialEq, Default)]
155pub struct Span {
156    /// Line number (0-indexed)
157    pub line: usize,
158    /// Start column (0-indexed)
159    pub column: usize,
160    /// Length of the span in characters
161    pub length: usize,
162}
163
164impl Span {
165    pub fn new(line: usize, column: usize, length: usize) -> Self {
166        Span {
167            line,
168            column,
169            length,
170        }
171    }
172}
173
174/// Source span for a quotation, supporting multi-line ranges
175#[derive(Debug, Clone, PartialEq, Default)]
176pub struct QuotationSpan {
177    /// Start line (0-indexed)
178    pub start_line: usize,
179    /// Start column (0-indexed)
180    pub start_column: usize,
181    /// End line (0-indexed)
182    pub end_line: usize,
183    /// End column (0-indexed, exclusive)
184    pub end_column: usize,
185}
186
187impl QuotationSpan {
188    pub fn new(start_line: usize, start_column: usize, end_line: usize, end_column: usize) -> Self {
189        QuotationSpan {
190            start_line,
191            start_column,
192            end_line,
193            end_column,
194        }
195    }
196
197    /// Check if a position (line, column) falls within this span
198    pub fn contains(&self, line: usize, column: usize) -> bool {
199        if line < self.start_line || line > self.end_line {
200            return false;
201        }
202        if line == self.start_line && column < self.start_column {
203            return false;
204        }
205        if line == self.end_line && column >= self.end_column {
206            return false;
207        }
208        true
209    }
210}
211
212#[derive(Debug, Clone, PartialEq)]
213pub enum Statement {
214    /// Integer literal: pushes value onto stack
215    IntLiteral(i64),
216
217    /// Floating-point literal: pushes IEEE 754 double onto stack
218    FloatLiteral(f64),
219
220    /// Boolean literal: pushes true/false onto stack
221    BoolLiteral(bool),
222
223    /// String literal: pushes string onto stack
224    StringLiteral(String),
225
226    /// Symbol literal: pushes symbol onto stack
227    /// Syntax: :foo, :some-name, :ok
228    /// Used for dynamic variant construction and SON.
229    /// Note: Symbols are not currently interned (future optimization).
230    Symbol(String),
231
232    /// Word call: calls another word or built-in
233    /// Contains the word name and optional source span for precise diagnostics
234    WordCall { name: String, span: Option<Span> },
235
236    /// Conditional: if/else/then
237    ///
238    /// Pops an integer from the stack (0 = zero, non-zero = non-zero)
239    /// and executes the appropriate branch
240    If {
241        /// Statements to execute when condition is non-zero (the 'then' clause)
242        then_branch: Vec<Statement>,
243        /// Optional statements to execute when condition is zero (the 'else' clause)
244        else_branch: Option<Vec<Statement>>,
245        /// Source span for error reporting (points to 'if' keyword)
246        span: Option<Span>,
247    },
248
249    /// Quotation: [ ... ]
250    ///
251    /// A block of deferred code (quotation/lambda)
252    /// Quotations are first-class values that can be pushed onto the stack
253    /// and executed later with combinators like `call`, `times`, or `while`
254    ///
255    /// The id field is used by the typechecker to track the inferred type
256    /// (Quotation vs Closure) for this quotation. The id is assigned during parsing.
257    /// The span field records the source location for LSP hover support.
258    Quotation {
259        id: usize,
260        body: Vec<Statement>,
261        span: Option<QuotationSpan>,
262    },
263
264    /// Match expression: pattern matching on union types
265    ///
266    /// Pops a union value from the stack and dispatches to the
267    /// appropriate arm based on the variant tag.
268    ///
269    /// Example:
270    /// ```seq
271    /// match
272    ///   Get -> send-response
273    ///   Increment -> do-increment send-response
274    ///   Report -> aggregate-add
275    /// end
276    /// ```
277    Match {
278        /// The match arms in order
279        arms: Vec<MatchArm>,
280        /// Source span for error reporting (points to 'match' keyword)
281        span: Option<Span>,
282    },
283}
284
285impl Program {
286    pub fn new() -> Self {
287        Program {
288            includes: Vec::new(),
289            unions: Vec::new(),
290            words: Vec::new(),
291        }
292    }
293
294    pub fn find_word(&self, name: &str) -> Option<&WordDef> {
295        self.words.iter().find(|w| w.name == name)
296    }
297
298    /// Validate that all word calls reference either a defined word or a built-in
299    pub fn validate_word_calls(&self) -> Result<(), String> {
300        self.validate_word_calls_with_externals(&[])
301    }
302
303    /// Validate that all word calls reference a defined word, built-in, or external word.
304    ///
305    /// The `external_words` parameter should contain names of words available from
306    /// external sources (e.g., included modules) that should be considered valid.
307    pub fn validate_word_calls_with_externals(
308        &self,
309        external_words: &[&str],
310    ) -> Result<(), String> {
311        // List of known runtime built-ins
312        // IMPORTANT: Keep this in sync with codegen.rs WordCall matching
313        let builtins = [
314            // I/O operations
315            "io.write",
316            "io.write-line",
317            "io.read-line",
318            "io.read-line+",
319            "io.read-n",
320            "int->string",
321            "symbol->string",
322            "string->symbol",
323            // Command-line arguments
324            "args.count",
325            "args.at",
326            // File operations
327            "file.slurp",
328            "file.exists?",
329            "file.for-each-line+",
330            "file.spit",
331            "file.append",
332            "file.delete",
333            "file.size",
334            // Directory operations
335            "dir.exists?",
336            "dir.make",
337            "dir.delete",
338            "dir.list",
339            // String operations
340            "string.concat",
341            "string.length",
342            "string.byte-length",
343            "string.char-at",
344            "string.substring",
345            "char->string",
346            "string.find",
347            "string.split",
348            "string.contains",
349            "string.starts-with",
350            "string.empty?",
351            "string.trim",
352            "string.chomp",
353            "string.to-upper",
354            "string.to-lower",
355            "string.equal?",
356            "string.json-escape",
357            "string->int",
358            // Symbol operations
359            "symbol.=",
360            // Encoding operations
361            "encoding.base64-encode",
362            "encoding.base64-decode",
363            "encoding.base64url-encode",
364            "encoding.base64url-decode",
365            "encoding.hex-encode",
366            "encoding.hex-decode",
367            // Crypto operations
368            "crypto.sha256",
369            "crypto.hmac-sha256",
370            "crypto.constant-time-eq",
371            "crypto.random-bytes",
372            "crypto.random-int",
373            "crypto.uuid4",
374            "crypto.aes-gcm-encrypt",
375            "crypto.aes-gcm-decrypt",
376            "crypto.pbkdf2-sha256",
377            "crypto.ed25519-keypair",
378            "crypto.ed25519-sign",
379            "crypto.ed25519-verify",
380            // HTTP client operations
381            "http.get",
382            "http.post",
383            "http.put",
384            "http.delete",
385            // List operations
386            "list.make",
387            "list.push",
388            "list.get",
389            "list.set",
390            "list.map",
391            "list.filter",
392            "list.fold",
393            "list.each",
394            "list.length",
395            "list.empty?",
396            // Map operations
397            "map.make",
398            "map.get",
399            "map.set",
400            "map.has?",
401            "map.remove",
402            "map.keys",
403            "map.values",
404            "map.size",
405            "map.empty?",
406            // Variant operations
407            "variant.field-count",
408            "variant.tag",
409            "variant.field-at",
410            "variant.append",
411            "variant.last",
412            "variant.init",
413            "variant.make-0",
414            "variant.make-1",
415            "variant.make-2",
416            "variant.make-3",
417            "variant.make-4",
418            // SON wrap aliases
419            "wrap-0",
420            "wrap-1",
421            "wrap-2",
422            "wrap-3",
423            "wrap-4",
424            // Integer arithmetic operations
425            "i.add",
426            "i.subtract",
427            "i.multiply",
428            "i.divide",
429            "i.modulo",
430            // Terse integer arithmetic
431            "i.+",
432            "i.-",
433            "i.*",
434            "i./",
435            "i.%",
436            // Integer comparison operations (return 0 or 1)
437            "i.=",
438            "i.<",
439            "i.>",
440            "i.<=",
441            "i.>=",
442            "i.<>",
443            // Integer comparison operations (verbose form)
444            "i.eq",
445            "i.lt",
446            "i.gt",
447            "i.lte",
448            "i.gte",
449            "i.neq",
450            // Stack operations (simple - no parameters)
451            "dup",
452            "drop",
453            "swap",
454            "over",
455            "rot",
456            "nip",
457            "tuck",
458            "2dup",
459            "3drop",
460            "pick",
461            "roll",
462            // Aux stack operations
463            ">aux",
464            "aux>",
465            // Boolean operations
466            "and",
467            "or",
468            "not",
469            // Bitwise operations
470            "band",
471            "bor",
472            "bxor",
473            "bnot",
474            "shl",
475            "shr",
476            "popcount",
477            "clz",
478            "ctz",
479            "int-bits",
480            // Channel operations
481            "chan.make",
482            "chan.send",
483            "chan.receive",
484            "chan.close",
485            "chan.yield",
486            // Quotation operations
487            "call",
488            "strand.spawn",
489            "strand.weave",
490            "strand.resume",
491            "strand.weave-cancel",
492            "yield",
493            "cond",
494            // TCP operations
495            "tcp.listen",
496            "tcp.accept",
497            "tcp.read",
498            "tcp.write",
499            "tcp.close",
500            // OS operations
501            "os.getenv",
502            "os.home-dir",
503            "os.current-dir",
504            "os.path-exists",
505            "os.path-is-file",
506            "os.path-is-dir",
507            "os.path-join",
508            "os.path-parent",
509            "os.path-filename",
510            "os.exit",
511            "os.name",
512            "os.arch",
513            // Signal handling
514            "signal.trap",
515            "signal.received?",
516            "signal.pending?",
517            "signal.default",
518            "signal.ignore",
519            "signal.clear",
520            "signal.SIGINT",
521            "signal.SIGTERM",
522            "signal.SIGHUP",
523            "signal.SIGPIPE",
524            "signal.SIGUSR1",
525            "signal.SIGUSR2",
526            "signal.SIGCHLD",
527            "signal.SIGALRM",
528            "signal.SIGCONT",
529            // Terminal operations
530            "terminal.raw-mode",
531            "terminal.read-char",
532            "terminal.read-char?",
533            "terminal.width",
534            "terminal.height",
535            "terminal.flush",
536            // Float arithmetic operations (verbose form)
537            "f.add",
538            "f.subtract",
539            "f.multiply",
540            "f.divide",
541            // Float arithmetic operations (terse form)
542            "f.+",
543            "f.-",
544            "f.*",
545            "f./",
546            // Float comparison operations (symbol form)
547            "f.=",
548            "f.<",
549            "f.>",
550            "f.<=",
551            "f.>=",
552            "f.<>",
553            // Float comparison operations (verbose form)
554            "f.eq",
555            "f.lt",
556            "f.gt",
557            "f.lte",
558            "f.gte",
559            "f.neq",
560            // Type conversions
561            "int->float",
562            "float->int",
563            "float->string",
564            "string->float",
565            // Test framework operations
566            "test.init",
567            "test.finish",
568            "test.has-failures",
569            "test.assert",
570            "test.assert-not",
571            "test.assert-eq",
572            "test.assert-eq-str",
573            "test.fail",
574            "test.pass-count",
575            "test.fail-count",
576            // Time operations
577            "time.now",
578            "time.nanos",
579            "time.sleep-ms",
580            // SON serialization
581            "son.dump",
582            "son.dump-pretty",
583            // Stack introspection (for REPL)
584            "stack.dump",
585            // Regex operations
586            "regex.match?",
587            "regex.find",
588            "regex.find-all",
589            "regex.replace",
590            "regex.replace-all",
591            "regex.captures",
592            "regex.split",
593            "regex.valid?",
594            // Compression operations
595            "compress.gzip",
596            "compress.gzip-level",
597            "compress.gunzip",
598            "compress.zstd",
599            "compress.zstd-level",
600            "compress.unzstd",
601        ];
602
603        for word in &self.words {
604            self.validate_statements(&word.body, &word.name, &builtins, external_words)?;
605        }
606
607        Ok(())
608    }
609
610    /// Helper to validate word calls in a list of statements (recursively)
611    fn validate_statements(
612        &self,
613        statements: &[Statement],
614        word_name: &str,
615        builtins: &[&str],
616        external_words: &[&str],
617    ) -> Result<(), String> {
618        for statement in statements {
619            match statement {
620                Statement::WordCall { name, .. } => {
621                    // Check if it's a built-in
622                    if builtins.contains(&name.as_str()) {
623                        continue;
624                    }
625                    // Check if it's a user-defined word
626                    if self.find_word(name).is_some() {
627                        continue;
628                    }
629                    // Check if it's an external word (from includes)
630                    if external_words.contains(&name.as_str()) {
631                        continue;
632                    }
633                    // Undefined word!
634                    return Err(format!(
635                        "Undefined word '{}' called in word '{}'. \
636                         Did you forget to define it or misspell a built-in?",
637                        name, word_name
638                    ));
639                }
640                Statement::If {
641                    then_branch,
642                    else_branch,
643                    span: _,
644                } => {
645                    // Recursively validate both branches
646                    self.validate_statements(then_branch, word_name, builtins, external_words)?;
647                    if let Some(eb) = else_branch {
648                        self.validate_statements(eb, word_name, builtins, external_words)?;
649                    }
650                }
651                Statement::Quotation { body, .. } => {
652                    // Recursively validate quotation body
653                    self.validate_statements(body, word_name, builtins, external_words)?;
654                }
655                Statement::Match { arms, span: _ } => {
656                    // Recursively validate each match arm's body
657                    for arm in arms {
658                        self.validate_statements(&arm.body, word_name, builtins, external_words)?;
659                    }
660                }
661                _ => {} // Literals don't need validation
662            }
663        }
664        Ok(())
665    }
666
667    /// Generate constructor words for all union definitions
668    ///
669    /// Maximum number of fields a variant can have (limited by runtime support)
670    pub const MAX_VARIANT_FIELDS: usize = 12;
671
672    /// Generate helper words for union types:
673    /// 1. Constructors: `Make-VariantName` - creates variant instances
674    /// 2. Predicates: `is-VariantName?` - tests if value is a specific variant
675    /// 3. Accessors: `VariantName-fieldname` - extracts field values (RFC #345)
676    ///
677    /// Example: For `union Message { Get { chan: Int } }`
678    /// Generates:
679    ///   `: Make-Get ( Int -- Message ) :Get variant.make-1 ;`
680    ///   `: is-Get? ( Message -- Bool ) variant.tag :Get symbol.= ;`
681    ///   `: Get-chan ( Message -- Int ) 0 variant.field-at ;`
682    ///
683    /// Returns an error if any variant exceeds the maximum field count.
684    pub fn generate_constructors(&mut self) -> Result<(), String> {
685        let mut new_words = Vec::new();
686
687        for union_def in &self.unions {
688            for variant in &union_def.variants {
689                let field_count = variant.fields.len();
690
691                // Check field count limit before generating constructor
692                if field_count > Self::MAX_VARIANT_FIELDS {
693                    return Err(format!(
694                        "Variant '{}' in union '{}' has {} fields, but the maximum is {}. \
695                         Consider grouping fields into nested union types.",
696                        variant.name,
697                        union_def.name,
698                        field_count,
699                        Self::MAX_VARIANT_FIELDS
700                    ));
701                }
702
703                // 1. Generate constructor: Make-VariantName
704                let constructor_name = format!("Make-{}", variant.name);
705                let mut input_stack = StackType::RowVar("a".to_string());
706                for field in &variant.fields {
707                    let field_type = parse_type_name(&field.type_name);
708                    input_stack = input_stack.push(field_type);
709                }
710                let output_stack =
711                    StackType::RowVar("a".to_string()).push(Type::Union(union_def.name.clone()));
712                let effect = Effect::new(input_stack, output_stack);
713                let body = vec![
714                    Statement::Symbol(variant.name.clone()),
715                    Statement::WordCall {
716                        name: format!("variant.make-{}", field_count),
717                        span: None,
718                    },
719                ];
720                new_words.push(WordDef {
721                    name: constructor_name,
722                    effect: Some(effect),
723                    body,
724                    source: variant.source.clone(),
725                    allowed_lints: vec![],
726                });
727
728                // 2. Generate predicate: is-VariantName?
729                // Effect: ( UnionType -- Bool )
730                // Body: variant.tag :VariantName symbol.=
731                let predicate_name = format!("is-{}?", variant.name);
732                let predicate_input =
733                    StackType::RowVar("a".to_string()).push(Type::Union(union_def.name.clone()));
734                let predicate_output = StackType::RowVar("a".to_string()).push(Type::Bool);
735                let predicate_effect = Effect::new(predicate_input, predicate_output);
736                let predicate_body = vec![
737                    Statement::WordCall {
738                        name: "variant.tag".to_string(),
739                        span: None,
740                    },
741                    Statement::Symbol(variant.name.clone()),
742                    Statement::WordCall {
743                        name: "symbol.=".to_string(),
744                        span: None,
745                    },
746                ];
747                new_words.push(WordDef {
748                    name: predicate_name,
749                    effect: Some(predicate_effect),
750                    body: predicate_body,
751                    source: variant.source.clone(),
752                    allowed_lints: vec![],
753                });
754
755                // 3. Generate field accessors: VariantName-fieldname
756                // Effect: ( UnionType -- FieldType )
757                // Body: N variant.field-at
758                for (index, field) in variant.fields.iter().enumerate() {
759                    let accessor_name = format!("{}-{}", variant.name, field.name);
760                    let field_type = parse_type_name(&field.type_name);
761                    let accessor_input = StackType::RowVar("a".to_string())
762                        .push(Type::Union(union_def.name.clone()));
763                    let accessor_output = StackType::RowVar("a".to_string()).push(field_type);
764                    let accessor_effect = Effect::new(accessor_input, accessor_output);
765                    let accessor_body = vec![
766                        Statement::IntLiteral(index as i64),
767                        Statement::WordCall {
768                            name: "variant.field-at".to_string(),
769                            span: None,
770                        },
771                    ];
772                    new_words.push(WordDef {
773                        name: accessor_name,
774                        effect: Some(accessor_effect),
775                        body: accessor_body,
776                        source: variant.source.clone(), // Use variant's source for field accessors
777                        allowed_lints: vec![],
778                    });
779                }
780            }
781        }
782
783        self.words.extend(new_words);
784        Ok(())
785    }
786
787    /// RFC #345: Fix up type variables in stack effects that should be union types
788    ///
789    /// When parsing files with includes, type variables like "Message" in
790    /// `( Message -- Int )` may be parsed as `Type::Var("Message")` if the
791    /// union definition is in an included file. After resolving includes,
792    /// we know all union names and can convert these to `Type::Union("Message")`.
793    ///
794    /// This ensures proper nominal type checking for union types across files.
795    pub fn fixup_union_types(&mut self) {
796        // Collect all union names from the program
797        let union_names: std::collections::HashSet<String> =
798            self.unions.iter().map(|u| u.name.clone()).collect();
799
800        // Fix up types in all word effects
801        for word in &mut self.words {
802            if let Some(ref mut effect) = word.effect {
803                Self::fixup_stack_type(&mut effect.inputs, &union_names);
804                Self::fixup_stack_type(&mut effect.outputs, &union_names);
805            }
806        }
807    }
808
809    /// Recursively fix up types in a stack type
810    fn fixup_stack_type(stack: &mut StackType, union_names: &std::collections::HashSet<String>) {
811        match stack {
812            StackType::Empty | StackType::RowVar(_) => {}
813            StackType::Cons { rest, top } => {
814                Self::fixup_type(top, union_names);
815                Self::fixup_stack_type(rest, union_names);
816            }
817        }
818    }
819
820    /// Fix up a single type, converting Type::Var to Type::Union if it matches a union name
821    fn fixup_type(ty: &mut Type, union_names: &std::collections::HashSet<String>) {
822        match ty {
823            Type::Var(name) if union_names.contains(name) => {
824                *ty = Type::Union(name.clone());
825            }
826            Type::Quotation(effect) => {
827                Self::fixup_stack_type(&mut effect.inputs, union_names);
828                Self::fixup_stack_type(&mut effect.outputs, union_names);
829            }
830            Type::Closure { effect, captures } => {
831                Self::fixup_stack_type(&mut effect.inputs, union_names);
832                Self::fixup_stack_type(&mut effect.outputs, union_names);
833                for cap in captures {
834                    Self::fixup_type(cap, union_names);
835                }
836            }
837            _ => {}
838        }
839    }
840}
841
842/// Parse a type name string into a Type
843/// Used by constructor generation to build stack effects
844fn parse_type_name(name: &str) -> Type {
845    match name {
846        "Int" => Type::Int,
847        "Float" => Type::Float,
848        "Bool" => Type::Bool,
849        "String" => Type::String,
850        "Channel" => Type::Channel,
851        other => Type::Union(other.to_string()),
852    }
853}
854
855impl Default for Program {
856    fn default() -> Self {
857        Self::new()
858    }
859}
860
861#[cfg(test)]
862mod tests {
863    use super::*;
864
865    #[test]
866    fn test_validate_builtin_words() {
867        let program = Program {
868            includes: vec![],
869            unions: vec![],
870            words: vec![WordDef {
871                name: "main".to_string(),
872                effect: None,
873                body: vec![
874                    Statement::IntLiteral(2),
875                    Statement::IntLiteral(3),
876                    Statement::WordCall {
877                        name: "i.add".to_string(),
878                        span: None,
879                    },
880                    Statement::WordCall {
881                        name: "io.write-line".to_string(),
882                        span: None,
883                    },
884                ],
885                source: None,
886                allowed_lints: vec![],
887            }],
888        };
889
890        // Should succeed - i.add and io.write-line are built-ins
891        assert!(program.validate_word_calls().is_ok());
892    }
893
894    #[test]
895    fn test_validate_user_defined_words() {
896        let program = Program {
897            includes: vec![],
898            unions: vec![],
899            words: vec![
900                WordDef {
901                    name: "helper".to_string(),
902                    effect: None,
903                    body: vec![Statement::IntLiteral(42)],
904                    source: None,
905                    allowed_lints: vec![],
906                },
907                WordDef {
908                    name: "main".to_string(),
909                    effect: None,
910                    body: vec![Statement::WordCall {
911                        name: "helper".to_string(),
912                        span: None,
913                    }],
914                    source: None,
915                    allowed_lints: vec![],
916                },
917            ],
918        };
919
920        // Should succeed - helper is defined
921        assert!(program.validate_word_calls().is_ok());
922    }
923
924    #[test]
925    fn test_validate_undefined_word() {
926        let program = Program {
927            includes: vec![],
928            unions: vec![],
929            words: vec![WordDef {
930                name: "main".to_string(),
931                effect: None,
932                body: vec![Statement::WordCall {
933                    name: "undefined_word".to_string(),
934                    span: None,
935                }],
936                source: None,
937                allowed_lints: vec![],
938            }],
939        };
940
941        // Should fail - undefined_word is not a built-in or user-defined word
942        let result = program.validate_word_calls();
943        assert!(result.is_err());
944        let error = result.unwrap_err();
945        assert!(error.contains("undefined_word"));
946        assert!(error.contains("main"));
947    }
948
949    #[test]
950    fn test_validate_misspelled_builtin() {
951        let program = Program {
952            includes: vec![],
953            unions: vec![],
954            words: vec![WordDef {
955                name: "main".to_string(),
956                effect: None,
957                body: vec![Statement::WordCall {
958                    name: "wrte_line".to_string(),
959                    span: None,
960                }], // typo
961                source: None,
962                allowed_lints: vec![],
963            }],
964        };
965
966        // Should fail with helpful message
967        let result = program.validate_word_calls();
968        assert!(result.is_err());
969        let error = result.unwrap_err();
970        assert!(error.contains("wrte_line"));
971        assert!(error.contains("misspell"));
972    }
973
974    #[test]
975    fn test_generate_constructors() {
976        let mut program = Program {
977            includes: vec![],
978            unions: vec![UnionDef {
979                name: "Message".to_string(),
980                variants: vec![
981                    UnionVariant {
982                        name: "Get".to_string(),
983                        fields: vec![UnionField {
984                            name: "response-chan".to_string(),
985                            type_name: "Int".to_string(),
986                        }],
987                        source: None,
988                    },
989                    UnionVariant {
990                        name: "Put".to_string(),
991                        fields: vec![
992                            UnionField {
993                                name: "value".to_string(),
994                                type_name: "String".to_string(),
995                            },
996                            UnionField {
997                                name: "response-chan".to_string(),
998                                type_name: "Int".to_string(),
999                            },
1000                        ],
1001                        source: None,
1002                    },
1003                ],
1004                source: None,
1005            }],
1006            words: vec![],
1007        };
1008
1009        // Generate constructors, predicates, and accessors
1010        program.generate_constructors().unwrap();
1011
1012        // Should have 7 words:
1013        // - Get variant: Make-Get, is-Get?, Get-response-chan (1 field)
1014        // - Put variant: Make-Put, is-Put?, Put-value, Put-response-chan (2 fields)
1015        assert_eq!(program.words.len(), 7);
1016
1017        // Check Make-Get constructor
1018        let make_get = program
1019            .find_word("Make-Get")
1020            .expect("Make-Get should exist");
1021        assert_eq!(make_get.name, "Make-Get");
1022        assert!(make_get.effect.is_some());
1023        let effect = make_get.effect.as_ref().unwrap();
1024        // Input: ( ..a Int -- )
1025        // Output: ( ..a Message -- )
1026        assert_eq!(
1027            format!("{:?}", effect.outputs),
1028            "Cons { rest: RowVar(\"a\"), top: Union(\"Message\") }"
1029        );
1030
1031        // Check Make-Put constructor
1032        let make_put = program
1033            .find_word("Make-Put")
1034            .expect("Make-Put should exist");
1035        assert_eq!(make_put.name, "Make-Put");
1036        assert!(make_put.effect.is_some());
1037
1038        // Check the body generates correct code
1039        // Make-Get should be: :Get variant.make-1
1040        assert_eq!(make_get.body.len(), 2);
1041        match &make_get.body[0] {
1042            Statement::Symbol(s) if s == "Get" => {}
1043            other => panic!("Expected Symbol(\"Get\") for variant tag, got {:?}", other),
1044        }
1045        match &make_get.body[1] {
1046            Statement::WordCall { name, span: None } if name == "variant.make-1" => {}
1047            _ => panic!("Expected WordCall(variant.make-1)"),
1048        }
1049
1050        // Make-Put should be: :Put variant.make-2
1051        assert_eq!(make_put.body.len(), 2);
1052        match &make_put.body[0] {
1053            Statement::Symbol(s) if s == "Put" => {}
1054            other => panic!("Expected Symbol(\"Put\") for variant tag, got {:?}", other),
1055        }
1056        match &make_put.body[1] {
1057            Statement::WordCall { name, span: None } if name == "variant.make-2" => {}
1058            _ => panic!("Expected WordCall(variant.make-2)"),
1059        }
1060
1061        // Check is-Get? predicate
1062        let is_get = program.find_word("is-Get?").expect("is-Get? should exist");
1063        assert_eq!(is_get.name, "is-Get?");
1064        assert!(is_get.effect.is_some());
1065        let effect = is_get.effect.as_ref().unwrap();
1066        // Input: ( ..a Message -- )
1067        // Output: ( ..a Bool -- )
1068        assert_eq!(
1069            format!("{:?}", effect.outputs),
1070            "Cons { rest: RowVar(\"a\"), top: Bool }"
1071        );
1072
1073        // Check Get-response-chan accessor
1074        let get_chan = program
1075            .find_word("Get-response-chan")
1076            .expect("Get-response-chan should exist");
1077        assert_eq!(get_chan.name, "Get-response-chan");
1078        assert!(get_chan.effect.is_some());
1079        let effect = get_chan.effect.as_ref().unwrap();
1080        // Input: ( ..a Message -- )
1081        // Output: ( ..a Int -- )
1082        assert_eq!(
1083            format!("{:?}", effect.outputs),
1084            "Cons { rest: RowVar(\"a\"), top: Int }"
1085        );
1086    }
1087}