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