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.json-escape",
357 "string->int",
358 "symbol.=",
360 "encoding.base64-encode",
362 "encoding.base64-decode",
363 "encoding.base64url-encode",
364 "encoding.base64url-decode",
365 "encoding.hex-encode",
366 "encoding.hex-decode",
367 "crypto.sha256",
369 "crypto.hmac-sha256",
370 "crypto.constant-time-eq",
371 "crypto.random-bytes",
372 "crypto.random-int",
373 "crypto.uuid4",
374 "crypto.aes-gcm-encrypt",
375 "crypto.aes-gcm-decrypt",
376 "crypto.pbkdf2-sha256",
377 "crypto.ed25519-keypair",
378 "crypto.ed25519-sign",
379 "crypto.ed25519-verify",
380 "http.get",
382 "http.post",
383 "http.put",
384 "http.delete",
385 "list.make",
387 "list.push",
388 "list.get",
389 "list.set",
390 "list.map",
391 "list.filter",
392 "list.fold",
393 "list.each",
394 "list.length",
395 "list.empty?",
396 "map.make",
398 "map.get",
399 "map.set",
400 "map.has?",
401 "map.remove",
402 "map.keys",
403 "map.values",
404 "map.size",
405 "map.empty?",
406 "variant.field-count",
408 "variant.tag",
409 "variant.field-at",
410 "variant.append",
411 "variant.last",
412 "variant.init",
413 "variant.make-0",
414 "variant.make-1",
415 "variant.make-2",
416 "variant.make-3",
417 "variant.make-4",
418 "wrap-0",
420 "wrap-1",
421 "wrap-2",
422 "wrap-3",
423 "wrap-4",
424 "i.add",
426 "i.subtract",
427 "i.multiply",
428 "i.divide",
429 "i.modulo",
430 "i.+",
432 "i.-",
433 "i.*",
434 "i./",
435 "i.%",
436 "i.=",
438 "i.<",
439 "i.>",
440 "i.<=",
441 "i.>=",
442 "i.<>",
443 "i.eq",
445 "i.lt",
446 "i.gt",
447 "i.lte",
448 "i.gte",
449 "i.neq",
450 "dup",
452 "drop",
453 "swap",
454 "over",
455 "rot",
456 "nip",
457 "tuck",
458 "2dup",
459 "3drop",
460 "pick",
461 "roll",
462 ">aux",
464 "aux>",
465 "and",
467 "or",
468 "not",
469 "band",
471 "bor",
472 "bxor",
473 "bnot",
474 "shl",
475 "shr",
476 "popcount",
477 "clz",
478 "ctz",
479 "int-bits",
480 "chan.make",
482 "chan.send",
483 "chan.receive",
484 "chan.close",
485 "chan.yield",
486 "call",
488 "strand.spawn",
489 "strand.weave",
490 "strand.resume",
491 "strand.weave-cancel",
492 "yield",
493 "cond",
494 "tcp.listen",
496 "tcp.accept",
497 "tcp.read",
498 "tcp.write",
499 "tcp.close",
500 "os.getenv",
502 "os.home-dir",
503 "os.current-dir",
504 "os.path-exists",
505 "os.path-is-file",
506 "os.path-is-dir",
507 "os.path-join",
508 "os.path-parent",
509 "os.path-filename",
510 "os.exit",
511 "os.name",
512 "os.arch",
513 "signal.trap",
515 "signal.received?",
516 "signal.pending?",
517 "signal.default",
518 "signal.ignore",
519 "signal.clear",
520 "signal.SIGINT",
521 "signal.SIGTERM",
522 "signal.SIGHUP",
523 "signal.SIGPIPE",
524 "signal.SIGUSR1",
525 "signal.SIGUSR2",
526 "signal.SIGCHLD",
527 "signal.SIGALRM",
528 "signal.SIGCONT",
529 "terminal.raw-mode",
531 "terminal.read-char",
532 "terminal.read-char?",
533 "terminal.width",
534 "terminal.height",
535 "terminal.flush",
536 "f.add",
538 "f.subtract",
539 "f.multiply",
540 "f.divide",
541 "f.+",
543 "f.-",
544 "f.*",
545 "f./",
546 "f.=",
548 "f.<",
549 "f.>",
550 "f.<=",
551 "f.>=",
552 "f.<>",
553 "f.eq",
555 "f.lt",
556 "f.gt",
557 "f.lte",
558 "f.gte",
559 "f.neq",
560 "int->float",
562 "float->int",
563 "float->string",
564 "string->float",
565 "test.init",
567 "test.finish",
568 "test.has-failures",
569 "test.assert",
570 "test.assert-not",
571 "test.assert-eq",
572 "test.assert-eq-str",
573 "test.fail",
574 "test.pass-count",
575 "test.fail-count",
576 "time.now",
578 "time.nanos",
579 "time.sleep-ms",
580 "son.dump",
582 "son.dump-pretty",
583 "stack.dump",
585 "regex.match?",
587 "regex.find",
588 "regex.find-all",
589 "regex.replace",
590 "regex.replace-all",
591 "regex.captures",
592 "regex.split",
593 "regex.valid?",
594 "compress.gzip",
596 "compress.gzip-level",
597 "compress.gunzip",
598 "compress.zstd",
599 "compress.zstd-level",
600 "compress.unzstd",
601 ];
602
603 for word in &self.words {
604 self.validate_statements(&word.body, &word.name, &builtins, external_words)?;
605 }
606
607 Ok(())
608 }
609
610 fn validate_statements(
612 &self,
613 statements: &[Statement],
614 word_name: &str,
615 builtins: &[&str],
616 external_words: &[&str],
617 ) -> Result<(), String> {
618 for statement in statements {
619 match statement {
620 Statement::WordCall { name, .. } => {
621 if builtins.contains(&name.as_str()) {
623 continue;
624 }
625 if self.find_word(name).is_some() {
627 continue;
628 }
629 if external_words.contains(&name.as_str()) {
631 continue;
632 }
633 return Err(format!(
635 "Undefined word '{}' called in word '{}'. \
636 Did you forget to define it or misspell a built-in?",
637 name, word_name
638 ));
639 }
640 Statement::If {
641 then_branch,
642 else_branch,
643 span: _,
644 } => {
645 self.validate_statements(then_branch, word_name, builtins, external_words)?;
647 if let Some(eb) = else_branch {
648 self.validate_statements(eb, word_name, builtins, external_words)?;
649 }
650 }
651 Statement::Quotation { body, .. } => {
652 self.validate_statements(body, word_name, builtins, external_words)?;
654 }
655 Statement::Match { arms, span: _ } => {
656 for arm in arms {
658 self.validate_statements(&arm.body, word_name, builtins, external_words)?;
659 }
660 }
661 _ => {} }
663 }
664 Ok(())
665 }
666
667 pub const MAX_VARIANT_FIELDS: usize = 12;
671
672 pub fn generate_constructors(&mut self) -> Result<(), String> {
685 let mut new_words = Vec::new();
686
687 for union_def in &self.unions {
688 for variant in &union_def.variants {
689 let field_count = variant.fields.len();
690
691 if field_count > Self::MAX_VARIANT_FIELDS {
693 return Err(format!(
694 "Variant '{}' in union '{}' has {} fields, but the maximum is {}. \
695 Consider grouping fields into nested union types.",
696 variant.name,
697 union_def.name,
698 field_count,
699 Self::MAX_VARIANT_FIELDS
700 ));
701 }
702
703 let constructor_name = format!("Make-{}", variant.name);
705 let mut input_stack = StackType::RowVar("a".to_string());
706 for field in &variant.fields {
707 let field_type = parse_type_name(&field.type_name);
708 input_stack = input_stack.push(field_type);
709 }
710 let output_stack =
711 StackType::RowVar("a".to_string()).push(Type::Union(union_def.name.clone()));
712 let effect = Effect::new(input_stack, output_stack);
713 let body = vec![
714 Statement::Symbol(variant.name.clone()),
715 Statement::WordCall {
716 name: format!("variant.make-{}", field_count),
717 span: None,
718 },
719 ];
720 new_words.push(WordDef {
721 name: constructor_name,
722 effect: Some(effect),
723 body,
724 source: variant.source.clone(),
725 allowed_lints: vec![],
726 });
727
728 let predicate_name = format!("is-{}?", variant.name);
732 let predicate_input =
733 StackType::RowVar("a".to_string()).push(Type::Union(union_def.name.clone()));
734 let predicate_output = StackType::RowVar("a".to_string()).push(Type::Bool);
735 let predicate_effect = Effect::new(predicate_input, predicate_output);
736 let predicate_body = vec![
737 Statement::WordCall {
738 name: "variant.tag".to_string(),
739 span: None,
740 },
741 Statement::Symbol(variant.name.clone()),
742 Statement::WordCall {
743 name: "symbol.=".to_string(),
744 span: None,
745 },
746 ];
747 new_words.push(WordDef {
748 name: predicate_name,
749 effect: Some(predicate_effect),
750 body: predicate_body,
751 source: variant.source.clone(),
752 allowed_lints: vec![],
753 });
754
755 for (index, field) in variant.fields.iter().enumerate() {
759 let accessor_name = format!("{}-{}", variant.name, field.name);
760 let field_type = parse_type_name(&field.type_name);
761 let accessor_input = StackType::RowVar("a".to_string())
762 .push(Type::Union(union_def.name.clone()));
763 let accessor_output = StackType::RowVar("a".to_string()).push(field_type);
764 let accessor_effect = Effect::new(accessor_input, accessor_output);
765 let accessor_body = vec![
766 Statement::IntLiteral(index as i64),
767 Statement::WordCall {
768 name: "variant.field-at".to_string(),
769 span: None,
770 },
771 ];
772 new_words.push(WordDef {
773 name: accessor_name,
774 effect: Some(accessor_effect),
775 body: accessor_body,
776 source: variant.source.clone(), allowed_lints: vec![],
778 });
779 }
780 }
781 }
782
783 self.words.extend(new_words);
784 Ok(())
785 }
786
787 pub fn fixup_union_types(&mut self) {
796 let union_names: std::collections::HashSet<String> =
798 self.unions.iter().map(|u| u.name.clone()).collect();
799
800 for word in &mut self.words {
802 if let Some(ref mut effect) = word.effect {
803 Self::fixup_stack_type(&mut effect.inputs, &union_names);
804 Self::fixup_stack_type(&mut effect.outputs, &union_names);
805 }
806 }
807 }
808
809 fn fixup_stack_type(stack: &mut StackType, union_names: &std::collections::HashSet<String>) {
811 match stack {
812 StackType::Empty | StackType::RowVar(_) => {}
813 StackType::Cons { rest, top } => {
814 Self::fixup_type(top, union_names);
815 Self::fixup_stack_type(rest, union_names);
816 }
817 }
818 }
819
820 fn fixup_type(ty: &mut Type, union_names: &std::collections::HashSet<String>) {
822 match ty {
823 Type::Var(name) if union_names.contains(name) => {
824 *ty = Type::Union(name.clone());
825 }
826 Type::Quotation(effect) => {
827 Self::fixup_stack_type(&mut effect.inputs, union_names);
828 Self::fixup_stack_type(&mut effect.outputs, union_names);
829 }
830 Type::Closure { effect, captures } => {
831 Self::fixup_stack_type(&mut effect.inputs, union_names);
832 Self::fixup_stack_type(&mut effect.outputs, union_names);
833 for cap in captures {
834 Self::fixup_type(cap, union_names);
835 }
836 }
837 _ => {}
838 }
839 }
840}
841
842fn parse_type_name(name: &str) -> Type {
845 match name {
846 "Int" => Type::Int,
847 "Float" => Type::Float,
848 "Bool" => Type::Bool,
849 "String" => Type::String,
850 "Channel" => Type::Channel,
851 other => Type::Union(other.to_string()),
852 }
853}
854
855impl Default for Program {
856 fn default() -> Self {
857 Self::new()
858 }
859}
860
861#[cfg(test)]
862mod tests {
863 use super::*;
864
865 #[test]
866 fn test_validate_builtin_words() {
867 let program = Program {
868 includes: vec![],
869 unions: vec![],
870 words: vec![WordDef {
871 name: "main".to_string(),
872 effect: None,
873 body: vec![
874 Statement::IntLiteral(2),
875 Statement::IntLiteral(3),
876 Statement::WordCall {
877 name: "i.add".to_string(),
878 span: None,
879 },
880 Statement::WordCall {
881 name: "io.write-line".to_string(),
882 span: None,
883 },
884 ],
885 source: None,
886 allowed_lints: vec![],
887 }],
888 };
889
890 assert!(program.validate_word_calls().is_ok());
892 }
893
894 #[test]
895 fn test_validate_user_defined_words() {
896 let program = Program {
897 includes: vec![],
898 unions: vec![],
899 words: vec![
900 WordDef {
901 name: "helper".to_string(),
902 effect: None,
903 body: vec![Statement::IntLiteral(42)],
904 source: None,
905 allowed_lints: vec![],
906 },
907 WordDef {
908 name: "main".to_string(),
909 effect: None,
910 body: vec![Statement::WordCall {
911 name: "helper".to_string(),
912 span: None,
913 }],
914 source: None,
915 allowed_lints: vec![],
916 },
917 ],
918 };
919
920 assert!(program.validate_word_calls().is_ok());
922 }
923
924 #[test]
925 fn test_validate_undefined_word() {
926 let program = Program {
927 includes: vec![],
928 unions: vec![],
929 words: vec![WordDef {
930 name: "main".to_string(),
931 effect: None,
932 body: vec![Statement::WordCall {
933 name: "undefined_word".to_string(),
934 span: None,
935 }],
936 source: None,
937 allowed_lints: vec![],
938 }],
939 };
940
941 let result = program.validate_word_calls();
943 assert!(result.is_err());
944 let error = result.unwrap_err();
945 assert!(error.contains("undefined_word"));
946 assert!(error.contains("main"));
947 }
948
949 #[test]
950 fn test_validate_misspelled_builtin() {
951 let program = Program {
952 includes: vec![],
953 unions: vec![],
954 words: vec![WordDef {
955 name: "main".to_string(),
956 effect: None,
957 body: vec![Statement::WordCall {
958 name: "wrte_line".to_string(),
959 span: None,
960 }], source: None,
962 allowed_lints: vec![],
963 }],
964 };
965
966 let result = program.validate_word_calls();
968 assert!(result.is_err());
969 let error = result.unwrap_err();
970 assert!(error.contains("wrte_line"));
971 assert!(error.contains("misspell"));
972 }
973
974 #[test]
975 fn test_generate_constructors() {
976 let mut program = Program {
977 includes: vec![],
978 unions: vec![UnionDef {
979 name: "Message".to_string(),
980 variants: vec![
981 UnionVariant {
982 name: "Get".to_string(),
983 fields: vec![UnionField {
984 name: "response-chan".to_string(),
985 type_name: "Int".to_string(),
986 }],
987 source: None,
988 },
989 UnionVariant {
990 name: "Put".to_string(),
991 fields: vec![
992 UnionField {
993 name: "value".to_string(),
994 type_name: "String".to_string(),
995 },
996 UnionField {
997 name: "response-chan".to_string(),
998 type_name: "Int".to_string(),
999 },
1000 ],
1001 source: None,
1002 },
1003 ],
1004 source: None,
1005 }],
1006 words: vec![],
1007 };
1008
1009 program.generate_constructors().unwrap();
1011
1012 assert_eq!(program.words.len(), 7);
1016
1017 let make_get = program
1019 .find_word("Make-Get")
1020 .expect("Make-Get should exist");
1021 assert_eq!(make_get.name, "Make-Get");
1022 assert!(make_get.effect.is_some());
1023 let effect = make_get.effect.as_ref().unwrap();
1024 assert_eq!(
1027 format!("{:?}", effect.outputs),
1028 "Cons { rest: RowVar(\"a\"), top: Union(\"Message\") }"
1029 );
1030
1031 let make_put = program
1033 .find_word("Make-Put")
1034 .expect("Make-Put should exist");
1035 assert_eq!(make_put.name, "Make-Put");
1036 assert!(make_put.effect.is_some());
1037
1038 assert_eq!(make_get.body.len(), 2);
1041 match &make_get.body[0] {
1042 Statement::Symbol(s) if s == "Get" => {}
1043 other => panic!("Expected Symbol(\"Get\") for variant tag, got {:?}", other),
1044 }
1045 match &make_get.body[1] {
1046 Statement::WordCall { name, span: None } if name == "variant.make-1" => {}
1047 _ => panic!("Expected WordCall(variant.make-1)"),
1048 }
1049
1050 assert_eq!(make_put.body.len(), 2);
1052 match &make_put.body[0] {
1053 Statement::Symbol(s) if s == "Put" => {}
1054 other => panic!("Expected Symbol(\"Put\") for variant tag, got {:?}", other),
1055 }
1056 match &make_put.body[1] {
1057 Statement::WordCall { name, span: None } if name == "variant.make-2" => {}
1058 _ => panic!("Expected WordCall(variant.make-2)"),
1059 }
1060
1061 let is_get = program.find_word("is-Get?").expect("is-Get? should exist");
1063 assert_eq!(is_get.name, "is-Get?");
1064 assert!(is_get.effect.is_some());
1065 let effect = is_get.effect.as_ref().unwrap();
1066 assert_eq!(
1069 format!("{:?}", effect.outputs),
1070 "Cons { rest: RowVar(\"a\"), top: Bool }"
1071 );
1072
1073 let get_chan = program
1075 .find_word("Get-response-chan")
1076 .expect("Get-response-chan should exist");
1077 assert_eq!(get_chan.name, "Get-response-chan");
1078 assert!(get_chan.effect.is_some());
1079 let effect = get_chan.effect.as_ref().unwrap();
1080 assert_eq!(
1083 format!("{:?}", effect.outputs),
1084 "Cons { rest: RowVar(\"a\"), top: Int }"
1085 );
1086 }
1087}