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}
129
130#[derive(Debug, Clone, PartialEq)]
131pub struct Program {
132 pub includes: Vec<Include>,
133 pub unions: Vec<UnionDef>,
134 pub words: Vec<WordDef>,
135}
136
137#[derive(Debug, Clone, PartialEq)]
138pub struct WordDef {
139 pub name: String,
140 pub effect: Option<Effect>,
143 pub body: Vec<Statement>,
144 pub source: Option<SourceLocation>,
146 pub allowed_lints: Vec<String>,
149}
150
151#[derive(Debug, Clone, PartialEq, Default)]
153pub struct Span {
154 pub line: usize,
156 pub column: usize,
158 pub length: usize,
160}
161
162impl Span {
163 pub fn new(line: usize, column: usize, length: usize) -> Self {
164 Span {
165 line,
166 column,
167 length,
168 }
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Default)]
174pub struct QuotationSpan {
175 pub start_line: usize,
177 pub start_column: usize,
179 pub end_line: usize,
181 pub end_column: usize,
183}
184
185impl QuotationSpan {
186 pub fn new(start_line: usize, start_column: usize, end_line: usize, end_column: usize) -> Self {
187 QuotationSpan {
188 start_line,
189 start_column,
190 end_line,
191 end_column,
192 }
193 }
194
195 pub fn contains(&self, line: usize, column: usize) -> bool {
197 if line < self.start_line || line > self.end_line {
198 return false;
199 }
200 if line == self.start_line && column < self.start_column {
201 return false;
202 }
203 if line == self.end_line && column >= self.end_column {
204 return false;
205 }
206 true
207 }
208}
209
210#[derive(Debug, Clone, PartialEq)]
211pub enum Statement {
212 IntLiteral(i64),
214
215 FloatLiteral(f64),
217
218 BoolLiteral(bool),
220
221 StringLiteral(String),
223
224 Symbol(String),
229
230 WordCall { name: String, span: Option<Span> },
233
234 If {
239 then_branch: Vec<Statement>,
241 else_branch: Option<Vec<Statement>>,
243 },
244
245 Quotation {
255 id: usize,
256 body: Vec<Statement>,
257 span: Option<QuotationSpan>,
258 },
259
260 Match {
274 arms: Vec<MatchArm>,
276 },
277}
278
279impl Program {
280 pub fn new() -> Self {
281 Program {
282 includes: Vec::new(),
283 unions: Vec::new(),
284 words: Vec::new(),
285 }
286 }
287
288 pub fn find_word(&self, name: &str) -> Option<&WordDef> {
289 self.words.iter().find(|w| w.name == name)
290 }
291
292 pub fn validate_word_calls(&self) -> Result<(), String> {
294 self.validate_word_calls_with_externals(&[])
295 }
296
297 pub fn validate_word_calls_with_externals(
302 &self,
303 external_words: &[&str],
304 ) -> Result<(), String> {
305 let builtins = [
308 "io.write",
310 "io.write-line",
311 "io.read-line",
312 "io.read-line+",
313 "io.read-n",
314 "int->string",
315 "symbol->string",
316 "string->symbol",
317 "args.count",
319 "args.at",
320 "file.slurp",
322 "file.exists?",
323 "file.for-each-line+",
324 "string.concat",
326 "string.length",
327 "string.byte-length",
328 "string.char-at",
329 "string.substring",
330 "char->string",
331 "string.find",
332 "string.split",
333 "string.contains",
334 "string.starts-with",
335 "string.empty?",
336 "string.trim",
337 "string.chomp",
338 "string.to-upper",
339 "string.to-lower",
340 "string.equal?",
341 "string.json-escape",
342 "string->int",
343 "symbol.=",
345 "encoding.base64-encode",
347 "encoding.base64-decode",
348 "encoding.base64url-encode",
349 "encoding.base64url-decode",
350 "encoding.hex-encode",
351 "encoding.hex-decode",
352 "crypto.sha256",
354 "crypto.hmac-sha256",
355 "crypto.constant-time-eq",
356 "crypto.random-bytes",
357 "crypto.random-int",
358 "crypto.uuid4",
359 "crypto.aes-gcm-encrypt",
360 "crypto.aes-gcm-decrypt",
361 "crypto.pbkdf2-sha256",
362 "crypto.ed25519-keypair",
363 "crypto.ed25519-sign",
364 "crypto.ed25519-verify",
365 "http.get",
367 "http.post",
368 "http.put",
369 "http.delete",
370 "list.make",
372 "list.push",
373 "list.get",
374 "list.set",
375 "list.map",
376 "list.filter",
377 "list.fold",
378 "list.each",
379 "list.length",
380 "list.empty?",
381 "map.make",
383 "map.get",
384 "map.set",
385 "map.has?",
386 "map.remove",
387 "map.keys",
388 "map.values",
389 "map.size",
390 "map.empty?",
391 "variant.field-count",
393 "variant.tag",
394 "variant.field-at",
395 "variant.append",
396 "variant.last",
397 "variant.init",
398 "variant.make-0",
399 "variant.make-1",
400 "variant.make-2",
401 "variant.make-3",
402 "variant.make-4",
403 "wrap-0",
405 "wrap-1",
406 "wrap-2",
407 "wrap-3",
408 "wrap-4",
409 "i.add",
411 "i.subtract",
412 "i.multiply",
413 "i.divide",
414 "i.modulo",
415 "i.+",
417 "i.-",
418 "i.*",
419 "i./",
420 "i.%",
421 "i.=",
423 "i.<",
424 "i.>",
425 "i.<=",
426 "i.>=",
427 "i.<>",
428 "i.eq",
430 "i.lt",
431 "i.gt",
432 "i.lte",
433 "i.gte",
434 "i.neq",
435 "dup",
437 "drop",
438 "swap",
439 "over",
440 "rot",
441 "nip",
442 "tuck",
443 "2dup",
444 "3drop",
445 "pick",
446 "roll",
447 "and",
449 "or",
450 "not",
451 "band",
453 "bor",
454 "bxor",
455 "bnot",
456 "shl",
457 "shr",
458 "popcount",
459 "clz",
460 "ctz",
461 "int-bits",
462 "chan.make",
464 "chan.send",
465 "chan.receive",
466 "chan.close",
467 "chan.yield",
468 "call",
470 "strand.spawn",
471 "strand.weave",
472 "strand.resume",
473 "strand.weave-cancel",
474 "yield",
475 "cond",
476 "tcp.listen",
478 "tcp.accept",
479 "tcp.read",
480 "tcp.write",
481 "tcp.close",
482 "os.getenv",
484 "os.home-dir",
485 "os.current-dir",
486 "os.path-exists",
487 "os.path-is-file",
488 "os.path-is-dir",
489 "os.path-join",
490 "os.path-parent",
491 "os.path-filename",
492 "os.exit",
493 "os.name",
494 "os.arch",
495 "signal.trap",
497 "signal.received?",
498 "signal.pending?",
499 "signal.default",
500 "signal.ignore",
501 "signal.clear",
502 "signal.SIGINT",
503 "signal.SIGTERM",
504 "signal.SIGHUP",
505 "signal.SIGPIPE",
506 "signal.SIGUSR1",
507 "signal.SIGUSR2",
508 "signal.SIGCHLD",
509 "signal.SIGALRM",
510 "signal.SIGCONT",
511 "terminal.raw-mode",
513 "terminal.read-char",
514 "terminal.read-char?",
515 "terminal.width",
516 "terminal.height",
517 "terminal.flush",
518 "f.add",
520 "f.subtract",
521 "f.multiply",
522 "f.divide",
523 "f.+",
525 "f.-",
526 "f.*",
527 "f./",
528 "f.=",
530 "f.<",
531 "f.>",
532 "f.<=",
533 "f.>=",
534 "f.<>",
535 "f.eq",
537 "f.lt",
538 "f.gt",
539 "f.lte",
540 "f.gte",
541 "f.neq",
542 "int->float",
544 "float->int",
545 "float->string",
546 "string->float",
547 "test.init",
549 "test.finish",
550 "test.has-failures",
551 "test.assert",
552 "test.assert-not",
553 "test.assert-eq",
554 "test.assert-eq-str",
555 "test.fail",
556 "test.pass-count",
557 "test.fail-count",
558 "time.now",
560 "time.nanos",
561 "time.sleep-ms",
562 "son.dump",
564 "son.dump-pretty",
565 "stack.dump",
567 "regex.match?",
569 "regex.find",
570 "regex.find-all",
571 "regex.replace",
572 "regex.replace-all",
573 "regex.captures",
574 "regex.split",
575 "regex.valid?",
576 "compress.gzip",
578 "compress.gzip-level",
579 "compress.gunzip",
580 "compress.zstd",
581 "compress.zstd-level",
582 "compress.unzstd",
583 ];
584
585 for word in &self.words {
586 self.validate_statements(&word.body, &word.name, &builtins, external_words)?;
587 }
588
589 Ok(())
590 }
591
592 fn validate_statements(
594 &self,
595 statements: &[Statement],
596 word_name: &str,
597 builtins: &[&str],
598 external_words: &[&str],
599 ) -> Result<(), String> {
600 for statement in statements {
601 match statement {
602 Statement::WordCall { name, .. } => {
603 if builtins.contains(&name.as_str()) {
605 continue;
606 }
607 if self.find_word(name).is_some() {
609 continue;
610 }
611 if external_words.contains(&name.as_str()) {
613 continue;
614 }
615 return Err(format!(
617 "Undefined word '{}' called in word '{}'. \
618 Did you forget to define it or misspell a built-in?",
619 name, word_name
620 ));
621 }
622 Statement::If {
623 then_branch,
624 else_branch,
625 } => {
626 self.validate_statements(then_branch, word_name, builtins, external_words)?;
628 if let Some(eb) = else_branch {
629 self.validate_statements(eb, word_name, builtins, external_words)?;
630 }
631 }
632 Statement::Quotation { body, .. } => {
633 self.validate_statements(body, word_name, builtins, external_words)?;
635 }
636 Statement::Match { arms } => {
637 for arm in arms {
639 self.validate_statements(&arm.body, word_name, builtins, external_words)?;
640 }
641 }
642 _ => {} }
644 }
645 Ok(())
646 }
647
648 pub const MAX_VARIANT_FIELDS: usize = 12;
652
653 pub fn generate_constructors(&mut self) -> Result<(), String> {
663 let mut new_words = Vec::new();
664
665 for union_def in &self.unions {
666 for variant in &union_def.variants {
667 let constructor_name = format!("Make-{}", variant.name);
668 let field_count = variant.fields.len();
669
670 if field_count > Self::MAX_VARIANT_FIELDS {
672 return Err(format!(
673 "Variant '{}' in union '{}' has {} fields, but the maximum is {}. \
674 Consider grouping fields into nested union types.",
675 variant.name,
676 union_def.name,
677 field_count,
678 Self::MAX_VARIANT_FIELDS
679 ));
680 }
681
682 let mut input_stack = StackType::RowVar("a".to_string());
685 for field in &variant.fields {
686 let field_type = parse_type_name(&field.type_name);
687 input_stack = input_stack.push(field_type);
688 }
689
690 let output_stack =
692 StackType::RowVar("a".to_string()).push(Type::Union(union_def.name.clone()));
693
694 let effect = Effect::new(input_stack, output_stack);
695
696 let body = vec![
700 Statement::Symbol(variant.name.clone()),
701 Statement::WordCall {
702 name: format!("variant.make-{}", field_count),
703 span: None, },
705 ];
706
707 new_words.push(WordDef {
708 name: constructor_name,
709 effect: Some(effect),
710 body,
711 source: variant.source.clone(),
712 allowed_lints: vec![],
713 });
714 }
715 }
716
717 self.words.extend(new_words);
718 Ok(())
719 }
720}
721
722fn parse_type_name(name: &str) -> Type {
725 match name {
726 "Int" => Type::Int,
727 "Float" => Type::Float,
728 "Bool" => Type::Bool,
729 "String" => Type::String,
730 "Channel" => Type::Channel,
731 other => Type::Union(other.to_string()),
732 }
733}
734
735impl Default for Program {
736 fn default() -> Self {
737 Self::new()
738 }
739}
740
741#[cfg(test)]
742mod tests {
743 use super::*;
744
745 #[test]
746 fn test_validate_builtin_words() {
747 let program = Program {
748 includes: vec![],
749 unions: vec![],
750 words: vec![WordDef {
751 name: "main".to_string(),
752 effect: None,
753 body: vec![
754 Statement::IntLiteral(2),
755 Statement::IntLiteral(3),
756 Statement::WordCall {
757 name: "i.add".to_string(),
758 span: None,
759 },
760 Statement::WordCall {
761 name: "io.write-line".to_string(),
762 span: None,
763 },
764 ],
765 source: None,
766 allowed_lints: vec![],
767 }],
768 };
769
770 assert!(program.validate_word_calls().is_ok());
772 }
773
774 #[test]
775 fn test_validate_user_defined_words() {
776 let program = Program {
777 includes: vec![],
778 unions: vec![],
779 words: vec![
780 WordDef {
781 name: "helper".to_string(),
782 effect: None,
783 body: vec![Statement::IntLiteral(42)],
784 source: None,
785 allowed_lints: vec![],
786 },
787 WordDef {
788 name: "main".to_string(),
789 effect: None,
790 body: vec![Statement::WordCall {
791 name: "helper".to_string(),
792 span: None,
793 }],
794 source: None,
795 allowed_lints: vec![],
796 },
797 ],
798 };
799
800 assert!(program.validate_word_calls().is_ok());
802 }
803
804 #[test]
805 fn test_validate_undefined_word() {
806 let program = Program {
807 includes: vec![],
808 unions: vec![],
809 words: vec![WordDef {
810 name: "main".to_string(),
811 effect: None,
812 body: vec![Statement::WordCall {
813 name: "undefined_word".to_string(),
814 span: None,
815 }],
816 source: None,
817 allowed_lints: vec![],
818 }],
819 };
820
821 let result = program.validate_word_calls();
823 assert!(result.is_err());
824 let error = result.unwrap_err();
825 assert!(error.contains("undefined_word"));
826 assert!(error.contains("main"));
827 }
828
829 #[test]
830 fn test_validate_misspelled_builtin() {
831 let program = Program {
832 includes: vec![],
833 unions: vec![],
834 words: vec![WordDef {
835 name: "main".to_string(),
836 effect: None,
837 body: vec![Statement::WordCall {
838 name: "wrte_line".to_string(),
839 span: None,
840 }], source: None,
842 allowed_lints: vec![],
843 }],
844 };
845
846 let result = program.validate_word_calls();
848 assert!(result.is_err());
849 let error = result.unwrap_err();
850 assert!(error.contains("wrte_line"));
851 assert!(error.contains("misspell"));
852 }
853
854 #[test]
855 fn test_generate_constructors() {
856 let mut program = Program {
857 includes: vec![],
858 unions: vec![UnionDef {
859 name: "Message".to_string(),
860 variants: vec![
861 UnionVariant {
862 name: "Get".to_string(),
863 fields: vec![UnionField {
864 name: "response-chan".to_string(),
865 type_name: "Int".to_string(),
866 }],
867 source: None,
868 },
869 UnionVariant {
870 name: "Put".to_string(),
871 fields: vec![
872 UnionField {
873 name: "value".to_string(),
874 type_name: "String".to_string(),
875 },
876 UnionField {
877 name: "response-chan".to_string(),
878 type_name: "Int".to_string(),
879 },
880 ],
881 source: None,
882 },
883 ],
884 source: None,
885 }],
886 words: vec![],
887 };
888
889 program.generate_constructors().unwrap();
891
892 assert_eq!(program.words.len(), 2);
894
895 let make_get = program
897 .find_word("Make-Get")
898 .expect("Make-Get should exist");
899 assert_eq!(make_get.name, "Make-Get");
900 assert!(make_get.effect.is_some());
901 let effect = make_get.effect.as_ref().unwrap();
902 assert_eq!(
905 format!("{:?}", effect.outputs),
906 "Cons { rest: RowVar(\"a\"), top: Union(\"Message\") }"
907 );
908
909 let make_put = program
911 .find_word("Make-Put")
912 .expect("Make-Put should exist");
913 assert_eq!(make_put.name, "Make-Put");
914 assert!(make_put.effect.is_some());
915
916 assert_eq!(make_get.body.len(), 2);
919 match &make_get.body[0] {
920 Statement::Symbol(s) if s == "Get" => {}
921 other => panic!("Expected Symbol(\"Get\") for variant tag, got {:?}", other),
922 }
923 match &make_get.body[1] {
924 Statement::WordCall { name, span: None } if name == "variant.make-1" => {}
925 _ => panic!("Expected WordCall(variant.make-1)"),
926 }
927
928 assert_eq!(make_put.body.len(), 2);
930 match &make_put.body[0] {
931 Statement::Symbol(s) if s == "Put" => {}
932 other => panic!("Expected Symbol(\"Put\") for variant tag, got {:?}", other),
933 }
934 match &make_put.body[1] {
935 Statement::WordCall { name, span: None } if name == "variant.make-2" => {}
936 _ => panic!("Expected WordCall(variant.make-2)"),
937 }
938 }
939}