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