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