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 "times",
478 "while",
479 "until",
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 "terminal.raw-mode",
507 "terminal.read-char",
508 "terminal.read-char?",
509 "terminal.width",
510 "terminal.height",
511 "terminal.flush",
512 "f.add",
514 "f.subtract",
515 "f.multiply",
516 "f.divide",
517 "f.+",
519 "f.-",
520 "f.*",
521 "f./",
522 "f.=",
524 "f.<",
525 "f.>",
526 "f.<=",
527 "f.>=",
528 "f.<>",
529 "f.eq",
531 "f.lt",
532 "f.gt",
533 "f.lte",
534 "f.gte",
535 "f.neq",
536 "int->float",
538 "float->int",
539 "float->string",
540 "string->float",
541 "test.init",
543 "test.finish",
544 "test.has-failures",
545 "test.assert",
546 "test.assert-not",
547 "test.assert-eq",
548 "test.assert-eq-str",
549 "test.fail",
550 "test.pass-count",
551 "test.fail-count",
552 "time.now",
554 "time.nanos",
555 "time.sleep-ms",
556 "son.dump",
558 "son.dump-pretty",
559 "stack.dump",
561 "regex.match?",
563 "regex.find",
564 "regex.find-all",
565 "regex.replace",
566 "regex.replace-all",
567 "regex.captures",
568 "regex.split",
569 "regex.valid?",
570 "compress.gzip",
572 "compress.gzip-level",
573 "compress.gunzip",
574 "compress.zstd",
575 "compress.zstd-level",
576 "compress.unzstd",
577 ];
578
579 for word in &self.words {
580 self.validate_statements(&word.body, &word.name, &builtins, external_words)?;
581 }
582
583 Ok(())
584 }
585
586 fn validate_statements(
588 &self,
589 statements: &[Statement],
590 word_name: &str,
591 builtins: &[&str],
592 external_words: &[&str],
593 ) -> Result<(), String> {
594 for statement in statements {
595 match statement {
596 Statement::WordCall { name, .. } => {
597 if builtins.contains(&name.as_str()) {
599 continue;
600 }
601 if self.find_word(name).is_some() {
603 continue;
604 }
605 if external_words.contains(&name.as_str()) {
607 continue;
608 }
609 return Err(format!(
611 "Undefined word '{}' called in word '{}'. \
612 Did you forget to define it or misspell a built-in?",
613 name, word_name
614 ));
615 }
616 Statement::If {
617 then_branch,
618 else_branch,
619 } => {
620 self.validate_statements(then_branch, word_name, builtins, external_words)?;
622 if let Some(eb) = else_branch {
623 self.validate_statements(eb, word_name, builtins, external_words)?;
624 }
625 }
626 Statement::Quotation { body, .. } => {
627 self.validate_statements(body, word_name, builtins, external_words)?;
629 }
630 Statement::Match { arms } => {
631 for arm in arms {
633 self.validate_statements(&arm.body, word_name, builtins, external_words)?;
634 }
635 }
636 _ => {} }
638 }
639 Ok(())
640 }
641
642 pub const MAX_VARIANT_FIELDS: usize = 4;
646
647 pub fn generate_constructors(&mut self) -> Result<(), String> {
657 let mut new_words = Vec::new();
658
659 for union_def in &self.unions {
660 for variant in &union_def.variants {
661 let constructor_name = format!("Make-{}", variant.name);
662 let field_count = variant.fields.len();
663
664 if field_count > Self::MAX_VARIANT_FIELDS {
666 return Err(format!(
667 "Variant '{}' in union '{}' has {} fields, but the maximum is {}. \
668 Consider grouping fields into nested union types.",
669 variant.name,
670 union_def.name,
671 field_count,
672 Self::MAX_VARIANT_FIELDS
673 ));
674 }
675
676 let mut input_stack = StackType::RowVar("a".to_string());
679 for field in &variant.fields {
680 let field_type = parse_type_name(&field.type_name);
681 input_stack = input_stack.push(field_type);
682 }
683
684 let output_stack =
686 StackType::RowVar("a".to_string()).push(Type::Union(union_def.name.clone()));
687
688 let effect = Effect::new(input_stack, output_stack);
689
690 let body = vec![
694 Statement::Symbol(variant.name.clone()),
695 Statement::WordCall {
696 name: format!("variant.make-{}", field_count),
697 span: None, },
699 ];
700
701 new_words.push(WordDef {
702 name: constructor_name,
703 effect: Some(effect),
704 body,
705 source: variant.source.clone(),
706 });
707 }
708 }
709
710 self.words.extend(new_words);
711 Ok(())
712 }
713}
714
715fn parse_type_name(name: &str) -> Type {
718 match name {
719 "Int" => Type::Int,
720 "Float" => Type::Float,
721 "Bool" => Type::Bool,
722 "String" => Type::String,
723 "Channel" => Type::Channel,
724 other => Type::Union(other.to_string()),
725 }
726}
727
728impl Default for Program {
729 fn default() -> Self {
730 Self::new()
731 }
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737
738 #[test]
739 fn test_validate_builtin_words() {
740 let program = Program {
741 includes: vec![],
742 unions: vec![],
743 words: vec![WordDef {
744 name: "main".to_string(),
745 effect: None,
746 body: vec![
747 Statement::IntLiteral(2),
748 Statement::IntLiteral(3),
749 Statement::WordCall {
750 name: "i.add".to_string(),
751 span: None,
752 },
753 Statement::WordCall {
754 name: "io.write-line".to_string(),
755 span: None,
756 },
757 ],
758 source: None,
759 }],
760 };
761
762 assert!(program.validate_word_calls().is_ok());
764 }
765
766 #[test]
767 fn test_validate_user_defined_words() {
768 let program = Program {
769 includes: vec![],
770 unions: vec![],
771 words: vec![
772 WordDef {
773 name: "helper".to_string(),
774 effect: None,
775 body: vec![Statement::IntLiteral(42)],
776 source: None,
777 },
778 WordDef {
779 name: "main".to_string(),
780 effect: None,
781 body: vec![Statement::WordCall {
782 name: "helper".to_string(),
783 span: None,
784 }],
785 source: None,
786 },
787 ],
788 };
789
790 assert!(program.validate_word_calls().is_ok());
792 }
793
794 #[test]
795 fn test_validate_undefined_word() {
796 let program = Program {
797 includes: vec![],
798 unions: vec![],
799 words: vec![WordDef {
800 name: "main".to_string(),
801 effect: None,
802 body: vec![Statement::WordCall {
803 name: "undefined_word".to_string(),
804 span: None,
805 }],
806 source: None,
807 }],
808 };
809
810 let result = program.validate_word_calls();
812 assert!(result.is_err());
813 let error = result.unwrap_err();
814 assert!(error.contains("undefined_word"));
815 assert!(error.contains("main"));
816 }
817
818 #[test]
819 fn test_validate_misspelled_builtin() {
820 let program = Program {
821 includes: vec![],
822 unions: vec![],
823 words: vec![WordDef {
824 name: "main".to_string(),
825 effect: None,
826 body: vec![Statement::WordCall {
827 name: "wrte_line".to_string(),
828 span: None,
829 }], source: None,
831 }],
832 };
833
834 let result = program.validate_word_calls();
836 assert!(result.is_err());
837 let error = result.unwrap_err();
838 assert!(error.contains("wrte_line"));
839 assert!(error.contains("misspell"));
840 }
841
842 #[test]
843 fn test_generate_constructors() {
844 let mut program = Program {
845 includes: vec![],
846 unions: vec![UnionDef {
847 name: "Message".to_string(),
848 variants: vec![
849 UnionVariant {
850 name: "Get".to_string(),
851 fields: vec![UnionField {
852 name: "response-chan".to_string(),
853 type_name: "Int".to_string(),
854 }],
855 source: None,
856 },
857 UnionVariant {
858 name: "Put".to_string(),
859 fields: vec![
860 UnionField {
861 name: "value".to_string(),
862 type_name: "String".to_string(),
863 },
864 UnionField {
865 name: "response-chan".to_string(),
866 type_name: "Int".to_string(),
867 },
868 ],
869 source: None,
870 },
871 ],
872 source: None,
873 }],
874 words: vec![],
875 };
876
877 program.generate_constructors().unwrap();
879
880 assert_eq!(program.words.len(), 2);
882
883 let make_get = program
885 .find_word("Make-Get")
886 .expect("Make-Get should exist");
887 assert_eq!(make_get.name, "Make-Get");
888 assert!(make_get.effect.is_some());
889 let effect = make_get.effect.as_ref().unwrap();
890 assert_eq!(
893 format!("{:?}", effect.outputs),
894 "Cons { rest: RowVar(\"a\"), top: Union(\"Message\") }"
895 );
896
897 let make_put = program
899 .find_word("Make-Put")
900 .expect("Make-Put should exist");
901 assert_eq!(make_put.name, "Make-Put");
902 assert!(make_put.effect.is_some());
903
904 assert_eq!(make_get.body.len(), 2);
907 match &make_get.body[0] {
908 Statement::Symbol(s) if s == "Get" => {}
909 other => panic!("Expected Symbol(\"Get\") for variant tag, got {:?}", other),
910 }
911 match &make_get.body[1] {
912 Statement::WordCall { name, span: None } if name == "variant.make-1" => {}
913 _ => panic!("Expected WordCall(variant.make-1)"),
914 }
915
916 assert_eq!(make_put.body.len(), 2);
918 match &make_put.body[0] {
919 Statement::Symbol(s) if s == "Put" => {}
920 other => panic!("Expected Symbol(\"Put\") for variant tag, got {:?}", other),
921 }
922 match &make_put.body[1] {
923 Statement::WordCall { name, span: None } if name == "variant.make-2" => {}
924 _ => panic!("Expected WordCall(variant.make-2)"),
925 }
926 }
927}