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            // Boolean operations
463            "and",
464            "or",
465            "not",
466            // Bitwise operations
467            "band",
468            "bor",
469            "bxor",
470            "bnot",
471            "shl",
472            "shr",
473            "popcount",
474            "clz",
475            "ctz",
476            "int-bits",
477            // Channel operations
478            "chan.make",
479            "chan.send",
480            "chan.receive",
481            "chan.close",
482            "chan.yield",
483            // Quotation operations
484            "call",
485            "strand.spawn",
486            "strand.weave",
487            "strand.resume",
488            "strand.weave-cancel",
489            "yield",
490            "cond",
491            // TCP operations
492            "tcp.listen",
493            "tcp.accept",
494            "tcp.read",
495            "tcp.write",
496            "tcp.close",
497            // OS operations
498            "os.getenv",
499            "os.home-dir",
500            "os.current-dir",
501            "os.path-exists",
502            "os.path-is-file",
503            "os.path-is-dir",
504            "os.path-join",
505            "os.path-parent",
506            "os.path-filename",
507            "os.exit",
508            "os.name",
509            "os.arch",
510            // Signal handling
511            "signal.trap",
512            "signal.received?",
513            "signal.pending?",
514            "signal.default",
515            "signal.ignore",
516            "signal.clear",
517            "signal.SIGINT",
518            "signal.SIGTERM",
519            "signal.SIGHUP",
520            "signal.SIGPIPE",
521            "signal.SIGUSR1",
522            "signal.SIGUSR2",
523            "signal.SIGCHLD",
524            "signal.SIGALRM",
525            "signal.SIGCONT",
526            // Terminal operations
527            "terminal.raw-mode",
528            "terminal.read-char",
529            "terminal.read-char?",
530            "terminal.width",
531            "terminal.height",
532            "terminal.flush",
533            // Float arithmetic operations (verbose form)
534            "f.add",
535            "f.subtract",
536            "f.multiply",
537            "f.divide",
538            // Float arithmetic operations (terse form)
539            "f.+",
540            "f.-",
541            "f.*",
542            "f./",
543            // Float comparison operations (symbol form)
544            "f.=",
545            "f.<",
546            "f.>",
547            "f.<=",
548            "f.>=",
549            "f.<>",
550            // Float comparison operations (verbose form)
551            "f.eq",
552            "f.lt",
553            "f.gt",
554            "f.lte",
555            "f.gte",
556            "f.neq",
557            // Type conversions
558            "int->float",
559            "float->int",
560            "float->string",
561            "string->float",
562            // Test framework operations
563            "test.init",
564            "test.finish",
565            "test.has-failures",
566            "test.assert",
567            "test.assert-not",
568            "test.assert-eq",
569            "test.assert-eq-str",
570            "test.fail",
571            "test.pass-count",
572            "test.fail-count",
573            // Time operations
574            "time.now",
575            "time.nanos",
576            "time.sleep-ms",
577            // SON serialization
578            "son.dump",
579            "son.dump-pretty",
580            // Stack introspection (for REPL)
581            "stack.dump",
582            // Regex operations
583            "regex.match?",
584            "regex.find",
585            "regex.find-all",
586            "regex.replace",
587            "regex.replace-all",
588            "regex.captures",
589            "regex.split",
590            "regex.valid?",
591            // Compression operations
592            "compress.gzip",
593            "compress.gzip-level",
594            "compress.gunzip",
595            "compress.zstd",
596            "compress.zstd-level",
597            "compress.unzstd",
598        ];
599
600        for word in &self.words {
601            self.validate_statements(&word.body, &word.name, &builtins, external_words)?;
602        }
603
604        Ok(())
605    }
606
607    /// Helper to validate word calls in a list of statements (recursively)
608    fn validate_statements(
609        &self,
610        statements: &[Statement],
611        word_name: &str,
612        builtins: &[&str],
613        external_words: &[&str],
614    ) -> Result<(), String> {
615        for statement in statements {
616            match statement {
617                Statement::WordCall { name, .. } => {
618                    // Check if it's a built-in
619                    if builtins.contains(&name.as_str()) {
620                        continue;
621                    }
622                    // Check if it's a user-defined word
623                    if self.find_word(name).is_some() {
624                        continue;
625                    }
626                    // Check if it's an external word (from includes)
627                    if external_words.contains(&name.as_str()) {
628                        continue;
629                    }
630                    // Undefined word!
631                    return Err(format!(
632                        "Undefined word '{}' called in word '{}'. \
633                         Did you forget to define it or misspell a built-in?",
634                        name, word_name
635                    ));
636                }
637                Statement::If {
638                    then_branch,
639                    else_branch,
640                    span: _,
641                } => {
642                    // Recursively validate both branches
643                    self.validate_statements(then_branch, word_name, builtins, external_words)?;
644                    if let Some(eb) = else_branch {
645                        self.validate_statements(eb, word_name, builtins, external_words)?;
646                    }
647                }
648                Statement::Quotation { body, .. } => {
649                    // Recursively validate quotation body
650                    self.validate_statements(body, word_name, builtins, external_words)?;
651                }
652                Statement::Match { arms, span: _ } => {
653                    // Recursively validate each match arm's body
654                    for arm in arms {
655                        self.validate_statements(&arm.body, word_name, builtins, external_words)?;
656                    }
657                }
658                _ => {} // Literals don't need validation
659            }
660        }
661        Ok(())
662    }
663
664    /// Generate constructor words for all union definitions
665    ///
666    /// Maximum number of fields a variant can have (limited by runtime support)
667    pub const MAX_VARIANT_FIELDS: usize = 12;
668
669    /// For each union variant, generates a `Make-VariantName` word that:
670    /// 1. Takes the variant's field values from the stack
671    /// 2. Pushes the variant tag (index)
672    /// 3. Calls the appropriate `variant.make-N` builtin
673    ///
674    /// Example: For `union Message { Get { chan: Int } }`
675    /// Generates: `: Make-Get ( Int -- Message ) 0 variant.make-1 ;`
676    ///
677    /// Returns an error if any variant exceeds the maximum field count.
678    pub fn generate_constructors(&mut self) -> Result<(), String> {
679        let mut new_words = Vec::new();
680
681        for union_def in &self.unions {
682            for variant in &union_def.variants {
683                let constructor_name = format!("Make-{}", variant.name);
684                let field_count = variant.fields.len();
685
686                // Check field count limit before generating constructor
687                if field_count > Self::MAX_VARIANT_FIELDS {
688                    return Err(format!(
689                        "Variant '{}' in union '{}' has {} fields, but the maximum is {}. \
690                         Consider grouping fields into nested union types.",
691                        variant.name,
692                        union_def.name,
693                        field_count,
694                        Self::MAX_VARIANT_FIELDS
695                    ));
696                }
697
698                // Build the stack effect: ( field_types... -- UnionType )
699                // Input stack has fields in declaration order
700                let mut input_stack = StackType::RowVar("a".to_string());
701                for field in &variant.fields {
702                    let field_type = parse_type_name(&field.type_name);
703                    input_stack = input_stack.push(field_type);
704                }
705
706                // Output stack has the union type
707                let output_stack =
708                    StackType::RowVar("a".to_string()).push(Type::Union(union_def.name.clone()));
709
710                let effect = Effect::new(input_stack, output_stack);
711
712                // Build the body:
713                // 1. Push the variant name as a symbol (for dynamic matching)
714                // 2. Call variant.make-N which now accepts Symbol tags
715                let body = vec![
716                    Statement::Symbol(variant.name.clone()),
717                    Statement::WordCall {
718                        name: format!("variant.make-{}", field_count),
719                        span: None, // Generated code, no source span
720                    },
721                ];
722
723                new_words.push(WordDef {
724                    name: constructor_name,
725                    effect: Some(effect),
726                    body,
727                    source: variant.source.clone(),
728                    allowed_lints: vec![],
729                });
730            }
731        }
732
733        self.words.extend(new_words);
734        Ok(())
735    }
736}
737
738/// Parse a type name string into a Type
739/// Used by constructor generation to build stack effects
740fn parse_type_name(name: &str) -> Type {
741    match name {
742        "Int" => Type::Int,
743        "Float" => Type::Float,
744        "Bool" => Type::Bool,
745        "String" => Type::String,
746        "Channel" => Type::Channel,
747        other => Type::Union(other.to_string()),
748    }
749}
750
751impl Default for Program {
752    fn default() -> Self {
753        Self::new()
754    }
755}
756
757#[cfg(test)]
758mod tests {
759    use super::*;
760
761    #[test]
762    fn test_validate_builtin_words() {
763        let program = Program {
764            includes: vec![],
765            unions: vec![],
766            words: vec![WordDef {
767                name: "main".to_string(),
768                effect: None,
769                body: vec![
770                    Statement::IntLiteral(2),
771                    Statement::IntLiteral(3),
772                    Statement::WordCall {
773                        name: "i.add".to_string(),
774                        span: None,
775                    },
776                    Statement::WordCall {
777                        name: "io.write-line".to_string(),
778                        span: None,
779                    },
780                ],
781                source: None,
782                allowed_lints: vec![],
783            }],
784        };
785
786        // Should succeed - i.add and io.write-line are built-ins
787        assert!(program.validate_word_calls().is_ok());
788    }
789
790    #[test]
791    fn test_validate_user_defined_words() {
792        let program = Program {
793            includes: vec![],
794            unions: vec![],
795            words: vec![
796                WordDef {
797                    name: "helper".to_string(),
798                    effect: None,
799                    body: vec![Statement::IntLiteral(42)],
800                    source: None,
801                    allowed_lints: vec![],
802                },
803                WordDef {
804                    name: "main".to_string(),
805                    effect: None,
806                    body: vec![Statement::WordCall {
807                        name: "helper".to_string(),
808                        span: None,
809                    }],
810                    source: None,
811                    allowed_lints: vec![],
812                },
813            ],
814        };
815
816        // Should succeed - helper is defined
817        assert!(program.validate_word_calls().is_ok());
818    }
819
820    #[test]
821    fn test_validate_undefined_word() {
822        let program = Program {
823            includes: vec![],
824            unions: vec![],
825            words: vec![WordDef {
826                name: "main".to_string(),
827                effect: None,
828                body: vec![Statement::WordCall {
829                    name: "undefined_word".to_string(),
830                    span: None,
831                }],
832                source: None,
833                allowed_lints: vec![],
834            }],
835        };
836
837        // Should fail - undefined_word is not a built-in or user-defined word
838        let result = program.validate_word_calls();
839        assert!(result.is_err());
840        let error = result.unwrap_err();
841        assert!(error.contains("undefined_word"));
842        assert!(error.contains("main"));
843    }
844
845    #[test]
846    fn test_validate_misspelled_builtin() {
847        let program = Program {
848            includes: vec![],
849            unions: vec![],
850            words: vec![WordDef {
851                name: "main".to_string(),
852                effect: None,
853                body: vec![Statement::WordCall {
854                    name: "wrte_line".to_string(),
855                    span: None,
856                }], // typo
857                source: None,
858                allowed_lints: vec![],
859            }],
860        };
861
862        // Should fail with helpful message
863        let result = program.validate_word_calls();
864        assert!(result.is_err());
865        let error = result.unwrap_err();
866        assert!(error.contains("wrte_line"));
867        assert!(error.contains("misspell"));
868    }
869
870    #[test]
871    fn test_generate_constructors() {
872        let mut program = Program {
873            includes: vec![],
874            unions: vec![UnionDef {
875                name: "Message".to_string(),
876                variants: vec![
877                    UnionVariant {
878                        name: "Get".to_string(),
879                        fields: vec![UnionField {
880                            name: "response-chan".to_string(),
881                            type_name: "Int".to_string(),
882                        }],
883                        source: None,
884                    },
885                    UnionVariant {
886                        name: "Put".to_string(),
887                        fields: vec![
888                            UnionField {
889                                name: "value".to_string(),
890                                type_name: "String".to_string(),
891                            },
892                            UnionField {
893                                name: "response-chan".to_string(),
894                                type_name: "Int".to_string(),
895                            },
896                        ],
897                        source: None,
898                    },
899                ],
900                source: None,
901            }],
902            words: vec![],
903        };
904
905        // Generate constructors
906        program.generate_constructors().unwrap();
907
908        // Should have 2 constructor words
909        assert_eq!(program.words.len(), 2);
910
911        // Check Make-Get constructor
912        let make_get = program
913            .find_word("Make-Get")
914            .expect("Make-Get should exist");
915        assert_eq!(make_get.name, "Make-Get");
916        assert!(make_get.effect.is_some());
917        let effect = make_get.effect.as_ref().unwrap();
918        // Input: ( ..a Int -- )
919        // Output: ( ..a Message -- )
920        assert_eq!(
921            format!("{:?}", effect.outputs),
922            "Cons { rest: RowVar(\"a\"), top: Union(\"Message\") }"
923        );
924
925        // Check Make-Put constructor
926        let make_put = program
927            .find_word("Make-Put")
928            .expect("Make-Put should exist");
929        assert_eq!(make_put.name, "Make-Put");
930        assert!(make_put.effect.is_some());
931
932        // Check the body generates correct code
933        // Make-Get should be: :Get variant.make-1
934        assert_eq!(make_get.body.len(), 2);
935        match &make_get.body[0] {
936            Statement::Symbol(s) if s == "Get" => {}
937            other => panic!("Expected Symbol(\"Get\") for variant tag, got {:?}", other),
938        }
939        match &make_get.body[1] {
940            Statement::WordCall { name, span: None } if name == "variant.make-1" => {}
941            _ => panic!("Expected WordCall(variant.make-1)"),
942        }
943
944        // Make-Put should be: :Put variant.make-2
945        assert_eq!(make_put.body.len(), 2);
946        match &make_put.body[0] {
947            Statement::Symbol(s) if s == "Put" => {}
948            other => panic!("Expected Symbol(\"Put\") for variant tag, got {:?}", other),
949        }
950        match &make_put.body[1] {
951            Statement::WordCall { name, span: None } if name == "variant.make-2" => {}
952            _ => panic!("Expected WordCall(variant.make-2)"),
953        }
954    }
955}