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 "and",
464 "or",
465 "not",
466 "band",
468 "bor",
469 "bxor",
470 "bnot",
471 "shl",
472 "shr",
473 "popcount",
474 "clz",
475 "ctz",
476 "int-bits",
477 "chan.make",
479 "chan.send",
480 "chan.receive",
481 "chan.close",
482 "chan.yield",
483 "call",
485 "strand.spawn",
486 "strand.weave",
487 "strand.resume",
488 "strand.weave-cancel",
489 "yield",
490 "cond",
491 "tcp.listen",
493 "tcp.accept",
494 "tcp.read",
495 "tcp.write",
496 "tcp.close",
497 "os.getenv",
499 "os.home-dir",
500 "os.current-dir",
501 "os.path-exists",
502 "os.path-is-file",
503 "os.path-is-dir",
504 "os.path-join",
505 "os.path-parent",
506 "os.path-filename",
507 "os.exit",
508 "os.name",
509 "os.arch",
510 "signal.trap",
512 "signal.received?",
513 "signal.pending?",
514 "signal.default",
515 "signal.ignore",
516 "signal.clear",
517 "signal.SIGINT",
518 "signal.SIGTERM",
519 "signal.SIGHUP",
520 "signal.SIGPIPE",
521 "signal.SIGUSR1",
522 "signal.SIGUSR2",
523 "signal.SIGCHLD",
524 "signal.SIGALRM",
525 "signal.SIGCONT",
526 "terminal.raw-mode",
528 "terminal.read-char",
529 "terminal.read-char?",
530 "terminal.width",
531 "terminal.height",
532 "terminal.flush",
533 "f.add",
535 "f.subtract",
536 "f.multiply",
537 "f.divide",
538 "f.+",
540 "f.-",
541 "f.*",
542 "f./",
543 "f.=",
545 "f.<",
546 "f.>",
547 "f.<=",
548 "f.>=",
549 "f.<>",
550 "f.eq",
552 "f.lt",
553 "f.gt",
554 "f.lte",
555 "f.gte",
556 "f.neq",
557 "int->float",
559 "float->int",
560 "float->string",
561 "string->float",
562 "test.init",
564 "test.finish",
565 "test.has-failures",
566 "test.assert",
567 "test.assert-not",
568 "test.assert-eq",
569 "test.assert-eq-str",
570 "test.fail",
571 "test.pass-count",
572 "test.fail-count",
573 "time.now",
575 "time.nanos",
576 "time.sleep-ms",
577 "son.dump",
579 "son.dump-pretty",
580 "stack.dump",
582 "regex.match?",
584 "regex.find",
585 "regex.find-all",
586 "regex.replace",
587 "regex.replace-all",
588 "regex.captures",
589 "regex.split",
590 "regex.valid?",
591 "compress.gzip",
593 "compress.gzip-level",
594 "compress.gunzip",
595 "compress.zstd",
596 "compress.zstd-level",
597 "compress.unzstd",
598 ];
599
600 for word in &self.words {
601 self.validate_statements(&word.body, &word.name, &builtins, external_words)?;
602 }
603
604 Ok(())
605 }
606
607 fn validate_statements(
609 &self,
610 statements: &[Statement],
611 word_name: &str,
612 builtins: &[&str],
613 external_words: &[&str],
614 ) -> Result<(), String> {
615 for statement in statements {
616 match statement {
617 Statement::WordCall { name, .. } => {
618 if builtins.contains(&name.as_str()) {
620 continue;
621 }
622 if self.find_word(name).is_some() {
624 continue;
625 }
626 if external_words.contains(&name.as_str()) {
628 continue;
629 }
630 return Err(format!(
632 "Undefined word '{}' called in word '{}'. \
633 Did you forget to define it or misspell a built-in?",
634 name, word_name
635 ));
636 }
637 Statement::If {
638 then_branch,
639 else_branch,
640 span: _,
641 } => {
642 self.validate_statements(then_branch, word_name, builtins, external_words)?;
644 if let Some(eb) = else_branch {
645 self.validate_statements(eb, word_name, builtins, external_words)?;
646 }
647 }
648 Statement::Quotation { body, .. } => {
649 self.validate_statements(body, word_name, builtins, external_words)?;
651 }
652 Statement::Match { arms, span: _ } => {
653 for arm in arms {
655 self.validate_statements(&arm.body, word_name, builtins, external_words)?;
656 }
657 }
658 _ => {} }
660 }
661 Ok(())
662 }
663
664 pub const MAX_VARIANT_FIELDS: usize = 12;
668
669 pub fn generate_constructors(&mut self) -> Result<(), String> {
679 let mut new_words = Vec::new();
680
681 for union_def in &self.unions {
682 for variant in &union_def.variants {
683 let constructor_name = format!("Make-{}", variant.name);
684 let field_count = variant.fields.len();
685
686 if field_count > Self::MAX_VARIANT_FIELDS {
688 return Err(format!(
689 "Variant '{}' in union '{}' has {} fields, but the maximum is {}. \
690 Consider grouping fields into nested union types.",
691 variant.name,
692 union_def.name,
693 field_count,
694 Self::MAX_VARIANT_FIELDS
695 ));
696 }
697
698 let mut input_stack = StackType::RowVar("a".to_string());
701 for field in &variant.fields {
702 let field_type = parse_type_name(&field.type_name);
703 input_stack = input_stack.push(field_type);
704 }
705
706 let output_stack =
708 StackType::RowVar("a".to_string()).push(Type::Union(union_def.name.clone()));
709
710 let effect = Effect::new(input_stack, output_stack);
711
712 let body = vec![
716 Statement::Symbol(variant.name.clone()),
717 Statement::WordCall {
718 name: format!("variant.make-{}", field_count),
719 span: None, },
721 ];
722
723 new_words.push(WordDef {
724 name: constructor_name,
725 effect: Some(effect),
726 body,
727 source: variant.source.clone(),
728 allowed_lints: vec![],
729 });
730 }
731 }
732
733 self.words.extend(new_words);
734 Ok(())
735 }
736}
737
738fn parse_type_name(name: &str) -> Type {
741 match name {
742 "Int" => Type::Int,
743 "Float" => Type::Float,
744 "Bool" => Type::Bool,
745 "String" => Type::String,
746 "Channel" => Type::Channel,
747 other => Type::Union(other.to_string()),
748 }
749}
750
751impl Default for Program {
752 fn default() -> Self {
753 Self::new()
754 }
755}
756
757#[cfg(test)]
758mod tests {
759 use super::*;
760
761 #[test]
762 fn test_validate_builtin_words() {
763 let program = Program {
764 includes: vec![],
765 unions: vec![],
766 words: vec![WordDef {
767 name: "main".to_string(),
768 effect: None,
769 body: vec![
770 Statement::IntLiteral(2),
771 Statement::IntLiteral(3),
772 Statement::WordCall {
773 name: "i.add".to_string(),
774 span: None,
775 },
776 Statement::WordCall {
777 name: "io.write-line".to_string(),
778 span: None,
779 },
780 ],
781 source: None,
782 allowed_lints: vec![],
783 }],
784 };
785
786 assert!(program.validate_word_calls().is_ok());
788 }
789
790 #[test]
791 fn test_validate_user_defined_words() {
792 let program = Program {
793 includes: vec![],
794 unions: vec![],
795 words: vec![
796 WordDef {
797 name: "helper".to_string(),
798 effect: None,
799 body: vec![Statement::IntLiteral(42)],
800 source: None,
801 allowed_lints: vec![],
802 },
803 WordDef {
804 name: "main".to_string(),
805 effect: None,
806 body: vec![Statement::WordCall {
807 name: "helper".to_string(),
808 span: None,
809 }],
810 source: None,
811 allowed_lints: vec![],
812 },
813 ],
814 };
815
816 assert!(program.validate_word_calls().is_ok());
818 }
819
820 #[test]
821 fn test_validate_undefined_word() {
822 let program = Program {
823 includes: vec![],
824 unions: vec![],
825 words: vec![WordDef {
826 name: "main".to_string(),
827 effect: None,
828 body: vec![Statement::WordCall {
829 name: "undefined_word".to_string(),
830 span: None,
831 }],
832 source: None,
833 allowed_lints: vec![],
834 }],
835 };
836
837 let result = program.validate_word_calls();
839 assert!(result.is_err());
840 let error = result.unwrap_err();
841 assert!(error.contains("undefined_word"));
842 assert!(error.contains("main"));
843 }
844
845 #[test]
846 fn test_validate_misspelled_builtin() {
847 let program = Program {
848 includes: vec![],
849 unions: vec![],
850 words: vec![WordDef {
851 name: "main".to_string(),
852 effect: None,
853 body: vec![Statement::WordCall {
854 name: "wrte_line".to_string(),
855 span: None,
856 }], source: None,
858 allowed_lints: vec![],
859 }],
860 };
861
862 let result = program.validate_word_calls();
864 assert!(result.is_err());
865 let error = result.unwrap_err();
866 assert!(error.contains("wrte_line"));
867 assert!(error.contains("misspell"));
868 }
869
870 #[test]
871 fn test_generate_constructors() {
872 let mut program = Program {
873 includes: vec![],
874 unions: vec![UnionDef {
875 name: "Message".to_string(),
876 variants: vec![
877 UnionVariant {
878 name: "Get".to_string(),
879 fields: vec![UnionField {
880 name: "response-chan".to_string(),
881 type_name: "Int".to_string(),
882 }],
883 source: None,
884 },
885 UnionVariant {
886 name: "Put".to_string(),
887 fields: vec![
888 UnionField {
889 name: "value".to_string(),
890 type_name: "String".to_string(),
891 },
892 UnionField {
893 name: "response-chan".to_string(),
894 type_name: "Int".to_string(),
895 },
896 ],
897 source: None,
898 },
899 ],
900 source: None,
901 }],
902 words: vec![],
903 };
904
905 program.generate_constructors().unwrap();
907
908 assert_eq!(program.words.len(), 2);
910
911 let make_get = program
913 .find_word("Make-Get")
914 .expect("Make-Get should exist");
915 assert_eq!(make_get.name, "Make-Get");
916 assert!(make_get.effect.is_some());
917 let effect = make_get.effect.as_ref().unwrap();
918 assert_eq!(
921 format!("{:?}", effect.outputs),
922 "Cons { rest: RowVar(\"a\"), top: Union(\"Message\") }"
923 );
924
925 let make_put = program
927 .find_word("Make-Put")
928 .expect("Make-Put should exist");
929 assert_eq!(make_put.name, "Make-Put");
930 assert!(make_put.effect.is_some());
931
932 assert_eq!(make_get.body.len(), 2);
935 match &make_get.body[0] {
936 Statement::Symbol(s) if s == "Get" => {}
937 other => panic!("Expected Symbol(\"Get\") for variant tag, got {:?}", other),
938 }
939 match &make_get.body[1] {
940 Statement::WordCall { name, span: None } if name == "variant.make-1" => {}
941 _ => panic!("Expected WordCall(variant.make-1)"),
942 }
943
944 assert_eq!(make_put.body.len(), 2);
946 match &make_put.body[0] {
947 Statement::Symbol(s) if s == "Put" => {}
948 other => panic!("Expected Symbol(\"Put\") for variant tag, got {:?}", other),
949 }
950 match &make_put.body[1] {
951 Statement::WordCall { name, span: None } if name == "variant.make-2" => {}
952 _ => panic!("Expected WordCall(variant.make-2)"),
953 }
954 }
955}