1use crate::ast::{Program, Statement, WordDef};
7use crate::builtins::builtin_signature;
8use crate::call_graph::CallGraph;
9use crate::capture_analysis::calculate_captures;
10use crate::types::{
11 Effect, SideEffect, StackType, Type, UnionTypeInfo, VariantFieldInfo, VariantInfo,
12};
13use crate::unification::{Subst, unify_stacks, unify_types};
14use std::collections::HashMap;
15
16fn format_line_prefix(line: usize) -> String {
19 format!("at line {}: ", line + 1)
20}
21
22fn validate_main_effect(effect: &Effect) -> Result<(), String> {
31 let inputs_ok = matches!(&effect.inputs, StackType::Empty | StackType::RowVar(_));
33
34 let outputs_ok = match &effect.outputs {
36 StackType::Empty | StackType::RowVar(_) => true,
37 StackType::Cons {
38 rest,
39 top: Type::Int,
40 } if matches!(**rest, StackType::Empty | StackType::RowVar(_)) => true,
41 _ => false,
42 };
43
44 if inputs_ok && outputs_ok {
45 return Ok(());
46 }
47
48 Err(format!(
49 "Word 'main' has an invalid stack effect: ( {} -- {} ).\n\
50 `main` must be declared with one of:\n\
51 ( -- ) — void main, process exits with code 0\n\
52 ( -- Int ) — exit code is the returned Int\n\
53 Other shapes are not allowed.",
54 effect.inputs, effect.outputs
55 ))
56}
57
58pub struct TypeChecker {
59 env: HashMap<String, Effect>,
61 unions: HashMap<String, UnionTypeInfo>,
64 fresh_counter: std::cell::Cell<usize>,
66 quotation_types: std::cell::RefCell<HashMap<usize, Type>>,
70 expected_quotation_type: std::cell::RefCell<Option<Type>>,
73 current_word: std::cell::RefCell<Option<(String, Option<usize>)>>,
77 statement_top_types: std::cell::RefCell<HashMap<(String, usize), Type>>,
81 call_graph: Option<CallGraph>,
84 current_aux_stack: std::cell::RefCell<StackType>,
87 aux_max_depths: std::cell::RefCell<HashMap<String, usize>>,
90 in_quotation_scope: std::cell::Cell<bool>,
93 resolved_sugar: std::cell::RefCell<HashMap<(usize, usize), String>>,
97}
98
99impl TypeChecker {
100 pub fn new() -> Self {
101 TypeChecker {
102 env: HashMap::new(),
103 unions: HashMap::new(),
104 fresh_counter: std::cell::Cell::new(0),
105 quotation_types: std::cell::RefCell::new(HashMap::new()),
106 expected_quotation_type: std::cell::RefCell::new(None),
107 current_word: std::cell::RefCell::new(None),
108 statement_top_types: std::cell::RefCell::new(HashMap::new()),
109 call_graph: None,
110 current_aux_stack: std::cell::RefCell::new(StackType::Empty),
111 aux_max_depths: std::cell::RefCell::new(HashMap::new()),
112 in_quotation_scope: std::cell::Cell::new(false),
113 resolved_sugar: std::cell::RefCell::new(HashMap::new()),
114 }
115 }
116
117 pub fn set_call_graph(&mut self, call_graph: CallGraph) {
122 self.call_graph = Some(call_graph);
123 }
124
125 fn line_prefix(&self) -> String {
127 self.current_word
128 .borrow()
129 .as_ref()
130 .and_then(|(_, line)| line.map(format_line_prefix))
131 .unwrap_or_default()
132 }
133
134 pub fn get_union(&self, name: &str) -> Option<&UnionTypeInfo> {
136 self.unions.get(name)
137 }
138
139 pub fn get_unions(&self) -> &HashMap<String, UnionTypeInfo> {
141 &self.unions
142 }
143
144 fn find_variant(&self, variant_name: &str) -> Option<(&str, &VariantInfo)> {
148 for (union_name, union_info) in &self.unions {
149 for variant in &union_info.variants {
150 if variant.name == variant_name {
151 return Some((union_name.as_str(), variant));
152 }
153 }
154 }
155 None
156 }
157
158 pub fn register_external_words(&mut self, words: &[(&str, &Effect)]) {
162 for (name, effect) in words {
163 self.env.insert(name.to_string(), (*effect).clone());
164 }
165 }
166
167 pub fn register_external_unions(&mut self, union_names: &[&str]) {
173 for name in union_names {
174 self.unions.insert(
177 name.to_string(),
178 UnionTypeInfo {
179 name: name.to_string(),
180 variants: vec![],
181 },
182 );
183 }
184 }
185
186 pub fn take_quotation_types(&self) -> HashMap<usize, Type> {
192 self.quotation_types.replace(HashMap::new())
193 }
194
195 pub fn take_statement_top_types(&self) -> HashMap<(String, usize), Type> {
198 self.statement_top_types.replace(HashMap::new())
199 }
200
201 pub fn take_resolved_sugar(&self) -> HashMap<(usize, usize), String> {
204 self.resolved_sugar.replace(HashMap::new())
205 }
206
207 pub fn take_aux_max_depths(&self) -> HashMap<String, usize> {
209 self.aux_max_depths.replace(HashMap::new())
210 }
211
212 fn stack_depth(stack: &StackType) -> usize {
214 let mut depth = 0;
215 let mut current = stack;
216 while let StackType::Cons { rest, .. } = current {
217 depth += 1;
218 current = rest;
219 }
220 depth
221 }
222
223 fn get_trivially_copyable_top(stack: &StackType) -> Option<Type> {
226 match stack {
227 StackType::Cons { top, .. } => match top {
228 Type::Int | Type::Float | Type::Bool => Some(top.clone()),
229 _ => None,
230 },
231 _ => None,
232 }
233 }
234
235 fn capture_statement_type(&self, word_name: &str, stmt_index: usize, stack: &StackType) {
237 if let Some(top_type) = Self::get_trivially_copyable_top(stack) {
238 self.statement_top_types
239 .borrow_mut()
240 .insert((word_name.to_string(), stmt_index), top_type);
241 }
242 }
243
244 fn fresh_var(&self, prefix: &str) -> String {
246 let n = self.fresh_counter.get();
247 self.fresh_counter.set(n + 1);
248 format!("{}${}", prefix, n)
249 }
250
251 fn freshen_effect(&self, effect: &Effect) -> Effect {
253 let mut type_map = HashMap::new();
254 let mut row_map = HashMap::new();
255
256 let fresh_inputs = self.freshen_stack(&effect.inputs, &mut type_map, &mut row_map);
257 let fresh_outputs = self.freshen_stack(&effect.outputs, &mut type_map, &mut row_map);
258
259 let fresh_effects = effect
261 .effects
262 .iter()
263 .map(|e| self.freshen_side_effect(e, &mut type_map, &mut row_map))
264 .collect();
265
266 Effect::with_effects(fresh_inputs, fresh_outputs, fresh_effects)
267 }
268
269 fn freshen_side_effect(
270 &self,
271 effect: &SideEffect,
272 type_map: &mut HashMap<String, String>,
273 row_map: &mut HashMap<String, String>,
274 ) -> SideEffect {
275 match effect {
276 SideEffect::Yield(ty) => {
277 SideEffect::Yield(Box::new(self.freshen_type(ty, type_map, row_map)))
278 }
279 }
280 }
281
282 fn freshen_stack(
283 &self,
284 stack: &StackType,
285 type_map: &mut HashMap<String, String>,
286 row_map: &mut HashMap<String, String>,
287 ) -> StackType {
288 match stack {
289 StackType::Empty => StackType::Empty,
290 StackType::RowVar(name) => {
291 let fresh_name = row_map
292 .entry(name.clone())
293 .or_insert_with(|| self.fresh_var(name));
294 StackType::RowVar(fresh_name.clone())
295 }
296 StackType::Cons { rest, top } => {
297 let fresh_rest = self.freshen_stack(rest, type_map, row_map);
298 let fresh_top = self.freshen_type(top, type_map, row_map);
299 StackType::Cons {
300 rest: Box::new(fresh_rest),
301 top: fresh_top,
302 }
303 }
304 }
305 }
306
307 fn freshen_type(
308 &self,
309 ty: &Type,
310 type_map: &mut HashMap<String, String>,
311 row_map: &mut HashMap<String, String>,
312 ) -> Type {
313 match ty {
314 Type::Int | Type::Float | Type::Bool | Type::String | Type::Symbol | Type::Channel => {
315 ty.clone()
316 }
317 Type::Var(name) => {
318 let fresh_name = type_map
319 .entry(name.clone())
320 .or_insert_with(|| self.fresh_var(name));
321 Type::Var(fresh_name.clone())
322 }
323 Type::Quotation(effect) => {
324 let fresh_inputs = self.freshen_stack(&effect.inputs, type_map, row_map);
325 let fresh_outputs = self.freshen_stack(&effect.outputs, type_map, row_map);
326 Type::Quotation(Box::new(Effect::new(fresh_inputs, fresh_outputs)))
327 }
328 Type::Closure { effect, captures } => {
329 let fresh_inputs = self.freshen_stack(&effect.inputs, type_map, row_map);
330 let fresh_outputs = self.freshen_stack(&effect.outputs, type_map, row_map);
331 let fresh_captures = captures
332 .iter()
333 .map(|t| self.freshen_type(t, type_map, row_map))
334 .collect();
335 Type::Closure {
336 effect: Box::new(Effect::new(fresh_inputs, fresh_outputs)),
337 captures: fresh_captures,
338 }
339 }
340 Type::Union(name) => Type::Union(name.clone()),
342 }
343 }
344
345 fn parse_type_name(&self, name: &str) -> Type {
349 match name {
350 "Int" => Type::Int,
351 "Float" => Type::Float,
352 "Bool" => Type::Bool,
353 "String" => Type::String,
354 "Channel" => Type::Channel,
355 other => Type::Union(other.to_string()),
357 }
358 }
359
360 fn is_valid_type_name(&self, name: &str) -> bool {
365 matches!(name, "Int" | "Float" | "Bool" | "String" | "Channel")
366 || self.unions.contains_key(name)
367 }
368
369 fn validate_union_field_types(&self, program: &Program) -> Result<(), String> {
373 for union_def in &program.unions {
374 for variant in &union_def.variants {
375 for field in &variant.fields {
376 if !self.is_valid_type_name(&field.type_name) {
377 return Err(format!(
378 "Unknown type '{}' in field '{}' of variant '{}' in union '{}'. \
379 Valid types are: Int, Float, Bool, String, Channel, or a defined union name.",
380 field.type_name, field.name, variant.name, union_def.name
381 ));
382 }
383 }
384 }
385 }
386 Ok(())
387 }
388
389 fn validate_effect_types(&self, effect: &Effect, word_name: &str) -> Result<(), String> {
395 self.validate_stack_types(&effect.inputs, word_name)?;
396 self.validate_stack_types(&effect.outputs, word_name)?;
397 Ok(())
398 }
399
400 fn validate_stack_types(&self, stack: &StackType, word_name: &str) -> Result<(), String> {
402 match stack {
403 StackType::Empty | StackType::RowVar(_) => Ok(()),
404 StackType::Cons { rest, top } => {
405 self.validate_type(top, word_name)?;
406 self.validate_stack_types(rest, word_name)
407 }
408 }
409 }
410
411 fn validate_type(&self, ty: &Type, word_name: &str) -> Result<(), String> {
416 match ty {
417 Type::Var(_) => {
418 Ok(())
423 }
424 Type::Quotation(effect) => self.validate_effect_types(effect, word_name),
425 Type::Closure { effect, captures } => {
426 self.validate_effect_types(effect, word_name)?;
427 for cap in captures {
428 self.validate_type(cap, word_name)?;
429 }
430 Ok(())
431 }
432 Type::Int | Type::Float | Type::Bool | Type::String | Type::Symbol | Type::Channel => {
434 Ok(())
435 }
436 Type::Union(name) => {
438 if !self.unions.contains_key(name) {
439 return Err(format!(
440 "In word '{}': Unknown union type '{}' in stack effect.\n\
441 Make sure this union is defined before the word that uses it.",
442 word_name, name
443 ));
444 }
445 Ok(())
446 }
447 }
448 }
449
450 pub fn check_program(&mut self, program: &Program) -> Result<(), String> {
452 for union_def in &program.unions {
454 let variants = union_def
455 .variants
456 .iter()
457 .map(|v| VariantInfo {
458 name: v.name.clone(),
459 fields: v
460 .fields
461 .iter()
462 .map(|f| VariantFieldInfo {
463 name: f.name.clone(),
464 field_type: self.parse_type_name(&f.type_name),
465 })
466 .collect(),
467 })
468 .collect();
469
470 self.unions.insert(
471 union_def.name.clone(),
472 UnionTypeInfo {
473 name: union_def.name.clone(),
474 variants,
475 },
476 );
477 }
478
479 self.validate_union_field_types(program)?;
481
482 for word in &program.words {
485 if let Some(effect) = &word.effect {
486 self.validate_effect_types(effect, &word.name)?;
490 self.env.insert(word.name.clone(), effect.clone());
491 } else {
492 return Err(format!(
493 "Word '{}' is missing a stack effect declaration.\n\
494 All words must declare their stack effect, e.g.: : {} ( -- ) ... ;",
495 word.name, word.name
496 ));
497 }
498 }
499
500 if let Some(main_effect) = self.env.get("main") {
503 validate_main_effect(main_effect)?;
504 }
505
506 for word in &program.words {
508 self.check_word(word)?;
509 }
510
511 Ok(())
512 }
513
514 fn check_word(&self, word: &WordDef) -> Result<(), String> {
516 let line = word.source.as_ref().map(|s| s.start_line);
518 *self.current_word.borrow_mut() = Some((word.name.clone(), line));
519
520 *self.current_aux_stack.borrow_mut() = StackType::Empty;
522
523 let declared_effect = word.effect.as_ref().expect("word must have effect");
525
526 if let Some((_rest, top_type)) = declared_effect.outputs.clone().pop()
529 && matches!(top_type, Type::Quotation(_) | Type::Closure { .. })
530 {
531 *self.expected_quotation_type.borrow_mut() = Some(top_type);
532 }
533
534 let (result_stack, _subst, inferred_effects) =
536 self.infer_statements_from(&word.body, &declared_effect.inputs, true)?;
537
538 *self.expected_quotation_type.borrow_mut() = None;
540
541 let line_info = line.map(format_line_prefix).unwrap_or_default();
543 unify_stacks(&declared_effect.outputs, &result_stack).map_err(|e| {
544 format!(
545 "{}Word '{}': declared output stack ({}) doesn't match inferred ({}): {}",
546 line_info, word.name, declared_effect.outputs, result_stack, e
547 )
548 })?;
549
550 for inferred in &inferred_effects {
554 if !self.effect_matches_any(inferred, &declared_effect.effects) {
555 return Err(format!(
556 "{}Word '{}': body produces effect '{}' but no matching effect is declared.\n\
557 Hint: Add '| Yield <type>' to the word's stack effect declaration.",
558 line_info, word.name, inferred
559 ));
560 }
561 }
562
563 for declared in &declared_effect.effects {
566 if !self.effect_matches_any(declared, &inferred_effects) {
567 return Err(format!(
568 "{}Word '{}': declares effect '{}' but body doesn't produce it.\n\
569 Hint: Remove the effect declaration or ensure the body uses yield.",
570 line_info, word.name, declared
571 ));
572 }
573 }
574
575 let aux_stack = self.current_aux_stack.borrow().clone();
577 if aux_stack != StackType::Empty {
578 return Err(format!(
579 "{}Word '{}': aux stack is not empty at word return.\n\
580 Remaining aux stack: {}\n\
581 Every >aux must be matched by a corresponding aux> before the word returns.",
582 line_info, word.name, aux_stack
583 ));
584 }
585
586 *self.current_word.borrow_mut() = None;
588
589 Ok(())
590 }
591
592 fn infer_statements_from(
599 &self,
600 statements: &[Statement],
601 start_stack: &StackType,
602 capture_stmt_types: bool,
603 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
604 let mut current_stack = start_stack.clone();
605 let mut accumulated_subst = Subst::empty();
606 let mut accumulated_effects: Vec<SideEffect> = Vec::new();
607 let mut skip_next = false;
608
609 for (i, stmt) in statements.iter().enumerate() {
610 if skip_next {
612 skip_next = false;
613 continue;
614 }
615
616 if let Statement::IntLiteral(n) = stmt
619 && let Some(Statement::WordCall {
620 name: next_word, ..
621 }) = statements.get(i + 1)
622 {
623 if next_word == "pick" {
624 let (new_stack, subst) = self.handle_literal_pick(*n, current_stack.clone())?;
625 current_stack = new_stack;
626 accumulated_subst = accumulated_subst.compose(&subst);
627 skip_next = true; continue;
629 } else if next_word == "roll" {
630 let (new_stack, subst) = self.handle_literal_roll(*n, current_stack.clone())?;
631 current_stack = new_stack;
632 accumulated_subst = accumulated_subst.compose(&subst);
633 skip_next = true; continue;
635 }
636 }
637
638 let saved_expected_type = if matches!(stmt, Statement::Quotation { .. }) {
641 let saved = self.expected_quotation_type.borrow().clone();
643
644 if let Some(Statement::WordCall {
646 name: next_word, ..
647 }) = statements.get(i + 1)
648 {
649 if let Some(next_effect) = self.lookup_word_effect(next_word) {
651 if let Some((_rest, quot_type)) = next_effect.inputs.clone().pop()
654 && matches!(quot_type, Type::Quotation(_))
655 {
656 *self.expected_quotation_type.borrow_mut() = Some(quot_type);
657 }
658 }
659 }
660 Some(saved)
661 } else {
662 None
663 };
664
665 if capture_stmt_types && let Some((word_name, _)) = self.current_word.borrow().as_ref()
669 {
670 self.capture_statement_type(word_name, i, ¤t_stack);
671 }
672
673 let (new_stack, subst, effects) = self.infer_statement(stmt, current_stack)?;
674 current_stack = new_stack;
675 accumulated_subst = accumulated_subst.compose(&subst);
676
677 for effect in effects {
679 if !accumulated_effects.contains(&effect) {
680 accumulated_effects.push(effect);
681 }
682 }
683
684 if let Some(saved) = saved_expected_type {
686 *self.expected_quotation_type.borrow_mut() = saved;
687 }
688 }
689
690 Ok((current_stack, accumulated_subst, accumulated_effects))
691 }
692
693 fn handle_literal_pick(
704 &self,
705 n: i64,
706 current_stack: StackType,
707 ) -> Result<(StackType, Subst), String> {
708 if n < 0 {
709 return Err(format!("pick: index must be non-negative, got {}", n));
710 }
711
712 let type_at_n = self.get_type_at_position(¤t_stack, n as usize, "pick")?;
714
715 Ok((current_stack.push(type_at_n), Subst::empty()))
717 }
718
719 fn handle_literal_roll(
730 &self,
731 n: i64,
732 current_stack: StackType,
733 ) -> Result<(StackType, Subst), String> {
734 if n < 0 {
735 return Err(format!("roll: index must be non-negative, got {}", n));
736 }
737
738 self.rotate_type_to_top(current_stack, n as usize)
743 }
744
745 fn get_type_at_position(&self, stack: &StackType, n: usize, op: &str) -> Result<Type, String> {
747 let mut current = stack;
748 let mut pos = 0;
749
750 loop {
751 match current {
752 StackType::Cons { rest, top } => {
753 if pos == n {
754 return Ok(top.clone());
755 }
756 pos += 1;
757 current = rest;
758 }
759 StackType::RowVar(name) => {
760 let fresh_type = Type::Var(self.fresh_var(&format!("{}_{}", op, name)));
770 return Ok(fresh_type);
771 }
772 StackType::Empty => {
773 return Err(format!(
774 "{}{}: stack underflow - position {} requested but stack has only {} concrete items",
775 self.line_prefix(),
776 op,
777 n,
778 pos
779 ));
780 }
781 }
782 }
783 }
784
785 fn rotate_type_to_top(&self, stack: StackType, n: usize) -> Result<(StackType, Subst), String> {
787 if n == 0 {
788 return Ok((stack, Subst::empty()));
790 }
791
792 let mut types_above: Vec<Type> = Vec::new();
794 let mut current = stack;
795 let mut pos = 0;
796
797 loop {
799 match current {
800 StackType::Cons { rest, top } => {
801 if pos == n {
802 let mut result = *rest;
805 for ty in types_above.into_iter().rev() {
807 result = result.push(ty);
808 }
809 result = result.push(top);
811 return Ok((result, Subst::empty()));
812 }
813 types_above.push(top);
814 pos += 1;
815 current = *rest;
816 }
817 StackType::RowVar(name) => {
818 let fresh_type = Type::Var(self.fresh_var(&format!("roll_{}", name)));
831
832 let mut result = StackType::RowVar(name.clone());
834 for ty in types_above.into_iter().rev() {
835 result = result.push(ty);
836 }
837 result = result.push(fresh_type);
838 return Ok((result, Subst::empty()));
839 }
840 StackType::Empty => {
841 return Err(format!(
842 "{}roll: stack underflow - position {} requested but stack has only {} items",
843 self.line_prefix(),
844 n,
845 pos
846 ));
847 }
848 }
849 }
850 }
851
852 fn infer_statements(&self, statements: &[Statement]) -> Result<Effect, String> {
856 let start = StackType::RowVar("input".to_string());
857 let (result, subst, effects) = self.infer_statements_from(statements, &start, false)?;
859
860 let normalized_start = subst.apply_stack(&start);
863 let normalized_result = subst.apply_stack(&result);
864
865 Ok(Effect::with_effects(
866 normalized_start,
867 normalized_result,
868 effects,
869 ))
870 }
871
872 fn infer_match(
874 &self,
875 arms: &[crate::ast::MatchArm],
876 match_span: &Option<crate::ast::Span>,
877 current_stack: StackType,
878 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
879 if arms.is_empty() {
880 return Err("match expression must have at least one arm".to_string());
881 }
882
883 let (stack_after_match, _matched_type) =
885 self.pop_type(¤t_stack, "match expression")?;
886
887 let mut arm_results: Vec<StackType> = Vec::new();
889 let mut combined_subst = Subst::empty();
890 let mut merged_effects: Vec<SideEffect> = Vec::new();
891
892 let aux_before_match = self.current_aux_stack.borrow().clone();
894 let mut aux_after_arms: Vec<StackType> = Vec::new();
895
896 for arm in arms {
897 *self.current_aux_stack.borrow_mut() = aux_before_match.clone();
899
900 let variant_name = match &arm.pattern {
902 crate::ast::Pattern::Variant(name) => name.as_str(),
903 crate::ast::Pattern::VariantWithBindings { name, .. } => name.as_str(),
904 };
905
906 let (_union_name, variant_info) = self
908 .find_variant(variant_name)
909 .ok_or_else(|| format!("Unknown variant '{}' in match pattern", variant_name))?;
910
911 let arm_stack = self.push_variant_fields(
913 &stack_after_match,
914 &arm.pattern,
915 variant_info,
916 variant_name,
917 )?;
918
919 let (arm_result, arm_subst, arm_effects) =
922 self.infer_statements_from(&arm.body, &arm_stack, false)?;
923
924 combined_subst = combined_subst.compose(&arm_subst);
925 arm_results.push(arm_result);
926 aux_after_arms.push(self.current_aux_stack.borrow().clone());
927
928 for effect in arm_effects {
930 if !merged_effects.contains(&effect) {
931 merged_effects.push(effect);
932 }
933 }
934 }
935
936 if aux_after_arms.len() > 1 {
938 let first_aux = &aux_after_arms[0];
939 for (i, arm_aux) in aux_after_arms.iter().enumerate().skip(1) {
940 if arm_aux != first_aux {
941 let match_line = match_span.as_ref().map(|s| s.line + 1).unwrap_or(0);
942 return Err(format!(
943 "at line {}: match arms have incompatible aux stack effects:\n\
944 \x20 arm 0 aux: {}\n\
945 \x20 arm {} aux: {}\n\
946 \x20 All match arms must leave the aux stack in the same state.",
947 match_line, first_aux, i, arm_aux
948 ));
949 }
950 }
951 }
952 if let Some(aux) = aux_after_arms.into_iter().next() {
954 *self.current_aux_stack.borrow_mut() = aux;
955 }
956
957 let mut final_result = arm_results[0].clone();
959 for (i, arm_result) in arm_results.iter().enumerate().skip(1) {
960 let match_line = match_span.as_ref().map(|s| s.line + 1).unwrap_or(0);
962 let arm0_line = arms[0].span.as_ref().map(|s| s.line + 1).unwrap_or(0);
963 let arm_i_line = arms[i].span.as_ref().map(|s| s.line + 1).unwrap_or(0);
964
965 let arm_subst = unify_stacks(&final_result, arm_result).map_err(|e| {
966 if match_line > 0 && arm0_line > 0 && arm_i_line > 0 {
967 format!(
968 "at line {}: match arms have incompatible stack effects:\n\
969 \x20 arm 0 (line {}) produces: {}\n\
970 \x20 arm {} (line {}) produces: {}\n\
971 \x20 All match arms must produce the same stack shape.\n\
972 \x20 Error: {}",
973 match_line, arm0_line, final_result, i, arm_i_line, arm_result, e
974 )
975 } else {
976 format!(
977 "match arms have incompatible stack effects:\n\
978 \x20 arm 0 produces: {}\n\
979 \x20 arm {} produces: {}\n\
980 \x20 All match arms must produce the same stack shape.\n\
981 \x20 Error: {}",
982 final_result, i, arm_result, e
983 )
984 }
985 })?;
986 combined_subst = combined_subst.compose(&arm_subst);
987 final_result = arm_subst.apply_stack(&final_result);
988 }
989
990 Ok((final_result, combined_subst, merged_effects))
991 }
992
993 fn push_variant_fields(
995 &self,
996 stack: &StackType,
997 pattern: &crate::ast::Pattern,
998 variant_info: &VariantInfo,
999 variant_name: &str,
1000 ) -> Result<StackType, String> {
1001 let mut arm_stack = stack.clone();
1002 match pattern {
1003 crate::ast::Pattern::Variant(_) => {
1004 for field in &variant_info.fields {
1006 arm_stack = arm_stack.push(field.field_type.clone());
1007 }
1008 }
1009 crate::ast::Pattern::VariantWithBindings { bindings, .. } => {
1010 for binding in bindings {
1012 let field = variant_info
1013 .fields
1014 .iter()
1015 .find(|f| &f.name == binding)
1016 .ok_or_else(|| {
1017 let available: Vec<_> = variant_info
1018 .fields
1019 .iter()
1020 .map(|f| f.name.as_str())
1021 .collect();
1022 format!(
1023 "Unknown field '{}' in pattern for variant '{}'.\n\
1024 Available fields: {}",
1025 binding,
1026 variant_name,
1027 available.join(", ")
1028 )
1029 })?;
1030 arm_stack = arm_stack.push(field.field_type.clone());
1031 }
1032 }
1033 }
1034 Ok(arm_stack)
1035 }
1036
1037 fn is_divergent_branch(&self, statements: &[Statement]) -> bool {
1058 let Some((current_word_name, _)) = self.current_word.borrow().as_ref().cloned() else {
1059 return false;
1060 };
1061 let Some(Statement::WordCall { name, .. }) = statements.last() else {
1062 return false;
1063 };
1064
1065 if name == ¤t_word_name {
1067 return true;
1068 }
1069
1070 if let Some(ref graph) = self.call_graph
1072 && graph.are_mutually_recursive(¤t_word_name, name)
1073 {
1074 return true;
1075 }
1076
1077 false
1078 }
1079
1080 fn infer_if(
1082 &self,
1083 then_branch: &[Statement],
1084 else_branch: &Option<Vec<Statement>>,
1085 if_span: &Option<crate::ast::Span>,
1086 current_stack: StackType,
1087 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1088 let (stack_after_cond, cond_type) = self.pop_type(¤t_stack, "if condition")?;
1090
1091 let cond_subst = unify_stacks(
1093 &StackType::singleton(Type::Bool),
1094 &StackType::singleton(cond_type),
1095 )
1096 .map_err(|e| format!("if condition must be Bool: {}", e))?;
1097
1098 let stack_after_cond = cond_subst.apply_stack(&stack_after_cond);
1099
1100 let then_diverges = self.is_divergent_branch(then_branch);
1102 let else_diverges = else_branch
1103 .as_ref()
1104 .map(|stmts| self.is_divergent_branch(stmts))
1105 .unwrap_or(false);
1106
1107 let aux_before_branches = self.current_aux_stack.borrow().clone();
1109
1110 let (then_result, then_subst, then_effects) =
1113 self.infer_statements_from(then_branch, &stack_after_cond, false)?;
1114 let aux_after_then = self.current_aux_stack.borrow().clone();
1115
1116 *self.current_aux_stack.borrow_mut() = aux_before_branches.clone();
1118
1119 let (else_result, else_subst, else_effects) = if let Some(else_stmts) = else_branch {
1121 self.infer_statements_from(else_stmts, &stack_after_cond, false)?
1122 } else {
1123 (stack_after_cond.clone(), Subst::empty(), vec![])
1124 };
1125 let aux_after_else = self.current_aux_stack.borrow().clone();
1126
1127 if !then_diverges && !else_diverges && aux_after_then != aux_after_else {
1130 let if_line = if_span.as_ref().map(|s| s.line + 1).unwrap_or(0);
1131 return Err(format!(
1132 "at line {}: if/else branches have incompatible aux stack effects:\n\
1133 \x20 then branch aux: {}\n\
1134 \x20 else branch aux: {}\n\
1135 \x20 Both branches must leave the aux stack in the same state.",
1136 if_line, aux_after_then, aux_after_else
1137 ));
1138 }
1139
1140 if then_diverges && !else_diverges {
1142 *self.current_aux_stack.borrow_mut() = aux_after_else;
1143 } else {
1144 *self.current_aux_stack.borrow_mut() = aux_after_then;
1145 }
1146
1147 let mut merged_effects = then_effects;
1149 for effect in else_effects {
1150 if !merged_effects.contains(&effect) {
1151 merged_effects.push(effect);
1152 }
1153 }
1154
1155 let (result, branch_subst) = if then_diverges && !else_diverges {
1161 (else_result, Subst::empty())
1163 } else if else_diverges && !then_diverges {
1164 (then_result, Subst::empty())
1166 } else {
1167 let if_line = if_span.as_ref().map(|s| s.line + 1).unwrap_or(0);
1169 let branch_subst = unify_stacks(&then_result, &else_result).map_err(|e| {
1170 if if_line > 0 {
1171 format!(
1172 "at line {}: if/else branches have incompatible stack effects:\n\
1173 \x20 then branch produces: {}\n\
1174 \x20 else branch produces: {}\n\
1175 \x20 Both branches of an if/else must produce the same stack shape.\n\
1176 \x20 Hint: Make sure both branches push/pop the same number of values.\n\
1177 \x20 Error: {}",
1178 if_line, then_result, else_result, e
1179 )
1180 } else {
1181 format!(
1182 "if/else branches have incompatible stack effects:\n\
1183 \x20 then branch produces: {}\n\
1184 \x20 else branch produces: {}\n\
1185 \x20 Both branches of an if/else must produce the same stack shape.\n\
1186 \x20 Hint: Make sure both branches push/pop the same number of values.\n\
1187 \x20 Error: {}",
1188 then_result, else_result, e
1189 )
1190 }
1191 })?;
1192 (branch_subst.apply_stack(&then_result), branch_subst)
1193 };
1194
1195 let total_subst = cond_subst
1197 .compose(&then_subst)
1198 .compose(&else_subst)
1199 .compose(&branch_subst);
1200 Ok((result, total_subst, merged_effects))
1201 }
1202
1203 fn infer_quotation(
1206 &self,
1207 id: usize,
1208 body: &[Statement],
1209 current_stack: StackType,
1210 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1211 let expected_for_this_quotation = self.expected_quotation_type.borrow().clone();
1214 *self.expected_quotation_type.borrow_mut() = None;
1215
1216 let saved_aux = self.current_aux_stack.borrow().clone();
1221 *self.current_aux_stack.borrow_mut() = StackType::Empty;
1222 let saved_in_quotation = self.in_quotation_scope.get();
1223 self.in_quotation_scope.set(true);
1224
1225 let body_effect = self.infer_statements(body)?;
1227
1228 let quot_aux = self.current_aux_stack.borrow().clone();
1230 if quot_aux != StackType::Empty {
1231 return Err(format!(
1232 "Quotation has unbalanced aux stack.\n\
1233 Remaining aux stack: {}\n\
1234 Every >aux must be matched by a corresponding aux> within the quotation.",
1235 quot_aux
1236 ));
1237 }
1238
1239 *self.current_aux_stack.borrow_mut() = saved_aux;
1241 self.in_quotation_scope.set(saved_in_quotation);
1242
1243 *self.expected_quotation_type.borrow_mut() = expected_for_this_quotation;
1245
1246 let quot_type = self.analyze_captures(&body_effect, ¤t_stack)?;
1248
1249 self.quotation_types
1251 .borrow_mut()
1252 .insert(id, quot_type.clone());
1253
1254 let result_stack = match "_type {
1256 Type::Quotation(_) => {
1257 current_stack.push(quot_type)
1259 }
1260 Type::Closure { captures, .. } => {
1261 let mut stack = current_stack.clone();
1263 for _ in 0..captures.len() {
1264 let (new_stack, _value) = self.pop_type(&stack, "closure capture")?;
1265 stack = new_stack;
1266 }
1267 stack.push(quot_type)
1268 }
1269 _ => unreachable!("analyze_captures only returns Quotation or Closure"),
1270 };
1271
1272 Ok((result_stack, Subst::empty(), vec![]))
1276 }
1277
1278 fn infer_word_call(
1280 &self,
1281 name: &str,
1282 span: &Option<crate::ast::Span>,
1283 current_stack: StackType,
1284 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1285 let is_sugar = matches!(
1288 name,
1289 "+" | "-" | "*" | "/" | "%" | "=" | "<" | ">" | "<=" | ">=" | "<>"
1290 );
1291 if is_sugar {
1292 if let Some(resolved) = self.resolve_arithmetic_sugar(name, ¤t_stack) {
1293 if let Some(s) = span {
1295 self.resolved_sugar
1296 .borrow_mut()
1297 .insert((s.line, s.column), resolved.clone());
1298 }
1299 return self.infer_word_call(&resolved, span, current_stack);
1301 }
1302 let line_prefix = self.line_prefix();
1304 let (top_desc, second_desc) = {
1305 let top = current_stack.clone().pop().map(|(_, t)| format!("{}", t));
1306 let second = current_stack
1307 .clone()
1308 .pop()
1309 .and_then(|(r, _)| r.pop().map(|(_, t)| format!("{}", t)));
1310 (
1311 top.unwrap_or_else(|| "empty".to_string()),
1312 second.unwrap_or_else(|| "empty".to_string()),
1313 )
1314 };
1315 let (type_options, suggestion) = match name {
1316 "+" => (
1317 "Int+Int, Float+Float, or String+String",
1318 "Use `i.+`, `f.+`, or `string.concat`.",
1319 ),
1320 "=" => (
1321 "Int+Int, Float+Float, or String+String (equality)",
1322 "Use `i.=`, `f.=`, or `string.equal?`.",
1323 ),
1324 "%" => (
1325 "Int+Int only — float modulo is not supported",
1326 "Use `i.%` for integer modulo.",
1327 ),
1328 _ => (
1329 "Int+Int or Float+Float",
1330 "Use the `i.` or `f.` prefixed variant.",
1331 ),
1332 };
1333 return Err(format!(
1334 "{}`{}` requires matching types ({}), got ({}, {}). {}",
1335 line_prefix, name, type_options, second_desc, top_desc, suggestion,
1336 ));
1337 }
1338
1339 if name == ">aux" {
1341 return self.infer_to_aux(span, current_stack);
1342 }
1343 if name == "aux>" {
1344 return self.infer_from_aux(span, current_stack);
1345 }
1346
1347 if name == "call" {
1350 return self.infer_call(span, current_stack);
1351 }
1352
1353 if name == "dip" {
1355 return self.infer_dip(span, current_stack);
1356 }
1357 if name == "keep" {
1358 return self.infer_keep(span, current_stack);
1359 }
1360 if name == "bi" {
1361 return self.infer_bi(span, current_stack);
1362 }
1363
1364 let effect = self
1366 .lookup_word_effect(name)
1367 .ok_or_else(|| format!("Unknown word: '{}'", name))?;
1368
1369 let fresh_effect = self.freshen_effect(&effect);
1371
1372 let adjusted_stack = if name == "strand.spawn" {
1374 self.adjust_stack_for_spawn(current_stack, &fresh_effect)?
1375 } else {
1376 current_stack
1377 };
1378
1379 let (result_stack, subst) = self.apply_effect(&fresh_effect, adjusted_stack, name, span)?;
1381
1382 let propagated_effects = fresh_effect.effects.clone();
1386
1387 Ok((result_stack, subst, propagated_effects))
1388 }
1389
1390 fn infer_to_aux(
1392 &self,
1393 _span: &Option<crate::ast::Span>,
1394 current_stack: StackType,
1395 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1396 if self.in_quotation_scope.get() {
1397 return Err(">aux is not supported inside quotations.\n\
1398 Quotations are compiled as separate functions without aux stack slots.\n\
1399 Extract the quotation body into a named word if you need aux."
1400 .to_string());
1401 }
1402 let (rest, top_type) = self.pop_type(¤t_stack, ">aux")?;
1403
1404 let mut aux = self.current_aux_stack.borrow_mut();
1406 *aux = aux.clone().push(top_type);
1407
1408 let depth = Self::stack_depth(&aux);
1410 if let Some((word_name, _)) = self.current_word.borrow().as_ref() {
1411 let mut depths = self.aux_max_depths.borrow_mut();
1412 let entry = depths.entry(word_name.clone()).or_insert(0);
1413 if depth > *entry {
1414 *entry = depth;
1415 }
1416 }
1417
1418 Ok((rest, Subst::empty(), vec![]))
1419 }
1420
1421 fn infer_from_aux(
1423 &self,
1424 _span: &Option<crate::ast::Span>,
1425 current_stack: StackType,
1426 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1427 if self.in_quotation_scope.get() {
1428 return Err("aux> is not supported inside quotations.\n\
1429 Quotations are compiled as separate functions without aux stack slots.\n\
1430 Extract the quotation body into a named word if you need aux."
1431 .to_string());
1432 }
1433 let mut aux = self.current_aux_stack.borrow_mut();
1434 match aux.clone().pop() {
1435 Some((rest, top_type)) => {
1436 *aux = rest;
1437 Ok((current_stack.push(top_type), Subst::empty(), vec![]))
1438 }
1439 None => {
1440 let line_info = self.line_prefix();
1441 Err(format!(
1442 "{}aux>: aux stack is empty. Every aux> must be paired with a preceding >aux.",
1443 line_info
1444 ))
1445 }
1446 }
1447 }
1448
1449 fn infer_call(
1455 &self,
1456 span: &Option<crate::ast::Span>,
1457 current_stack: StackType,
1458 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1459 let line_prefix = self.line_prefix();
1461 let (remaining_stack, quot_type) = current_stack.clone().pop().ok_or_else(|| {
1462 format!(
1463 "{}call: stack underflow - expected quotation on stack",
1464 line_prefix
1465 )
1466 })?;
1467
1468 let quot_effect = match "_type {
1470 Type::Quotation(effect) => (**effect).clone(),
1471 Type::Closure { effect, .. } => (**effect).clone(),
1472 Type::Var(_) => {
1473 let effect = self
1476 .lookup_word_effect("call")
1477 .ok_or_else(|| "Unknown word: 'call'".to_string())?;
1478 let fresh_effect = self.freshen_effect(&effect);
1479 let (result_stack, subst) =
1480 self.apply_effect(&fresh_effect, current_stack, "call", span)?;
1481 return Ok((result_stack, subst, vec![]));
1482 }
1483 _ => {
1484 return Err(format!(
1485 "call: expected quotation or closure on stack, got {}",
1486 quot_type
1487 ));
1488 }
1489 };
1490
1491 if quot_effect.has_yield() {
1493 return Err("Cannot call quotation with Yield effect directly.\n\
1494 Quotations that yield values must be wrapped with `strand.weave`.\n\
1495 Example: `[ yielding-code ] strand.weave` instead of `[ yielding-code ] call`"
1496 .to_string());
1497 }
1498
1499 let fresh_effect = self.freshen_effect("_effect);
1501
1502 let (result_stack, subst) =
1504 self.apply_effect(&fresh_effect, remaining_stack, "call", span)?;
1505
1506 let propagated_effects = fresh_effect.effects.clone();
1508
1509 Ok((result_stack, subst, propagated_effects))
1510 }
1511
1512 fn resolve_arithmetic_sugar(&self, name: &str, stack: &StackType) -> Option<String> {
1515 let is_binary = matches!(
1517 name,
1518 "+" | "-" | "*" | "/" | "%" | "=" | "<" | ">" | "<=" | ">=" | "<>"
1519 );
1520 if !is_binary {
1521 return None;
1522 }
1523
1524 let (rest, top) = stack.clone().pop()?;
1526 let (_, second) = rest.pop()?;
1527
1528 match (name, &second, &top) {
1529 ("+", Type::Int, Type::Int) => Some("i.+".to_string()),
1531 ("-", Type::Int, Type::Int) => Some("i.-".to_string()),
1532 ("*", Type::Int, Type::Int) => Some("i.*".to_string()),
1533 ("/", Type::Int, Type::Int) => Some("i./".to_string()),
1534 ("%", Type::Int, Type::Int) => Some("i.%".to_string()),
1535 ("=", Type::Int, Type::Int) => Some("i.=".to_string()),
1536 ("<", Type::Int, Type::Int) => Some("i.<".to_string()),
1537 (">", Type::Int, Type::Int) => Some("i.>".to_string()),
1538 ("<=", Type::Int, Type::Int) => Some("i.<=".to_string()),
1539 (">=", Type::Int, Type::Int) => Some("i.>=".to_string()),
1540 ("<>", Type::Int, Type::Int) => Some("i.<>".to_string()),
1541
1542 ("+", Type::Float, Type::Float) => Some("f.+".to_string()),
1544 ("-", Type::Float, Type::Float) => Some("f.-".to_string()),
1545 ("*", Type::Float, Type::Float) => Some("f.*".to_string()),
1546 ("/", Type::Float, Type::Float) => Some("f./".to_string()),
1547 ("=", Type::Float, Type::Float) => Some("f.=".to_string()),
1548 ("<", Type::Float, Type::Float) => Some("f.<".to_string()),
1549 (">", Type::Float, Type::Float) => Some("f.>".to_string()),
1550 ("<=", Type::Float, Type::Float) => Some("f.<=".to_string()),
1551 (">=", Type::Float, Type::Float) => Some("f.>=".to_string()),
1552 ("<>", Type::Float, Type::Float) => Some("f.<>".to_string()),
1553
1554 ("+", Type::String, Type::String) => Some("string.concat".to_string()),
1556 ("=", Type::String, Type::String) => Some("string.equal?".to_string()),
1557
1558 _ => None,
1562 }
1563 }
1564
1565 fn infer_dip(
1570 &self,
1571 span: &Option<crate::ast::Span>,
1572 current_stack: StackType,
1573 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1574 let line_prefix = self.line_prefix();
1575
1576 let (stack_after_quot, quot_type) = current_stack.clone().pop().ok_or_else(|| {
1578 format!(
1579 "{}dip: stack underflow - expected quotation on stack",
1580 line_prefix
1581 )
1582 })?;
1583
1584 let quot_effect = match "_type {
1586 Type::Quotation(effect) => (**effect).clone(),
1587 Type::Closure { effect, .. } => (**effect).clone(),
1588 Type::Var(_) => {
1589 let effect = self
1591 .lookup_word_effect("dip")
1592 .ok_or_else(|| "Unknown word: 'dip'".to_string())?;
1593 let fresh_effect = self.freshen_effect(&effect);
1594 let (result_stack, subst) =
1595 self.apply_effect(&fresh_effect, current_stack, "dip", span)?;
1596 return Ok((result_stack, subst, vec![]));
1597 }
1598 _ => {
1599 return Err(format!(
1600 "{}dip: expected quotation or closure on top of stack, got {}",
1601 line_prefix, quot_type
1602 ));
1603 }
1604 };
1605
1606 if quot_effect.has_yield() {
1607 return Err("dip: quotation must not have Yield effects.\n\
1608 Use strand.weave for quotations that yield."
1609 .to_string());
1610 }
1611
1612 let (rest_stack, preserved_type) = stack_after_quot.clone().pop().ok_or_else(|| {
1614 format!(
1615 "{}dip: stack underflow - expected a value below the quotation",
1616 line_prefix
1617 )
1618 })?;
1619
1620 let fresh_effect = self.freshen_effect("_effect);
1622 let (result_stack, subst) =
1623 self.apply_effect(&fresh_effect, rest_stack, "dip (quotation)", span)?;
1624
1625 let resolved_preserved = subst.apply_type(&preserved_type);
1628 let result_stack = result_stack.push(resolved_preserved);
1629
1630 let propagated_effects = fresh_effect.effects.clone();
1631 Ok((result_stack, subst, propagated_effects))
1632 }
1633
1634 fn infer_keep(
1639 &self,
1640 span: &Option<crate::ast::Span>,
1641 current_stack: StackType,
1642 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1643 let line_prefix = self.line_prefix();
1644
1645 let (stack_after_quot, quot_type) = current_stack.clone().pop().ok_or_else(|| {
1647 format!(
1648 "{}keep: stack underflow - expected quotation on stack",
1649 line_prefix
1650 )
1651 })?;
1652
1653 let quot_effect = match "_type {
1655 Type::Quotation(effect) => (**effect).clone(),
1656 Type::Closure { effect, .. } => (**effect).clone(),
1657 Type::Var(_) => {
1658 let effect = self
1659 .lookup_word_effect("keep")
1660 .ok_or_else(|| "Unknown word: 'keep'".to_string())?;
1661 let fresh_effect = self.freshen_effect(&effect);
1662 let (result_stack, subst) =
1663 self.apply_effect(&fresh_effect, current_stack, "keep", span)?;
1664 return Ok((result_stack, subst, vec![]));
1665 }
1666 _ => {
1667 return Err(format!(
1668 "{}keep: expected quotation or closure on top of stack, got {}",
1669 line_prefix, quot_type
1670 ));
1671 }
1672 };
1673
1674 if quot_effect.has_yield() {
1675 return Err("keep: quotation must not have Yield effects.\n\
1676 Use strand.weave for quotations that yield."
1677 .to_string());
1678 }
1679
1680 let (_rest_stack, preserved_type) = stack_after_quot.clone().pop().ok_or_else(|| {
1682 format!(
1683 "{}keep: stack underflow - expected a value below the quotation",
1684 line_prefix
1685 )
1686 })?;
1687
1688 let fresh_effect = self.freshen_effect("_effect);
1691 let (result_stack, subst) =
1692 self.apply_effect(&fresh_effect, stack_after_quot, "keep (quotation)", span)?;
1693
1694 let resolved_preserved = subst.apply_type(&preserved_type);
1697 let result_stack = result_stack.push(resolved_preserved);
1698
1699 let propagated_effects = fresh_effect.effects.clone();
1700 Ok((result_stack, subst, propagated_effects))
1701 }
1702
1703 fn infer_bi(
1708 &self,
1709 span: &Option<crate::ast::Span>,
1710 current_stack: StackType,
1711 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1712 let line_prefix = self.line_prefix();
1713
1714 let (stack1, quot2_type) = current_stack.clone().pop().ok_or_else(|| {
1716 format!(
1717 "{}bi: stack underflow - expected second quotation on stack",
1718 line_prefix
1719 )
1720 })?;
1721
1722 let (stack2, quot1_type) = stack1.clone().pop().ok_or_else(|| {
1724 format!(
1725 "{}bi: stack underflow - expected first quotation on stack",
1726 line_prefix
1727 )
1728 })?;
1729
1730 let quot1_effect = match "1_type {
1732 Type::Quotation(effect) => (**effect).clone(),
1733 Type::Closure { effect, .. } => (**effect).clone(),
1734 Type::Var(_) => {
1735 let effect = self
1736 .lookup_word_effect("bi")
1737 .ok_or_else(|| "Unknown word: 'bi'".to_string())?;
1738 let fresh_effect = self.freshen_effect(&effect);
1739 let (result_stack, subst) =
1740 self.apply_effect(&fresh_effect, current_stack, "bi", span)?;
1741 return Ok((result_stack, subst, vec![]));
1742 }
1743 _ => {
1744 return Err(format!(
1745 "{}bi: expected quotation or closure as first quotation, got {}",
1746 line_prefix, quot1_type
1747 ));
1748 }
1749 };
1750
1751 let quot2_effect = match "2_type {
1752 Type::Quotation(effect) => (**effect).clone(),
1753 Type::Closure { effect, .. } => (**effect).clone(),
1754 Type::Var(_) => {
1755 let effect = self
1756 .lookup_word_effect("bi")
1757 .ok_or_else(|| "Unknown word: 'bi'".to_string())?;
1758 let fresh_effect = self.freshen_effect(&effect);
1759 let (result_stack, subst) =
1760 self.apply_effect(&fresh_effect, current_stack, "bi", span)?;
1761 return Ok((result_stack, subst, vec![]));
1762 }
1763 _ => {
1764 return Err(format!(
1765 "{}bi: expected quotation or closure as second quotation, got {}",
1766 line_prefix, quot2_type
1767 ));
1768 }
1769 };
1770
1771 if quot1_effect.has_yield() || quot2_effect.has_yield() {
1772 return Err("bi: quotations must not have Yield effects.\n\
1773 Use strand.weave for quotations that yield."
1774 .to_string());
1775 }
1776
1777 let (_rest, preserved_type) = stack2.clone().pop().ok_or_else(|| {
1780 format!(
1781 "{}bi: stack underflow - expected a value below the quotations",
1782 line_prefix
1783 )
1784 })?;
1785
1786 let fresh_effect1 = self.freshen_effect("1_effect);
1788 let (after_quot1, subst1) =
1789 self.apply_effect(&fresh_effect1, stack2, "bi (first quotation)", span)?;
1790
1791 let resolved_preserved = subst1.apply_type(&preserved_type);
1794 let with_x = after_quot1.push(resolved_preserved);
1795
1796 let fresh_effect2 = self.freshen_effect("2_effect);
1798 let (result_stack, subst2) =
1799 self.apply_effect(&fresh_effect2, with_x, "bi (second quotation)", span)?;
1800
1801 let subst = subst1.compose(&subst2);
1802
1803 let mut effects = fresh_effect1.effects.clone();
1804 for e in fresh_effect2.effects.clone() {
1805 if !effects.contains(&e) {
1806 effects.push(e);
1807 }
1808 }
1809
1810 Ok((result_stack, subst, effects))
1811 }
1812
1813 fn infer_statement(
1816 &self,
1817 statement: &Statement,
1818 current_stack: StackType,
1819 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1820 match statement {
1821 Statement::IntLiteral(_) => Ok((current_stack.push(Type::Int), Subst::empty(), vec![])),
1822 Statement::BoolLiteral(_) => {
1823 Ok((current_stack.push(Type::Bool), Subst::empty(), vec![]))
1824 }
1825 Statement::StringLiteral(_) => {
1826 Ok((current_stack.push(Type::String), Subst::empty(), vec![]))
1827 }
1828 Statement::FloatLiteral(_) => {
1829 Ok((current_stack.push(Type::Float), Subst::empty(), vec![]))
1830 }
1831 Statement::Symbol(_) => Ok((current_stack.push(Type::Symbol), Subst::empty(), vec![])),
1832 Statement::Match { arms, span } => self.infer_match(arms, span, current_stack),
1833 Statement::WordCall { name, span } => self.infer_word_call(name, span, current_stack),
1834 Statement::If {
1835 then_branch,
1836 else_branch,
1837 span,
1838 } => self.infer_if(then_branch, else_branch, span, current_stack),
1839 Statement::Quotation { id, body, .. } => self.infer_quotation(*id, body, current_stack),
1840 }
1841 }
1842
1843 fn lookup_word_effect(&self, name: &str) -> Option<Effect> {
1845 if let Some(effect) = builtin_signature(name) {
1847 return Some(effect);
1848 }
1849
1850 self.env.get(name).cloned()
1852 }
1853
1854 fn apply_effect(
1859 &self,
1860 effect: &Effect,
1861 current_stack: StackType,
1862 operation: &str,
1863 span: &Option<crate::ast::Span>,
1864 ) -> Result<(StackType, Subst), String> {
1865 let effect_concrete = Self::count_concrete_types(&effect.inputs);
1873 let stack_concrete = Self::count_concrete_types(¤t_stack);
1874
1875 if let Some(row_var_name) = Self::get_row_var_base(¤t_stack) {
1876 let is_rigid = row_var_name == "rest";
1889
1890 if is_rigid && effect_concrete > stack_concrete {
1891 let word_name = self
1892 .current_word
1893 .borrow()
1894 .as_ref()
1895 .map(|(n, _)| n.clone())
1896 .unwrap_or_else(|| "unknown".to_string());
1897 return Err(format!(
1898 "{}In '{}': {}: stack underflow - requires {} value(s), only {} provided",
1899 self.line_prefix(),
1900 word_name,
1901 operation,
1902 effect_concrete,
1903 stack_concrete
1904 ));
1905 }
1906 }
1907
1908 let subst = unify_stacks(&effect.inputs, ¤t_stack).map_err(|e| {
1910 let line_info = span
1911 .as_ref()
1912 .map(|s| format_line_prefix(s.line))
1913 .unwrap_or_default();
1914 format!(
1915 "{}{}: stack type mismatch. Expected {}, got {}: {}",
1916 line_info, operation, effect.inputs, current_stack, e
1917 )
1918 })?;
1919
1920 let result_stack = subst.apply_stack(&effect.outputs);
1922
1923 Ok((result_stack, subst))
1924 }
1925
1926 fn count_concrete_types(stack: &StackType) -> usize {
1928 let mut count = 0;
1929 let mut current = stack;
1930 while let StackType::Cons { rest, top: _ } = current {
1931 count += 1;
1932 current = rest;
1933 }
1934 count
1935 }
1936
1937 fn get_row_var_base(stack: &StackType) -> Option<String> {
1939 let mut current = stack;
1940 while let StackType::Cons { rest, top: _ } = current {
1941 current = rest;
1942 }
1943 match current {
1944 StackType::RowVar(name) => Some(name.clone()),
1945 _ => None,
1946 }
1947 }
1948
1949 fn adjust_stack_for_spawn(
1954 &self,
1955 current_stack: StackType,
1956 spawn_effect: &Effect,
1957 ) -> Result<StackType, String> {
1958 let expected_quot_type = match &spawn_effect.inputs {
1961 StackType::Cons { top, rest: _ } => {
1962 if !matches!(top, Type::Quotation(_)) {
1963 return Ok(current_stack); }
1965 top
1966 }
1967 _ => return Ok(current_stack),
1968 };
1969
1970 let (rest_stack, actual_type) = match ¤t_stack {
1972 StackType::Cons { rest, top } => (rest.as_ref().clone(), top),
1973 _ => return Ok(current_stack), };
1975
1976 if let Type::Quotation(actual_effect) = actual_type {
1978 if !matches!(actual_effect.inputs, StackType::Empty) {
1980 let expected_effect = match expected_quot_type {
1982 Type::Quotation(eff) => eff.as_ref(),
1983 _ => return Ok(current_stack),
1984 };
1985
1986 let captures = calculate_captures(actual_effect, expected_effect)?;
1988
1989 let closure_type = Type::Closure {
1991 effect: Box::new(expected_effect.clone()),
1992 captures: captures.clone(),
1993 };
1994
1995 let mut adjusted_stack = rest_stack;
1998 for _ in &captures {
1999 adjusted_stack = match adjusted_stack {
2000 StackType::Cons { rest, .. } => rest.as_ref().clone(),
2001 _ => {
2002 return Err(format!(
2003 "strand.spawn: not enough values on stack to capture. Need {} values",
2004 captures.len()
2005 ));
2006 }
2007 };
2008 }
2009
2010 return Ok(adjusted_stack.push(closure_type));
2012 }
2013 }
2014
2015 Ok(current_stack)
2016 }
2017
2018 fn analyze_captures(
2044 &self,
2045 body_effect: &Effect,
2046 _current_stack: &StackType,
2047 ) -> Result<Type, String> {
2048 let expected = self.expected_quotation_type.borrow().clone();
2050
2051 match expected {
2052 Some(Type::Closure { effect, .. }) => {
2053 let captures = calculate_captures(body_effect, &effect)?;
2055 Ok(Type::Closure { effect, captures })
2056 }
2057 Some(Type::Quotation(expected_effect)) => {
2058 let expected_is_empty = matches!(expected_effect.inputs, StackType::Empty);
2064 let body_needs_inputs = !matches!(body_effect.inputs, StackType::Empty);
2065
2066 if expected_is_empty && body_needs_inputs {
2067 let captures = calculate_captures(body_effect, &expected_effect)?;
2070 Ok(Type::Closure {
2071 effect: expected_effect,
2072 captures,
2073 })
2074 } else {
2075 let body_quot = Type::Quotation(Box::new(body_effect.clone()));
2081 let expected_quot = Type::Quotation(expected_effect.clone());
2082 unify_types(&body_quot, &expected_quot).map_err(|e| {
2083 format!(
2084 "quotation effect mismatch: expected {}, got {}: {}",
2085 expected_effect, body_effect, e
2086 )
2087 })?;
2088
2089 Ok(Type::Quotation(expected_effect))
2091 }
2092 }
2093 _ => {
2094 Ok(Type::Quotation(Box::new(body_effect.clone())))
2096 }
2097 }
2098 }
2099
2100 fn effect_matches_any(&self, inferred: &SideEffect, declared: &[SideEffect]) -> bool {
2104 declared.iter().any(|decl| match (inferred, decl) {
2105 (SideEffect::Yield(_), SideEffect::Yield(_)) => true,
2106 })
2107 }
2108
2109 fn pop_type(&self, stack: &StackType, context: &str) -> Result<(StackType, Type), String> {
2111 match stack {
2112 StackType::Cons { rest, top } => Ok(((**rest).clone(), top.clone())),
2113 StackType::Empty => Err(format!(
2114 "{}: stack underflow - expected value on stack but stack is empty",
2115 context
2116 )),
2117 StackType::RowVar(_) => {
2118 Err(format!(
2122 "{}: cannot pop from polymorphic stack without more type information",
2123 context
2124 ))
2125 }
2126 }
2127 }
2128}
2129
2130impl Default for TypeChecker {
2131 fn default() -> Self {
2132 Self::new()
2133 }
2134}
2135
2136#[cfg(test)]
2137mod tests {
2138 use super::*;
2139
2140 #[test]
2141 fn test_simple_literal() {
2142 let program = Program {
2143 includes: vec![],
2144 unions: vec![],
2145 words: vec![WordDef {
2146 name: "test".to_string(),
2147 effect: Some(Effect::new(
2148 StackType::Empty,
2149 StackType::singleton(Type::Int),
2150 )),
2151 body: vec![Statement::IntLiteral(42)],
2152 source: None,
2153 allowed_lints: vec![],
2154 }],
2155 };
2156
2157 let mut checker = TypeChecker::new();
2158 assert!(checker.check_program(&program).is_ok());
2159 }
2160
2161 #[test]
2162 fn test_simple_operation() {
2163 let program = Program {
2165 includes: vec![],
2166 unions: vec![],
2167 words: vec![WordDef {
2168 name: "test".to_string(),
2169 effect: Some(Effect::new(
2170 StackType::Empty.push(Type::Int).push(Type::Int),
2171 StackType::singleton(Type::Int),
2172 )),
2173 body: vec![Statement::WordCall {
2174 name: "i.add".to_string(),
2175 span: None,
2176 }],
2177 source: None,
2178 allowed_lints: vec![],
2179 }],
2180 };
2181
2182 let mut checker = TypeChecker::new();
2183 assert!(checker.check_program(&program).is_ok());
2184 }
2185
2186 #[test]
2187 fn test_type_mismatch() {
2188 let program = Program {
2190 includes: vec![],
2191 unions: vec![],
2192 words: vec![WordDef {
2193 name: "test".to_string(),
2194 effect: Some(Effect::new(
2195 StackType::singleton(Type::String),
2196 StackType::Empty,
2197 )),
2198 body: vec![
2199 Statement::IntLiteral(42), Statement::WordCall {
2201 name: "io.write-line".to_string(),
2202 span: None,
2203 },
2204 ],
2205 source: None,
2206 allowed_lints: vec![],
2207 }],
2208 };
2209
2210 let mut checker = TypeChecker::new();
2211 let result = checker.check_program(&program);
2212 assert!(result.is_err());
2213 assert!(result.unwrap_err().contains("Type mismatch"));
2214 }
2215
2216 #[test]
2217 fn test_polymorphic_dup() {
2218 let program = Program {
2220 includes: vec![],
2221 unions: vec![],
2222 words: vec![WordDef {
2223 name: "my-dup".to_string(),
2224 effect: Some(Effect::new(
2225 StackType::singleton(Type::Int),
2226 StackType::Empty.push(Type::Int).push(Type::Int),
2227 )),
2228 body: vec![Statement::WordCall {
2229 name: "dup".to_string(),
2230 span: None,
2231 }],
2232 source: None,
2233 allowed_lints: vec![],
2234 }],
2235 };
2236
2237 let mut checker = TypeChecker::new();
2238 assert!(checker.check_program(&program).is_ok());
2239 }
2240
2241 #[test]
2242 fn test_conditional_branches() {
2243 let program = Program {
2246 includes: vec![],
2247 unions: vec![],
2248 words: vec![WordDef {
2249 name: "test".to_string(),
2250 effect: Some(Effect::new(
2251 StackType::Empty.push(Type::Int).push(Type::Int),
2252 StackType::singleton(Type::String),
2253 )),
2254 body: vec![
2255 Statement::WordCall {
2256 name: "i.>".to_string(),
2257 span: None,
2258 },
2259 Statement::If {
2260 then_branch: vec![Statement::StringLiteral("greater".to_string())],
2261 else_branch: Some(vec![Statement::StringLiteral(
2262 "not greater".to_string(),
2263 )]),
2264 span: None,
2265 },
2266 ],
2267 source: None,
2268 allowed_lints: vec![],
2269 }],
2270 };
2271
2272 let mut checker = TypeChecker::new();
2273 assert!(checker.check_program(&program).is_ok());
2274 }
2275
2276 #[test]
2277 fn test_mismatched_branches() {
2278 let program = Program {
2281 includes: vec![],
2282 unions: vec![],
2283 words: vec![WordDef {
2284 name: "test".to_string(),
2285 effect: Some(Effect::new(
2286 StackType::Empty,
2287 StackType::singleton(Type::Int),
2288 )),
2289 body: vec![
2290 Statement::BoolLiteral(true),
2291 Statement::If {
2292 then_branch: vec![Statement::IntLiteral(42)],
2293 else_branch: Some(vec![Statement::StringLiteral("string".to_string())]),
2294 span: None,
2295 },
2296 ],
2297 source: None,
2298 allowed_lints: vec![],
2299 }],
2300 };
2301
2302 let mut checker = TypeChecker::new();
2303 let result = checker.check_program(&program);
2304 assert!(result.is_err());
2305 assert!(result.unwrap_err().contains("incompatible"));
2306 }
2307
2308 #[test]
2309 fn test_user_defined_word_call() {
2310 let program = Program {
2313 includes: vec![],
2314 unions: vec![],
2315 words: vec![
2316 WordDef {
2317 name: "helper".to_string(),
2318 effect: Some(Effect::new(
2319 StackType::singleton(Type::Int),
2320 StackType::singleton(Type::String),
2321 )),
2322 body: vec![Statement::WordCall {
2323 name: "int->string".to_string(),
2324 span: None,
2325 }],
2326 source: None,
2327 allowed_lints: vec![],
2328 },
2329 WordDef {
2330 name: "main".to_string(),
2331 effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2332 body: vec![
2333 Statement::IntLiteral(42),
2334 Statement::WordCall {
2335 name: "helper".to_string(),
2336 span: None,
2337 },
2338 Statement::WordCall {
2339 name: "io.write-line".to_string(),
2340 span: None,
2341 },
2342 ],
2343 source: None,
2344 allowed_lints: vec![],
2345 },
2346 ],
2347 };
2348
2349 let mut checker = TypeChecker::new();
2350 assert!(checker.check_program(&program).is_ok());
2351 }
2352
2353 #[test]
2354 fn test_arithmetic_chain() {
2355 let program = Program {
2358 includes: vec![],
2359 unions: vec![],
2360 words: vec![WordDef {
2361 name: "test".to_string(),
2362 effect: Some(Effect::new(
2363 StackType::Empty
2364 .push(Type::Int)
2365 .push(Type::Int)
2366 .push(Type::Int),
2367 StackType::singleton(Type::Int),
2368 )),
2369 body: vec![
2370 Statement::WordCall {
2371 name: "i.add".to_string(),
2372 span: None,
2373 },
2374 Statement::WordCall {
2375 name: "i.multiply".to_string(),
2376 span: None,
2377 },
2378 ],
2379 source: None,
2380 allowed_lints: vec![],
2381 }],
2382 };
2383
2384 let mut checker = TypeChecker::new();
2385 assert!(checker.check_program(&program).is_ok());
2386 }
2387
2388 #[test]
2389 fn test_write_line_type_error() {
2390 let program = Program {
2392 includes: vec![],
2393 unions: vec![],
2394 words: vec![WordDef {
2395 name: "test".to_string(),
2396 effect: Some(Effect::new(
2397 StackType::singleton(Type::Int),
2398 StackType::Empty,
2399 )),
2400 body: vec![Statement::WordCall {
2401 name: "io.write-line".to_string(),
2402 span: None,
2403 }],
2404 source: None,
2405 allowed_lints: vec![],
2406 }],
2407 };
2408
2409 let mut checker = TypeChecker::new();
2410 let result = checker.check_program(&program);
2411 assert!(result.is_err());
2412 assert!(result.unwrap_err().contains("Type mismatch"));
2413 }
2414
2415 #[test]
2416 fn test_stack_underflow_drop() {
2417 let program = Program {
2419 includes: vec![],
2420 unions: vec![],
2421 words: vec![WordDef {
2422 name: "test".to_string(),
2423 effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2424 body: vec![Statement::WordCall {
2425 name: "drop".to_string(),
2426 span: None,
2427 }],
2428 source: None,
2429 allowed_lints: vec![],
2430 }],
2431 };
2432
2433 let mut checker = TypeChecker::new();
2434 let result = checker.check_program(&program);
2435 assert!(result.is_err());
2436 assert!(result.unwrap_err().contains("mismatch"));
2437 }
2438
2439 #[test]
2440 fn test_stack_underflow_add() {
2441 let program = Program {
2443 includes: vec![],
2444 unions: vec![],
2445 words: vec![WordDef {
2446 name: "test".to_string(),
2447 effect: Some(Effect::new(
2448 StackType::singleton(Type::Int),
2449 StackType::singleton(Type::Int),
2450 )),
2451 body: vec![Statement::WordCall {
2452 name: "i.add".to_string(),
2453 span: None,
2454 }],
2455 source: None,
2456 allowed_lints: vec![],
2457 }],
2458 };
2459
2460 let mut checker = TypeChecker::new();
2461 let result = checker.check_program(&program);
2462 assert!(result.is_err());
2463 assert!(result.unwrap_err().contains("mismatch"));
2464 }
2465
2466 #[test]
2469 fn test_stack_underflow_rot_issue_169() {
2470 let program = Program {
2474 includes: vec![],
2475 unions: vec![],
2476 words: vec![WordDef {
2477 name: "test".to_string(),
2478 effect: Some(Effect::new(
2479 StackType::RowVar("rest".to_string()),
2480 StackType::RowVar("rest".to_string()),
2481 )),
2482 body: vec![
2483 Statement::IntLiteral(3),
2484 Statement::IntLiteral(4),
2485 Statement::WordCall {
2486 name: "rot".to_string(),
2487 span: None,
2488 },
2489 ],
2490 source: None,
2491 allowed_lints: vec![],
2492 }],
2493 };
2494
2495 let mut checker = TypeChecker::new();
2496 let result = checker.check_program(&program);
2497 assert!(result.is_err(), "rot with 2 values should fail");
2498 let err = result.unwrap_err();
2499 assert!(
2500 err.contains("stack underflow") || err.contains("requires 3"),
2501 "Error should mention underflow: {}",
2502 err
2503 );
2504 }
2505
2506 #[test]
2507 fn test_csp_operations() {
2508 let program = Program {
2515 includes: vec![],
2516 unions: vec![],
2517 words: vec![WordDef {
2518 name: "test".to_string(),
2519 effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2520 body: vec![
2521 Statement::WordCall {
2522 name: "chan.make".to_string(),
2523 span: None,
2524 },
2525 Statement::IntLiteral(42),
2526 Statement::WordCall {
2527 name: "swap".to_string(),
2528 span: None,
2529 },
2530 Statement::WordCall {
2531 name: "chan.send".to_string(),
2532 span: None,
2533 },
2534 Statement::WordCall {
2535 name: "drop".to_string(),
2536 span: None,
2537 },
2538 ],
2539 source: None,
2540 allowed_lints: vec![],
2541 }],
2542 };
2543
2544 let mut checker = TypeChecker::new();
2545 assert!(checker.check_program(&program).is_ok());
2546 }
2547
2548 #[test]
2549 fn test_complex_stack_shuffling() {
2550 let program = Program {
2553 includes: vec![],
2554 unions: vec![],
2555 words: vec![WordDef {
2556 name: "test".to_string(),
2557 effect: Some(Effect::new(
2558 StackType::Empty
2559 .push(Type::Int)
2560 .push(Type::Int)
2561 .push(Type::Int),
2562 StackType::singleton(Type::Int),
2563 )),
2564 body: vec![
2565 Statement::WordCall {
2566 name: "rot".to_string(),
2567 span: None,
2568 },
2569 Statement::WordCall {
2570 name: "i.add".to_string(),
2571 span: None,
2572 },
2573 Statement::WordCall {
2574 name: "i.add".to_string(),
2575 span: None,
2576 },
2577 ],
2578 source: None,
2579 allowed_lints: vec![],
2580 }],
2581 };
2582
2583 let mut checker = TypeChecker::new();
2584 assert!(checker.check_program(&program).is_ok());
2585 }
2586
2587 #[test]
2588 fn test_empty_program() {
2589 let program = Program {
2591 includes: vec![],
2592 unions: vec![],
2593 words: vec![],
2594 };
2595
2596 let mut checker = TypeChecker::new();
2597 assert!(checker.check_program(&program).is_ok());
2598 }
2599
2600 #[test]
2601 fn test_word_without_effect_declaration() {
2602 let program = Program {
2604 includes: vec![],
2605 unions: vec![],
2606 words: vec![WordDef {
2607 name: "helper".to_string(),
2608 effect: None,
2609 body: vec![Statement::IntLiteral(42)],
2610 source: None,
2611 allowed_lints: vec![],
2612 }],
2613 };
2614
2615 let mut checker = TypeChecker::new();
2616 let result = checker.check_program(&program);
2617 assert!(result.is_err());
2618 assert!(
2619 result
2620 .unwrap_err()
2621 .contains("missing a stack effect declaration")
2622 );
2623 }
2624
2625 #[test]
2626 fn test_nested_conditionals() {
2627 let program = Program {
2636 includes: vec![],
2637 unions: vec![],
2638 words: vec![WordDef {
2639 name: "test".to_string(),
2640 effect: Some(Effect::new(
2641 StackType::Empty
2642 .push(Type::Int)
2643 .push(Type::Int)
2644 .push(Type::Int)
2645 .push(Type::Int),
2646 StackType::singleton(Type::String),
2647 )),
2648 body: vec![
2649 Statement::WordCall {
2650 name: "i.>".to_string(),
2651 span: None,
2652 },
2653 Statement::If {
2654 then_branch: vec![
2655 Statement::WordCall {
2656 name: "i.>".to_string(),
2657 span: None,
2658 },
2659 Statement::If {
2660 then_branch: vec![Statement::StringLiteral(
2661 "both true".to_string(),
2662 )],
2663 else_branch: Some(vec![Statement::StringLiteral(
2664 "first true".to_string(),
2665 )]),
2666 span: None,
2667 },
2668 ],
2669 else_branch: Some(vec![
2670 Statement::WordCall {
2671 name: "drop".to_string(),
2672 span: None,
2673 },
2674 Statement::WordCall {
2675 name: "drop".to_string(),
2676 span: None,
2677 },
2678 Statement::StringLiteral("first false".to_string()),
2679 ]),
2680 span: None,
2681 },
2682 ],
2683 source: None,
2684 allowed_lints: vec![],
2685 }],
2686 };
2687
2688 let mut checker = TypeChecker::new();
2689 match checker.check_program(&program) {
2690 Ok(_) => {}
2691 Err(e) => panic!("Type check failed: {}", e),
2692 }
2693 }
2694
2695 #[test]
2696 fn test_conditional_without_else() {
2697 let program = Program {
2701 includes: vec![],
2702 unions: vec![],
2703 words: vec![WordDef {
2704 name: "test".to_string(),
2705 effect: Some(Effect::new(
2706 StackType::Empty.push(Type::Int).push(Type::Int),
2707 StackType::singleton(Type::Int),
2708 )),
2709 body: vec![
2710 Statement::WordCall {
2711 name: "i.>".to_string(),
2712 span: None,
2713 },
2714 Statement::If {
2715 then_branch: vec![Statement::IntLiteral(100)],
2716 else_branch: None, span: None,
2718 },
2719 ],
2720 source: None,
2721 allowed_lints: vec![],
2722 }],
2723 };
2724
2725 let mut checker = TypeChecker::new();
2726 let result = checker.check_program(&program);
2727 assert!(result.is_err());
2729 }
2730
2731 #[test]
2732 fn test_multiple_word_chain() {
2733 let program = Program {
2737 includes: vec![],
2738 unions: vec![],
2739 words: vec![
2740 WordDef {
2741 name: "helper1".to_string(),
2742 effect: Some(Effect::new(
2743 StackType::singleton(Type::Int),
2744 StackType::singleton(Type::String),
2745 )),
2746 body: vec![Statement::WordCall {
2747 name: "int->string".to_string(),
2748 span: None,
2749 }],
2750 source: None,
2751 allowed_lints: vec![],
2752 },
2753 WordDef {
2754 name: "helper2".to_string(),
2755 effect: Some(Effect::new(
2756 StackType::singleton(Type::String),
2757 StackType::Empty,
2758 )),
2759 body: vec![Statement::WordCall {
2760 name: "io.write-line".to_string(),
2761 span: None,
2762 }],
2763 source: None,
2764 allowed_lints: vec![],
2765 },
2766 WordDef {
2767 name: "main".to_string(),
2768 effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2769 body: vec![
2770 Statement::IntLiteral(42),
2771 Statement::WordCall {
2772 name: "helper1".to_string(),
2773 span: None,
2774 },
2775 Statement::WordCall {
2776 name: "helper2".to_string(),
2777 span: None,
2778 },
2779 ],
2780 source: None,
2781 allowed_lints: vec![],
2782 },
2783 ],
2784 };
2785
2786 let mut checker = TypeChecker::new();
2787 assert!(checker.check_program(&program).is_ok());
2788 }
2789
2790 #[test]
2791 fn test_all_stack_ops() {
2792 let program = Program {
2795 includes: vec![],
2796 unions: vec![],
2797 words: vec![WordDef {
2798 name: "test".to_string(),
2799 effect: Some(Effect::new(
2800 StackType::Empty
2801 .push(Type::Int)
2802 .push(Type::Int)
2803 .push(Type::Int),
2804 StackType::Empty
2805 .push(Type::Int)
2806 .push(Type::Int)
2807 .push(Type::Int)
2808 .push(Type::Int),
2809 )),
2810 body: vec![
2811 Statement::WordCall {
2812 name: "over".to_string(),
2813 span: None,
2814 },
2815 Statement::WordCall {
2816 name: "nip".to_string(),
2817 span: None,
2818 },
2819 Statement::WordCall {
2820 name: "tuck".to_string(),
2821 span: None,
2822 },
2823 ],
2824 source: None,
2825 allowed_lints: vec![],
2826 }],
2827 };
2828
2829 let mut checker = TypeChecker::new();
2830 assert!(checker.check_program(&program).is_ok());
2831 }
2832
2833 #[test]
2834 fn test_mixed_types_complex() {
2835 let program = Program {
2844 includes: vec![],
2845 unions: vec![],
2846 words: vec![WordDef {
2847 name: "test".to_string(),
2848 effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2849 body: vec![
2850 Statement::IntLiteral(42),
2851 Statement::WordCall {
2852 name: "int->string".to_string(),
2853 span: None,
2854 },
2855 Statement::IntLiteral(100),
2856 Statement::IntLiteral(200),
2857 Statement::WordCall {
2858 name: "i.>".to_string(),
2859 span: None,
2860 },
2861 Statement::If {
2862 then_branch: vec![Statement::WordCall {
2863 name: "io.write-line".to_string(),
2864 span: None,
2865 }],
2866 else_branch: Some(vec![Statement::WordCall {
2867 name: "io.write-line".to_string(),
2868 span: None,
2869 }]),
2870 span: None,
2871 },
2872 ],
2873 source: None,
2874 allowed_lints: vec![],
2875 }],
2876 };
2877
2878 let mut checker = TypeChecker::new();
2879 assert!(checker.check_program(&program).is_ok());
2880 }
2881
2882 #[test]
2883 fn test_string_literal() {
2884 let program = Program {
2886 includes: vec![],
2887 unions: vec![],
2888 words: vec![WordDef {
2889 name: "test".to_string(),
2890 effect: Some(Effect::new(
2891 StackType::Empty,
2892 StackType::singleton(Type::String),
2893 )),
2894 body: vec![Statement::StringLiteral("hello".to_string())],
2895 source: None,
2896 allowed_lints: vec![],
2897 }],
2898 };
2899
2900 let mut checker = TypeChecker::new();
2901 assert!(checker.check_program(&program).is_ok());
2902 }
2903
2904 #[test]
2905 fn test_bool_literal() {
2906 let program = Program {
2909 includes: vec![],
2910 unions: vec![],
2911 words: vec![WordDef {
2912 name: "test".to_string(),
2913 effect: Some(Effect::new(
2914 StackType::Empty,
2915 StackType::singleton(Type::Bool),
2916 )),
2917 body: vec![Statement::BoolLiteral(true)],
2918 source: None,
2919 allowed_lints: vec![],
2920 }],
2921 };
2922
2923 let mut checker = TypeChecker::new();
2924 assert!(checker.check_program(&program).is_ok());
2925 }
2926
2927 #[test]
2928 fn test_type_error_in_nested_conditional() {
2929 let program = Program {
2936 includes: vec![],
2937 unions: vec![],
2938 words: vec![WordDef {
2939 name: "test".to_string(),
2940 effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2941 body: vec![
2942 Statement::IntLiteral(10),
2943 Statement::IntLiteral(20),
2944 Statement::WordCall {
2945 name: "i.>".to_string(),
2946 span: None,
2947 },
2948 Statement::If {
2949 then_branch: vec![
2950 Statement::IntLiteral(42),
2951 Statement::WordCall {
2952 name: "io.write-line".to_string(),
2953 span: None,
2954 },
2955 ],
2956 else_branch: Some(vec![
2957 Statement::StringLiteral("ok".to_string()),
2958 Statement::WordCall {
2959 name: "io.write-line".to_string(),
2960 span: None,
2961 },
2962 ]),
2963 span: None,
2964 },
2965 ],
2966 source: None,
2967 allowed_lints: vec![],
2968 }],
2969 };
2970
2971 let mut checker = TypeChecker::new();
2972 let result = checker.check_program(&program);
2973 assert!(result.is_err());
2974 assert!(result.unwrap_err().contains("Type mismatch"));
2975 }
2976
2977 #[test]
2978 fn test_read_line_operation() {
2979 let program = Program {
2982 includes: vec![],
2983 unions: vec![],
2984 words: vec![WordDef {
2985 name: "test".to_string(),
2986 effect: Some(Effect::new(
2987 StackType::Empty,
2988 StackType::from_vec(vec![Type::String, Type::Bool]),
2989 )),
2990 body: vec![Statement::WordCall {
2991 name: "io.read-line".to_string(),
2992 span: None,
2993 }],
2994 source: None,
2995 allowed_lints: vec![],
2996 }],
2997 };
2998
2999 let mut checker = TypeChecker::new();
3000 assert!(checker.check_program(&program).is_ok());
3001 }
3002
3003 #[test]
3004 fn test_comparison_operations() {
3005 let program = Program {
3010 includes: vec![],
3011 unions: vec![],
3012 words: vec![WordDef {
3013 name: "test".to_string(),
3014 effect: Some(Effect::new(
3015 StackType::Empty.push(Type::Int).push(Type::Int),
3016 StackType::singleton(Type::Bool),
3017 )),
3018 body: vec![Statement::WordCall {
3019 name: "i.<=".to_string(),
3020 span: None,
3021 }],
3022 source: None,
3023 allowed_lints: vec![],
3024 }],
3025 };
3026
3027 let mut checker = TypeChecker::new();
3028 assert!(checker.check_program(&program).is_ok());
3029 }
3030
3031 #[test]
3032 fn test_recursive_word_definitions() {
3033 let program = Program {
3039 includes: vec![],
3040 unions: vec![],
3041 words: vec![
3042 WordDef {
3043 name: "is-even".to_string(),
3044 effect: Some(Effect::new(
3045 StackType::singleton(Type::Int),
3046 StackType::singleton(Type::Int),
3047 )),
3048 body: vec![
3049 Statement::WordCall {
3050 name: "dup".to_string(),
3051 span: None,
3052 },
3053 Statement::IntLiteral(0),
3054 Statement::WordCall {
3055 name: "i.=".to_string(),
3056 span: None,
3057 },
3058 Statement::If {
3059 then_branch: vec![
3060 Statement::WordCall {
3061 name: "drop".to_string(),
3062 span: None,
3063 },
3064 Statement::IntLiteral(1),
3065 ],
3066 else_branch: Some(vec![
3067 Statement::IntLiteral(1),
3068 Statement::WordCall {
3069 name: "i.subtract".to_string(),
3070 span: None,
3071 },
3072 Statement::WordCall {
3073 name: "is-odd".to_string(),
3074 span: None,
3075 },
3076 ]),
3077 span: None,
3078 },
3079 ],
3080 source: None,
3081 allowed_lints: vec![],
3082 },
3083 WordDef {
3084 name: "is-odd".to_string(),
3085 effect: Some(Effect::new(
3086 StackType::singleton(Type::Int),
3087 StackType::singleton(Type::Int),
3088 )),
3089 body: vec![
3090 Statement::WordCall {
3091 name: "dup".to_string(),
3092 span: None,
3093 },
3094 Statement::IntLiteral(0),
3095 Statement::WordCall {
3096 name: "i.=".to_string(),
3097 span: None,
3098 },
3099 Statement::If {
3100 then_branch: vec![
3101 Statement::WordCall {
3102 name: "drop".to_string(),
3103 span: None,
3104 },
3105 Statement::IntLiteral(0),
3106 ],
3107 else_branch: Some(vec![
3108 Statement::IntLiteral(1),
3109 Statement::WordCall {
3110 name: "i.subtract".to_string(),
3111 span: None,
3112 },
3113 Statement::WordCall {
3114 name: "is-even".to_string(),
3115 span: None,
3116 },
3117 ]),
3118 span: None,
3119 },
3120 ],
3121 source: None,
3122 allowed_lints: vec![],
3123 },
3124 ],
3125 };
3126
3127 let mut checker = TypeChecker::new();
3128 assert!(checker.check_program(&program).is_ok());
3129 }
3130
3131 #[test]
3132 fn test_word_calling_word_with_row_polymorphism() {
3133 let program = Program {
3138 includes: vec![],
3139 unions: vec![],
3140 words: vec![
3141 WordDef {
3142 name: "apply-twice".to_string(),
3143 effect: Some(Effect::new(
3144 StackType::singleton(Type::Int),
3145 StackType::singleton(Type::Int),
3146 )),
3147 body: vec![
3148 Statement::WordCall {
3149 name: "dup".to_string(),
3150 span: None,
3151 },
3152 Statement::WordCall {
3153 name: "i.add".to_string(),
3154 span: None,
3155 },
3156 ],
3157 source: None,
3158 allowed_lints: vec![],
3159 },
3160 WordDef {
3161 name: "quad".to_string(),
3162 effect: Some(Effect::new(
3163 StackType::singleton(Type::Int),
3164 StackType::singleton(Type::Int),
3165 )),
3166 body: vec![
3167 Statement::WordCall {
3168 name: "apply-twice".to_string(),
3169 span: None,
3170 },
3171 Statement::WordCall {
3172 name: "apply-twice".to_string(),
3173 span: None,
3174 },
3175 ],
3176 source: None,
3177 allowed_lints: vec![],
3178 },
3179 ],
3180 };
3181
3182 let mut checker = TypeChecker::new();
3183 assert!(checker.check_program(&program).is_ok());
3184 }
3185
3186 #[test]
3187 fn test_deep_stack_types() {
3188 let mut stack_type = StackType::Empty;
3192 for _ in 0..10 {
3193 stack_type = stack_type.push(Type::Int);
3194 }
3195
3196 let program = Program {
3197 includes: vec![],
3198 unions: vec![],
3199 words: vec![WordDef {
3200 name: "test".to_string(),
3201 effect: Some(Effect::new(stack_type, StackType::singleton(Type::Int))),
3202 body: vec![
3203 Statement::WordCall {
3204 name: "i.add".to_string(),
3205 span: None,
3206 },
3207 Statement::WordCall {
3208 name: "i.add".to_string(),
3209 span: None,
3210 },
3211 Statement::WordCall {
3212 name: "i.add".to_string(),
3213 span: None,
3214 },
3215 Statement::WordCall {
3216 name: "i.add".to_string(),
3217 span: None,
3218 },
3219 Statement::WordCall {
3220 name: "i.add".to_string(),
3221 span: None,
3222 },
3223 Statement::WordCall {
3224 name: "i.add".to_string(),
3225 span: None,
3226 },
3227 Statement::WordCall {
3228 name: "i.add".to_string(),
3229 span: None,
3230 },
3231 Statement::WordCall {
3232 name: "i.add".to_string(),
3233 span: None,
3234 },
3235 Statement::WordCall {
3236 name: "i.add".to_string(),
3237 span: None,
3238 },
3239 ],
3240 source: None,
3241 allowed_lints: vec![],
3242 }],
3243 };
3244
3245 let mut checker = TypeChecker::new();
3246 assert!(checker.check_program(&program).is_ok());
3247 }
3248
3249 #[test]
3250 fn test_simple_quotation() {
3251 let program = Program {
3255 includes: vec![],
3256 unions: vec![],
3257 words: vec![WordDef {
3258 name: "test".to_string(),
3259 effect: Some(Effect::new(
3260 StackType::Empty,
3261 StackType::singleton(Type::Quotation(Box::new(Effect::new(
3262 StackType::RowVar("input".to_string()).push(Type::Int),
3263 StackType::RowVar("input".to_string()).push(Type::Int),
3264 )))),
3265 )),
3266 body: vec![Statement::Quotation {
3267 span: None,
3268 id: 0,
3269 body: vec![
3270 Statement::IntLiteral(1),
3271 Statement::WordCall {
3272 name: "i.add".to_string(),
3273 span: None,
3274 },
3275 ],
3276 }],
3277 source: None,
3278 allowed_lints: vec![],
3279 }],
3280 };
3281
3282 let mut checker = TypeChecker::new();
3283 match checker.check_program(&program) {
3284 Ok(_) => {}
3285 Err(e) => panic!("Type check failed: {}", e),
3286 }
3287 }
3288
3289 #[test]
3290 fn test_empty_quotation() {
3291 let program = Program {
3295 includes: vec![],
3296 unions: vec![],
3297 words: vec![WordDef {
3298 name: "test".to_string(),
3299 effect: Some(Effect::new(
3300 StackType::Empty,
3301 StackType::singleton(Type::Quotation(Box::new(Effect::new(
3302 StackType::RowVar("input".to_string()),
3303 StackType::RowVar("input".to_string()),
3304 )))),
3305 )),
3306 body: vec![Statement::Quotation {
3307 span: None,
3308 id: 1,
3309 body: vec![],
3310 }],
3311 source: None,
3312 allowed_lints: vec![],
3313 }],
3314 };
3315
3316 let mut checker = TypeChecker::new();
3317 assert!(checker.check_program(&program).is_ok());
3318 }
3319
3320 #[test]
3321 fn test_nested_quotation() {
3322 let inner_quot_type = Type::Quotation(Box::new(Effect::new(
3326 StackType::RowVar("input".to_string()).push(Type::Int),
3327 StackType::RowVar("input".to_string()).push(Type::Int),
3328 )));
3329
3330 let outer_quot_type = Type::Quotation(Box::new(Effect::new(
3331 StackType::RowVar("input".to_string()),
3332 StackType::RowVar("input".to_string()).push(inner_quot_type.clone()),
3333 )));
3334
3335 let program = Program {
3336 includes: vec![],
3337 unions: vec![],
3338 words: vec![WordDef {
3339 name: "test".to_string(),
3340 effect: Some(Effect::new(
3341 StackType::Empty,
3342 StackType::singleton(outer_quot_type),
3343 )),
3344 body: vec![Statement::Quotation {
3345 span: None,
3346 id: 2,
3347 body: vec![Statement::Quotation {
3348 span: None,
3349 id: 3,
3350 body: vec![
3351 Statement::IntLiteral(1),
3352 Statement::WordCall {
3353 name: "i.add".to_string(),
3354 span: None,
3355 },
3356 ],
3357 }],
3358 }],
3359 source: None,
3360 allowed_lints: vec![],
3361 }],
3362 };
3363
3364 let mut checker = TypeChecker::new();
3365 assert!(checker.check_program(&program).is_ok());
3366 }
3367
3368 #[test]
3369 fn test_invalid_field_type_error() {
3370 use crate::ast::{UnionDef, UnionField, UnionVariant};
3371
3372 let program = Program {
3373 includes: vec![],
3374 unions: vec![UnionDef {
3375 name: "Message".to_string(),
3376 variants: vec![UnionVariant {
3377 name: "Get".to_string(),
3378 fields: vec![UnionField {
3379 name: "chan".to_string(),
3380 type_name: "InvalidType".to_string(),
3381 }],
3382 source: None,
3383 }],
3384 source: None,
3385 }],
3386 words: vec![],
3387 };
3388
3389 let mut checker = TypeChecker::new();
3390 let result = checker.check_program(&program);
3391 assert!(result.is_err());
3392 let err = result.unwrap_err();
3393 assert!(err.contains("Unknown type 'InvalidType'"));
3394 assert!(err.contains("chan"));
3395 assert!(err.contains("Get"));
3396 assert!(err.contains("Message"));
3397 }
3398
3399 #[test]
3400 fn test_roll_inside_conditional_with_concrete_stack() {
3401 let program = Program {
3409 includes: vec![],
3410 unions: vec![],
3411 words: vec![WordDef {
3412 name: "test".to_string(),
3413 effect: Some(Effect::new(
3414 StackType::Empty
3415 .push(Type::Int)
3416 .push(Type::Int)
3417 .push(Type::Int)
3418 .push(Type::Int),
3419 StackType::Empty
3420 .push(Type::Int)
3421 .push(Type::Int)
3422 .push(Type::Int)
3423 .push(Type::Int),
3424 )),
3425 body: vec![
3426 Statement::WordCall {
3427 name: "dup".to_string(),
3428 span: None,
3429 },
3430 Statement::IntLiteral(0),
3431 Statement::WordCall {
3432 name: "i.>".to_string(),
3433 span: None,
3434 },
3435 Statement::If {
3436 then_branch: vec![
3437 Statement::IntLiteral(3),
3438 Statement::WordCall {
3439 name: "roll".to_string(),
3440 span: None,
3441 },
3442 ],
3443 else_branch: Some(vec![
3444 Statement::WordCall {
3445 name: "rot".to_string(),
3446 span: None,
3447 },
3448 Statement::WordCall {
3449 name: "rot".to_string(),
3450 span: None,
3451 },
3452 ]),
3453 span: None,
3454 },
3455 ],
3456 source: None,
3457 allowed_lints: vec![],
3458 }],
3459 };
3460
3461 let mut checker = TypeChecker::new();
3462 match checker.check_program(&program) {
3464 Ok(_) => {}
3465 Err(e) => panic!("Type check failed: {}", e),
3466 }
3467 }
3468
3469 #[test]
3470 fn test_roll_inside_match_arm_with_concrete_stack() {
3471 use crate::ast::{MatchArm, Pattern, UnionDef, UnionVariant};
3474
3475 let union_def = UnionDef {
3477 name: "Result".to_string(),
3478 variants: vec![
3479 UnionVariant {
3480 name: "Ok".to_string(),
3481 fields: vec![],
3482 source: None,
3483 },
3484 UnionVariant {
3485 name: "Err".to_string(),
3486 fields: vec![],
3487 source: None,
3488 },
3489 ],
3490 source: None,
3491 };
3492
3493 let program = Program {
3499 includes: vec![],
3500 unions: vec![union_def],
3501 words: vec![WordDef {
3502 name: "test".to_string(),
3503 effect: Some(Effect::new(
3504 StackType::Empty
3505 .push(Type::Int)
3506 .push(Type::Int)
3507 .push(Type::Int)
3508 .push(Type::Int)
3509 .push(Type::Union("Result".to_string())),
3510 StackType::Empty
3511 .push(Type::Int)
3512 .push(Type::Int)
3513 .push(Type::Int)
3514 .push(Type::Int),
3515 )),
3516 body: vec![Statement::Match {
3517 arms: vec![
3518 MatchArm {
3519 pattern: Pattern::Variant("Ok".to_string()),
3520 body: vec![
3521 Statement::IntLiteral(3),
3522 Statement::WordCall {
3523 name: "roll".to_string(),
3524 span: None,
3525 },
3526 ],
3527 span: None,
3528 },
3529 MatchArm {
3530 pattern: Pattern::Variant("Err".to_string()),
3531 body: vec![
3532 Statement::WordCall {
3533 name: "rot".to_string(),
3534 span: None,
3535 },
3536 Statement::WordCall {
3537 name: "rot".to_string(),
3538 span: None,
3539 },
3540 ],
3541 span: None,
3542 },
3543 ],
3544 span: None,
3545 }],
3546 source: None,
3547 allowed_lints: vec![],
3548 }],
3549 };
3550
3551 let mut checker = TypeChecker::new();
3552 match checker.check_program(&program) {
3553 Ok(_) => {}
3554 Err(e) => panic!("Type check failed: {}", e),
3555 }
3556 }
3557
3558 #[test]
3559 fn test_roll_with_row_polymorphic_input() {
3560 let program = Program {
3564 includes: vec![],
3565 unions: vec![],
3566 words: vec![WordDef {
3567 name: "test".to_string(),
3568 effect: Some(Effect::new(
3569 StackType::Empty
3570 .push(Type::Var("T".to_string()))
3571 .push(Type::Var("U".to_string()))
3572 .push(Type::Var("V".to_string()))
3573 .push(Type::Var("W".to_string())),
3574 StackType::Empty
3575 .push(Type::Var("U".to_string()))
3576 .push(Type::Var("V".to_string()))
3577 .push(Type::Var("W".to_string()))
3578 .push(Type::Var("T".to_string())),
3579 )),
3580 body: vec![
3581 Statement::IntLiteral(3),
3582 Statement::WordCall {
3583 name: "roll".to_string(),
3584 span: None,
3585 },
3586 ],
3587 source: None,
3588 allowed_lints: vec![],
3589 }],
3590 };
3591
3592 let mut checker = TypeChecker::new();
3593 let result = checker.check_program(&program);
3594 assert!(result.is_ok(), "roll test failed: {:?}", result.err());
3595 }
3596
3597 #[test]
3598 fn test_pick_with_row_polymorphic_input() {
3599 let program = Program {
3603 includes: vec![],
3604 unions: vec![],
3605 words: vec![WordDef {
3606 name: "test".to_string(),
3607 effect: Some(Effect::new(
3608 StackType::Empty
3609 .push(Type::Var("T".to_string()))
3610 .push(Type::Var("U".to_string()))
3611 .push(Type::Var("V".to_string())),
3612 StackType::Empty
3613 .push(Type::Var("T".to_string()))
3614 .push(Type::Var("U".to_string()))
3615 .push(Type::Var("V".to_string()))
3616 .push(Type::Var("T".to_string())),
3617 )),
3618 body: vec![
3619 Statement::IntLiteral(2),
3620 Statement::WordCall {
3621 name: "pick".to_string(),
3622 span: None,
3623 },
3624 ],
3625 source: None,
3626 allowed_lints: vec![],
3627 }],
3628 };
3629
3630 let mut checker = TypeChecker::new();
3631 assert!(checker.check_program(&program).is_ok());
3632 }
3633
3634 #[test]
3635 fn test_valid_union_reference_in_field() {
3636 use crate::ast::{UnionDef, UnionField, UnionVariant};
3637
3638 let program = Program {
3639 includes: vec![],
3640 unions: vec![
3641 UnionDef {
3642 name: "Inner".to_string(),
3643 variants: vec![UnionVariant {
3644 name: "Val".to_string(),
3645 fields: vec![UnionField {
3646 name: "x".to_string(),
3647 type_name: "Int".to_string(),
3648 }],
3649 source: None,
3650 }],
3651 source: None,
3652 },
3653 UnionDef {
3654 name: "Outer".to_string(),
3655 variants: vec![UnionVariant {
3656 name: "Wrap".to_string(),
3657 fields: vec![UnionField {
3658 name: "inner".to_string(),
3659 type_name: "Inner".to_string(), }],
3661 source: None,
3662 }],
3663 source: None,
3664 },
3665 ],
3666 words: vec![],
3667 };
3668
3669 let mut checker = TypeChecker::new();
3670 assert!(
3671 checker.check_program(&program).is_ok(),
3672 "Union reference in field should be valid"
3673 );
3674 }
3675
3676 #[test]
3677 fn test_divergent_recursive_tail_call() {
3678 let program = Program {
3700 includes: vec![],
3701 unions: vec![],
3702 words: vec![WordDef {
3703 name: "store-loop".to_string(),
3704 effect: Some(Effect::new(
3705 StackType::singleton(Type::Channel), StackType::Empty,
3707 )),
3708 body: vec![
3709 Statement::WordCall {
3711 name: "dup".to_string(),
3712 span: None,
3713 },
3714 Statement::WordCall {
3716 name: "chan.receive".to_string(),
3717 span: None,
3718 },
3719 Statement::WordCall {
3721 name: "not".to_string(),
3722 span: None,
3723 },
3724 Statement::If {
3726 then_branch: vec![
3727 Statement::WordCall {
3729 name: "drop".to_string(),
3730 span: None,
3731 },
3732 Statement::WordCall {
3734 name: "store-loop".to_string(), span: None,
3736 },
3737 ],
3738 else_branch: None, span: None,
3740 },
3741 Statement::WordCall {
3743 name: "drop".to_string(),
3744 span: None,
3745 },
3746 Statement::WordCall {
3747 name: "drop".to_string(),
3748 span: None,
3749 },
3750 ],
3751 source: None,
3752 allowed_lints: vec![],
3753 }],
3754 };
3755
3756 let mut checker = TypeChecker::new();
3757 let result = checker.check_program(&program);
3758 assert!(
3759 result.is_ok(),
3760 "Divergent recursive tail call should be accepted: {:?}",
3761 result.err()
3762 );
3763 }
3764
3765 #[test]
3766 fn test_divergent_else_branch() {
3767 let program = Program {
3780 includes: vec![],
3781 unions: vec![],
3782 words: vec![WordDef {
3783 name: "process-loop".to_string(),
3784 effect: Some(Effect::new(
3785 StackType::singleton(Type::Channel), StackType::Empty,
3787 )),
3788 body: vec![
3789 Statement::WordCall {
3790 name: "dup".to_string(),
3791 span: None,
3792 },
3793 Statement::WordCall {
3794 name: "chan.receive".to_string(),
3795 span: None,
3796 },
3797 Statement::If {
3798 then_branch: vec![
3799 Statement::WordCall {
3801 name: "drop".to_string(),
3802 span: None,
3803 },
3804 Statement::WordCall {
3805 name: "drop".to_string(),
3806 span: None,
3807 },
3808 ],
3809 else_branch: Some(vec![
3810 Statement::WordCall {
3812 name: "drop".to_string(),
3813 span: None,
3814 },
3815 Statement::WordCall {
3816 name: "process-loop".to_string(), span: None,
3818 },
3819 ]),
3820 span: None,
3821 },
3822 ],
3823 source: None,
3824 allowed_lints: vec![],
3825 }],
3826 };
3827
3828 let mut checker = TypeChecker::new();
3829 let result = checker.check_program(&program);
3830 assert!(
3831 result.is_ok(),
3832 "Divergent else branch should be accepted: {:?}",
3833 result.err()
3834 );
3835 }
3836
3837 #[test]
3838 fn test_non_tail_call_recursion_not_divergent() {
3839 let program = Program {
3856 includes: vec![],
3857 unions: vec![],
3858 words: vec![WordDef {
3859 name: "bad-loop".to_string(),
3860 effect: Some(Effect::new(
3861 StackType::singleton(Type::Int),
3862 StackType::singleton(Type::Int),
3863 )),
3864 body: vec![
3865 Statement::WordCall {
3866 name: "dup".to_string(),
3867 span: None,
3868 },
3869 Statement::IntLiteral(0),
3870 Statement::WordCall {
3871 name: "i.>".to_string(),
3872 span: None,
3873 },
3874 Statement::If {
3875 then_branch: vec![
3876 Statement::IntLiteral(1),
3877 Statement::WordCall {
3878 name: "i.subtract".to_string(),
3879 span: None,
3880 },
3881 Statement::WordCall {
3882 name: "bad-loop".to_string(), span: None,
3884 },
3885 Statement::IntLiteral(1),
3886 Statement::WordCall {
3887 name: "i.add".to_string(), span: None,
3889 },
3890 ],
3891 else_branch: None,
3892 span: None,
3893 },
3894 ],
3895 source: None,
3896 allowed_lints: vec![],
3897 }],
3898 };
3899
3900 let mut checker = TypeChecker::new();
3901 let result = checker.check_program(&program);
3906 assert!(
3907 result.is_ok(),
3908 "Non-tail recursion should type check normally: {:?}",
3909 result.err()
3910 );
3911 }
3912
3913 #[test]
3914 fn test_call_yield_quotation_error() {
3915 let program = Program {
3919 includes: vec![],
3920 unions: vec![],
3921 words: vec![WordDef {
3922 name: "bad".to_string(),
3923 effect: Some(Effect::new(
3924 StackType::singleton(Type::Var("Ctx".to_string())),
3925 StackType::singleton(Type::Var("Ctx".to_string())),
3926 )),
3927 body: vec![
3928 Statement::IntLiteral(42),
3930 Statement::Quotation {
3931 span: None,
3932 id: 0,
3933 body: vec![Statement::WordCall {
3934 name: "yield".to_string(),
3935 span: None,
3936 }],
3937 },
3938 Statement::WordCall {
3939 name: "call".to_string(),
3940 span: None,
3941 },
3942 ],
3943 source: None,
3944 allowed_lints: vec![],
3945 }],
3946 };
3947
3948 let mut checker = TypeChecker::new();
3949 let result = checker.check_program(&program);
3950 assert!(
3951 result.is_err(),
3952 "Calling yield quotation directly should fail"
3953 );
3954 let err = result.unwrap_err();
3955 assert!(
3956 err.contains("Yield") || err.contains("strand.weave"),
3957 "Error should mention Yield or strand.weave: {}",
3958 err
3959 );
3960 }
3961
3962 #[test]
3963 fn test_strand_weave_yield_quotation_ok() {
3964 let program = Program {
3967 includes: vec![],
3968 unions: vec![],
3969 words: vec![WordDef {
3970 name: "good".to_string(),
3971 effect: Some(Effect::new(
3972 StackType::Empty,
3973 StackType::Empty
3974 .push(Type::Int)
3975 .push(Type::Var("Handle".to_string())),
3976 )),
3977 body: vec![
3978 Statement::IntLiteral(42),
3979 Statement::Quotation {
3980 span: None,
3981 id: 0,
3982 body: vec![Statement::WordCall {
3983 name: "yield".to_string(),
3984 span: None,
3985 }],
3986 },
3987 Statement::WordCall {
3988 name: "strand.weave".to_string(),
3989 span: None,
3990 },
3991 ],
3992 source: None,
3993 allowed_lints: vec![],
3994 }],
3995 };
3996
3997 let mut checker = TypeChecker::new();
3998 let result = checker.check_program(&program);
3999 assert!(
4000 result.is_ok(),
4001 "strand.weave on yield quotation should pass: {:?}",
4002 result.err()
4003 );
4004 }
4005
4006 #[test]
4007 fn test_call_pure_quotation_ok() {
4008 let program = Program {
4011 includes: vec![],
4012 unions: vec![],
4013 words: vec![WordDef {
4014 name: "ok".to_string(),
4015 effect: Some(Effect::new(
4016 StackType::singleton(Type::Int),
4017 StackType::singleton(Type::Int),
4018 )),
4019 body: vec![
4020 Statement::Quotation {
4021 span: None,
4022 id: 0,
4023 body: vec![
4024 Statement::IntLiteral(1),
4025 Statement::WordCall {
4026 name: "i.add".to_string(),
4027 span: None,
4028 },
4029 ],
4030 },
4031 Statement::WordCall {
4032 name: "call".to_string(),
4033 span: None,
4034 },
4035 ],
4036 source: None,
4037 allowed_lints: vec![],
4038 }],
4039 };
4040
4041 let mut checker = TypeChecker::new();
4042 let result = checker.check_program(&program);
4043 assert!(
4044 result.is_ok(),
4045 "Calling pure quotation should pass: {:?}",
4046 result.err()
4047 );
4048 }
4049
4050 #[test]
4056 fn test_pollution_extra_push() {
4057 let program = Program {
4061 includes: vec![],
4062 unions: vec![],
4063 words: vec![WordDef {
4064 name: "test".to_string(),
4065 effect: Some(Effect::new(
4066 StackType::singleton(Type::Int),
4067 StackType::singleton(Type::Int),
4068 )),
4069 body: vec![Statement::IntLiteral(42)],
4070 source: None,
4071 allowed_lints: vec![],
4072 }],
4073 };
4074
4075 let mut checker = TypeChecker::new();
4076 let result = checker.check_program(&program);
4077 assert!(
4078 result.is_err(),
4079 "Should reject: declares ( Int -- Int ) but leaves 2 values on stack"
4080 );
4081 }
4082
4083 #[test]
4084 fn test_pollution_extra_dup() {
4085 let program = Program {
4088 includes: vec![],
4089 unions: vec![],
4090 words: vec![WordDef {
4091 name: "test".to_string(),
4092 effect: Some(Effect::new(
4093 StackType::singleton(Type::Int),
4094 StackType::singleton(Type::Int),
4095 )),
4096 body: vec![Statement::WordCall {
4097 name: "dup".to_string(),
4098 span: None,
4099 }],
4100 source: None,
4101 allowed_lints: vec![],
4102 }],
4103 };
4104
4105 let mut checker = TypeChecker::new();
4106 let result = checker.check_program(&program);
4107 assert!(
4108 result.is_err(),
4109 "Should reject: declares ( Int -- Int ) but dup produces 2 values"
4110 );
4111 }
4112
4113 #[test]
4114 fn test_pollution_consumes_extra() {
4115 let program = Program {
4118 includes: vec![],
4119 unions: vec![],
4120 words: vec![WordDef {
4121 name: "test".to_string(),
4122 effect: Some(Effect::new(
4123 StackType::singleton(Type::Int),
4124 StackType::singleton(Type::Int),
4125 )),
4126 body: vec![
4127 Statement::WordCall {
4128 name: "drop".to_string(),
4129 span: None,
4130 },
4131 Statement::WordCall {
4132 name: "drop".to_string(),
4133 span: None,
4134 },
4135 Statement::IntLiteral(42),
4136 ],
4137 source: None,
4138 allowed_lints: vec![],
4139 }],
4140 };
4141
4142 let mut checker = TypeChecker::new();
4143 let result = checker.check_program(&program);
4144 assert!(
4145 result.is_err(),
4146 "Should reject: declares ( Int -- Int ) but consumes 2 values"
4147 );
4148 }
4149
4150 #[test]
4151 fn test_pollution_in_then_branch() {
4152 let program = Program {
4156 includes: vec![],
4157 unions: vec![],
4158 words: vec![WordDef {
4159 name: "test".to_string(),
4160 effect: Some(Effect::new(
4161 StackType::singleton(Type::Bool),
4162 StackType::singleton(Type::Int),
4163 )),
4164 body: vec![Statement::If {
4165 then_branch: vec![
4166 Statement::IntLiteral(1),
4167 Statement::IntLiteral(2), ],
4169 else_branch: Some(vec![Statement::IntLiteral(3)]),
4170 span: None,
4171 }],
4172 source: None,
4173 allowed_lints: vec![],
4174 }],
4175 };
4176
4177 let mut checker = TypeChecker::new();
4178 let result = checker.check_program(&program);
4179 assert!(
4180 result.is_err(),
4181 "Should reject: then branch pushes 2 values, else pushes 1"
4182 );
4183 }
4184
4185 #[test]
4186 fn test_pollution_in_else_branch() {
4187 let program = Program {
4191 includes: vec![],
4192 unions: vec![],
4193 words: vec![WordDef {
4194 name: "test".to_string(),
4195 effect: Some(Effect::new(
4196 StackType::singleton(Type::Bool),
4197 StackType::singleton(Type::Int),
4198 )),
4199 body: vec![Statement::If {
4200 then_branch: vec![Statement::IntLiteral(1)],
4201 else_branch: Some(vec![
4202 Statement::IntLiteral(2),
4203 Statement::IntLiteral(3), ]),
4205 span: None,
4206 }],
4207 source: None,
4208 allowed_lints: vec![],
4209 }],
4210 };
4211
4212 let mut checker = TypeChecker::new();
4213 let result = checker.check_program(&program);
4214 assert!(
4215 result.is_err(),
4216 "Should reject: then branch pushes 1 value, else pushes 2"
4217 );
4218 }
4219
4220 #[test]
4221 fn test_pollution_both_branches_extra() {
4222 let program = Program {
4226 includes: vec![],
4227 unions: vec![],
4228 words: vec![WordDef {
4229 name: "test".to_string(),
4230 effect: Some(Effect::new(
4231 StackType::singleton(Type::Bool),
4232 StackType::singleton(Type::Int),
4233 )),
4234 body: vec![Statement::If {
4235 then_branch: vec![Statement::IntLiteral(1), Statement::IntLiteral(2)],
4236 else_branch: Some(vec![Statement::IntLiteral(3), Statement::IntLiteral(4)]),
4237 span: None,
4238 }],
4239 source: None,
4240 allowed_lints: vec![],
4241 }],
4242 };
4243
4244 let mut checker = TypeChecker::new();
4245 let result = checker.check_program(&program);
4246 assert!(
4247 result.is_err(),
4248 "Should reject: both branches push 2 values, but declared output is 1"
4249 );
4250 }
4251
4252 #[test]
4253 fn test_pollution_branch_consumes_extra() {
4254 let program = Program {
4258 includes: vec![],
4259 unions: vec![],
4260 words: vec![WordDef {
4261 name: "test".to_string(),
4262 effect: Some(Effect::new(
4263 StackType::Empty.push(Type::Bool).push(Type::Int),
4264 StackType::singleton(Type::Int),
4265 )),
4266 body: vec![Statement::If {
4267 then_branch: vec![
4268 Statement::WordCall {
4269 name: "drop".to_string(),
4270 span: None,
4271 },
4272 Statement::WordCall {
4273 name: "drop".to_string(),
4274 span: None,
4275 },
4276 Statement::IntLiteral(1),
4277 ],
4278 else_branch: Some(vec![]),
4279 span: None,
4280 }],
4281 source: None,
4282 allowed_lints: vec![],
4283 }],
4284 };
4285
4286 let mut checker = TypeChecker::new();
4287 let result = checker.check_program(&program);
4288 assert!(
4289 result.is_err(),
4290 "Should reject: then branch consumes Bool (should only have Int after if)"
4291 );
4292 }
4293
4294 #[test]
4295 fn test_pollution_quotation_wrong_arity_output() {
4296 let program = Program {
4300 includes: vec![],
4301 unions: vec![],
4302 words: vec![WordDef {
4303 name: "test".to_string(),
4304 effect: Some(Effect::new(
4305 StackType::singleton(Type::Int),
4306 StackType::singleton(Type::Int),
4307 )),
4308 body: vec![
4309 Statement::Quotation {
4310 span: None,
4311 id: 0,
4312 body: vec![Statement::WordCall {
4313 name: "dup".to_string(),
4314 span: None,
4315 }],
4316 },
4317 Statement::WordCall {
4318 name: "call".to_string(),
4319 span: None,
4320 },
4321 ],
4322 source: None,
4323 allowed_lints: vec![],
4324 }],
4325 };
4326
4327 let mut checker = TypeChecker::new();
4328 let result = checker.check_program(&program);
4329 assert!(
4330 result.is_err(),
4331 "Should reject: quotation [dup] produces 2 values, declared output is 1"
4332 );
4333 }
4334
4335 #[test]
4336 fn test_pollution_quotation_wrong_arity_input() {
4337 let program = Program {
4341 includes: vec![],
4342 unions: vec![],
4343 words: vec![WordDef {
4344 name: "test".to_string(),
4345 effect: Some(Effect::new(
4346 StackType::singleton(Type::Int),
4347 StackType::singleton(Type::Int),
4348 )),
4349 body: vec![
4350 Statement::Quotation {
4351 span: None,
4352 id: 0,
4353 body: vec![
4354 Statement::WordCall {
4355 name: "drop".to_string(),
4356 span: None,
4357 },
4358 Statement::WordCall {
4359 name: "drop".to_string(),
4360 span: None,
4361 },
4362 Statement::IntLiteral(42),
4363 ],
4364 },
4365 Statement::WordCall {
4366 name: "call".to_string(),
4367 span: None,
4368 },
4369 ],
4370 source: None,
4371 allowed_lints: vec![],
4372 }],
4373 };
4374
4375 let mut checker = TypeChecker::new();
4376 let result = checker.check_program(&program);
4377 assert!(
4378 result.is_err(),
4379 "Should reject: quotation [drop drop 42] consumes 2 values, only 1 available"
4380 );
4381 }
4382
4383 #[test]
4384 fn test_missing_effect_provides_helpful_error() {
4385 let program = Program {
4388 includes: vec![],
4389 unions: vec![],
4390 words: vec![WordDef {
4391 name: "myword".to_string(),
4392 effect: None, body: vec![Statement::IntLiteral(42)],
4394 source: None,
4395 allowed_lints: vec![],
4396 }],
4397 };
4398
4399 let mut checker = TypeChecker::new();
4400 let result = checker.check_program(&program);
4401 assert!(result.is_err());
4402 let err = result.unwrap_err();
4403 assert!(err.contains("myword"), "Error should mention word name");
4404 assert!(
4405 err.contains("stack effect"),
4406 "Error should mention stack effect"
4407 );
4408 }
4409
4410 #[test]
4411 fn test_valid_effect_exact_match() {
4412 let program = Program {
4415 includes: vec![],
4416 unions: vec![],
4417 words: vec![WordDef {
4418 name: "test".to_string(),
4419 effect: Some(Effect::new(
4420 StackType::Empty.push(Type::Int).push(Type::Int),
4421 StackType::singleton(Type::Int),
4422 )),
4423 body: vec![Statement::WordCall {
4424 name: "i.add".to_string(),
4425 span: None,
4426 }],
4427 source: None,
4428 allowed_lints: vec![],
4429 }],
4430 };
4431
4432 let mut checker = TypeChecker::new();
4433 let result = checker.check_program(&program);
4434 assert!(result.is_ok(), "Should accept: effect matches exactly");
4435 }
4436
4437 #[test]
4438 fn test_valid_polymorphic_passthrough() {
4439 let program = Program {
4442 includes: vec![],
4443 unions: vec![],
4444 words: vec![WordDef {
4445 name: "test".to_string(),
4446 effect: Some(Effect::new(
4447 StackType::Cons {
4448 rest: Box::new(StackType::RowVar("rest".to_string())),
4449 top: Type::Var("a".to_string()),
4450 },
4451 StackType::Cons {
4452 rest: Box::new(StackType::RowVar("rest".to_string())),
4453 top: Type::Var("a".to_string()),
4454 },
4455 )),
4456 body: vec![], source: None,
4458 allowed_lints: vec![],
4459 }],
4460 };
4461
4462 let mut checker = TypeChecker::new();
4463 let result = checker.check_program(&program);
4464 assert!(result.is_ok(), "Should accept: polymorphic identity");
4465 }
4466
4467 #[test]
4473 fn test_closure_basic_capture() {
4474 let program = Program {
4480 includes: vec![],
4481 unions: vec![],
4482 words: vec![WordDef {
4483 name: "make-adder".to_string(),
4484 effect: Some(Effect::new(
4485 StackType::singleton(Type::Int),
4486 StackType::singleton(Type::Closure {
4487 effect: Box::new(Effect::new(
4488 StackType::RowVar("r".to_string()).push(Type::Int),
4489 StackType::RowVar("r".to_string()).push(Type::Int),
4490 )),
4491 captures: vec![Type::Int], }),
4493 )),
4494 body: vec![Statement::Quotation {
4495 span: None,
4496 id: 0,
4497 body: vec![Statement::WordCall {
4498 name: "i.add".to_string(),
4499 span: None,
4500 }],
4501 }],
4502 source: None,
4503 allowed_lints: vec![],
4504 }],
4505 };
4506
4507 let mut checker = TypeChecker::new();
4508 let result = checker.check_program(&program);
4509 assert!(
4510 result.is_ok(),
4511 "Basic closure capture should work: {:?}",
4512 result.err()
4513 );
4514 }
4515
4516 #[test]
4517 fn test_closure_nested_two_levels() {
4518 let program = Program {
4523 includes: vec![],
4524 unions: vec![],
4525 words: vec![WordDef {
4526 name: "outer".to_string(),
4527 effect: Some(Effect::new(
4528 StackType::Empty,
4529 StackType::singleton(Type::Quotation(Box::new(Effect::new(
4530 StackType::RowVar("r".to_string()),
4531 StackType::RowVar("r".to_string()).push(Type::Quotation(Box::new(
4532 Effect::new(
4533 StackType::RowVar("s".to_string()).push(Type::Int),
4534 StackType::RowVar("s".to_string()).push(Type::Int),
4535 ),
4536 ))),
4537 )))),
4538 )),
4539 body: vec![Statement::Quotation {
4540 span: None,
4541 id: 0,
4542 body: vec![Statement::Quotation {
4543 span: None,
4544 id: 1,
4545 body: vec![
4546 Statement::IntLiteral(1),
4547 Statement::WordCall {
4548 name: "i.add".to_string(),
4549 span: None,
4550 },
4551 ],
4552 }],
4553 }],
4554 source: None,
4555 allowed_lints: vec![],
4556 }],
4557 };
4558
4559 let mut checker = TypeChecker::new();
4560 let result = checker.check_program(&program);
4561 assert!(
4562 result.is_ok(),
4563 "Two-level nested quotations should work: {:?}",
4564 result.err()
4565 );
4566 }
4567
4568 #[test]
4569 fn test_closure_nested_three_levels() {
4570 let inner_effect = Effect::new(
4574 StackType::RowVar("a".to_string()).push(Type::Int),
4575 StackType::RowVar("a".to_string()).push(Type::Int),
4576 );
4577 let middle_effect = Effect::new(
4578 StackType::RowVar("b".to_string()),
4579 StackType::RowVar("b".to_string()).push(Type::Quotation(Box::new(inner_effect))),
4580 );
4581 let outer_effect = Effect::new(
4582 StackType::RowVar("c".to_string()),
4583 StackType::RowVar("c".to_string()).push(Type::Quotation(Box::new(middle_effect))),
4584 );
4585
4586 let program = Program {
4587 includes: vec![],
4588 unions: vec![],
4589 words: vec![WordDef {
4590 name: "deep".to_string(),
4591 effect: Some(Effect::new(
4592 StackType::Empty,
4593 StackType::singleton(Type::Quotation(Box::new(outer_effect))),
4594 )),
4595 body: vec![Statement::Quotation {
4596 span: None,
4597 id: 0,
4598 body: vec![Statement::Quotation {
4599 span: None,
4600 id: 1,
4601 body: vec![Statement::Quotation {
4602 span: None,
4603 id: 2,
4604 body: vec![
4605 Statement::IntLiteral(1),
4606 Statement::WordCall {
4607 name: "i.add".to_string(),
4608 span: None,
4609 },
4610 ],
4611 }],
4612 }],
4613 }],
4614 source: None,
4615 allowed_lints: vec![],
4616 }],
4617 };
4618
4619 let mut checker = TypeChecker::new();
4620 let result = checker.check_program(&program);
4621 assert!(
4622 result.is_ok(),
4623 "Three-level nested quotations should work: {:?}",
4624 result.err()
4625 );
4626 }
4627
4628 #[test]
4629 fn test_closure_use_after_creation() {
4630 let adder_type = Type::Closure {
4636 effect: Box::new(Effect::new(
4637 StackType::RowVar("r".to_string()).push(Type::Int),
4638 StackType::RowVar("r".to_string()).push(Type::Int),
4639 )),
4640 captures: vec![Type::Int],
4641 };
4642
4643 let program = Program {
4644 includes: vec![],
4645 unions: vec![],
4646 words: vec![
4647 WordDef {
4648 name: "make-adder".to_string(),
4649 effect: Some(Effect::new(
4650 StackType::singleton(Type::Int),
4651 StackType::singleton(adder_type.clone()),
4652 )),
4653 body: vec![Statement::Quotation {
4654 span: None,
4655 id: 0,
4656 body: vec![Statement::WordCall {
4657 name: "i.add".to_string(),
4658 span: None,
4659 }],
4660 }],
4661 source: None,
4662 allowed_lints: vec![],
4663 },
4664 WordDef {
4665 name: "use-adder".to_string(),
4666 effect: Some(Effect::new(
4667 StackType::Empty,
4668 StackType::singleton(Type::Int),
4669 )),
4670 body: vec![
4671 Statement::IntLiteral(5),
4672 Statement::WordCall {
4673 name: "make-adder".to_string(),
4674 span: None,
4675 },
4676 Statement::IntLiteral(10),
4677 Statement::WordCall {
4678 name: "swap".to_string(),
4679 span: None,
4680 },
4681 Statement::WordCall {
4682 name: "call".to_string(),
4683 span: None,
4684 },
4685 ],
4686 source: None,
4687 allowed_lints: vec![],
4688 },
4689 ],
4690 };
4691
4692 let mut checker = TypeChecker::new();
4693 let result = checker.check_program(&program);
4694 assert!(
4695 result.is_ok(),
4696 "Closure usage after creation should work: {:?}",
4697 result.err()
4698 );
4699 }
4700
4701 #[test]
4702 fn test_closure_wrong_call_type() {
4703 let adder_type = Type::Closure {
4707 effect: Box::new(Effect::new(
4708 StackType::RowVar("r".to_string()).push(Type::Int),
4709 StackType::RowVar("r".to_string()).push(Type::Int),
4710 )),
4711 captures: vec![Type::Int],
4712 };
4713
4714 let program = Program {
4715 includes: vec![],
4716 unions: vec![],
4717 words: vec![
4718 WordDef {
4719 name: "make-adder".to_string(),
4720 effect: Some(Effect::new(
4721 StackType::singleton(Type::Int),
4722 StackType::singleton(adder_type.clone()),
4723 )),
4724 body: vec![Statement::Quotation {
4725 span: None,
4726 id: 0,
4727 body: vec![Statement::WordCall {
4728 name: "i.add".to_string(),
4729 span: None,
4730 }],
4731 }],
4732 source: None,
4733 allowed_lints: vec![],
4734 },
4735 WordDef {
4736 name: "bad-use".to_string(),
4737 effect: Some(Effect::new(
4738 StackType::Empty,
4739 StackType::singleton(Type::Int),
4740 )),
4741 body: vec![
4742 Statement::IntLiteral(5),
4743 Statement::WordCall {
4744 name: "make-adder".to_string(),
4745 span: None,
4746 },
4747 Statement::StringLiteral("hello".to_string()), Statement::WordCall {
4749 name: "swap".to_string(),
4750 span: None,
4751 },
4752 Statement::WordCall {
4753 name: "call".to_string(),
4754 span: None,
4755 },
4756 ],
4757 source: None,
4758 allowed_lints: vec![],
4759 },
4760 ],
4761 };
4762
4763 let mut checker = TypeChecker::new();
4764 let result = checker.check_program(&program);
4765 assert!(
4766 result.is_err(),
4767 "Calling Int closure with String should fail"
4768 );
4769 }
4770
4771 #[test]
4772 fn test_closure_multiple_captures() {
4773 let program = Program {
4780 includes: vec![],
4781 unions: vec![],
4782 words: vec![WordDef {
4783 name: "make-between".to_string(),
4784 effect: Some(Effect::new(
4785 StackType::Empty.push(Type::Int).push(Type::Int),
4786 StackType::singleton(Type::Quotation(Box::new(Effect::new(
4787 StackType::RowVar("r".to_string()).push(Type::Int),
4788 StackType::RowVar("r".to_string()).push(Type::Bool),
4789 )))),
4790 )),
4791 body: vec![Statement::Quotation {
4792 span: None,
4793 id: 0,
4794 body: vec![
4795 Statement::WordCall {
4797 name: "i.>=".to_string(),
4798 span: None,
4799 },
4800 ],
4802 }],
4803 source: None,
4804 allowed_lints: vec![],
4805 }],
4806 };
4807
4808 let mut checker = TypeChecker::new();
4809 let result = checker.check_program(&program);
4810 assert!(
4814 result.is_ok() || result.is_err(),
4815 "Multiple captures should be handled (pass or fail gracefully)"
4816 );
4817 }
4818
4819 #[test]
4820 fn test_quotation_type_preserved_through_word() {
4821 let quot_type = Type::Quotation(Box::new(Effect::new(
4824 StackType::RowVar("r".to_string()).push(Type::Int),
4825 StackType::RowVar("r".to_string()).push(Type::Int),
4826 )));
4827
4828 let program = Program {
4829 includes: vec![],
4830 unions: vec![],
4831 words: vec![WordDef {
4832 name: "identity-quot".to_string(),
4833 effect: Some(Effect::new(
4834 StackType::singleton(quot_type.clone()),
4835 StackType::singleton(quot_type.clone()),
4836 )),
4837 body: vec![], source: None,
4839 allowed_lints: vec![],
4840 }],
4841 };
4842
4843 let mut checker = TypeChecker::new();
4844 let result = checker.check_program(&program);
4845 assert!(
4846 result.is_ok(),
4847 "Quotation type should be preserved through identity word: {:?}",
4848 result.err()
4849 );
4850 }
4851
4852 #[test]
4853 fn test_closure_captures_value_for_inner_quotation() {
4854 let closure_effect = Effect::new(
4860 StackType::RowVar("r".to_string()).push(Type::Int),
4861 StackType::RowVar("r".to_string()).push(Type::Int),
4862 );
4863
4864 let program = Program {
4865 includes: vec![],
4866 unions: vec![],
4867 words: vec![WordDef {
4868 name: "make-inner-adder".to_string(),
4869 effect: Some(Effect::new(
4870 StackType::singleton(Type::Int),
4871 StackType::singleton(Type::Closure {
4872 effect: Box::new(closure_effect),
4873 captures: vec![Type::Int],
4874 }),
4875 )),
4876 body: vec![Statement::Quotation {
4877 span: None,
4878 id: 0,
4879 body: vec![
4880 Statement::WordCall {
4882 name: "i.add".to_string(),
4883 span: None,
4884 },
4885 ],
4886 }],
4887 source: None,
4888 allowed_lints: vec![],
4889 }],
4890 };
4891
4892 let mut checker = TypeChecker::new();
4893 let result = checker.check_program(&program);
4894 assert!(
4895 result.is_ok(),
4896 "Closure with capture for inner work should pass: {:?}",
4897 result.err()
4898 );
4899 }
4900
4901 #[test]
4902 fn test_union_type_mismatch_should_fail() {
4903 use crate::ast::{UnionDef, UnionField, UnionVariant};
4906
4907 let mut program = Program {
4908 includes: vec![],
4909 unions: vec![
4910 UnionDef {
4911 name: "UnionA".to_string(),
4912 variants: vec![UnionVariant {
4913 name: "AVal".to_string(),
4914 fields: vec![UnionField {
4915 name: "x".to_string(),
4916 type_name: "Int".to_string(),
4917 }],
4918 source: None,
4919 }],
4920 source: None,
4921 },
4922 UnionDef {
4923 name: "UnionB".to_string(),
4924 variants: vec![UnionVariant {
4925 name: "BVal".to_string(),
4926 fields: vec![UnionField {
4927 name: "y".to_string(),
4928 type_name: "Int".to_string(),
4929 }],
4930 source: None,
4931 }],
4932 source: None,
4933 },
4934 ],
4935 words: vec![
4936 WordDef {
4938 name: "takes-a".to_string(),
4939 effect: Some(Effect::new(
4940 StackType::RowVar("rest".to_string())
4941 .push(Type::Union("UnionA".to_string())),
4942 StackType::RowVar("rest".to_string()),
4943 )),
4944 body: vec![Statement::WordCall {
4945 name: "drop".to_string(),
4946 span: None,
4947 }],
4948 source: None,
4949 allowed_lints: vec![],
4950 },
4951 WordDef {
4954 name: "main".to_string(),
4955 effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
4956 body: vec![
4957 Statement::IntLiteral(99),
4958 Statement::WordCall {
4959 name: "Make-BVal".to_string(),
4960 span: None,
4961 },
4962 Statement::WordCall {
4963 name: "takes-a".to_string(),
4964 span: None,
4965 },
4966 ],
4967 source: None,
4968 allowed_lints: vec![],
4969 },
4970 ],
4971 };
4972
4973 program.generate_constructors().unwrap();
4975
4976 let mut checker = TypeChecker::new();
4977 let result = checker.check_program(&program);
4978
4979 assert!(
4981 result.is_err(),
4982 "Passing UnionB to function expecting UnionA should fail, but got: {:?}",
4983 result
4984 );
4985 let err = result.unwrap_err();
4986 assert!(
4987 err.contains("Union") || err.contains("mismatch"),
4988 "Error should mention union type mismatch, got: {}",
4989 err
4990 );
4991 }
4992
4993 fn make_word_call(name: &str) -> Statement {
4998 Statement::WordCall {
4999 name: name.to_string(),
5000 span: None,
5001 }
5002 }
5003
5004 #[test]
5005 fn test_aux_basic_round_trip() {
5006 let program = Program {
5008 includes: vec![],
5009 unions: vec![],
5010 words: vec![WordDef {
5011 name: "test".to_string(),
5012 effect: Some(Effect::new(
5013 StackType::singleton(Type::Int),
5014 StackType::singleton(Type::Int),
5015 )),
5016 body: vec![make_word_call(">aux"), make_word_call("aux>")],
5017 source: None,
5018 allowed_lints: vec![],
5019 }],
5020 };
5021
5022 let mut checker = TypeChecker::new();
5023 assert!(checker.check_program(&program).is_ok());
5024 }
5025
5026 #[test]
5027 fn test_aux_preserves_type() {
5028 let program = Program {
5030 includes: vec![],
5031 unions: vec![],
5032 words: vec![WordDef {
5033 name: "test".to_string(),
5034 effect: Some(Effect::new(
5035 StackType::singleton(Type::String),
5036 StackType::singleton(Type::String),
5037 )),
5038 body: vec![make_word_call(">aux"), make_word_call("aux>")],
5039 source: None,
5040 allowed_lints: vec![],
5041 }],
5042 };
5043
5044 let mut checker = TypeChecker::new();
5045 assert!(checker.check_program(&program).is_ok());
5046 }
5047
5048 #[test]
5049 fn test_aux_unbalanced_error() {
5050 let program = Program {
5052 includes: vec![],
5053 unions: vec![],
5054 words: vec![WordDef {
5055 name: "test".to_string(),
5056 effect: Some(Effect::new(
5057 StackType::singleton(Type::Int),
5058 StackType::Empty,
5059 )),
5060 body: vec![make_word_call(">aux")],
5061 source: None,
5062 allowed_lints: vec![],
5063 }],
5064 };
5065
5066 let mut checker = TypeChecker::new();
5067 let result = checker.check_program(&program);
5068 assert!(result.is_err());
5069 let err = result.unwrap_err();
5070 assert!(
5071 err.contains("aux stack is not empty"),
5072 "Expected aux stack balance error, got: {}",
5073 err
5074 );
5075 }
5076
5077 #[test]
5078 fn test_aux_pop_empty_error() {
5079 let program = Program {
5081 includes: vec![],
5082 unions: vec![],
5083 words: vec![WordDef {
5084 name: "test".to_string(),
5085 effect: Some(Effect::new(
5086 StackType::Empty,
5087 StackType::singleton(Type::Int),
5088 )),
5089 body: vec![make_word_call("aux>")],
5090 source: None,
5091 allowed_lints: vec![],
5092 }],
5093 };
5094
5095 let mut checker = TypeChecker::new();
5096 let result = checker.check_program(&program);
5097 assert!(result.is_err());
5098 let err = result.unwrap_err();
5099 assert!(
5100 err.contains("aux stack is empty"),
5101 "Expected aux empty error, got: {}",
5102 err
5103 );
5104 }
5105
5106 #[test]
5107 fn test_aux_multiple_values() {
5108 let program = Program {
5112 includes: vec![],
5113 unions: vec![],
5114 words: vec![WordDef {
5115 name: "test".to_string(),
5116 effect: Some(Effect::new(
5117 StackType::Empty.push(Type::Int).push(Type::String),
5118 StackType::Empty.push(Type::Int).push(Type::String),
5119 )),
5120 body: vec![
5121 make_word_call(">aux"),
5122 make_word_call(">aux"),
5123 make_word_call("aux>"),
5124 make_word_call("aux>"),
5125 ],
5126 source: None,
5127 allowed_lints: vec![],
5128 }],
5129 };
5130
5131 let mut checker = TypeChecker::new();
5132 assert!(checker.check_program(&program).is_ok());
5133 }
5134
5135 #[test]
5136 fn test_aux_max_depths_tracked() {
5137 let program = Program {
5139 includes: vec![],
5140 unions: vec![],
5141 words: vec![WordDef {
5142 name: "test".to_string(),
5143 effect: Some(Effect::new(
5144 StackType::singleton(Type::Int),
5145 StackType::singleton(Type::Int),
5146 )),
5147 body: vec![make_word_call(">aux"), make_word_call("aux>")],
5148 source: None,
5149 allowed_lints: vec![],
5150 }],
5151 };
5152
5153 let mut checker = TypeChecker::new();
5154 checker.check_program(&program).unwrap();
5155 let depths = checker.take_aux_max_depths();
5156 assert_eq!(depths.get("test"), Some(&1));
5157 }
5158
5159 #[test]
5160 fn test_aux_in_match_balanced() {
5161 use crate::ast::{MatchArm, Pattern, UnionDef, UnionVariant};
5163
5164 let union_def = UnionDef {
5165 name: "Choice".to_string(),
5166 variants: vec![
5167 UnionVariant {
5168 name: "Left".to_string(),
5169 fields: vec![],
5170 source: None,
5171 },
5172 UnionVariant {
5173 name: "Right".to_string(),
5174 fields: vec![],
5175 source: None,
5176 },
5177 ],
5178 source: None,
5179 };
5180
5181 let program = Program {
5184 includes: vec![],
5185 unions: vec![union_def],
5186 words: vec![WordDef {
5187 name: "test".to_string(),
5188 effect: Some(Effect::new(
5189 StackType::Empty
5190 .push(Type::Int)
5191 .push(Type::Union("Choice".to_string())),
5192 StackType::singleton(Type::Int),
5193 )),
5194 body: vec![
5195 make_word_call("swap"),
5196 make_word_call(">aux"),
5197 Statement::Match {
5198 arms: vec![
5199 MatchArm {
5200 pattern: Pattern::Variant("Left".to_string()),
5201 body: vec![make_word_call("aux>")],
5202 span: None,
5203 },
5204 MatchArm {
5205 pattern: Pattern::Variant("Right".to_string()),
5206 body: vec![make_word_call("aux>")],
5207 span: None,
5208 },
5209 ],
5210 span: None,
5211 },
5212 ],
5213 source: None,
5214 allowed_lints: vec![],
5215 }],
5216 };
5217
5218 let mut checker = TypeChecker::new();
5219 assert!(checker.check_program(&program).is_ok());
5220 }
5221
5222 #[test]
5223 fn test_aux_in_match_unbalanced_error() {
5224 use crate::ast::{MatchArm, Pattern, UnionDef, UnionVariant};
5226
5227 let union_def = UnionDef {
5228 name: "Choice".to_string(),
5229 variants: vec![
5230 UnionVariant {
5231 name: "Left".to_string(),
5232 fields: vec![],
5233 source: None,
5234 },
5235 UnionVariant {
5236 name: "Right".to_string(),
5237 fields: vec![],
5238 source: None,
5239 },
5240 ],
5241 source: None,
5242 };
5243
5244 let program = Program {
5247 includes: vec![],
5248 unions: vec![union_def],
5249 words: vec![WordDef {
5250 name: "test".to_string(),
5251 effect: Some(Effect::new(
5252 StackType::Empty
5253 .push(Type::Int)
5254 .push(Type::Union("Choice".to_string())),
5255 StackType::singleton(Type::Int),
5256 )),
5257 body: vec![
5258 make_word_call("swap"),
5259 make_word_call(">aux"),
5260 Statement::Match {
5261 arms: vec![
5262 MatchArm {
5263 pattern: Pattern::Variant("Left".to_string()),
5264 body: vec![make_word_call("aux>")],
5265 span: None,
5266 },
5267 MatchArm {
5268 pattern: Pattern::Variant("Right".to_string()),
5269 body: vec![],
5270 span: None,
5271 },
5272 ],
5273 span: None,
5274 },
5275 ],
5276 source: None,
5277 allowed_lints: vec![],
5278 }],
5279 };
5280
5281 let mut checker = TypeChecker::new();
5282 let result = checker.check_program(&program);
5283 assert!(result.is_err());
5284 let err = result.unwrap_err();
5285 assert!(
5286 err.contains("aux stack"),
5287 "Expected aux stack mismatch error, got: {}",
5288 err
5289 );
5290 }
5291
5292 #[test]
5293 fn test_aux_in_quotation_rejected() {
5294 let program = Program {
5296 includes: vec![],
5297 unions: vec![],
5298 words: vec![WordDef {
5299 name: "test".to_string(),
5300 effect: Some(Effect::new(
5301 StackType::singleton(Type::Int),
5302 StackType::singleton(Type::Quotation(Box::new(Effect::new(
5303 StackType::singleton(Type::Int),
5304 StackType::singleton(Type::Int),
5305 )))),
5306 )),
5307 body: vec![Statement::Quotation {
5308 span: None,
5309 id: 0,
5310 body: vec![make_word_call(">aux"), make_word_call("aux>")],
5311 }],
5312 source: None,
5313 allowed_lints: vec![],
5314 }],
5315 };
5316
5317 let mut checker = TypeChecker::new();
5318 let result = checker.check_program(&program);
5319 assert!(result.is_err());
5320 let err = result.unwrap_err();
5321 assert!(
5322 err.contains("not supported inside quotations"),
5323 "Expected quotation rejection error, got: {}",
5324 err
5325 );
5326 }
5327
5328 #[test]
5333 fn test_dip_basic() {
5334 let program = Program {
5337 includes: vec![],
5338 unions: vec![],
5339 words: vec![WordDef {
5340 name: "test".to_string(),
5341 effect: Some(Effect::new(
5342 StackType::Empty.push(Type::Int).push(Type::Int),
5343 StackType::Empty.push(Type::Int).push(Type::Int),
5344 )),
5345 body: vec![
5346 Statement::Quotation {
5347 id: 0,
5348 body: vec![
5349 Statement::IntLiteral(1),
5350 Statement::WordCall {
5351 name: "i.+".to_string(),
5352 span: None,
5353 },
5354 ],
5355 span: None,
5356 },
5357 Statement::WordCall {
5358 name: "dip".to_string(),
5359 span: None,
5360 },
5361 ],
5362 source: None,
5363 allowed_lints: vec![],
5364 }],
5365 };
5366
5367 let mut checker = TypeChecker::new();
5368 assert!(checker.check_program(&program).is_ok());
5369 }
5370
5371 #[test]
5372 fn test_dip_type_mismatch() {
5373 let program = Program {
5376 includes: vec![],
5377 unions: vec![],
5378 words: vec![WordDef {
5379 name: "test".to_string(),
5380 effect: Some(Effect::new(
5381 StackType::Empty.push(Type::String).push(Type::Int),
5382 StackType::Empty.push(Type::Int).push(Type::Int),
5383 )),
5384 body: vec![
5385 Statement::Quotation {
5386 id: 0,
5387 body: vec![
5388 Statement::IntLiteral(1),
5389 Statement::WordCall {
5390 name: "i.+".to_string(),
5391 span: None,
5392 },
5393 ],
5394 span: None,
5395 },
5396 Statement::WordCall {
5397 name: "dip".to_string(),
5398 span: None,
5399 },
5400 ],
5401 source: None,
5402 allowed_lints: vec![],
5403 }],
5404 };
5405
5406 let mut checker = TypeChecker::new();
5407 assert!(checker.check_program(&program).is_err());
5408 }
5409
5410 #[test]
5411 fn test_keep_basic() {
5412 let program = Program {
5415 includes: vec![],
5416 unions: vec![],
5417 words: vec![WordDef {
5418 name: "test".to_string(),
5419 effect: Some(Effect::new(
5420 StackType::singleton(Type::Int),
5421 StackType::Empty.push(Type::Int).push(Type::Int),
5422 )),
5423 body: vec![
5424 Statement::Quotation {
5425 id: 0,
5426 body: vec![
5427 Statement::WordCall {
5428 name: "dup".to_string(),
5429 span: None,
5430 },
5431 Statement::WordCall {
5432 name: "i.*".to_string(),
5433 span: None,
5434 },
5435 ],
5436 span: None,
5437 },
5438 Statement::WordCall {
5439 name: "keep".to_string(),
5440 span: None,
5441 },
5442 ],
5443 source: None,
5444 allowed_lints: vec![],
5445 }],
5446 };
5447
5448 let mut checker = TypeChecker::new();
5449 assert!(checker.check_program(&program).is_ok());
5450 }
5451
5452 #[test]
5453 fn test_bi_basic() {
5454 let program = Program {
5457 includes: vec![],
5458 unions: vec![],
5459 words: vec![WordDef {
5460 name: "test".to_string(),
5461 effect: Some(Effect::new(
5462 StackType::singleton(Type::Int),
5463 StackType::Empty.push(Type::Int).push(Type::Int),
5464 )),
5465 body: vec![
5466 Statement::Quotation {
5467 id: 0,
5468 body: vec![
5469 Statement::IntLiteral(2),
5470 Statement::WordCall {
5471 name: "i.*".to_string(),
5472 span: None,
5473 },
5474 ],
5475 span: None,
5476 },
5477 Statement::Quotation {
5478 id: 1,
5479 body: vec![
5480 Statement::IntLiteral(3),
5481 Statement::WordCall {
5482 name: "i.*".to_string(),
5483 span: None,
5484 },
5485 ],
5486 span: None,
5487 },
5488 Statement::WordCall {
5489 name: "bi".to_string(),
5490 span: None,
5491 },
5492 ],
5493 source: None,
5494 allowed_lints: vec![],
5495 }],
5496 };
5497
5498 let mut checker = TypeChecker::new();
5499 assert!(checker.check_program(&program).is_ok());
5500 }
5501
5502 #[test]
5503 fn test_keep_type_mismatch() {
5504 let program = Program {
5507 includes: vec![],
5508 unions: vec![],
5509 words: vec![WordDef {
5510 name: "test".to_string(),
5511 effect: Some(Effect::new(
5512 StackType::singleton(Type::String),
5513 StackType::Empty.push(Type::Int).push(Type::String),
5514 )),
5515 body: vec![
5516 Statement::Quotation {
5517 id: 0,
5518 body: vec![
5519 Statement::IntLiteral(1),
5520 Statement::WordCall {
5521 name: "i.+".to_string(),
5522 span: None,
5523 },
5524 ],
5525 span: None,
5526 },
5527 Statement::WordCall {
5528 name: "keep".to_string(),
5529 span: None,
5530 },
5531 ],
5532 source: None,
5533 allowed_lints: vec![],
5534 }],
5535 };
5536
5537 let mut checker = TypeChecker::new();
5538 assert!(checker.check_program(&program).is_err());
5539 }
5540
5541 #[test]
5542 fn test_bi_type_mismatch() {
5543 let program = Program {
5546 includes: vec![],
5547 unions: vec![],
5548 words: vec![WordDef {
5549 name: "test".to_string(),
5550 effect: Some(Effect::new(
5551 StackType::singleton(Type::String),
5552 StackType::Empty.push(Type::Int).push(Type::Int),
5553 )),
5554 body: vec![
5555 Statement::Quotation {
5556 id: 0,
5557 body: vec![Statement::WordCall {
5558 name: "string.length".to_string(),
5559 span: None,
5560 }],
5561 span: None,
5562 },
5563 Statement::Quotation {
5564 id: 1,
5565 body: vec![
5566 Statement::IntLiteral(1),
5567 Statement::WordCall {
5568 name: "i.+".to_string(),
5569 span: None,
5570 },
5571 ],
5572 span: None,
5573 },
5574 Statement::WordCall {
5575 name: "bi".to_string(),
5576 span: None,
5577 },
5578 ],
5579 source: None,
5580 allowed_lints: vec![],
5581 }],
5582 };
5583
5584 let mut checker = TypeChecker::new();
5585 assert!(checker.check_program(&program).is_err());
5586 }
5587
5588 #[test]
5589 fn test_dip_underflow() {
5590 let program = Program {
5593 includes: vec![],
5594 unions: vec![],
5595 words: vec![WordDef {
5596 name: "test".to_string(),
5597 effect: Some(Effect::new(
5598 StackType::Empty,
5599 StackType::singleton(Type::Int),
5600 )),
5601 body: vec![
5602 Statement::Quotation {
5603 id: 0,
5604 body: vec![Statement::IntLiteral(1)],
5605 span: None,
5606 },
5607 Statement::WordCall {
5608 name: "dip".to_string(),
5609 span: None,
5610 },
5611 ],
5612 source: None,
5613 allowed_lints: vec![],
5614 }],
5615 };
5616
5617 let mut checker = TypeChecker::new();
5618 let result = checker.check_program(&program);
5619 assert!(result.is_err());
5620 let err = result.unwrap_err();
5621 assert!(
5622 err.contains("stack underflow"),
5623 "Expected underflow error, got: {}",
5624 err
5625 );
5626 }
5627
5628 #[test]
5629 fn test_dip_preserves_type() {
5630 let program = Program {
5633 includes: vec![],
5634 unions: vec![],
5635 words: vec![WordDef {
5636 name: "test".to_string(),
5637 effect: Some(Effect::new(
5638 StackType::Empty.push(Type::Int).push(Type::String),
5639 StackType::Empty.push(Type::Int).push(Type::String),
5640 )),
5641 body: vec![
5642 Statement::Quotation {
5643 id: 0,
5644 body: vec![
5645 Statement::IntLiteral(1),
5646 Statement::WordCall {
5647 name: "i.+".to_string(),
5648 span: None,
5649 },
5650 ],
5651 span: None,
5652 },
5653 Statement::WordCall {
5654 name: "dip".to_string(),
5655 span: None,
5656 },
5657 ],
5658 source: None,
5659 allowed_lints: vec![],
5660 }],
5661 };
5662
5663 let mut checker = TypeChecker::new();
5664 assert!(checker.check_program(&program).is_ok());
5665 }
5666
5667 #[test]
5668 fn test_keep_underflow() {
5669 let program = Program {
5672 includes: vec![],
5673 unions: vec![],
5674 words: vec![WordDef {
5675 name: "test".to_string(),
5676 effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
5677 body: vec![
5678 Statement::Quotation {
5679 id: 0,
5680 body: vec![Statement::WordCall {
5681 name: "drop".to_string(),
5682 span: None,
5683 }],
5684 span: None,
5685 },
5686 Statement::WordCall {
5687 name: "keep".to_string(),
5688 span: None,
5689 },
5690 ],
5691 source: None,
5692 allowed_lints: vec![],
5693 }],
5694 };
5695
5696 let mut checker = TypeChecker::new();
5697 let result = checker.check_program(&program);
5698 assert!(result.is_err());
5699 let err = result.unwrap_err();
5700 assert!(
5701 err.contains("stack underflow") || err.contains("underflow"),
5702 "Expected underflow error, got: {}",
5703 err
5704 );
5705 }
5706
5707 #[test]
5708 fn test_bi_underflow() {
5709 let program = Program {
5712 includes: vec![],
5713 unions: vec![],
5714 words: vec![WordDef {
5715 name: "test".to_string(),
5716 effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
5717 body: vec![
5718 Statement::Quotation {
5719 id: 0,
5720 body: vec![Statement::IntLiteral(1)],
5721 span: None,
5722 },
5723 Statement::Quotation {
5724 id: 1,
5725 body: vec![Statement::IntLiteral(2)],
5726 span: None,
5727 },
5728 Statement::WordCall {
5729 name: "bi".to_string(),
5730 span: None,
5731 },
5732 ],
5733 source: None,
5734 allowed_lints: vec![],
5735 }],
5736 };
5737
5738 let mut checker = TypeChecker::new();
5739 let result = checker.check_program(&program);
5740 assert!(result.is_err());
5741 let err = result.unwrap_err();
5742 assert!(
5743 err.contains("stack underflow") || err.contains("underflow"),
5744 "Expected underflow error, got: {}",
5745 err
5746 );
5747 }
5748
5749 #[test]
5750 fn test_bi_polymorphic_quotations() {
5751 let program = Program {
5754 includes: vec![],
5755 unions: vec![],
5756 words: vec![WordDef {
5757 name: "test".to_string(),
5758 effect: Some(Effect::new(
5759 StackType::singleton(Type::Int),
5760 StackType::Empty.push(Type::Int).push(Type::String),
5761 )),
5762 body: vec![
5763 Statement::Quotation {
5764 id: 0,
5765 body: vec![
5766 Statement::IntLiteral(2),
5767 Statement::WordCall {
5768 name: "i.*".to_string(),
5769 span: None,
5770 },
5771 ],
5772 span: None,
5773 },
5774 Statement::Quotation {
5775 id: 1,
5776 body: vec![Statement::WordCall {
5777 name: "int->string".to_string(),
5778 span: None,
5779 }],
5780 span: None,
5781 },
5782 Statement::WordCall {
5783 name: "bi".to_string(),
5784 span: None,
5785 },
5786 ],
5787 source: None,
5788 allowed_lints: vec![],
5789 }],
5790 };
5791
5792 let mut checker = TypeChecker::new();
5793 assert!(checker.check_program(&program).is_ok());
5794 }
5795}