Skip to main content

seqc/ast/
program.rs

1//! Program-level AST methods: word-call validation, auto-generated variant
2//! constructors (`Make-Variant`), and type fix-up for union types declared
3//! in stack effects.
4
5use crate::types::{Effect, StackType, Type};
6
7use super::{Program, Statement, WordDef};
8
9impl Program {
10    pub fn new() -> Self {
11        Program {
12            includes: Vec::new(),
13            unions: Vec::new(),
14            words: Vec::new(),
15        }
16    }
17
18    pub fn find_word(&self, name: &str) -> Option<&WordDef> {
19        self.words.iter().find(|w| w.name == name)
20    }
21
22    /// Validate that all word calls reference either a defined word or a built-in
23    pub fn validate_word_calls(&self) -> Result<(), String> {
24        self.validate_word_calls_with_externals(&[])
25    }
26
27    /// Validate that all word calls reference a defined word, built-in, or external word.
28    ///
29    /// The `external_words` parameter should contain names of words available from
30    /// external sources (e.g., included modules) that should be considered valid.
31    pub fn validate_word_calls_with_externals(
32        &self,
33        external_words: &[&str],
34    ) -> Result<(), String> {
35        // List of known runtime built-ins
36        // IMPORTANT: Keep this in sync with codegen.rs WordCall matching
37        let builtins = [
38            // I/O operations
39            "io.write",
40            "io.write-line",
41            "io.read-line",
42            "io.read-n",
43            "int->string",
44            "symbol->string",
45            "string->symbol",
46            // Command-line arguments
47            "args.count",
48            "args.at",
49            // File operations
50            "file.slurp",
51            "file.exists?",
52            "file.for-each-line",
53            "file.spit",
54            "file.append",
55            "file.delete",
56            "file.size",
57            // Directory operations
58            "dir.exists?",
59            "dir.make",
60            "dir.delete",
61            "dir.list",
62            // String operations
63            "string.concat",
64            "string.length",
65            "string.byte-length",
66            "string.char-at",
67            "string.substring",
68            "char->string",
69            "string.find",
70            "string.split",
71            "string.contains",
72            "string.starts-with",
73            "string.empty?",
74            "string.trim",
75            "string.chomp",
76            "string.to-upper",
77            "string.to-lower",
78            "string.equal?",
79            "string.join",
80            "string.json-escape",
81            "string->int",
82            // Symbol operations
83            "symbol.=",
84            // Encoding operations
85            "encoding.base64-encode",
86            "encoding.base64-decode",
87            "encoding.base64url-encode",
88            "encoding.base64url-decode",
89            "encoding.hex-encode",
90            "encoding.hex-decode",
91            // Crypto operations
92            "crypto.sha256",
93            "crypto.hmac-sha256",
94            "crypto.constant-time-eq",
95            "crypto.random-bytes",
96            "crypto.random-int",
97            "crypto.uuid4",
98            "crypto.aes-gcm-encrypt",
99            "crypto.aes-gcm-decrypt",
100            "crypto.pbkdf2-sha256",
101            "crypto.ed25519-keypair",
102            "crypto.ed25519-sign",
103            "crypto.ed25519-verify",
104            // HTTP client operations
105            "net.http.get",
106            "net.http.post",
107            "net.http.put",
108            "net.http.delete",
109            // List operations
110            "list.make",
111            "list.push",
112            "list.get",
113            "list.set",
114            "list.map",
115            "list.filter",
116            "list.fold",
117            "list.each",
118            "list.length",
119            "list.empty?",
120            "list.reverse",
121            "list.first",
122            "list.last",
123            // Map operations
124            "map.make",
125            "map.get",
126            "map.set",
127            "map.has?",
128            "map.remove",
129            "map.keys",
130            "map.values",
131            "map.size",
132            "map.empty?",
133            "map.each",
134            "map.fold",
135            // Variant operations
136            "variant.field-count",
137            "variant.tag",
138            "variant.field-at",
139            "variant.append",
140            "variant.first",
141            "variant.last",
142            "variant.init",
143            "variant.make-0",
144            "variant.make-1",
145            "variant.make-2",
146            "variant.make-3",
147            "variant.make-4",
148            // SON wrap aliases
149            "wrap-0",
150            "wrap-1",
151            "wrap-2",
152            "wrap-3",
153            "wrap-4",
154            // Integer arithmetic operations
155            "i.add",
156            "i.subtract",
157            "i.multiply",
158            "i.divide",
159            "i.modulo",
160            // Terse integer arithmetic
161            "i.+",
162            "i.-",
163            "i.*",
164            "i./",
165            "i.%",
166            // Integer comparison operations (return 0 or 1)
167            "i.=",
168            "i.<",
169            "i.>",
170            "i.<=",
171            "i.>=",
172            "i.<>",
173            // Integer comparison operations (verbose form)
174            "i.eq",
175            "i.lt",
176            "i.gt",
177            "i.lte",
178            "i.gte",
179            "i.neq",
180            // Stack operations (simple - no parameters)
181            "dup",
182            "drop",
183            "swap",
184            "over",
185            "rot",
186            "nip",
187            "tuck",
188            "2dup",
189            "3drop",
190            "pick",
191            "roll",
192            // Aux stack operations
193            ">aux",
194            "aux>",
195            // Boolean operations
196            "and",
197            "or",
198            "not",
199            // Bitwise operations
200            "band",
201            "bor",
202            "bxor",
203            "bnot",
204            "i.neg",
205            "negate",
206            // Arithmetic sugar (resolved to concrete ops by typechecker)
207            "+",
208            "-",
209            "*",
210            "/",
211            "%",
212            "=",
213            "<",
214            ">",
215            "<=",
216            ">=",
217            "<>",
218            "shl",
219            "shr",
220            "popcount",
221            "clz",
222            "ctz",
223            "int-bits",
224            // Channel operations
225            "chan.make",
226            "chan.send",
227            "chan.receive",
228            "chan.close",
229            "chan.yield",
230            // Quotation operations
231            "call",
232            // Dataflow combinators
233            "dip",
234            "keep",
235            "bi",
236            "if",
237            "strand.spawn",
238            "strand.weave",
239            "strand.resume",
240            "strand.weave-cancel",
241            "yield",
242            "cond",
243            // TCP operations
244            "net.tcp.listen",
245            "net.tcp.accept",
246            "net.tcp.read",
247            "net.tcp.write",
248            "net.tcp.close",
249            // Socket <-> Int casts (FFI escape hatches)
250            "fd->socket",
251            "socket->fd",
252            // UDP operations
253            "net.udp.bind",
254            "net.udp.send-to",
255            "net.udp.receive-from",
256            "net.udp.close",
257            // OS operations
258            "os.getenv",
259            "os.home-dir",
260            "os.current-dir",
261            "os.path-exists",
262            "os.path-is-file",
263            "os.path-is-dir",
264            "os.path-join",
265            "os.path-parent",
266            "os.path-filename",
267            "os.exit",
268            "os.name",
269            "os.arch",
270            // Signal handling
271            "signal.trap",
272            "signal.received?",
273            "signal.pending?",
274            "signal.default",
275            "signal.ignore",
276            "signal.clear",
277            "signal.SIGINT",
278            "signal.SIGTERM",
279            "signal.SIGHUP",
280            "signal.SIGPIPE",
281            "signal.SIGUSR1",
282            "signal.SIGUSR2",
283            "signal.SIGCHLD",
284            "signal.SIGALRM",
285            "signal.SIGCONT",
286            // Terminal operations
287            "terminal.raw-mode",
288            "terminal.read-char",
289            "terminal.read-char?",
290            "terminal.width",
291            "terminal.height",
292            "terminal.flush",
293            // Float arithmetic operations (verbose form)
294            "f.add",
295            "f.subtract",
296            "f.multiply",
297            "f.divide",
298            // Float arithmetic operations (terse form)
299            "f.+",
300            "f.-",
301            "f.*",
302            "f./",
303            // Float comparison operations (symbol form)
304            "f.=",
305            "f.<",
306            "f.>",
307            "f.<=",
308            "f.>=",
309            "f.<>",
310            // Float comparison operations (verbose form)
311            "f.eq",
312            "f.lt",
313            "f.gt",
314            "f.lte",
315            "f.gte",
316            "f.neq",
317            // Float math — roots/powers
318            "f.sqrt",
319            "f.cbrt",
320            "f.pow",
321            // Float math — exp/log
322            "f.exp",
323            "f.ln",
324            "f.log10",
325            "f.log2",
326            // Float math — trig
327            "f.sin",
328            "f.cos",
329            "f.tan",
330            "f.asin",
331            "f.acos",
332            "f.atan",
333            "f.atan2",
334            // Float math — rounding
335            "f.floor",
336            "f.ceil",
337            "f.round",
338            "f.trunc",
339            // Float constants
340            "f.pi",
341            "f.e",
342            "f.tau",
343            // Type conversions
344            "int->float",
345            "float->int",
346            "float->string",
347            "string->float",
348            // Byte construction (binary protocol encoders)
349            "int.to-bytes-i32-be",
350            "float.to-bytes-f32-be",
351            // Test framework operations
352            "test.init",
353            "test.set-name",
354            "test.finish",
355            "test.has-failures",
356            "test.assert",
357            "test.assert-not",
358            "test.assert-eq",
359            "test.assert-eq-str",
360            "test.fail",
361            "test.pass-count",
362            "test.fail-count",
363            // Time operations
364            "time.now",
365            "time.nanos",
366            "time.sleep-ms",
367            // SON serialization
368            "son.dump",
369            "son.dump-pretty",
370            // Stack introspection (for REPL)
371            "stack.dump",
372            // Regex operations
373            "regex.match?",
374            "regex.find",
375            "regex.find-all",
376            "regex.replace",
377            "regex.replace-all",
378            "regex.captures",
379            "regex.split",
380            "regex.valid?",
381            // Compression operations
382            "compress.gzip",
383            "compress.gzip-level",
384            "compress.gunzip",
385            "compress.zstd",
386            "compress.zstd-level",
387            "compress.unzstd",
388        ];
389
390        for word in &self.words {
391            self.validate_statements(&word.body, &word.name, &builtins, external_words)?;
392        }
393
394        Ok(())
395    }
396
397    /// Helper to validate word calls in a list of statements (recursively)
398    fn validate_statements(
399        &self,
400        statements: &[Statement],
401        word_name: &str,
402        builtins: &[&str],
403        external_words: &[&str],
404    ) -> Result<(), String> {
405        for statement in statements {
406            match statement {
407                Statement::WordCall { name, .. } => {
408                    // Check if it's a built-in
409                    if builtins.contains(&name.as_str()) {
410                        continue;
411                    }
412                    // Check if it's a user-defined word
413                    if self.find_word(name).is_some() {
414                        continue;
415                    }
416                    // Check if it's an external word (from includes)
417                    if external_words.contains(&name.as_str()) {
418                        continue;
419                    }
420                    // v7.0 rename: pre-net.* networking names get a targeted
421                    // hint instead of the generic "did you misspell" message,
422                    // so the migration is obvious.
423                    if let Some(replacement) = v7_renamed_to(name) {
424                        return Err(format!(
425                            "'{}' was renamed to '{}' in v7.0 (called in word '{}'). \
426                             See docs/MIGRATION_7_0.md.",
427                            name, replacement, word_name
428                        ));
429                    }
430                    // Undefined word!
431                    return Err(format!(
432                        "Undefined word '{}' called in word '{}'. \
433                         Did you forget to define it or misspell a built-in?",
434                        name, word_name
435                    ));
436                }
437                Statement::If {
438                    then_branch,
439                    else_branch,
440                    span: _,
441                } => {
442                    // Recursively validate both branches
443                    self.validate_statements(then_branch, word_name, builtins, external_words)?;
444                    if let Some(eb) = else_branch {
445                        self.validate_statements(eb, word_name, builtins, external_words)?;
446                    }
447                }
448                Statement::Quotation { body, .. } => {
449                    // Recursively validate quotation body
450                    self.validate_statements(body, word_name, builtins, external_words)?;
451                }
452                Statement::Match { arms, span: _ } => {
453                    // Recursively validate each match arm's body
454                    for arm in arms {
455                        self.validate_statements(&arm.body, word_name, builtins, external_words)?;
456                    }
457                }
458                _ => {} // Literals don't need validation
459            }
460        }
461        Ok(())
462    }
463
464    /// Generate constructor words for all union definitions
465    ///
466    /// Maximum number of fields a variant can have (limited by runtime support)
467    pub const MAX_VARIANT_FIELDS: usize = 12;
468
469    /// Generate helper words for union types:
470    /// 1. Constructors: `Make-VariantName` - creates variant instances
471    /// 2. Predicates: `is-VariantName?` - tests if value is a specific variant
472    /// 3. Accessors: `VariantName-fieldname` - extracts field values (RFC #345)
473    ///
474    /// Example: For `union Message { Get { chan: Int } }`
475    /// Generates:
476    ///   `: Make-Get ( Int -- Message ) :Get variant.make-1 ;`
477    ///   `: is-Get? ( Message -- Bool ) variant.tag :Get symbol.= ;`
478    ///   `: Get-chan ( Message -- Int ) 0 variant.field-at ;`
479    ///
480    /// Returns an error if any variant exceeds the maximum field count.
481    pub fn generate_constructors(&mut self) -> Result<(), String> {
482        let mut new_words = Vec::new();
483
484        for union_def in &self.unions {
485            for variant in &union_def.variants {
486                let field_count = variant.fields.len();
487
488                // Check field count limit before generating constructor
489                if field_count > Self::MAX_VARIANT_FIELDS {
490                    return Err(format!(
491                        "Variant '{}' in union '{}' has {} fields, but the maximum is {}. \
492                         Consider grouping fields into nested union types.",
493                        variant.name,
494                        union_def.name,
495                        field_count,
496                        Self::MAX_VARIANT_FIELDS
497                    ));
498                }
499
500                // 1. Generate constructor: Make-VariantName
501                let constructor_name = format!("Make-{}", variant.name);
502                let mut input_stack = StackType::RowVar("a".to_string());
503                for field in &variant.fields {
504                    let field_type = parse_type_name(&field.type_name);
505                    input_stack = input_stack.push(field_type);
506                }
507                let output_stack =
508                    StackType::RowVar("a".to_string()).push(Type::Union(union_def.name.clone()));
509                let effect = Effect::new(input_stack, output_stack);
510                let body = vec![
511                    Statement::Symbol(variant.name.clone()),
512                    Statement::WordCall {
513                        name: format!("variant.make-{}", field_count),
514                        span: None,
515                    },
516                ];
517                new_words.push(WordDef {
518                    name: constructor_name,
519                    effect: Some(effect),
520                    body,
521                    source: variant.source.clone(),
522                    allowed_lints: vec![],
523                });
524
525                // 2. Generate predicate: is-VariantName?
526                // Effect: ( UnionType -- Bool )
527                // Body: variant.tag :VariantName symbol.=
528                let predicate_name = format!("is-{}?", variant.name);
529                let predicate_input =
530                    StackType::RowVar("a".to_string()).push(Type::Union(union_def.name.clone()));
531                let predicate_output = StackType::RowVar("a".to_string()).push(Type::Bool);
532                let predicate_effect = Effect::new(predicate_input, predicate_output);
533                let predicate_body = vec![
534                    Statement::WordCall {
535                        name: "variant.tag".to_string(),
536                        span: None,
537                    },
538                    Statement::Symbol(variant.name.clone()),
539                    Statement::WordCall {
540                        name: "symbol.=".to_string(),
541                        span: None,
542                    },
543                ];
544                new_words.push(WordDef {
545                    name: predicate_name,
546                    effect: Some(predicate_effect),
547                    body: predicate_body,
548                    source: variant.source.clone(),
549                    allowed_lints: vec![],
550                });
551
552                // 3. Generate field accessors: VariantName-fieldname
553                // Effect: ( UnionType -- FieldType )
554                // Body: N variant.field-at
555                for (index, field) in variant.fields.iter().enumerate() {
556                    let accessor_name = format!("{}-{}", variant.name, field.name);
557                    let field_type = parse_type_name(&field.type_name);
558                    let accessor_input = StackType::RowVar("a".to_string())
559                        .push(Type::Union(union_def.name.clone()));
560                    let accessor_output = StackType::RowVar("a".to_string()).push(field_type);
561                    let accessor_effect = Effect::new(accessor_input, accessor_output);
562                    let accessor_body = vec![
563                        Statement::IntLiteral(index as i64),
564                        Statement::WordCall {
565                            name: "variant.field-at".to_string(),
566                            span: None,
567                        },
568                    ];
569                    new_words.push(WordDef {
570                        name: accessor_name,
571                        effect: Some(accessor_effect),
572                        body: accessor_body,
573                        source: variant.source.clone(), // Use variant's source for field accessors
574                        allowed_lints: vec![],
575                    });
576                }
577            }
578        }
579
580        self.words.extend(new_words);
581        Ok(())
582    }
583
584    /// RFC #345: Fix up type variables in stack effects that should be union types
585    ///
586    /// When parsing files with includes, type variables like "Message" in
587    /// `( Message -- Int )` may be parsed as `Type::Var("Message")` if the
588    /// union definition is in an included file. After resolving includes,
589    /// we know all union names and can convert these to `Type::Union("Message")`.
590    ///
591    /// This ensures proper nominal type checking for union types across files.
592    pub fn fixup_union_types(&mut self) {
593        // Collect all union names from the program
594        let union_names: std::collections::HashSet<String> =
595            self.unions.iter().map(|u| u.name.clone()).collect();
596
597        // Fix up types in all word effects
598        for word in &mut self.words {
599            if let Some(ref mut effect) = word.effect {
600                Self::fixup_stack_type(&mut effect.inputs, &union_names);
601                Self::fixup_stack_type(&mut effect.outputs, &union_names);
602            }
603        }
604    }
605
606    /// Recursively fix up types in a stack type
607    fn fixup_stack_type(stack: &mut StackType, union_names: &std::collections::HashSet<String>) {
608        match stack {
609            StackType::Empty | StackType::RowVar(_) => {}
610            StackType::Cons { rest, top } => {
611                Self::fixup_type(top, union_names);
612                Self::fixup_stack_type(rest, union_names);
613            }
614        }
615    }
616
617    /// Fix up a single type, converting Type::Var to Type::Union if it matches a union name
618    fn fixup_type(ty: &mut Type, union_names: &std::collections::HashSet<String>) {
619        match ty {
620            Type::Var(name) if union_names.contains(name) => {
621                *ty = Type::Union(name.clone());
622            }
623            Type::Quotation(effect) => {
624                Self::fixup_stack_type(&mut effect.inputs, union_names);
625                Self::fixup_stack_type(&mut effect.outputs, union_names);
626            }
627            Type::Closure { effect, captures } => {
628                Self::fixup_stack_type(&mut effect.inputs, union_names);
629                Self::fixup_stack_type(&mut effect.outputs, union_names);
630                for cap in captures {
631                    Self::fixup_type(cap, union_names);
632                }
633            }
634            _ => {}
635        }
636    }
637}
638
639/// Parse a type name string into a Type
640/// Used by constructor generation to build stack effects
641fn parse_type_name(name: &str) -> Type {
642    match name {
643        "Int" => Type::Int,
644        "Float" => Type::Float,
645        "Bool" => Type::Bool,
646        "String" => Type::String,
647        "Channel" => Type::Channel,
648        "Socket" => Type::Socket,
649        other => Type::Union(other.to_string()),
650    }
651}
652
653/// Map a pre-v7.0 networking word to its current name, or None if unknown.
654/// Used to turn the generic "Undefined word" error into a targeted migration
655/// hint when a user calls one of the renamed builtins. Remove this table
656/// in v8.0.
657fn v7_renamed_to(name: &str) -> Option<&'static str> {
658    Some(match name {
659        "tcp.listen" => "net.tcp.listen",
660        "tcp.accept" => "net.tcp.accept",
661        "tcp.read" => "net.tcp.read",
662        "tcp.write" => "net.tcp.write",
663        "tcp.close" => "net.tcp.close",
664        "udp.bind" => "net.udp.bind",
665        "udp.send-to" => "net.udp.send-to",
666        "udp.receive-from" => "net.udp.receive-from",
667        "udp.close" => "net.udp.close",
668        "http.get" => "net.http.get",
669        "http.post" => "net.http.post",
670        "http.put" => "net.http.put",
671        "http.delete" => "net.http.delete",
672        // imath stdlib pass-through removed in v7.0; route to the underlying
673        // builtin so callers learn the right name.
674        "mod" => "i.modulo",
675        _ => return None,
676    })
677}
678
679impl Default for Program {
680    fn default() -> Self {
681        Self::new()
682    }
683}