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