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