1use crate::types::{Effect, StackType, Type};
7use std::path::PathBuf;
8
9#[derive(Debug, Clone, PartialEq)]
11pub struct SourceLocation {
12 pub file: PathBuf,
13 pub start_line: usize,
15 pub end_line: usize,
17}
18
19impl SourceLocation {
20 pub fn new(file: PathBuf, line: usize) -> Self {
22 SourceLocation {
23 file,
24 start_line: line,
25 end_line: line,
26 }
27 }
28
29 pub fn span(file: PathBuf, start_line: usize, end_line: usize) -> Self {
31 debug_assert!(
32 start_line <= end_line,
33 "SourceLocation: start_line ({}) must be <= end_line ({})",
34 start_line,
35 end_line
36 );
37 SourceLocation {
38 file,
39 start_line,
40 end_line,
41 }
42 }
43}
44
45impl std::fmt::Display for SourceLocation {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 if self.start_line == self.end_line {
48 write!(f, "{}:{}", self.file.display(), self.start_line + 1)
49 } else {
50 write!(
51 f,
52 "{}:{}-{}",
53 self.file.display(),
54 self.start_line + 1,
55 self.end_line + 1
56 )
57 }
58 }
59}
60
61#[derive(Debug, Clone, PartialEq)]
63pub enum Include {
64 Std(String),
66 Relative(String),
68 Ffi(String),
70}
71
72#[derive(Debug, Clone, PartialEq)]
79pub struct UnionField {
80 pub name: String,
81 pub type_name: String, }
83
84#[derive(Debug, Clone, PartialEq)]
87pub struct UnionVariant {
88 pub name: String,
89 pub fields: Vec<UnionField>,
90 pub source: Option<SourceLocation>,
91}
92
93#[derive(Debug, Clone, PartialEq)]
103pub struct UnionDef {
104 pub name: String,
105 pub variants: Vec<UnionVariant>,
106 pub source: Option<SourceLocation>,
107}
108
109#[derive(Debug, Clone, PartialEq)]
113pub enum Pattern {
114 Variant(String),
117
118 VariantWithBindings { name: String, bindings: Vec<String> },
121}
122
123#[derive(Debug, Clone, PartialEq)]
125pub struct MatchArm {
126 pub pattern: Pattern,
127 pub body: Vec<Statement>,
128 pub span: Option<Span>,
130}
131
132#[derive(Debug, Clone, PartialEq)]
133pub struct Program {
134 pub includes: Vec<Include>,
135 pub unions: Vec<UnionDef>,
136 pub words: Vec<WordDef>,
137}
138
139#[derive(Debug, Clone, PartialEq)]
140pub struct WordDef {
141 pub name: String,
142 pub effect: Option<Effect>,
145 pub body: Vec<Statement>,
146 pub source: Option<SourceLocation>,
148 pub allowed_lints: Vec<String>,
151}
152
153#[derive(Debug, Clone, PartialEq, Default)]
155pub struct Span {
156 pub line: usize,
158 pub column: usize,
160 pub length: usize,
162}
163
164impl Span {
165 pub fn new(line: usize, column: usize, length: usize) -> Self {
166 Span {
167 line,
168 column,
169 length,
170 }
171 }
172}
173
174#[derive(Debug, Clone, PartialEq, Default)]
176pub struct QuotationSpan {
177 pub start_line: usize,
179 pub start_column: usize,
181 pub end_line: usize,
183 pub end_column: usize,
185}
186
187impl QuotationSpan {
188 pub fn new(start_line: usize, start_column: usize, end_line: usize, end_column: usize) -> Self {
189 QuotationSpan {
190 start_line,
191 start_column,
192 end_line,
193 end_column,
194 }
195 }
196
197 pub fn contains(&self, line: usize, column: usize) -> bool {
199 if line < self.start_line || line > self.end_line {
200 return false;
201 }
202 if line == self.start_line && column < self.start_column {
203 return false;
204 }
205 if line == self.end_line && column >= self.end_column {
206 return false;
207 }
208 true
209 }
210}
211
212#[derive(Debug, Clone, PartialEq)]
213pub enum Statement {
214 IntLiteral(i64),
216
217 FloatLiteral(f64),
219
220 BoolLiteral(bool),
222
223 StringLiteral(String),
225
226 Symbol(String),
231
232 WordCall { name: String, span: Option<Span> },
235
236 If {
241 then_branch: Vec<Statement>,
243 else_branch: Option<Vec<Statement>>,
245 span: Option<Span>,
247 },
248
249 Quotation {
259 id: usize,
260 body: Vec<Statement>,
261 span: Option<QuotationSpan>,
262 },
263
264 Match {
278 arms: Vec<MatchArm>,
280 span: Option<Span>,
282 },
283}
284
285impl Program {
286 pub fn new() -> Self {
287 Program {
288 includes: Vec::new(),
289 unions: Vec::new(),
290 words: Vec::new(),
291 }
292 }
293
294 pub fn find_word(&self, name: &str) -> Option<&WordDef> {
295 self.words.iter().find(|w| w.name == name)
296 }
297
298 pub fn validate_word_calls(&self) -> Result<(), String> {
300 self.validate_word_calls_with_externals(&[])
301 }
302
303 pub fn validate_word_calls_with_externals(
308 &self,
309 external_words: &[&str],
310 ) -> Result<(), String> {
311 let builtins = [
314 "io.write",
316 "io.write-line",
317 "io.read-line",
318 "io.read-line+",
319 "io.read-n",
320 "int->string",
321 "symbol->string",
322 "string->symbol",
323 "args.count",
325 "args.at",
326 "file.slurp",
328 "file.exists?",
329 "file.for-each-line+",
330 "file.spit",
331 "file.append",
332 "file.delete",
333 "file.size",
334 "dir.exists?",
336 "dir.make",
337 "dir.delete",
338 "dir.list",
339 "string.concat",
341 "string.length",
342 "string.byte-length",
343 "string.char-at",
344 "string.substring",
345 "char->string",
346 "string.find",
347 "string.split",
348 "string.contains",
349 "string.starts-with",
350 "string.empty?",
351 "string.trim",
352 "string.chomp",
353 "string.to-upper",
354 "string.to-lower",
355 "string.equal?",
356 "string.join",
357 "string.json-escape",
358 "string->int",
359 "symbol.=",
361 "encoding.base64-encode",
363 "encoding.base64-decode",
364 "encoding.base64url-encode",
365 "encoding.base64url-decode",
366 "encoding.hex-encode",
367 "encoding.hex-decode",
368 "crypto.sha256",
370 "crypto.hmac-sha256",
371 "crypto.constant-time-eq",
372 "crypto.random-bytes",
373 "crypto.random-int",
374 "crypto.uuid4",
375 "crypto.aes-gcm-encrypt",
376 "crypto.aes-gcm-decrypt",
377 "crypto.pbkdf2-sha256",
378 "crypto.ed25519-keypair",
379 "crypto.ed25519-sign",
380 "crypto.ed25519-verify",
381 "http.get",
383 "http.post",
384 "http.put",
385 "http.delete",
386 "list.make",
388 "list.push",
389 "list.get",
390 "list.set",
391 "list.map",
392 "list.filter",
393 "list.fold",
394 "list.each",
395 "list.length",
396 "list.empty?",
397 "list.reverse",
398 "map.make",
400 "map.get",
401 "map.set",
402 "map.has?",
403 "map.remove",
404 "map.keys",
405 "map.values",
406 "map.size",
407 "map.empty?",
408 "map.each",
409 "map.fold",
410 "variant.field-count",
412 "variant.tag",
413 "variant.field-at",
414 "variant.append",
415 "variant.last",
416 "variant.init",
417 "variant.make-0",
418 "variant.make-1",
419 "variant.make-2",
420 "variant.make-3",
421 "variant.make-4",
422 "wrap-0",
424 "wrap-1",
425 "wrap-2",
426 "wrap-3",
427 "wrap-4",
428 "i.add",
430 "i.subtract",
431 "i.multiply",
432 "i.divide",
433 "i.modulo",
434 "i.+",
436 "i.-",
437 "i.*",
438 "i./",
439 "i.%",
440 "i.=",
442 "i.<",
443 "i.>",
444 "i.<=",
445 "i.>=",
446 "i.<>",
447 "i.eq",
449 "i.lt",
450 "i.gt",
451 "i.lte",
452 "i.gte",
453 "i.neq",
454 "dup",
456 "drop",
457 "swap",
458 "over",
459 "rot",
460 "nip",
461 "tuck",
462 "2dup",
463 "3drop",
464 "pick",
465 "roll",
466 ">aux",
468 "aux>",
469 "and",
471 "or",
472 "not",
473 "band",
475 "bor",
476 "bxor",
477 "bnot",
478 "i.neg",
479 "negate",
480 "+",
482 "-",
483 "*",
484 "/",
485 "%",
486 "=",
487 "<",
488 ">",
489 "<=",
490 ">=",
491 "<>",
492 "shl",
493 "shr",
494 "popcount",
495 "clz",
496 "ctz",
497 "int-bits",
498 "chan.make",
500 "chan.send",
501 "chan.receive",
502 "chan.close",
503 "chan.yield",
504 "call",
506 "dip",
508 "keep",
509 "bi",
510 "strand.spawn",
511 "strand.weave",
512 "strand.resume",
513 "strand.weave-cancel",
514 "yield",
515 "cond",
516 "tcp.listen",
518 "tcp.accept",
519 "tcp.read",
520 "tcp.write",
521 "tcp.close",
522 "os.getenv",
524 "os.home-dir",
525 "os.current-dir",
526 "os.path-exists",
527 "os.path-is-file",
528 "os.path-is-dir",
529 "os.path-join",
530 "os.path-parent",
531 "os.path-filename",
532 "os.exit",
533 "os.name",
534 "os.arch",
535 "signal.trap",
537 "signal.received?",
538 "signal.pending?",
539 "signal.default",
540 "signal.ignore",
541 "signal.clear",
542 "signal.SIGINT",
543 "signal.SIGTERM",
544 "signal.SIGHUP",
545 "signal.SIGPIPE",
546 "signal.SIGUSR1",
547 "signal.SIGUSR2",
548 "signal.SIGCHLD",
549 "signal.SIGALRM",
550 "signal.SIGCONT",
551 "terminal.raw-mode",
553 "terminal.read-char",
554 "terminal.read-char?",
555 "terminal.width",
556 "terminal.height",
557 "terminal.flush",
558 "f.add",
560 "f.subtract",
561 "f.multiply",
562 "f.divide",
563 "f.+",
565 "f.-",
566 "f.*",
567 "f./",
568 "f.=",
570 "f.<",
571 "f.>",
572 "f.<=",
573 "f.>=",
574 "f.<>",
575 "f.eq",
577 "f.lt",
578 "f.gt",
579 "f.lte",
580 "f.gte",
581 "f.neq",
582 "int->float",
584 "float->int",
585 "float->string",
586 "string->float",
587 "test.init",
589 "test.finish",
590 "test.has-failures",
591 "test.assert",
592 "test.assert-not",
593 "test.assert-eq",
594 "test.assert-eq-str",
595 "test.fail",
596 "test.pass-count",
597 "test.fail-count",
598 "time.now",
600 "time.nanos",
601 "time.sleep-ms",
602 "son.dump",
604 "son.dump-pretty",
605 "stack.dump",
607 "regex.match?",
609 "regex.find",
610 "regex.find-all",
611 "regex.replace",
612 "regex.replace-all",
613 "regex.captures",
614 "regex.split",
615 "regex.valid?",
616 "compress.gzip",
618 "compress.gzip-level",
619 "compress.gunzip",
620 "compress.zstd",
621 "compress.zstd-level",
622 "compress.unzstd",
623 ];
624
625 for word in &self.words {
626 self.validate_statements(&word.body, &word.name, &builtins, external_words)?;
627 }
628
629 Ok(())
630 }
631
632 fn validate_statements(
634 &self,
635 statements: &[Statement],
636 word_name: &str,
637 builtins: &[&str],
638 external_words: &[&str],
639 ) -> Result<(), String> {
640 for statement in statements {
641 match statement {
642 Statement::WordCall { name, .. } => {
643 if builtins.contains(&name.as_str()) {
645 continue;
646 }
647 if self.find_word(name).is_some() {
649 continue;
650 }
651 if external_words.contains(&name.as_str()) {
653 continue;
654 }
655 return Err(format!(
657 "Undefined word '{}' called in word '{}'. \
658 Did you forget to define it or misspell a built-in?",
659 name, word_name
660 ));
661 }
662 Statement::If {
663 then_branch,
664 else_branch,
665 span: _,
666 } => {
667 self.validate_statements(then_branch, word_name, builtins, external_words)?;
669 if let Some(eb) = else_branch {
670 self.validate_statements(eb, word_name, builtins, external_words)?;
671 }
672 }
673 Statement::Quotation { body, .. } => {
674 self.validate_statements(body, word_name, builtins, external_words)?;
676 }
677 Statement::Match { arms, span: _ } => {
678 for arm in arms {
680 self.validate_statements(&arm.body, word_name, builtins, external_words)?;
681 }
682 }
683 _ => {} }
685 }
686 Ok(())
687 }
688
689 pub const MAX_VARIANT_FIELDS: usize = 12;
693
694 pub fn generate_constructors(&mut self) -> Result<(), String> {
707 let mut new_words = Vec::new();
708
709 for union_def in &self.unions {
710 for variant in &union_def.variants {
711 let field_count = variant.fields.len();
712
713 if field_count > Self::MAX_VARIANT_FIELDS {
715 return Err(format!(
716 "Variant '{}' in union '{}' has {} fields, but the maximum is {}. \
717 Consider grouping fields into nested union types.",
718 variant.name,
719 union_def.name,
720 field_count,
721 Self::MAX_VARIANT_FIELDS
722 ));
723 }
724
725 let constructor_name = format!("Make-{}", variant.name);
727 let mut input_stack = StackType::RowVar("a".to_string());
728 for field in &variant.fields {
729 let field_type = parse_type_name(&field.type_name);
730 input_stack = input_stack.push(field_type);
731 }
732 let output_stack =
733 StackType::RowVar("a".to_string()).push(Type::Union(union_def.name.clone()));
734 let effect = Effect::new(input_stack, output_stack);
735 let body = vec![
736 Statement::Symbol(variant.name.clone()),
737 Statement::WordCall {
738 name: format!("variant.make-{}", field_count),
739 span: None,
740 },
741 ];
742 new_words.push(WordDef {
743 name: constructor_name,
744 effect: Some(effect),
745 body,
746 source: variant.source.clone(),
747 allowed_lints: vec![],
748 });
749
750 let predicate_name = format!("is-{}?", variant.name);
754 let predicate_input =
755 StackType::RowVar("a".to_string()).push(Type::Union(union_def.name.clone()));
756 let predicate_output = StackType::RowVar("a".to_string()).push(Type::Bool);
757 let predicate_effect = Effect::new(predicate_input, predicate_output);
758 let predicate_body = vec![
759 Statement::WordCall {
760 name: "variant.tag".to_string(),
761 span: None,
762 },
763 Statement::Symbol(variant.name.clone()),
764 Statement::WordCall {
765 name: "symbol.=".to_string(),
766 span: None,
767 },
768 ];
769 new_words.push(WordDef {
770 name: predicate_name,
771 effect: Some(predicate_effect),
772 body: predicate_body,
773 source: variant.source.clone(),
774 allowed_lints: vec![],
775 });
776
777 for (index, field) in variant.fields.iter().enumerate() {
781 let accessor_name = format!("{}-{}", variant.name, field.name);
782 let field_type = parse_type_name(&field.type_name);
783 let accessor_input = StackType::RowVar("a".to_string())
784 .push(Type::Union(union_def.name.clone()));
785 let accessor_output = StackType::RowVar("a".to_string()).push(field_type);
786 let accessor_effect = Effect::new(accessor_input, accessor_output);
787 let accessor_body = vec![
788 Statement::IntLiteral(index as i64),
789 Statement::WordCall {
790 name: "variant.field-at".to_string(),
791 span: None,
792 },
793 ];
794 new_words.push(WordDef {
795 name: accessor_name,
796 effect: Some(accessor_effect),
797 body: accessor_body,
798 source: variant.source.clone(), allowed_lints: vec![],
800 });
801 }
802 }
803 }
804
805 self.words.extend(new_words);
806 Ok(())
807 }
808
809 pub fn fixup_union_types(&mut self) {
818 let union_names: std::collections::HashSet<String> =
820 self.unions.iter().map(|u| u.name.clone()).collect();
821
822 for word in &mut self.words {
824 if let Some(ref mut effect) = word.effect {
825 Self::fixup_stack_type(&mut effect.inputs, &union_names);
826 Self::fixup_stack_type(&mut effect.outputs, &union_names);
827 }
828 }
829 }
830
831 fn fixup_stack_type(stack: &mut StackType, union_names: &std::collections::HashSet<String>) {
833 match stack {
834 StackType::Empty | StackType::RowVar(_) => {}
835 StackType::Cons { rest, top } => {
836 Self::fixup_type(top, union_names);
837 Self::fixup_stack_type(rest, union_names);
838 }
839 }
840 }
841
842 fn fixup_type(ty: &mut Type, union_names: &std::collections::HashSet<String>) {
844 match ty {
845 Type::Var(name) if union_names.contains(name) => {
846 *ty = Type::Union(name.clone());
847 }
848 Type::Quotation(effect) => {
849 Self::fixup_stack_type(&mut effect.inputs, union_names);
850 Self::fixup_stack_type(&mut effect.outputs, union_names);
851 }
852 Type::Closure { effect, captures } => {
853 Self::fixup_stack_type(&mut effect.inputs, union_names);
854 Self::fixup_stack_type(&mut effect.outputs, union_names);
855 for cap in captures {
856 Self::fixup_type(cap, union_names);
857 }
858 }
859 _ => {}
860 }
861 }
862}
863
864fn parse_type_name(name: &str) -> Type {
867 match name {
868 "Int" => Type::Int,
869 "Float" => Type::Float,
870 "Bool" => Type::Bool,
871 "String" => Type::String,
872 "Channel" => Type::Channel,
873 other => Type::Union(other.to_string()),
874 }
875}
876
877impl Default for Program {
878 fn default() -> Self {
879 Self::new()
880 }
881}
882
883#[cfg(test)]
884mod tests {
885 use super::*;
886
887 #[test]
888 fn test_validate_builtin_words() {
889 let program = Program {
890 includes: vec![],
891 unions: vec![],
892 words: vec![WordDef {
893 name: "main".to_string(),
894 effect: None,
895 body: vec![
896 Statement::IntLiteral(2),
897 Statement::IntLiteral(3),
898 Statement::WordCall {
899 name: "i.add".to_string(),
900 span: None,
901 },
902 Statement::WordCall {
903 name: "io.write-line".to_string(),
904 span: None,
905 },
906 ],
907 source: None,
908 allowed_lints: vec![],
909 }],
910 };
911
912 assert!(program.validate_word_calls().is_ok());
914 }
915
916 #[test]
917 fn test_validate_user_defined_words() {
918 let program = Program {
919 includes: vec![],
920 unions: vec![],
921 words: vec![
922 WordDef {
923 name: "helper".to_string(),
924 effect: None,
925 body: vec![Statement::IntLiteral(42)],
926 source: None,
927 allowed_lints: vec![],
928 },
929 WordDef {
930 name: "main".to_string(),
931 effect: None,
932 body: vec![Statement::WordCall {
933 name: "helper".to_string(),
934 span: None,
935 }],
936 source: None,
937 allowed_lints: vec![],
938 },
939 ],
940 };
941
942 assert!(program.validate_word_calls().is_ok());
944 }
945
946 #[test]
947 fn test_validate_undefined_word() {
948 let program = Program {
949 includes: vec![],
950 unions: vec![],
951 words: vec![WordDef {
952 name: "main".to_string(),
953 effect: None,
954 body: vec![Statement::WordCall {
955 name: "undefined_word".to_string(),
956 span: None,
957 }],
958 source: None,
959 allowed_lints: vec![],
960 }],
961 };
962
963 let result = program.validate_word_calls();
965 assert!(result.is_err());
966 let error = result.unwrap_err();
967 assert!(error.contains("undefined_word"));
968 assert!(error.contains("main"));
969 }
970
971 #[test]
972 fn test_validate_misspelled_builtin() {
973 let program = Program {
974 includes: vec![],
975 unions: vec![],
976 words: vec![WordDef {
977 name: "main".to_string(),
978 effect: None,
979 body: vec![Statement::WordCall {
980 name: "wrte_line".to_string(),
981 span: None,
982 }], source: None,
984 allowed_lints: vec![],
985 }],
986 };
987
988 let result = program.validate_word_calls();
990 assert!(result.is_err());
991 let error = result.unwrap_err();
992 assert!(error.contains("wrte_line"));
993 assert!(error.contains("misspell"));
994 }
995
996 #[test]
997 fn test_generate_constructors() {
998 let mut program = Program {
999 includes: vec![],
1000 unions: vec![UnionDef {
1001 name: "Message".to_string(),
1002 variants: vec![
1003 UnionVariant {
1004 name: "Get".to_string(),
1005 fields: vec![UnionField {
1006 name: "response-chan".to_string(),
1007 type_name: "Int".to_string(),
1008 }],
1009 source: None,
1010 },
1011 UnionVariant {
1012 name: "Put".to_string(),
1013 fields: vec![
1014 UnionField {
1015 name: "value".to_string(),
1016 type_name: "String".to_string(),
1017 },
1018 UnionField {
1019 name: "response-chan".to_string(),
1020 type_name: "Int".to_string(),
1021 },
1022 ],
1023 source: None,
1024 },
1025 ],
1026 source: None,
1027 }],
1028 words: vec![],
1029 };
1030
1031 program.generate_constructors().unwrap();
1033
1034 assert_eq!(program.words.len(), 7);
1038
1039 let make_get = program
1041 .find_word("Make-Get")
1042 .expect("Make-Get should exist");
1043 assert_eq!(make_get.name, "Make-Get");
1044 assert!(make_get.effect.is_some());
1045 let effect = make_get.effect.as_ref().unwrap();
1046 assert_eq!(
1049 format!("{:?}", effect.outputs),
1050 "Cons { rest: RowVar(\"a\"), top: Union(\"Message\") }"
1051 );
1052
1053 let make_put = program
1055 .find_word("Make-Put")
1056 .expect("Make-Put should exist");
1057 assert_eq!(make_put.name, "Make-Put");
1058 assert!(make_put.effect.is_some());
1059
1060 assert_eq!(make_get.body.len(), 2);
1063 match &make_get.body[0] {
1064 Statement::Symbol(s) if s == "Get" => {}
1065 other => panic!("Expected Symbol(\"Get\") for variant tag, got {:?}", other),
1066 }
1067 match &make_get.body[1] {
1068 Statement::WordCall { name, span: None } if name == "variant.make-1" => {}
1069 _ => panic!("Expected WordCall(variant.make-1)"),
1070 }
1071
1072 assert_eq!(make_put.body.len(), 2);
1074 match &make_put.body[0] {
1075 Statement::Symbol(s) if s == "Put" => {}
1076 other => panic!("Expected Symbol(\"Put\") for variant tag, got {:?}", other),
1077 }
1078 match &make_put.body[1] {
1079 Statement::WordCall { name, span: None } if name == "variant.make-2" => {}
1080 _ => panic!("Expected WordCall(variant.make-2)"),
1081 }
1082
1083 let is_get = program.find_word("is-Get?").expect("is-Get? should exist");
1085 assert_eq!(is_get.name, "is-Get?");
1086 assert!(is_get.effect.is_some());
1087 let effect = is_get.effect.as_ref().unwrap();
1088 assert_eq!(
1091 format!("{:?}", effect.outputs),
1092 "Cons { rest: RowVar(\"a\"), top: Bool }"
1093 );
1094
1095 let get_chan = program
1097 .find_word("Get-response-chan")
1098 .expect("Get-response-chan should exist");
1099 assert_eq!(get_chan.name, "Get-response-chan");
1100 assert!(get_chan.effect.is_some());
1101 let effect = get_chan.effect.as_ref().unwrap();
1102 assert_eq!(
1105 format!("{:?}", effect.outputs),
1106 "Cons { rest: RowVar(\"a\"), top: Int }"
1107 );
1108 }
1109}