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 pub fn line(&self) -> usize {
46 self.start_line
47 }
48}
49
50impl std::fmt::Display for SourceLocation {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 if self.start_line == self.end_line {
53 write!(f, "{}:{}", self.file.display(), self.start_line + 1)
54 } else {
55 write!(
56 f,
57 "{}:{}-{}",
58 self.file.display(),
59 self.start_line + 1,
60 self.end_line + 1
61 )
62 }
63 }
64}
65
66#[derive(Debug, Clone, PartialEq)]
68pub enum Include {
69 Std(String),
71 Relative(String),
73 Ffi(String),
75}
76
77#[derive(Debug, Clone, PartialEq)]
84pub struct UnionField {
85 pub name: String,
86 pub type_name: String, }
88
89#[derive(Debug, Clone, PartialEq)]
92pub struct UnionVariant {
93 pub name: String,
94 pub fields: Vec<UnionField>,
95 pub source: Option<SourceLocation>,
96}
97
98#[derive(Debug, Clone, PartialEq)]
108pub struct UnionDef {
109 pub name: String,
110 pub variants: Vec<UnionVariant>,
111 pub source: Option<SourceLocation>,
112}
113
114#[derive(Debug, Clone, PartialEq)]
118pub enum Pattern {
119 Variant(String),
122
123 VariantWithBindings { name: String, bindings: Vec<String> },
126}
127
128#[derive(Debug, Clone, PartialEq)]
130pub struct MatchArm {
131 pub pattern: Pattern,
132 pub body: Vec<Statement>,
133}
134
135#[derive(Debug, Clone, PartialEq)]
136pub struct Program {
137 pub includes: Vec<Include>,
138 pub unions: Vec<UnionDef>,
139 pub words: Vec<WordDef>,
140}
141
142#[derive(Debug, Clone, PartialEq)]
143pub struct WordDef {
144 pub name: String,
145 pub effect: Option<Effect>,
148 pub body: Vec<Statement>,
149 pub source: Option<SourceLocation>,
151 pub allowed_lints: Vec<String>,
154}
155
156#[derive(Debug, Clone, PartialEq, Default)]
158pub struct Span {
159 pub line: usize,
161 pub column: usize,
163 pub length: usize,
165}
166
167impl Span {
168 pub fn new(line: usize, column: usize, length: usize) -> Self {
169 Span {
170 line,
171 column,
172 length,
173 }
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, Default)]
179pub struct QuotationSpan {
180 pub start_line: usize,
182 pub start_column: usize,
184 pub end_line: usize,
186 pub end_column: usize,
188}
189
190impl QuotationSpan {
191 pub fn new(start_line: usize, start_column: usize, end_line: usize, end_column: usize) -> Self {
192 QuotationSpan {
193 start_line,
194 start_column,
195 end_line,
196 end_column,
197 }
198 }
199
200 pub fn contains(&self, line: usize, column: usize) -> bool {
202 if line < self.start_line || line > self.end_line {
203 return false;
204 }
205 if line == self.start_line && column < self.start_column {
206 return false;
207 }
208 if line == self.end_line && column >= self.end_column {
209 return false;
210 }
211 true
212 }
213}
214
215#[derive(Debug, Clone, PartialEq)]
216pub enum Statement {
217 IntLiteral(i64),
219
220 FloatLiteral(f64),
222
223 BoolLiteral(bool),
225
226 StringLiteral(String),
228
229 Symbol(String),
234
235 WordCall { name: String, span: Option<Span> },
238
239 If {
244 then_branch: Vec<Statement>,
246 else_branch: Option<Vec<Statement>>,
248 },
249
250 Quotation {
260 id: usize,
261 body: Vec<Statement>,
262 span: Option<QuotationSpan>,
263 },
264
265 Match {
279 arms: Vec<MatchArm>,
281 },
282}
283
284impl Program {
285 pub fn new() -> Self {
286 Program {
287 includes: Vec::new(),
288 unions: Vec::new(),
289 words: Vec::new(),
290 }
291 }
292
293 pub fn find_union(&self, name: &str) -> Option<&UnionDef> {
295 self.unions.iter().find(|u| u.name == name)
296 }
297
298 pub fn find_word(&self, name: &str) -> Option<&WordDef> {
299 self.words.iter().find(|w| w.name == name)
300 }
301
302 pub fn validate_word_calls(&self) -> Result<(), String> {
304 self.validate_word_calls_with_externals(&[])
305 }
306
307 pub fn validate_word_calls_with_externals(
312 &self,
313 external_words: &[&str],
314 ) -> Result<(), String> {
315 let builtins = [
318 "io.write",
320 "io.write-line",
321 "io.read-line",
322 "io.read-line+",
323 "io.read-n",
324 "int->string",
325 "symbol->string",
326 "string->symbol",
327 "args.count",
329 "args.at",
330 "file.slurp",
332 "file.exists?",
333 "file.for-each-line+",
334 "string.concat",
336 "string.length",
337 "string.byte-length",
338 "string.char-at",
339 "string.substring",
340 "char->string",
341 "string.find",
342 "string.split",
343 "string.contains",
344 "string.starts-with",
345 "string.empty?",
346 "string.trim",
347 "string.chomp",
348 "string.to-upper",
349 "string.to-lower",
350 "string.equal?",
351 "string.json-escape",
352 "string->int",
353 "symbol.=",
355 "encoding.base64-encode",
357 "encoding.base64-decode",
358 "encoding.base64url-encode",
359 "encoding.base64url-decode",
360 "encoding.hex-encode",
361 "encoding.hex-decode",
362 "crypto.sha256",
364 "crypto.hmac-sha256",
365 "crypto.constant-time-eq",
366 "crypto.random-bytes",
367 "crypto.random-int",
368 "crypto.uuid4",
369 "crypto.aes-gcm-encrypt",
370 "crypto.aes-gcm-decrypt",
371 "crypto.pbkdf2-sha256",
372 "crypto.ed25519-keypair",
373 "crypto.ed25519-sign",
374 "crypto.ed25519-verify",
375 "http.get",
377 "http.post",
378 "http.put",
379 "http.delete",
380 "list.make",
382 "list.push",
383 "list.get",
384 "list.set",
385 "list.map",
386 "list.filter",
387 "list.fold",
388 "list.each",
389 "list.length",
390 "list.empty?",
391 "map.make",
393 "map.get",
394 "map.set",
395 "map.has?",
396 "map.remove",
397 "map.keys",
398 "map.values",
399 "map.size",
400 "map.empty?",
401 "variant.field-count",
403 "variant.tag",
404 "variant.field-at",
405 "variant.append",
406 "variant.last",
407 "variant.init",
408 "variant.make-0",
409 "variant.make-1",
410 "variant.make-2",
411 "variant.make-3",
412 "variant.make-4",
413 "wrap-0",
415 "wrap-1",
416 "wrap-2",
417 "wrap-3",
418 "wrap-4",
419 "i.add",
421 "i.subtract",
422 "i.multiply",
423 "i.divide",
424 "i.modulo",
425 "i.+",
427 "i.-",
428 "i.*",
429 "i./",
430 "i.%",
431 "i.=",
433 "i.<",
434 "i.>",
435 "i.<=",
436 "i.>=",
437 "i.<>",
438 "i.eq",
440 "i.lt",
441 "i.gt",
442 "i.lte",
443 "i.gte",
444 "i.neq",
445 "dup",
447 "drop",
448 "swap",
449 "over",
450 "rot",
451 "nip",
452 "tuck",
453 "2dup",
454 "3drop",
455 "pick",
456 "roll",
457 "and",
459 "or",
460 "not",
461 "band",
463 "bor",
464 "bxor",
465 "bnot",
466 "shl",
467 "shr",
468 "popcount",
469 "clz",
470 "ctz",
471 "int-bits",
472 "chan.make",
474 "chan.send",
475 "chan.receive",
476 "chan.close",
477 "chan.yield",
478 "call",
480 "strand.spawn",
481 "strand.weave",
482 "strand.resume",
483 "strand.weave-cancel",
484 "yield",
485 "cond",
486 "tcp.listen",
488 "tcp.accept",
489 "tcp.read",
490 "tcp.write",
491 "tcp.close",
492 "os.getenv",
494 "os.home-dir",
495 "os.current-dir",
496 "os.path-exists",
497 "os.path-is-file",
498 "os.path-is-dir",
499 "os.path-join",
500 "os.path-parent",
501 "os.path-filename",
502 "os.exit",
503 "os.name",
504 "os.arch",
505 "signal.trap",
507 "signal.received?",
508 "signal.pending?",
509 "signal.default",
510 "signal.ignore",
511 "signal.clear",
512 "signal.SIGINT",
513 "signal.SIGTERM",
514 "signal.SIGHUP",
515 "signal.SIGPIPE",
516 "signal.SIGUSR1",
517 "signal.SIGUSR2",
518 "signal.SIGCHLD",
519 "signal.SIGALRM",
520 "signal.SIGCONT",
521 "terminal.raw-mode",
523 "terminal.read-char",
524 "terminal.read-char?",
525 "terminal.width",
526 "terminal.height",
527 "terminal.flush",
528 "f.add",
530 "f.subtract",
531 "f.multiply",
532 "f.divide",
533 "f.+",
535 "f.-",
536 "f.*",
537 "f./",
538 "f.=",
540 "f.<",
541 "f.>",
542 "f.<=",
543 "f.>=",
544 "f.<>",
545 "f.eq",
547 "f.lt",
548 "f.gt",
549 "f.lte",
550 "f.gte",
551 "f.neq",
552 "int->float",
554 "float->int",
555 "float->string",
556 "string->float",
557 "test.init",
559 "test.finish",
560 "test.has-failures",
561 "test.assert",
562 "test.assert-not",
563 "test.assert-eq",
564 "test.assert-eq-str",
565 "test.fail",
566 "test.pass-count",
567 "test.fail-count",
568 "time.now",
570 "time.nanos",
571 "time.sleep-ms",
572 "son.dump",
574 "son.dump-pretty",
575 "stack.dump",
577 "regex.match?",
579 "regex.find",
580 "regex.find-all",
581 "regex.replace",
582 "regex.replace-all",
583 "regex.captures",
584 "regex.split",
585 "regex.valid?",
586 "compress.gzip",
588 "compress.gzip-level",
589 "compress.gunzip",
590 "compress.zstd",
591 "compress.zstd-level",
592 "compress.unzstd",
593 ];
594
595 for word in &self.words {
596 self.validate_statements(&word.body, &word.name, &builtins, external_words)?;
597 }
598
599 Ok(())
600 }
601
602 fn validate_statements(
604 &self,
605 statements: &[Statement],
606 word_name: &str,
607 builtins: &[&str],
608 external_words: &[&str],
609 ) -> Result<(), String> {
610 for statement in statements {
611 match statement {
612 Statement::WordCall { name, .. } => {
613 if builtins.contains(&name.as_str()) {
615 continue;
616 }
617 if self.find_word(name).is_some() {
619 continue;
620 }
621 if external_words.contains(&name.as_str()) {
623 continue;
624 }
625 return Err(format!(
627 "Undefined word '{}' called in word '{}'. \
628 Did you forget to define it or misspell a built-in?",
629 name, word_name
630 ));
631 }
632 Statement::If {
633 then_branch,
634 else_branch,
635 } => {
636 self.validate_statements(then_branch, word_name, builtins, external_words)?;
638 if let Some(eb) = else_branch {
639 self.validate_statements(eb, word_name, builtins, external_words)?;
640 }
641 }
642 Statement::Quotation { body, .. } => {
643 self.validate_statements(body, word_name, builtins, external_words)?;
645 }
646 Statement::Match { arms } => {
647 for arm in arms {
649 self.validate_statements(&arm.body, word_name, builtins, external_words)?;
650 }
651 }
652 _ => {} }
654 }
655 Ok(())
656 }
657
658 pub const MAX_VARIANT_FIELDS: usize = 4;
662
663 pub fn generate_constructors(&mut self) -> Result<(), String> {
673 let mut new_words = Vec::new();
674
675 for union_def in &self.unions {
676 for variant in &union_def.variants {
677 let constructor_name = format!("Make-{}", variant.name);
678 let field_count = variant.fields.len();
679
680 if field_count > Self::MAX_VARIANT_FIELDS {
682 return Err(format!(
683 "Variant '{}' in union '{}' has {} fields, but the maximum is {}. \
684 Consider grouping fields into nested union types.",
685 variant.name,
686 union_def.name,
687 field_count,
688 Self::MAX_VARIANT_FIELDS
689 ));
690 }
691
692 let mut input_stack = StackType::RowVar("a".to_string());
695 for field in &variant.fields {
696 let field_type = parse_type_name(&field.type_name);
697 input_stack = input_stack.push(field_type);
698 }
699
700 let output_stack =
702 StackType::RowVar("a".to_string()).push(Type::Union(union_def.name.clone()));
703
704 let effect = Effect::new(input_stack, output_stack);
705
706 let body = vec![
710 Statement::Symbol(variant.name.clone()),
711 Statement::WordCall {
712 name: format!("variant.make-{}", field_count),
713 span: None, },
715 ];
716
717 new_words.push(WordDef {
718 name: constructor_name,
719 effect: Some(effect),
720 body,
721 source: variant.source.clone(),
722 allowed_lints: vec![],
723 });
724 }
725 }
726
727 self.words.extend(new_words);
728 Ok(())
729 }
730}
731
732fn parse_type_name(name: &str) -> Type {
735 match name {
736 "Int" => Type::Int,
737 "Float" => Type::Float,
738 "Bool" => Type::Bool,
739 "String" => Type::String,
740 "Channel" => Type::Channel,
741 other => Type::Union(other.to_string()),
742 }
743}
744
745impl Default for Program {
746 fn default() -> Self {
747 Self::new()
748 }
749}
750
751#[cfg(test)]
752mod tests {
753 use super::*;
754
755 #[test]
756 fn test_validate_builtin_words() {
757 let program = Program {
758 includes: vec![],
759 unions: vec![],
760 words: vec![WordDef {
761 name: "main".to_string(),
762 effect: None,
763 body: vec![
764 Statement::IntLiteral(2),
765 Statement::IntLiteral(3),
766 Statement::WordCall {
767 name: "i.add".to_string(),
768 span: None,
769 },
770 Statement::WordCall {
771 name: "io.write-line".to_string(),
772 span: None,
773 },
774 ],
775 source: None,
776 allowed_lints: vec![],
777 }],
778 };
779
780 assert!(program.validate_word_calls().is_ok());
782 }
783
784 #[test]
785 fn test_validate_user_defined_words() {
786 let program = Program {
787 includes: vec![],
788 unions: vec![],
789 words: vec![
790 WordDef {
791 name: "helper".to_string(),
792 effect: None,
793 body: vec![Statement::IntLiteral(42)],
794 source: None,
795 allowed_lints: vec![],
796 },
797 WordDef {
798 name: "main".to_string(),
799 effect: None,
800 body: vec![Statement::WordCall {
801 name: "helper".to_string(),
802 span: None,
803 }],
804 source: None,
805 allowed_lints: vec![],
806 },
807 ],
808 };
809
810 assert!(program.validate_word_calls().is_ok());
812 }
813
814 #[test]
815 fn test_validate_undefined_word() {
816 let program = Program {
817 includes: vec![],
818 unions: vec![],
819 words: vec![WordDef {
820 name: "main".to_string(),
821 effect: None,
822 body: vec![Statement::WordCall {
823 name: "undefined_word".to_string(),
824 span: None,
825 }],
826 source: None,
827 allowed_lints: vec![],
828 }],
829 };
830
831 let result = program.validate_word_calls();
833 assert!(result.is_err());
834 let error = result.unwrap_err();
835 assert!(error.contains("undefined_word"));
836 assert!(error.contains("main"));
837 }
838
839 #[test]
840 fn test_validate_misspelled_builtin() {
841 let program = Program {
842 includes: vec![],
843 unions: vec![],
844 words: vec![WordDef {
845 name: "main".to_string(),
846 effect: None,
847 body: vec![Statement::WordCall {
848 name: "wrte_line".to_string(),
849 span: None,
850 }], source: None,
852 allowed_lints: vec![],
853 }],
854 };
855
856 let result = program.validate_word_calls();
858 assert!(result.is_err());
859 let error = result.unwrap_err();
860 assert!(error.contains("wrte_line"));
861 assert!(error.contains("misspell"));
862 }
863
864 #[test]
865 fn test_generate_constructors() {
866 let mut program = Program {
867 includes: vec![],
868 unions: vec![UnionDef {
869 name: "Message".to_string(),
870 variants: vec![
871 UnionVariant {
872 name: "Get".to_string(),
873 fields: vec![UnionField {
874 name: "response-chan".to_string(),
875 type_name: "Int".to_string(),
876 }],
877 source: None,
878 },
879 UnionVariant {
880 name: "Put".to_string(),
881 fields: vec![
882 UnionField {
883 name: "value".to_string(),
884 type_name: "String".to_string(),
885 },
886 UnionField {
887 name: "response-chan".to_string(),
888 type_name: "Int".to_string(),
889 },
890 ],
891 source: None,
892 },
893 ],
894 source: None,
895 }],
896 words: vec![],
897 };
898
899 program.generate_constructors().unwrap();
901
902 assert_eq!(program.words.len(), 2);
904
905 let make_get = program
907 .find_word("Make-Get")
908 .expect("Make-Get should exist");
909 assert_eq!(make_get.name, "Make-Get");
910 assert!(make_get.effect.is_some());
911 let effect = make_get.effect.as_ref().unwrap();
912 assert_eq!(
915 format!("{:?}", effect.outputs),
916 "Cons { rest: RowVar(\"a\"), top: Union(\"Message\") }"
917 );
918
919 let make_put = program
921 .find_word("Make-Put")
922 .expect("Make-Put should exist");
923 assert_eq!(make_put.name, "Make-Put");
924 assert!(make_put.effect.is_some());
925
926 assert_eq!(make_get.body.len(), 2);
929 match &make_get.body[0] {
930 Statement::Symbol(s) if s == "Get" => {}
931 other => panic!("Expected Symbol(\"Get\") for variant tag, got {:?}", other),
932 }
933 match &make_get.body[1] {
934 Statement::WordCall { name, span: None } if name == "variant.make-1" => {}
935 _ => panic!("Expected WordCall(variant.make-1)"),
936 }
937
938 assert_eq!(make_put.body.len(), 2);
940 match &make_put.body[0] {
941 Statement::Symbol(s) if s == "Put" => {}
942 other => panic!("Expected Symbol(\"Put\") for variant tag, got {:?}", other),
943 }
944 match &make_put.body[1] {
945 Statement::WordCall { name, span: None } if name == "variant.make-2" => {}
946 _ => panic!("Expected WordCall(variant.make-2)"),
947 }
948 }
949}