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