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 "string.concat",
332 "string.length",
333 "string.byte-length",
334 "string.char-at",
335 "string.substring",
336 "char->string",
337 "string.find",
338 "string.split",
339 "string.contains",
340 "string.starts-with",
341 "string.empty?",
342 "string.trim",
343 "string.chomp",
344 "string.to-upper",
345 "string.to-lower",
346 "string.equal?",
347 "string.json-escape",
348 "string->int",
349 "symbol.=",
351 "encoding.base64-encode",
353 "encoding.base64-decode",
354 "encoding.base64url-encode",
355 "encoding.base64url-decode",
356 "encoding.hex-encode",
357 "encoding.hex-decode",
358 "crypto.sha256",
360 "crypto.hmac-sha256",
361 "crypto.constant-time-eq",
362 "crypto.random-bytes",
363 "crypto.random-int",
364 "crypto.uuid4",
365 "crypto.aes-gcm-encrypt",
366 "crypto.aes-gcm-decrypt",
367 "crypto.pbkdf2-sha256",
368 "crypto.ed25519-keypair",
369 "crypto.ed25519-sign",
370 "crypto.ed25519-verify",
371 "http.get",
373 "http.post",
374 "http.put",
375 "http.delete",
376 "list.make",
378 "list.push",
379 "list.get",
380 "list.set",
381 "list.map",
382 "list.filter",
383 "list.fold",
384 "list.each",
385 "list.length",
386 "list.empty?",
387 "map.make",
389 "map.get",
390 "map.set",
391 "map.has?",
392 "map.remove",
393 "map.keys",
394 "map.values",
395 "map.size",
396 "map.empty?",
397 "variant.field-count",
399 "variant.tag",
400 "variant.field-at",
401 "variant.append",
402 "variant.last",
403 "variant.init",
404 "variant.make-0",
405 "variant.make-1",
406 "variant.make-2",
407 "variant.make-3",
408 "variant.make-4",
409 "wrap-0",
411 "wrap-1",
412 "wrap-2",
413 "wrap-3",
414 "wrap-4",
415 "i.add",
417 "i.subtract",
418 "i.multiply",
419 "i.divide",
420 "i.modulo",
421 "i.+",
423 "i.-",
424 "i.*",
425 "i./",
426 "i.%",
427 "i.=",
429 "i.<",
430 "i.>",
431 "i.<=",
432 "i.>=",
433 "i.<>",
434 "i.eq",
436 "i.lt",
437 "i.gt",
438 "i.lte",
439 "i.gte",
440 "i.neq",
441 "dup",
443 "drop",
444 "swap",
445 "over",
446 "rot",
447 "nip",
448 "tuck",
449 "2dup",
450 "3drop",
451 "pick",
452 "roll",
453 "and",
455 "or",
456 "not",
457 "band",
459 "bor",
460 "bxor",
461 "bnot",
462 "shl",
463 "shr",
464 "popcount",
465 "clz",
466 "ctz",
467 "int-bits",
468 "chan.make",
470 "chan.send",
471 "chan.receive",
472 "chan.close",
473 "chan.yield",
474 "call",
476 "strand.spawn",
477 "strand.weave",
478 "strand.resume",
479 "strand.weave-cancel",
480 "yield",
481 "cond",
482 "tcp.listen",
484 "tcp.accept",
485 "tcp.read",
486 "tcp.write",
487 "tcp.close",
488 "os.getenv",
490 "os.home-dir",
491 "os.current-dir",
492 "os.path-exists",
493 "os.path-is-file",
494 "os.path-is-dir",
495 "os.path-join",
496 "os.path-parent",
497 "os.path-filename",
498 "os.exit",
499 "os.name",
500 "os.arch",
501 "signal.trap",
503 "signal.received?",
504 "signal.pending?",
505 "signal.default",
506 "signal.ignore",
507 "signal.clear",
508 "signal.SIGINT",
509 "signal.SIGTERM",
510 "signal.SIGHUP",
511 "signal.SIGPIPE",
512 "signal.SIGUSR1",
513 "signal.SIGUSR2",
514 "signal.SIGCHLD",
515 "signal.SIGALRM",
516 "signal.SIGCONT",
517 "terminal.raw-mode",
519 "terminal.read-char",
520 "terminal.read-char?",
521 "terminal.width",
522 "terminal.height",
523 "terminal.flush",
524 "f.add",
526 "f.subtract",
527 "f.multiply",
528 "f.divide",
529 "f.+",
531 "f.-",
532 "f.*",
533 "f./",
534 "f.=",
536 "f.<",
537 "f.>",
538 "f.<=",
539 "f.>=",
540 "f.<>",
541 "f.eq",
543 "f.lt",
544 "f.gt",
545 "f.lte",
546 "f.gte",
547 "f.neq",
548 "int->float",
550 "float->int",
551 "float->string",
552 "string->float",
553 "test.init",
555 "test.finish",
556 "test.has-failures",
557 "test.assert",
558 "test.assert-not",
559 "test.assert-eq",
560 "test.assert-eq-str",
561 "test.fail",
562 "test.pass-count",
563 "test.fail-count",
564 "time.now",
566 "time.nanos",
567 "time.sleep-ms",
568 "son.dump",
570 "son.dump-pretty",
571 "stack.dump",
573 "regex.match?",
575 "regex.find",
576 "regex.find-all",
577 "regex.replace",
578 "regex.replace-all",
579 "regex.captures",
580 "regex.split",
581 "regex.valid?",
582 "compress.gzip",
584 "compress.gzip-level",
585 "compress.gunzip",
586 "compress.zstd",
587 "compress.zstd-level",
588 "compress.unzstd",
589 ];
590
591 for word in &self.words {
592 self.validate_statements(&word.body, &word.name, &builtins, external_words)?;
593 }
594
595 Ok(())
596 }
597
598 fn validate_statements(
600 &self,
601 statements: &[Statement],
602 word_name: &str,
603 builtins: &[&str],
604 external_words: &[&str],
605 ) -> Result<(), String> {
606 for statement in statements {
607 match statement {
608 Statement::WordCall { name, .. } => {
609 if builtins.contains(&name.as_str()) {
611 continue;
612 }
613 if self.find_word(name).is_some() {
615 continue;
616 }
617 if external_words.contains(&name.as_str()) {
619 continue;
620 }
621 return Err(format!(
623 "Undefined word '{}' called in word '{}'. \
624 Did you forget to define it or misspell a built-in?",
625 name, word_name
626 ));
627 }
628 Statement::If {
629 then_branch,
630 else_branch,
631 span: _,
632 } => {
633 self.validate_statements(then_branch, word_name, builtins, external_words)?;
635 if let Some(eb) = else_branch {
636 self.validate_statements(eb, word_name, builtins, external_words)?;
637 }
638 }
639 Statement::Quotation { body, .. } => {
640 self.validate_statements(body, word_name, builtins, external_words)?;
642 }
643 Statement::Match { arms, span: _ } => {
644 for arm in arms {
646 self.validate_statements(&arm.body, word_name, builtins, external_words)?;
647 }
648 }
649 _ => {} }
651 }
652 Ok(())
653 }
654
655 pub const MAX_VARIANT_FIELDS: usize = 12;
659
660 pub fn generate_constructors(&mut self) -> Result<(), String> {
670 let mut new_words = Vec::new();
671
672 for union_def in &self.unions {
673 for variant in &union_def.variants {
674 let constructor_name = format!("Make-{}", variant.name);
675 let field_count = variant.fields.len();
676
677 if field_count > Self::MAX_VARIANT_FIELDS {
679 return Err(format!(
680 "Variant '{}' in union '{}' has {} fields, but the maximum is {}. \
681 Consider grouping fields into nested union types.",
682 variant.name,
683 union_def.name,
684 field_count,
685 Self::MAX_VARIANT_FIELDS
686 ));
687 }
688
689 let mut input_stack = StackType::RowVar("a".to_string());
692 for field in &variant.fields {
693 let field_type = parse_type_name(&field.type_name);
694 input_stack = input_stack.push(field_type);
695 }
696
697 let output_stack =
699 StackType::RowVar("a".to_string()).push(Type::Union(union_def.name.clone()));
700
701 let effect = Effect::new(input_stack, output_stack);
702
703 let body = vec![
707 Statement::Symbol(variant.name.clone()),
708 Statement::WordCall {
709 name: format!("variant.make-{}", field_count),
710 span: None, },
712 ];
713
714 new_words.push(WordDef {
715 name: constructor_name,
716 effect: Some(effect),
717 body,
718 source: variant.source.clone(),
719 allowed_lints: vec![],
720 });
721 }
722 }
723
724 self.words.extend(new_words);
725 Ok(())
726 }
727}
728
729fn parse_type_name(name: &str) -> Type {
732 match name {
733 "Int" => Type::Int,
734 "Float" => Type::Float,
735 "Bool" => Type::Bool,
736 "String" => Type::String,
737 "Channel" => Type::Channel,
738 other => Type::Union(other.to_string()),
739 }
740}
741
742impl Default for Program {
743 fn default() -> Self {
744 Self::new()
745 }
746}
747
748#[cfg(test)]
749mod tests {
750 use super::*;
751
752 #[test]
753 fn test_validate_builtin_words() {
754 let program = Program {
755 includes: vec![],
756 unions: vec![],
757 words: vec![WordDef {
758 name: "main".to_string(),
759 effect: None,
760 body: vec![
761 Statement::IntLiteral(2),
762 Statement::IntLiteral(3),
763 Statement::WordCall {
764 name: "i.add".to_string(),
765 span: None,
766 },
767 Statement::WordCall {
768 name: "io.write-line".to_string(),
769 span: None,
770 },
771 ],
772 source: None,
773 allowed_lints: vec![],
774 }],
775 };
776
777 assert!(program.validate_word_calls().is_ok());
779 }
780
781 #[test]
782 fn test_validate_user_defined_words() {
783 let program = Program {
784 includes: vec![],
785 unions: vec![],
786 words: vec![
787 WordDef {
788 name: "helper".to_string(),
789 effect: None,
790 body: vec![Statement::IntLiteral(42)],
791 source: None,
792 allowed_lints: vec![],
793 },
794 WordDef {
795 name: "main".to_string(),
796 effect: None,
797 body: vec![Statement::WordCall {
798 name: "helper".to_string(),
799 span: None,
800 }],
801 source: None,
802 allowed_lints: vec![],
803 },
804 ],
805 };
806
807 assert!(program.validate_word_calls().is_ok());
809 }
810
811 #[test]
812 fn test_validate_undefined_word() {
813 let program = Program {
814 includes: vec![],
815 unions: vec![],
816 words: vec![WordDef {
817 name: "main".to_string(),
818 effect: None,
819 body: vec![Statement::WordCall {
820 name: "undefined_word".to_string(),
821 span: None,
822 }],
823 source: None,
824 allowed_lints: vec![],
825 }],
826 };
827
828 let result = program.validate_word_calls();
830 assert!(result.is_err());
831 let error = result.unwrap_err();
832 assert!(error.contains("undefined_word"));
833 assert!(error.contains("main"));
834 }
835
836 #[test]
837 fn test_validate_misspelled_builtin() {
838 let program = Program {
839 includes: vec![],
840 unions: vec![],
841 words: vec![WordDef {
842 name: "main".to_string(),
843 effect: None,
844 body: vec![Statement::WordCall {
845 name: "wrte_line".to_string(),
846 span: None,
847 }], source: None,
849 allowed_lints: vec![],
850 }],
851 };
852
853 let result = program.validate_word_calls();
855 assert!(result.is_err());
856 let error = result.unwrap_err();
857 assert!(error.contains("wrte_line"));
858 assert!(error.contains("misspell"));
859 }
860
861 #[test]
862 fn test_generate_constructors() {
863 let mut program = Program {
864 includes: vec![],
865 unions: vec![UnionDef {
866 name: "Message".to_string(),
867 variants: vec![
868 UnionVariant {
869 name: "Get".to_string(),
870 fields: vec![UnionField {
871 name: "response-chan".to_string(),
872 type_name: "Int".to_string(),
873 }],
874 source: None,
875 },
876 UnionVariant {
877 name: "Put".to_string(),
878 fields: vec![
879 UnionField {
880 name: "value".to_string(),
881 type_name: "String".to_string(),
882 },
883 UnionField {
884 name: "response-chan".to_string(),
885 type_name: "Int".to_string(),
886 },
887 ],
888 source: None,
889 },
890 ],
891 source: None,
892 }],
893 words: vec![],
894 };
895
896 program.generate_constructors().unwrap();
898
899 assert_eq!(program.words.len(), 2);
901
902 let make_get = program
904 .find_word("Make-Get")
905 .expect("Make-Get should exist");
906 assert_eq!(make_get.name, "Make-Get");
907 assert!(make_get.effect.is_some());
908 let effect = make_get.effect.as_ref().unwrap();
909 assert_eq!(
912 format!("{:?}", effect.outputs),
913 "Cons { rest: RowVar(\"a\"), top: Union(\"Message\") }"
914 );
915
916 let make_put = program
918 .find_word("Make-Put")
919 .expect("Make-Put should exist");
920 assert_eq!(make_put.name, "Make-Put");
921 assert!(make_put.effect.is_some());
922
923 assert_eq!(make_get.body.len(), 2);
926 match &make_get.body[0] {
927 Statement::Symbol(s) if s == "Get" => {}
928 other => panic!("Expected Symbol(\"Get\") for variant tag, got {:?}", other),
929 }
930 match &make_get.body[1] {
931 Statement::WordCall { name, span: None } if name == "variant.make-1" => {}
932 _ => panic!("Expected WordCall(variant.make-1)"),
933 }
934
935 assert_eq!(make_put.body.len(), 2);
937 match &make_put.body[0] {
938 Statement::Symbol(s) if s == "Put" => {}
939 other => panic!("Expected Symbol(\"Put\") for variant tag, got {:?}", other),
940 }
941 match &make_put.body[1] {
942 Statement::WordCall { name, span: None } if name == "variant.make-2" => {}
943 _ => panic!("Expected WordCall(variant.make-2)"),
944 }
945 }
946}