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}
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 },
246
247 Quotation {
257 id: usize,
258 body: Vec<Statement>,
259 span: Option<QuotationSpan>,
260 },
261
262 Match {
276 arms: Vec<MatchArm>,
278 },
279}
280
281impl Program {
282 pub fn new() -> Self {
283 Program {
284 includes: Vec::new(),
285 unions: Vec::new(),
286 words: Vec::new(),
287 }
288 }
289
290 pub fn find_union(&self, name: &str) -> Option<&UnionDef> {
292 self.unions.iter().find(|u| u.name == name)
293 }
294
295 pub fn find_word(&self, name: &str) -> Option<&WordDef> {
296 self.words.iter().find(|w| w.name == name)
297 }
298
299 pub fn validate_word_calls(&self) -> Result<(), String> {
301 self.validate_word_calls_with_externals(&[])
302 }
303
304 pub fn validate_word_calls_with_externals(
309 &self,
310 external_words: &[&str],
311 ) -> Result<(), String> {
312 let builtins = [
315 "io.write",
317 "io.write-line",
318 "io.read-line",
319 "io.read-line+",
320 "io.read-n",
321 "int->string",
322 "symbol->string",
323 "string->symbol",
324 "args.count",
326 "args.at",
327 "file.slurp",
329 "file.exists?",
330 "file.for-each-line+",
331 "string.concat",
333 "string.length",
334 "string.byte-length",
335 "string.char-at",
336 "string.substring",
337 "char->string",
338 "string.find",
339 "string.split",
340 "string.contains",
341 "string.starts-with",
342 "string.empty?",
343 "string.trim",
344 "string.chomp",
345 "string.to-upper",
346 "string.to-lower",
347 "string.equal?",
348 "string.json-escape",
349 "string->int",
350 "symbol.=",
352 "encoding.base64-encode",
354 "encoding.base64-decode",
355 "encoding.base64url-encode",
356 "encoding.base64url-decode",
357 "encoding.hex-encode",
358 "encoding.hex-decode",
359 "crypto.sha256",
361 "crypto.hmac-sha256",
362 "crypto.constant-time-eq",
363 "crypto.random-bytes",
364 "crypto.random-int",
365 "crypto.uuid4",
366 "crypto.aes-gcm-encrypt",
367 "crypto.aes-gcm-decrypt",
368 "crypto.pbkdf2-sha256",
369 "crypto.ed25519-keypair",
370 "crypto.ed25519-sign",
371 "crypto.ed25519-verify",
372 "http.get",
374 "http.post",
375 "http.put",
376 "http.delete",
377 "list.make",
379 "list.push",
380 "list.get",
381 "list.set",
382 "list.map",
383 "list.filter",
384 "list.fold",
385 "list.each",
386 "list.length",
387 "list.empty?",
388 "map.make",
390 "map.get",
391 "map.set",
392 "map.has?",
393 "map.remove",
394 "map.keys",
395 "map.values",
396 "map.size",
397 "map.empty?",
398 "variant.field-count",
400 "variant.tag",
401 "variant.field-at",
402 "variant.append",
403 "variant.last",
404 "variant.init",
405 "variant.make-0",
406 "variant.make-1",
407 "variant.make-2",
408 "variant.make-3",
409 "variant.make-4",
410 "wrap-0",
412 "wrap-1",
413 "wrap-2",
414 "wrap-3",
415 "wrap-4",
416 "i.add",
418 "i.subtract",
419 "i.multiply",
420 "i.divide",
421 "i.modulo",
422 "i.+",
424 "i.-",
425 "i.*",
426 "i./",
427 "i.%",
428 "i.=",
430 "i.<",
431 "i.>",
432 "i.<=",
433 "i.>=",
434 "i.<>",
435 "i.eq",
437 "i.lt",
438 "i.gt",
439 "i.lte",
440 "i.gte",
441 "i.neq",
442 "dup",
444 "drop",
445 "swap",
446 "over",
447 "rot",
448 "nip",
449 "tuck",
450 "2dup",
451 "3drop",
452 "pick",
453 "roll",
454 "and",
456 "or",
457 "not",
458 "band",
460 "bor",
461 "bxor",
462 "bnot",
463 "shl",
464 "shr",
465 "popcount",
466 "clz",
467 "ctz",
468 "int-bits",
469 "chan.make",
471 "chan.send",
472 "chan.receive",
473 "chan.close",
474 "chan.yield",
475 "call",
477 "strand.spawn",
478 "strand.weave",
479 "strand.resume",
480 "strand.weave-cancel",
481 "yield",
482 "cond",
483 "tcp.listen",
485 "tcp.accept",
486 "tcp.read",
487 "tcp.write",
488 "tcp.close",
489 "os.getenv",
491 "os.home-dir",
492 "os.current-dir",
493 "os.path-exists",
494 "os.path-is-file",
495 "os.path-is-dir",
496 "os.path-join",
497 "os.path-parent",
498 "os.path-filename",
499 "os.exit",
500 "os.name",
501 "os.arch",
502 "terminal.raw-mode",
504 "terminal.read-char",
505 "terminal.read-char?",
506 "terminal.width",
507 "terminal.height",
508 "terminal.flush",
509 "f.add",
511 "f.subtract",
512 "f.multiply",
513 "f.divide",
514 "f.+",
516 "f.-",
517 "f.*",
518 "f./",
519 "f.=",
521 "f.<",
522 "f.>",
523 "f.<=",
524 "f.>=",
525 "f.<>",
526 "f.eq",
528 "f.lt",
529 "f.gt",
530 "f.lte",
531 "f.gte",
532 "f.neq",
533 "int->float",
535 "float->int",
536 "float->string",
537 "string->float",
538 "test.init",
540 "test.finish",
541 "test.has-failures",
542 "test.assert",
543 "test.assert-not",
544 "test.assert-eq",
545 "test.assert-eq-str",
546 "test.fail",
547 "test.pass-count",
548 "test.fail-count",
549 "time.now",
551 "time.nanos",
552 "time.sleep-ms",
553 "son.dump",
555 "son.dump-pretty",
556 "stack.dump",
558 "regex.match?",
560 "regex.find",
561 "regex.find-all",
562 "regex.replace",
563 "regex.replace-all",
564 "regex.captures",
565 "regex.split",
566 "regex.valid?",
567 "compress.gzip",
569 "compress.gzip-level",
570 "compress.gunzip",
571 "compress.zstd",
572 "compress.zstd-level",
573 "compress.unzstd",
574 ];
575
576 for word in &self.words {
577 self.validate_statements(&word.body, &word.name, &builtins, external_words)?;
578 }
579
580 Ok(())
581 }
582
583 fn validate_statements(
585 &self,
586 statements: &[Statement],
587 word_name: &str,
588 builtins: &[&str],
589 external_words: &[&str],
590 ) -> Result<(), String> {
591 for statement in statements {
592 match statement {
593 Statement::WordCall { name, .. } => {
594 if builtins.contains(&name.as_str()) {
596 continue;
597 }
598 if self.find_word(name).is_some() {
600 continue;
601 }
602 if external_words.contains(&name.as_str()) {
604 continue;
605 }
606 return Err(format!(
608 "Undefined word '{}' called in word '{}'. \
609 Did you forget to define it or misspell a built-in?",
610 name, word_name
611 ));
612 }
613 Statement::If {
614 then_branch,
615 else_branch,
616 } => {
617 self.validate_statements(then_branch, word_name, builtins, external_words)?;
619 if let Some(eb) = else_branch {
620 self.validate_statements(eb, word_name, builtins, external_words)?;
621 }
622 }
623 Statement::Quotation { body, .. } => {
624 self.validate_statements(body, word_name, builtins, external_words)?;
626 }
627 Statement::Match { arms } => {
628 for arm in arms {
630 self.validate_statements(&arm.body, word_name, builtins, external_words)?;
631 }
632 }
633 _ => {} }
635 }
636 Ok(())
637 }
638
639 pub const MAX_VARIANT_FIELDS: usize = 4;
643
644 pub fn generate_constructors(&mut self) -> Result<(), String> {
654 let mut new_words = Vec::new();
655
656 for union_def in &self.unions {
657 for variant in &union_def.variants {
658 let constructor_name = format!("Make-{}", variant.name);
659 let field_count = variant.fields.len();
660
661 if field_count > Self::MAX_VARIANT_FIELDS {
663 return Err(format!(
664 "Variant '{}' in union '{}' has {} fields, but the maximum is {}. \
665 Consider grouping fields into nested union types.",
666 variant.name,
667 union_def.name,
668 field_count,
669 Self::MAX_VARIANT_FIELDS
670 ));
671 }
672
673 let mut input_stack = StackType::RowVar("a".to_string());
676 for field in &variant.fields {
677 let field_type = parse_type_name(&field.type_name);
678 input_stack = input_stack.push(field_type);
679 }
680
681 let output_stack =
683 StackType::RowVar("a".to_string()).push(Type::Union(union_def.name.clone()));
684
685 let effect = Effect::new(input_stack, output_stack);
686
687 let body = vec![
691 Statement::Symbol(variant.name.clone()),
692 Statement::WordCall {
693 name: format!("variant.make-{}", field_count),
694 span: None, },
696 ];
697
698 new_words.push(WordDef {
699 name: constructor_name,
700 effect: Some(effect),
701 body,
702 source: variant.source.clone(),
703 });
704 }
705 }
706
707 self.words.extend(new_words);
708 Ok(())
709 }
710}
711
712fn parse_type_name(name: &str) -> Type {
715 match name {
716 "Int" => Type::Int,
717 "Float" => Type::Float,
718 "Bool" => Type::Bool,
719 "String" => Type::String,
720 "Channel" => Type::Channel,
721 other => Type::Union(other.to_string()),
722 }
723}
724
725impl Default for Program {
726 fn default() -> Self {
727 Self::new()
728 }
729}
730
731#[cfg(test)]
732mod tests {
733 use super::*;
734
735 #[test]
736 fn test_validate_builtin_words() {
737 let program = Program {
738 includes: vec![],
739 unions: vec![],
740 words: vec![WordDef {
741 name: "main".to_string(),
742 effect: None,
743 body: vec![
744 Statement::IntLiteral(2),
745 Statement::IntLiteral(3),
746 Statement::WordCall {
747 name: "i.add".to_string(),
748 span: None,
749 },
750 Statement::WordCall {
751 name: "io.write-line".to_string(),
752 span: None,
753 },
754 ],
755 source: None,
756 }],
757 };
758
759 assert!(program.validate_word_calls().is_ok());
761 }
762
763 #[test]
764 fn test_validate_user_defined_words() {
765 let program = Program {
766 includes: vec![],
767 unions: vec![],
768 words: vec![
769 WordDef {
770 name: "helper".to_string(),
771 effect: None,
772 body: vec![Statement::IntLiteral(42)],
773 source: None,
774 },
775 WordDef {
776 name: "main".to_string(),
777 effect: None,
778 body: vec![Statement::WordCall {
779 name: "helper".to_string(),
780 span: None,
781 }],
782 source: None,
783 },
784 ],
785 };
786
787 assert!(program.validate_word_calls().is_ok());
789 }
790
791 #[test]
792 fn test_validate_undefined_word() {
793 let program = Program {
794 includes: vec![],
795 unions: vec![],
796 words: vec![WordDef {
797 name: "main".to_string(),
798 effect: None,
799 body: vec![Statement::WordCall {
800 name: "undefined_word".to_string(),
801 span: None,
802 }],
803 source: None,
804 }],
805 };
806
807 let result = program.validate_word_calls();
809 assert!(result.is_err());
810 let error = result.unwrap_err();
811 assert!(error.contains("undefined_word"));
812 assert!(error.contains("main"));
813 }
814
815 #[test]
816 fn test_validate_misspelled_builtin() {
817 let program = Program {
818 includes: vec![],
819 unions: vec![],
820 words: vec![WordDef {
821 name: "main".to_string(),
822 effect: None,
823 body: vec![Statement::WordCall {
824 name: "wrte_line".to_string(),
825 span: None,
826 }], source: None,
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("wrte_line"));
836 assert!(error.contains("misspell"));
837 }
838
839 #[test]
840 fn test_generate_constructors() {
841 let mut program = Program {
842 includes: vec![],
843 unions: vec![UnionDef {
844 name: "Message".to_string(),
845 variants: vec![
846 UnionVariant {
847 name: "Get".to_string(),
848 fields: vec![UnionField {
849 name: "response-chan".to_string(),
850 type_name: "Int".to_string(),
851 }],
852 source: None,
853 },
854 UnionVariant {
855 name: "Put".to_string(),
856 fields: vec![
857 UnionField {
858 name: "value".to_string(),
859 type_name: "String".to_string(),
860 },
861 UnionField {
862 name: "response-chan".to_string(),
863 type_name: "Int".to_string(),
864 },
865 ],
866 source: None,
867 },
868 ],
869 source: None,
870 }],
871 words: vec![],
872 };
873
874 program.generate_constructors().unwrap();
876
877 assert_eq!(program.words.len(), 2);
879
880 let make_get = program
882 .find_word("Make-Get")
883 .expect("Make-Get should exist");
884 assert_eq!(make_get.name, "Make-Get");
885 assert!(make_get.effect.is_some());
886 let effect = make_get.effect.as_ref().unwrap();
887 assert_eq!(
890 format!("{:?}", effect.outputs),
891 "Cons { rest: RowVar(\"a\"), top: Union(\"Message\") }"
892 );
893
894 let make_put = program
896 .find_word("Make-Put")
897 .expect("Make-Put should exist");
898 assert_eq!(make_put.name, "Make-Put");
899 assert!(make_put.effect.is_some());
900
901 assert_eq!(make_get.body.len(), 2);
904 match &make_get.body[0] {
905 Statement::Symbol(s) if s == "Get" => {}
906 other => panic!("Expected Symbol(\"Get\") for variant tag, got {:?}", other),
907 }
908 match &make_get.body[1] {
909 Statement::WordCall { name, span: None } if name == "variant.make-1" => {}
910 _ => panic!("Expected WordCall(variant.make-1)"),
911 }
912
913 assert_eq!(make_put.body.len(), 2);
915 match &make_put.body[0] {
916 Statement::Symbol(s) if s == "Put" => {}
917 other => panic!("Expected Symbol(\"Put\") for variant tag, got {:?}", other),
918 }
919 match &make_put.body[1] {
920 Statement::WordCall { name, span: None } if name == "variant.make-2" => {}
921 _ => panic!("Expected WordCall(variant.make-2)"),
922 }
923 }
924}