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