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.push!",
389            "list.get",
390            "list.set",
391            "list.map",
392            "list.filter",
393            "list.fold",
394            "list.each",
395            "list.length",
396            "list.empty?",
397            // Map operations
398            "map.make",
399            "map.get",
400            "map.set",
401            "map.has?",
402            "map.remove",
403            "map.keys",
404            "map.values",
405            "map.size",
406            "map.empty?",
407            // Variant operations
408            "variant.field-count",
409            "variant.tag",
410            "variant.field-at",
411            "variant.append",
412            "variant.last",
413            "variant.init",
414            "variant.make-0",
415            "variant.make-1",
416            "variant.make-2",
417            "variant.make-3",
418            "variant.make-4",
419            // SON wrap aliases
420            "wrap-0",
421            "wrap-1",
422            "wrap-2",
423            "wrap-3",
424            "wrap-4",
425            // Integer arithmetic operations
426            "i.add",
427            "i.subtract",
428            "i.multiply",
429            "i.divide",
430            "i.modulo",
431            // Terse integer arithmetic
432            "i.+",
433            "i.-",
434            "i.*",
435            "i./",
436            "i.%",
437            // Integer comparison operations (return 0 or 1)
438            "i.=",
439            "i.<",
440            "i.>",
441            "i.<=",
442            "i.>=",
443            "i.<>",
444            // Integer comparison operations (verbose form)
445            "i.eq",
446            "i.lt",
447            "i.gt",
448            "i.lte",
449            "i.gte",
450            "i.neq",
451            // Stack operations (simple - no parameters)
452            "dup",
453            "drop",
454            "swap",
455            "over",
456            "rot",
457            "nip",
458            "tuck",
459            "2dup",
460            "3drop",
461            "pick",
462            "roll",
463            // Aux stack operations
464            ">aux",
465            "aux>",
466            // Boolean operations
467            "and",
468            "or",
469            "not",
470            // Bitwise operations
471            "band",
472            "bor",
473            "bxor",
474            "bnot",
475            "shl",
476            "shr",
477            "popcount",
478            "clz",
479            "ctz",
480            "int-bits",
481            // Channel operations
482            "chan.make",
483            "chan.send",
484            "chan.receive",
485            "chan.close",
486            "chan.yield",
487            // Quotation operations
488            "call",
489            "strand.spawn",
490            "strand.weave",
491            "strand.resume",
492            "strand.weave-cancel",
493            "yield",
494            "cond",
495            // TCP operations
496            "tcp.listen",
497            "tcp.accept",
498            "tcp.read",
499            "tcp.write",
500            "tcp.close",
501            // OS operations
502            "os.getenv",
503            "os.home-dir",
504            "os.current-dir",
505            "os.path-exists",
506            "os.path-is-file",
507            "os.path-is-dir",
508            "os.path-join",
509            "os.path-parent",
510            "os.path-filename",
511            "os.exit",
512            "os.name",
513            "os.arch",
514            // Signal handling
515            "signal.trap",
516            "signal.received?",
517            "signal.pending?",
518            "signal.default",
519            "signal.ignore",
520            "signal.clear",
521            "signal.SIGINT",
522            "signal.SIGTERM",
523            "signal.SIGHUP",
524            "signal.SIGPIPE",
525            "signal.SIGUSR1",
526            "signal.SIGUSR2",
527            "signal.SIGCHLD",
528            "signal.SIGALRM",
529            "signal.SIGCONT",
530            // Terminal operations
531            "terminal.raw-mode",
532            "terminal.read-char",
533            "terminal.read-char?",
534            "terminal.width",
535            "terminal.height",
536            "terminal.flush",
537            // Float arithmetic operations (verbose form)
538            "f.add",
539            "f.subtract",
540            "f.multiply",
541            "f.divide",
542            // Float arithmetic operations (terse form)
543            "f.+",
544            "f.-",
545            "f.*",
546            "f./",
547            // Float comparison operations (symbol form)
548            "f.=",
549            "f.<",
550            "f.>",
551            "f.<=",
552            "f.>=",
553            "f.<>",
554            // Float comparison operations (verbose form)
555            "f.eq",
556            "f.lt",
557            "f.gt",
558            "f.lte",
559            "f.gte",
560            "f.neq",
561            // Type conversions
562            "int->float",
563            "float->int",
564            "float->string",
565            "string->float",
566            // Test framework operations
567            "test.init",
568            "test.finish",
569            "test.has-failures",
570            "test.assert",
571            "test.assert-not",
572            "test.assert-eq",
573            "test.assert-eq-str",
574            "test.fail",
575            "test.pass-count",
576            "test.fail-count",
577            // Time operations
578            "time.now",
579            "time.nanos",
580            "time.sleep-ms",
581            // SON serialization
582            "son.dump",
583            "son.dump-pretty",
584            // Stack introspection (for REPL)
585            "stack.dump",
586            // Regex operations
587            "regex.match?",
588            "regex.find",
589            "regex.find-all",
590            "regex.replace",
591            "regex.replace-all",
592            "regex.captures",
593            "regex.split",
594            "regex.valid?",
595            // Compression operations
596            "compress.gzip",
597            "compress.gzip-level",
598            "compress.gunzip",
599            "compress.zstd",
600            "compress.zstd-level",
601            "compress.unzstd",
602        ];
603
604        for word in &self.words {
605            self.validate_statements(&word.body, &word.name, &builtins, external_words)?;
606        }
607
608        Ok(())
609    }
610
611    /// Helper to validate word calls in a list of statements (recursively)
612    fn validate_statements(
613        &self,
614        statements: &[Statement],
615        word_name: &str,
616        builtins: &[&str],
617        external_words: &[&str],
618    ) -> Result<(), String> {
619        for statement in statements {
620            match statement {
621                Statement::WordCall { name, .. } => {
622                    // Check if it's a built-in
623                    if builtins.contains(&name.as_str()) {
624                        continue;
625                    }
626                    // Check if it's a user-defined word
627                    if self.find_word(name).is_some() {
628                        continue;
629                    }
630                    // Check if it's an external word (from includes)
631                    if external_words.contains(&name.as_str()) {
632                        continue;
633                    }
634                    // Undefined word!
635                    return Err(format!(
636                        "Undefined word '{}' called in word '{}'. \
637                         Did you forget to define it or misspell a built-in?",
638                        name, word_name
639                    ));
640                }
641                Statement::If {
642                    then_branch,
643                    else_branch,
644                    span: _,
645                } => {
646                    // Recursively validate both branches
647                    self.validate_statements(then_branch, word_name, builtins, external_words)?;
648                    if let Some(eb) = else_branch {
649                        self.validate_statements(eb, word_name, builtins, external_words)?;
650                    }
651                }
652                Statement::Quotation { body, .. } => {
653                    // Recursively validate quotation body
654                    self.validate_statements(body, word_name, builtins, external_words)?;
655                }
656                Statement::Match { arms, span: _ } => {
657                    // Recursively validate each match arm's body
658                    for arm in arms {
659                        self.validate_statements(&arm.body, word_name, builtins, external_words)?;
660                    }
661                }
662                _ => {} // Literals don't need validation
663            }
664        }
665        Ok(())
666    }
667
668    /// Generate constructor words for all union definitions
669    ///
670    /// Maximum number of fields a variant can have (limited by runtime support)
671    pub const MAX_VARIANT_FIELDS: usize = 12;
672
673    /// Generate helper words for union types:
674    /// 1. Constructors: `Make-VariantName` - creates variant instances
675    /// 2. Predicates: `is-VariantName?` - tests if value is a specific variant
676    /// 3. Accessors: `VariantName-fieldname` - extracts field values (RFC #345)
677    ///
678    /// Example: For `union Message { Get { chan: Int } }`
679    /// Generates:
680    ///   `: Make-Get ( Int -- Message ) :Get variant.make-1 ;`
681    ///   `: is-Get? ( Message -- Bool ) variant.tag :Get symbol.= ;`
682    ///   `: Get-chan ( Message -- Int ) 0 variant.field-at ;`
683    ///
684    /// Returns an error if any variant exceeds the maximum field count.
685    pub fn generate_constructors(&mut self) -> Result<(), String> {
686        let mut new_words = Vec::new();
687
688        for union_def in &self.unions {
689            for variant in &union_def.variants {
690                let field_count = variant.fields.len();
691
692                // Check field count limit before generating constructor
693                if field_count > Self::MAX_VARIANT_FIELDS {
694                    return Err(format!(
695                        "Variant '{}' in union '{}' has {} fields, but the maximum is {}. \
696                         Consider grouping fields into nested union types.",
697                        variant.name,
698                        union_def.name,
699                        field_count,
700                        Self::MAX_VARIANT_FIELDS
701                    ));
702                }
703
704                // 1. Generate constructor: Make-VariantName
705                let constructor_name = format!("Make-{}", variant.name);
706                let mut input_stack = StackType::RowVar("a".to_string());
707                for field in &variant.fields {
708                    let field_type = parse_type_name(&field.type_name);
709                    input_stack = input_stack.push(field_type);
710                }
711                let output_stack =
712                    StackType::RowVar("a".to_string()).push(Type::Union(union_def.name.clone()));
713                let effect = Effect::new(input_stack, output_stack);
714                let body = vec![
715                    Statement::Symbol(variant.name.clone()),
716                    Statement::WordCall {
717                        name: format!("variant.make-{}", field_count),
718                        span: None,
719                    },
720                ];
721                new_words.push(WordDef {
722                    name: constructor_name,
723                    effect: Some(effect),
724                    body,
725                    source: variant.source.clone(),
726                    allowed_lints: vec![],
727                });
728
729                // 2. Generate predicate: is-VariantName?
730                // Effect: ( UnionType -- Bool )
731                // Body: variant.tag :VariantName symbol.=
732                let predicate_name = format!("is-{}?", variant.name);
733                let predicate_input =
734                    StackType::RowVar("a".to_string()).push(Type::Union(union_def.name.clone()));
735                let predicate_output = StackType::RowVar("a".to_string()).push(Type::Bool);
736                let predicate_effect = Effect::new(predicate_input, predicate_output);
737                let predicate_body = vec![
738                    Statement::WordCall {
739                        name: "variant.tag".to_string(),
740                        span: None,
741                    },
742                    Statement::Symbol(variant.name.clone()),
743                    Statement::WordCall {
744                        name: "symbol.=".to_string(),
745                        span: None,
746                    },
747                ];
748                new_words.push(WordDef {
749                    name: predicate_name,
750                    effect: Some(predicate_effect),
751                    body: predicate_body,
752                    source: variant.source.clone(),
753                    allowed_lints: vec![],
754                });
755
756                // 3. Generate field accessors: VariantName-fieldname
757                // Effect: ( UnionType -- FieldType )
758                // Body: N variant.field-at
759                for (index, field) in variant.fields.iter().enumerate() {
760                    let accessor_name = format!("{}-{}", variant.name, field.name);
761                    let field_type = parse_type_name(&field.type_name);
762                    let accessor_input = StackType::RowVar("a".to_string())
763                        .push(Type::Union(union_def.name.clone()));
764                    let accessor_output = StackType::RowVar("a".to_string()).push(field_type);
765                    let accessor_effect = Effect::new(accessor_input, accessor_output);
766                    let accessor_body = vec![
767                        Statement::IntLiteral(index as i64),
768                        Statement::WordCall {
769                            name: "variant.field-at".to_string(),
770                            span: None,
771                        },
772                    ];
773                    new_words.push(WordDef {
774                        name: accessor_name,
775                        effect: Some(accessor_effect),
776                        body: accessor_body,
777                        source: variant.source.clone(), // Use variant's source for field accessors
778                        allowed_lints: vec![],
779                    });
780                }
781            }
782        }
783
784        self.words.extend(new_words);
785        Ok(())
786    }
787
788    /// RFC #345: Fix up type variables in stack effects that should be union types
789    ///
790    /// When parsing files with includes, type variables like "Message" in
791    /// `( Message -- Int )` may be parsed as `Type::Var("Message")` if the
792    /// union definition is in an included file. After resolving includes,
793    /// we know all union names and can convert these to `Type::Union("Message")`.
794    ///
795    /// This ensures proper nominal type checking for union types across files.
796    pub fn fixup_union_types(&mut self) {
797        // Collect all union names from the program
798        let union_names: std::collections::HashSet<String> =
799            self.unions.iter().map(|u| u.name.clone()).collect();
800
801        // Fix up types in all word effects
802        for word in &mut self.words {
803            if let Some(ref mut effect) = word.effect {
804                Self::fixup_stack_type(&mut effect.inputs, &union_names);
805                Self::fixup_stack_type(&mut effect.outputs, &union_names);
806            }
807        }
808    }
809
810    /// Recursively fix up types in a stack type
811    fn fixup_stack_type(stack: &mut StackType, union_names: &std::collections::HashSet<String>) {
812        match stack {
813            StackType::Empty | StackType::RowVar(_) => {}
814            StackType::Cons { rest, top } => {
815                Self::fixup_type(top, union_names);
816                Self::fixup_stack_type(rest, union_names);
817            }
818        }
819    }
820
821    /// Fix up a single type, converting Type::Var to Type::Union if it matches a union name
822    fn fixup_type(ty: &mut Type, union_names: &std::collections::HashSet<String>) {
823        match ty {
824            Type::Var(name) if union_names.contains(name) => {
825                *ty = Type::Union(name.clone());
826            }
827            Type::Quotation(effect) => {
828                Self::fixup_stack_type(&mut effect.inputs, union_names);
829                Self::fixup_stack_type(&mut effect.outputs, union_names);
830            }
831            Type::Closure { effect, captures } => {
832                Self::fixup_stack_type(&mut effect.inputs, union_names);
833                Self::fixup_stack_type(&mut effect.outputs, union_names);
834                for cap in captures {
835                    Self::fixup_type(cap, union_names);
836                }
837            }
838            _ => {}
839        }
840    }
841}
842
843/// Parse a type name string into a Type
844/// Used by constructor generation to build stack effects
845fn parse_type_name(name: &str) -> Type {
846    match name {
847        "Int" => Type::Int,
848        "Float" => Type::Float,
849        "Bool" => Type::Bool,
850        "String" => Type::String,
851        "Channel" => Type::Channel,
852        other => Type::Union(other.to_string()),
853    }
854}
855
856impl Default for Program {
857    fn default() -> Self {
858        Self::new()
859    }
860}
861
862#[cfg(test)]
863mod tests {
864    use super::*;
865
866    #[test]
867    fn test_validate_builtin_words() {
868        let program = Program {
869            includes: vec![],
870            unions: vec![],
871            words: vec![WordDef {
872                name: "main".to_string(),
873                effect: None,
874                body: vec![
875                    Statement::IntLiteral(2),
876                    Statement::IntLiteral(3),
877                    Statement::WordCall {
878                        name: "i.add".to_string(),
879                        span: None,
880                    },
881                    Statement::WordCall {
882                        name: "io.write-line".to_string(),
883                        span: None,
884                    },
885                ],
886                source: None,
887                allowed_lints: vec![],
888            }],
889        };
890
891        // Should succeed - i.add and io.write-line are built-ins
892        assert!(program.validate_word_calls().is_ok());
893    }
894
895    #[test]
896    fn test_validate_user_defined_words() {
897        let program = Program {
898            includes: vec![],
899            unions: vec![],
900            words: vec![
901                WordDef {
902                    name: "helper".to_string(),
903                    effect: None,
904                    body: vec![Statement::IntLiteral(42)],
905                    source: None,
906                    allowed_lints: vec![],
907                },
908                WordDef {
909                    name: "main".to_string(),
910                    effect: None,
911                    body: vec![Statement::WordCall {
912                        name: "helper".to_string(),
913                        span: None,
914                    }],
915                    source: None,
916                    allowed_lints: vec![],
917                },
918            ],
919        };
920
921        // Should succeed - helper is defined
922        assert!(program.validate_word_calls().is_ok());
923    }
924
925    #[test]
926    fn test_validate_undefined_word() {
927        let program = Program {
928            includes: vec![],
929            unions: vec![],
930            words: vec![WordDef {
931                name: "main".to_string(),
932                effect: None,
933                body: vec![Statement::WordCall {
934                    name: "undefined_word".to_string(),
935                    span: None,
936                }],
937                source: None,
938                allowed_lints: vec![],
939            }],
940        };
941
942        // Should fail - undefined_word is not a built-in or user-defined word
943        let result = program.validate_word_calls();
944        assert!(result.is_err());
945        let error = result.unwrap_err();
946        assert!(error.contains("undefined_word"));
947        assert!(error.contains("main"));
948    }
949
950    #[test]
951    fn test_validate_misspelled_builtin() {
952        let program = Program {
953            includes: vec![],
954            unions: vec![],
955            words: vec![WordDef {
956                name: "main".to_string(),
957                effect: None,
958                body: vec![Statement::WordCall {
959                    name: "wrte_line".to_string(),
960                    span: None,
961                }], // typo
962                source: None,
963                allowed_lints: vec![],
964            }],
965        };
966
967        // Should fail with helpful message
968        let result = program.validate_word_calls();
969        assert!(result.is_err());
970        let error = result.unwrap_err();
971        assert!(error.contains("wrte_line"));
972        assert!(error.contains("misspell"));
973    }
974
975    #[test]
976    fn test_generate_constructors() {
977        let mut program = Program {
978            includes: vec![],
979            unions: vec![UnionDef {
980                name: "Message".to_string(),
981                variants: vec![
982                    UnionVariant {
983                        name: "Get".to_string(),
984                        fields: vec![UnionField {
985                            name: "response-chan".to_string(),
986                            type_name: "Int".to_string(),
987                        }],
988                        source: None,
989                    },
990                    UnionVariant {
991                        name: "Put".to_string(),
992                        fields: vec![
993                            UnionField {
994                                name: "value".to_string(),
995                                type_name: "String".to_string(),
996                            },
997                            UnionField {
998                                name: "response-chan".to_string(),
999                                type_name: "Int".to_string(),
1000                            },
1001                        ],
1002                        source: None,
1003                    },
1004                ],
1005                source: None,
1006            }],
1007            words: vec![],
1008        };
1009
1010        // Generate constructors, predicates, and accessors
1011        program.generate_constructors().unwrap();
1012
1013        // Should have 7 words:
1014        // - Get variant: Make-Get, is-Get?, Get-response-chan (1 field)
1015        // - Put variant: Make-Put, is-Put?, Put-value, Put-response-chan (2 fields)
1016        assert_eq!(program.words.len(), 7);
1017
1018        // Check Make-Get constructor
1019        let make_get = program
1020            .find_word("Make-Get")
1021            .expect("Make-Get should exist");
1022        assert_eq!(make_get.name, "Make-Get");
1023        assert!(make_get.effect.is_some());
1024        let effect = make_get.effect.as_ref().unwrap();
1025        // Input: ( ..a Int -- )
1026        // Output: ( ..a Message -- )
1027        assert_eq!(
1028            format!("{:?}", effect.outputs),
1029            "Cons { rest: RowVar(\"a\"), top: Union(\"Message\") }"
1030        );
1031
1032        // Check Make-Put constructor
1033        let make_put = program
1034            .find_word("Make-Put")
1035            .expect("Make-Put should exist");
1036        assert_eq!(make_put.name, "Make-Put");
1037        assert!(make_put.effect.is_some());
1038
1039        // Check the body generates correct code
1040        // Make-Get should be: :Get variant.make-1
1041        assert_eq!(make_get.body.len(), 2);
1042        match &make_get.body[0] {
1043            Statement::Symbol(s) if s == "Get" => {}
1044            other => panic!("Expected Symbol(\"Get\") for variant tag, got {:?}", other),
1045        }
1046        match &make_get.body[1] {
1047            Statement::WordCall { name, span: None } if name == "variant.make-1" => {}
1048            _ => panic!("Expected WordCall(variant.make-1)"),
1049        }
1050
1051        // Make-Put should be: :Put variant.make-2
1052        assert_eq!(make_put.body.len(), 2);
1053        match &make_put.body[0] {
1054            Statement::Symbol(s) if s == "Put" => {}
1055            other => panic!("Expected Symbol(\"Put\") for variant tag, got {:?}", other),
1056        }
1057        match &make_put.body[1] {
1058            Statement::WordCall { name, span: None } if name == "variant.make-2" => {}
1059            _ => panic!("Expected WordCall(variant.make-2)"),
1060        }
1061
1062        // Check is-Get? predicate
1063        let is_get = program.find_word("is-Get?").expect("is-Get? should exist");
1064        assert_eq!(is_get.name, "is-Get?");
1065        assert!(is_get.effect.is_some());
1066        let effect = is_get.effect.as_ref().unwrap();
1067        // Input: ( ..a Message -- )
1068        // Output: ( ..a Bool -- )
1069        assert_eq!(
1070            format!("{:?}", effect.outputs),
1071            "Cons { rest: RowVar(\"a\"), top: Bool }"
1072        );
1073
1074        // Check Get-response-chan accessor
1075        let get_chan = program
1076            .find_word("Get-response-chan")
1077            .expect("Get-response-chan should exist");
1078        assert_eq!(get_chan.name, "Get-response-chan");
1079        assert!(get_chan.effect.is_some());
1080        let effect = get_chan.effect.as_ref().unwrap();
1081        // Input: ( ..a Message -- )
1082        // Output: ( ..a Int -- )
1083        assert_eq!(
1084            format!("{:?}", effect.outputs),
1085            "Cons { rest: RowVar(\"a\"), top: Int }"
1086        );
1087    }
1088}