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.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 "times",
477 "while",
478 "until",
479 "strand.spawn",
480 "strand.weave",
481 "strand.resume",
482 "strand.weave-cancel",
483 "yield",
484 "cond",
485 "tcp.listen",
487 "tcp.accept",
488 "tcp.read",
489 "tcp.write",
490 "tcp.close",
491 "os.getenv",
493 "os.home-dir",
494 "os.current-dir",
495 "os.path-exists",
496 "os.path-is-file",
497 "os.path-is-dir",
498 "os.path-join",
499 "os.path-parent",
500 "os.path-filename",
501 "os.exit",
502 "os.name",
503 "os.arch",
504 "f.add",
506 "f.subtract",
507 "f.multiply",
508 "f.divide",
509 "f.+",
511 "f.-",
512 "f.*",
513 "f./",
514 "f.=",
516 "f.<",
517 "f.>",
518 "f.<=",
519 "f.>=",
520 "f.<>",
521 "f.eq",
523 "f.lt",
524 "f.gt",
525 "f.lte",
526 "f.gte",
527 "f.neq",
528 "int->float",
530 "float->int",
531 "float->string",
532 "string->float",
533 "test.init",
535 "test.finish",
536 "test.has-failures",
537 "test.assert",
538 "test.assert-not",
539 "test.assert-eq",
540 "test.assert-eq-str",
541 "test.fail",
542 "test.pass-count",
543 "test.fail-count",
544 "time.now",
546 "time.nanos",
547 "time.sleep-ms",
548 "son.dump",
550 "son.dump-pretty",
551 "stack.dump",
553 "regex.match?",
555 "regex.find",
556 "regex.find-all",
557 "regex.replace",
558 "regex.replace-all",
559 "regex.captures",
560 "regex.split",
561 "regex.valid?",
562 "compress.gzip",
564 "compress.gzip-level",
565 "compress.gunzip",
566 "compress.zstd",
567 "compress.zstd-level",
568 "compress.unzstd",
569 ];
570
571 for word in &self.words {
572 self.validate_statements(&word.body, &word.name, &builtins, external_words)?;
573 }
574
575 Ok(())
576 }
577
578 fn validate_statements(
580 &self,
581 statements: &[Statement],
582 word_name: &str,
583 builtins: &[&str],
584 external_words: &[&str],
585 ) -> Result<(), String> {
586 for statement in statements {
587 match statement {
588 Statement::WordCall { name, .. } => {
589 if builtins.contains(&name.as_str()) {
591 continue;
592 }
593 if self.find_word(name).is_some() {
595 continue;
596 }
597 if external_words.contains(&name.as_str()) {
599 continue;
600 }
601 return Err(format!(
603 "Undefined word '{}' called in word '{}'. \
604 Did you forget to define it or misspell a built-in?",
605 name, word_name
606 ));
607 }
608 Statement::If {
609 then_branch,
610 else_branch,
611 } => {
612 self.validate_statements(then_branch, word_name, builtins, external_words)?;
614 if let Some(eb) = else_branch {
615 self.validate_statements(eb, word_name, builtins, external_words)?;
616 }
617 }
618 Statement::Quotation { body, .. } => {
619 self.validate_statements(body, word_name, builtins, external_words)?;
621 }
622 Statement::Match { arms } => {
623 for arm in arms {
625 self.validate_statements(&arm.body, word_name, builtins, external_words)?;
626 }
627 }
628 _ => {} }
630 }
631 Ok(())
632 }
633
634 pub const MAX_VARIANT_FIELDS: usize = 4;
638
639 pub fn generate_constructors(&mut self) -> Result<(), String> {
649 let mut new_words = Vec::new();
650
651 for union_def in &self.unions {
652 for variant in &union_def.variants {
653 let constructor_name = format!("Make-{}", variant.name);
654 let field_count = variant.fields.len();
655
656 if field_count > Self::MAX_VARIANT_FIELDS {
658 return Err(format!(
659 "Variant '{}' in union '{}' has {} fields, but the maximum is {}. \
660 Consider grouping fields into nested union types.",
661 variant.name,
662 union_def.name,
663 field_count,
664 Self::MAX_VARIANT_FIELDS
665 ));
666 }
667
668 let mut input_stack = StackType::RowVar("a".to_string());
671 for field in &variant.fields {
672 let field_type = parse_type_name(&field.type_name);
673 input_stack = input_stack.push(field_type);
674 }
675
676 let output_stack =
678 StackType::RowVar("a".to_string()).push(Type::Union(union_def.name.clone()));
679
680 let effect = Effect::new(input_stack, output_stack);
681
682 let body = vec![
686 Statement::Symbol(variant.name.clone()),
687 Statement::WordCall {
688 name: format!("variant.make-{}", field_count),
689 span: None, },
691 ];
692
693 new_words.push(WordDef {
694 name: constructor_name,
695 effect: Some(effect),
696 body,
697 source: variant.source.clone(),
698 });
699 }
700 }
701
702 self.words.extend(new_words);
703 Ok(())
704 }
705}
706
707fn parse_type_name(name: &str) -> Type {
710 match name {
711 "Int" => Type::Int,
712 "Float" => Type::Float,
713 "Bool" => Type::Bool,
714 "String" => Type::String,
715 "Channel" => Type::Channel,
716 other => Type::Union(other.to_string()),
717 }
718}
719
720impl Default for Program {
721 fn default() -> Self {
722 Self::new()
723 }
724}
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729
730 #[test]
731 fn test_validate_builtin_words() {
732 let program = Program {
733 includes: vec![],
734 unions: vec![],
735 words: vec![WordDef {
736 name: "main".to_string(),
737 effect: None,
738 body: vec![
739 Statement::IntLiteral(2),
740 Statement::IntLiteral(3),
741 Statement::WordCall {
742 name: "i.add".to_string(),
743 span: None,
744 },
745 Statement::WordCall {
746 name: "io.write-line".to_string(),
747 span: None,
748 },
749 ],
750 source: None,
751 }],
752 };
753
754 assert!(program.validate_word_calls().is_ok());
756 }
757
758 #[test]
759 fn test_validate_user_defined_words() {
760 let program = Program {
761 includes: vec![],
762 unions: vec![],
763 words: vec![
764 WordDef {
765 name: "helper".to_string(),
766 effect: None,
767 body: vec![Statement::IntLiteral(42)],
768 source: None,
769 },
770 WordDef {
771 name: "main".to_string(),
772 effect: None,
773 body: vec![Statement::WordCall {
774 name: "helper".to_string(),
775 span: None,
776 }],
777 source: None,
778 },
779 ],
780 };
781
782 assert!(program.validate_word_calls().is_ok());
784 }
785
786 #[test]
787 fn test_validate_undefined_word() {
788 let program = Program {
789 includes: vec![],
790 unions: vec![],
791 words: vec![WordDef {
792 name: "main".to_string(),
793 effect: None,
794 body: vec![Statement::WordCall {
795 name: "undefined_word".to_string(),
796 span: None,
797 }],
798 source: None,
799 }],
800 };
801
802 let result = program.validate_word_calls();
804 assert!(result.is_err());
805 let error = result.unwrap_err();
806 assert!(error.contains("undefined_word"));
807 assert!(error.contains("main"));
808 }
809
810 #[test]
811 fn test_validate_misspelled_builtin() {
812 let program = Program {
813 includes: vec![],
814 unions: vec![],
815 words: vec![WordDef {
816 name: "main".to_string(),
817 effect: None,
818 body: vec![Statement::WordCall {
819 name: "wrte_line".to_string(),
820 span: None,
821 }], source: None,
823 }],
824 };
825
826 let result = program.validate_word_calls();
828 assert!(result.is_err());
829 let error = result.unwrap_err();
830 assert!(error.contains("wrte_line"));
831 assert!(error.contains("misspell"));
832 }
833
834 #[test]
835 fn test_generate_constructors() {
836 let mut program = Program {
837 includes: vec![],
838 unions: vec![UnionDef {
839 name: "Message".to_string(),
840 variants: vec![
841 UnionVariant {
842 name: "Get".to_string(),
843 fields: vec![UnionField {
844 name: "response-chan".to_string(),
845 type_name: "Int".to_string(),
846 }],
847 source: None,
848 },
849 UnionVariant {
850 name: "Put".to_string(),
851 fields: vec![
852 UnionField {
853 name: "value".to_string(),
854 type_name: "String".to_string(),
855 },
856 UnionField {
857 name: "response-chan".to_string(),
858 type_name: "Int".to_string(),
859 },
860 ],
861 source: None,
862 },
863 ],
864 source: None,
865 }],
866 words: vec![],
867 };
868
869 program.generate_constructors().unwrap();
871
872 assert_eq!(program.words.len(), 2);
874
875 let make_get = program
877 .find_word("Make-Get")
878 .expect("Make-Get should exist");
879 assert_eq!(make_get.name, "Make-Get");
880 assert!(make_get.effect.is_some());
881 let effect = make_get.effect.as_ref().unwrap();
882 assert_eq!(
885 format!("{:?}", effect.outputs),
886 "Cons { rest: RowVar(\"a\"), top: Union(\"Message\") }"
887 );
888
889 let make_put = program
891 .find_word("Make-Put")
892 .expect("Make-Put should exist");
893 assert_eq!(make_put.name, "Make-Put");
894 assert!(make_put.effect.is_some());
895
896 assert_eq!(make_get.body.len(), 2);
899 match &make_get.body[0] {
900 Statement::Symbol(s) if s == "Get" => {}
901 other => panic!("Expected Symbol(\"Get\") for variant tag, got {:?}", other),
902 }
903 match &make_get.body[1] {
904 Statement::WordCall { name, span: None } if name == "variant.make-1" => {}
905 _ => panic!("Expected WordCall(variant.make-1)"),
906 }
907
908 assert_eq!(make_put.body.len(), 2);
910 match &make_put.body[0] {
911 Statement::Symbol(s) if s == "Put" => {}
912 other => panic!("Expected Symbol(\"Put\") for variant tag, got {:?}", other),
913 }
914 match &make_put.body[1] {
915 Statement::WordCall { name, span: None } if name == "variant.make-2" => {}
916 _ => panic!("Expected WordCall(variant.make-2)"),
917 }
918 }
919}