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