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
22pub struct TypeChecker {
23 env: HashMap<String, Effect>,
25 unions: HashMap<String, UnionTypeInfo>,
28 fresh_counter: std::cell::Cell<usize>,
30 quotation_types: std::cell::RefCell<HashMap<usize, Type>>,
34 expected_quotation_type: std::cell::RefCell<Option<Type>>,
37 current_word: std::cell::RefCell<Option<(String, Option<usize>)>>,
41 statement_top_types: std::cell::RefCell<HashMap<(String, usize), Type>>,
45 call_graph: Option<CallGraph>,
48 current_aux_stack: std::cell::RefCell<StackType>,
51 aux_max_depths: std::cell::RefCell<HashMap<String, usize>>,
54 in_quotation_scope: std::cell::Cell<bool>,
57 resolved_sugar: std::cell::RefCell<HashMap<(usize, usize), String>>,
61}
62
63impl TypeChecker {
64 pub fn new() -> Self {
65 TypeChecker {
66 env: HashMap::new(),
67 unions: HashMap::new(),
68 fresh_counter: std::cell::Cell::new(0),
69 quotation_types: std::cell::RefCell::new(HashMap::new()),
70 expected_quotation_type: std::cell::RefCell::new(None),
71 current_word: std::cell::RefCell::new(None),
72 statement_top_types: std::cell::RefCell::new(HashMap::new()),
73 call_graph: None,
74 current_aux_stack: std::cell::RefCell::new(StackType::Empty),
75 aux_max_depths: std::cell::RefCell::new(HashMap::new()),
76 in_quotation_scope: std::cell::Cell::new(false),
77 resolved_sugar: std::cell::RefCell::new(HashMap::new()),
78 }
79 }
80
81 pub fn set_call_graph(&mut self, call_graph: CallGraph) {
86 self.call_graph = Some(call_graph);
87 }
88
89 fn line_prefix(&self) -> String {
91 self.current_word
92 .borrow()
93 .as_ref()
94 .and_then(|(_, line)| line.map(format_line_prefix))
95 .unwrap_or_default()
96 }
97
98 pub fn get_union(&self, name: &str) -> Option<&UnionTypeInfo> {
100 self.unions.get(name)
101 }
102
103 pub fn get_unions(&self) -> &HashMap<String, UnionTypeInfo> {
105 &self.unions
106 }
107
108 fn find_variant(&self, variant_name: &str) -> Option<(&str, &VariantInfo)> {
112 for (union_name, union_info) in &self.unions {
113 for variant in &union_info.variants {
114 if variant.name == variant_name {
115 return Some((union_name.as_str(), variant));
116 }
117 }
118 }
119 None
120 }
121
122 pub fn register_external_words(&mut self, words: &[(&str, &Effect)]) {
126 for (name, effect) in words {
127 self.env.insert(name.to_string(), (*effect).clone());
128 }
129 }
130
131 pub fn register_external_unions(&mut self, union_names: &[&str]) {
137 for name in union_names {
138 self.unions.insert(
141 name.to_string(),
142 UnionTypeInfo {
143 name: name.to_string(),
144 variants: vec![],
145 },
146 );
147 }
148 }
149
150 pub fn take_quotation_types(&self) -> HashMap<usize, Type> {
156 self.quotation_types.replace(HashMap::new())
157 }
158
159 pub fn take_statement_top_types(&self) -> HashMap<(String, usize), Type> {
162 self.statement_top_types.replace(HashMap::new())
163 }
164
165 pub fn take_resolved_sugar(&self) -> HashMap<(usize, usize), String> {
168 self.resolved_sugar.replace(HashMap::new())
169 }
170
171 pub fn take_aux_max_depths(&self) -> HashMap<String, usize> {
173 self.aux_max_depths.replace(HashMap::new())
174 }
175
176 fn stack_depth(stack: &StackType) -> usize {
178 let mut depth = 0;
179 let mut current = stack;
180 while let StackType::Cons { rest, .. } = current {
181 depth += 1;
182 current = rest;
183 }
184 depth
185 }
186
187 fn get_trivially_copyable_top(stack: &StackType) -> Option<Type> {
190 match stack {
191 StackType::Cons { top, .. } => match top {
192 Type::Int | Type::Float | Type::Bool => Some(top.clone()),
193 _ => None,
194 },
195 _ => None,
196 }
197 }
198
199 fn capture_statement_type(&self, word_name: &str, stmt_index: usize, stack: &StackType) {
201 if let Some(top_type) = Self::get_trivially_copyable_top(stack) {
202 self.statement_top_types
203 .borrow_mut()
204 .insert((word_name.to_string(), stmt_index), top_type);
205 }
206 }
207
208 fn fresh_var(&self, prefix: &str) -> String {
210 let n = self.fresh_counter.get();
211 self.fresh_counter.set(n + 1);
212 format!("{}${}", prefix, n)
213 }
214
215 fn freshen_effect(&self, effect: &Effect) -> Effect {
217 let mut type_map = HashMap::new();
218 let mut row_map = HashMap::new();
219
220 let fresh_inputs = self.freshen_stack(&effect.inputs, &mut type_map, &mut row_map);
221 let fresh_outputs = self.freshen_stack(&effect.outputs, &mut type_map, &mut row_map);
222
223 let fresh_effects = effect
225 .effects
226 .iter()
227 .map(|e| self.freshen_side_effect(e, &mut type_map, &mut row_map))
228 .collect();
229
230 Effect::with_effects(fresh_inputs, fresh_outputs, fresh_effects)
231 }
232
233 fn freshen_side_effect(
234 &self,
235 effect: &SideEffect,
236 type_map: &mut HashMap<String, String>,
237 row_map: &mut HashMap<String, String>,
238 ) -> SideEffect {
239 match effect {
240 SideEffect::Yield(ty) => {
241 SideEffect::Yield(Box::new(self.freshen_type(ty, type_map, row_map)))
242 }
243 }
244 }
245
246 fn freshen_stack(
247 &self,
248 stack: &StackType,
249 type_map: &mut HashMap<String, String>,
250 row_map: &mut HashMap<String, String>,
251 ) -> StackType {
252 match stack {
253 StackType::Empty => StackType::Empty,
254 StackType::RowVar(name) => {
255 let fresh_name = row_map
256 .entry(name.clone())
257 .or_insert_with(|| self.fresh_var(name));
258 StackType::RowVar(fresh_name.clone())
259 }
260 StackType::Cons { rest, top } => {
261 let fresh_rest = self.freshen_stack(rest, type_map, row_map);
262 let fresh_top = self.freshen_type(top, type_map, row_map);
263 StackType::Cons {
264 rest: Box::new(fresh_rest),
265 top: fresh_top,
266 }
267 }
268 }
269 }
270
271 fn freshen_type(
272 &self,
273 ty: &Type,
274 type_map: &mut HashMap<String, String>,
275 row_map: &mut HashMap<String, String>,
276 ) -> Type {
277 match ty {
278 Type::Int | Type::Float | Type::Bool | Type::String | Type::Symbol | Type::Channel => {
279 ty.clone()
280 }
281 Type::Var(name) => {
282 let fresh_name = type_map
283 .entry(name.clone())
284 .or_insert_with(|| self.fresh_var(name));
285 Type::Var(fresh_name.clone())
286 }
287 Type::Quotation(effect) => {
288 let fresh_inputs = self.freshen_stack(&effect.inputs, type_map, row_map);
289 let fresh_outputs = self.freshen_stack(&effect.outputs, type_map, row_map);
290 Type::Quotation(Box::new(Effect::new(fresh_inputs, fresh_outputs)))
291 }
292 Type::Closure { effect, captures } => {
293 let fresh_inputs = self.freshen_stack(&effect.inputs, type_map, row_map);
294 let fresh_outputs = self.freshen_stack(&effect.outputs, type_map, row_map);
295 let fresh_captures = captures
296 .iter()
297 .map(|t| self.freshen_type(t, type_map, row_map))
298 .collect();
299 Type::Closure {
300 effect: Box::new(Effect::new(fresh_inputs, fresh_outputs)),
301 captures: fresh_captures,
302 }
303 }
304 Type::Union(name) => Type::Union(name.clone()),
306 }
307 }
308
309 fn parse_type_name(&self, name: &str) -> Type {
313 match name {
314 "Int" => Type::Int,
315 "Float" => Type::Float,
316 "Bool" => Type::Bool,
317 "String" => Type::String,
318 "Channel" => Type::Channel,
319 other => Type::Union(other.to_string()),
321 }
322 }
323
324 fn is_valid_type_name(&self, name: &str) -> bool {
329 matches!(name, "Int" | "Float" | "Bool" | "String" | "Channel")
330 || self.unions.contains_key(name)
331 }
332
333 fn validate_union_field_types(&self, program: &Program) -> Result<(), String> {
337 for union_def in &program.unions {
338 for variant in &union_def.variants {
339 for field in &variant.fields {
340 if !self.is_valid_type_name(&field.type_name) {
341 return Err(format!(
342 "Unknown type '{}' in field '{}' of variant '{}' in union '{}'. \
343 Valid types are: Int, Float, Bool, String, Channel, or a defined union name.",
344 field.type_name, field.name, variant.name, union_def.name
345 ));
346 }
347 }
348 }
349 }
350 Ok(())
351 }
352
353 fn validate_effect_types(&self, effect: &Effect, word_name: &str) -> Result<(), String> {
359 self.validate_stack_types(&effect.inputs, word_name)?;
360 self.validate_stack_types(&effect.outputs, word_name)?;
361 Ok(())
362 }
363
364 fn validate_stack_types(&self, stack: &StackType, word_name: &str) -> Result<(), String> {
366 match stack {
367 StackType::Empty | StackType::RowVar(_) => Ok(()),
368 StackType::Cons { rest, top } => {
369 self.validate_type(top, word_name)?;
370 self.validate_stack_types(rest, word_name)
371 }
372 }
373 }
374
375 fn validate_type(&self, ty: &Type, word_name: &str) -> Result<(), String> {
380 match ty {
381 Type::Var(_) => {
382 Ok(())
387 }
388 Type::Quotation(effect) => self.validate_effect_types(effect, word_name),
389 Type::Closure { effect, captures } => {
390 self.validate_effect_types(effect, word_name)?;
391 for cap in captures {
392 self.validate_type(cap, word_name)?;
393 }
394 Ok(())
395 }
396 Type::Int | Type::Float | Type::Bool | Type::String | Type::Symbol | Type::Channel => {
398 Ok(())
399 }
400 Type::Union(name) => {
402 if !self.unions.contains_key(name) {
403 return Err(format!(
404 "In word '{}': Unknown union type '{}' in stack effect.\n\
405 Make sure this union is defined before the word that uses it.",
406 word_name, name
407 ));
408 }
409 Ok(())
410 }
411 }
412 }
413
414 pub fn check_program(&mut self, program: &Program) -> Result<(), String> {
416 for union_def in &program.unions {
418 let variants = union_def
419 .variants
420 .iter()
421 .map(|v| VariantInfo {
422 name: v.name.clone(),
423 fields: v
424 .fields
425 .iter()
426 .map(|f| VariantFieldInfo {
427 name: f.name.clone(),
428 field_type: self.parse_type_name(&f.type_name),
429 })
430 .collect(),
431 })
432 .collect();
433
434 self.unions.insert(
435 union_def.name.clone(),
436 UnionTypeInfo {
437 name: union_def.name.clone(),
438 variants,
439 },
440 );
441 }
442
443 self.validate_union_field_types(program)?;
445
446 for word in &program.words {
449 if let Some(effect) = &word.effect {
450 self.validate_effect_types(effect, &word.name)?;
454 self.env.insert(word.name.clone(), effect.clone());
455 } else {
456 return Err(format!(
457 "Word '{}' is missing a stack effect declaration.\n\
458 All words must declare their stack effect, e.g.: : {} ( -- ) ... ;",
459 word.name, word.name
460 ));
461 }
462 }
463
464 for word in &program.words {
466 self.check_word(word)?;
467 }
468
469 Ok(())
470 }
471
472 fn check_word(&self, word: &WordDef) -> Result<(), String> {
474 let line = word.source.as_ref().map(|s| s.start_line);
476 *self.current_word.borrow_mut() = Some((word.name.clone(), line));
477
478 *self.current_aux_stack.borrow_mut() = StackType::Empty;
480
481 let declared_effect = word.effect.as_ref().expect("word must have effect");
483
484 if let Some((_rest, top_type)) = declared_effect.outputs.clone().pop()
487 && matches!(top_type, Type::Quotation(_) | Type::Closure { .. })
488 {
489 *self.expected_quotation_type.borrow_mut() = Some(top_type);
490 }
491
492 let (result_stack, _subst, inferred_effects) =
494 self.infer_statements_from(&word.body, &declared_effect.inputs, true)?;
495
496 *self.expected_quotation_type.borrow_mut() = None;
498
499 let line_info = line.map(format_line_prefix).unwrap_or_default();
501 unify_stacks(&declared_effect.outputs, &result_stack).map_err(|e| {
502 format!(
503 "{}Word '{}': declared output stack ({}) doesn't match inferred ({}): {}",
504 line_info, word.name, declared_effect.outputs, result_stack, e
505 )
506 })?;
507
508 for inferred in &inferred_effects {
512 if !self.effect_matches_any(inferred, &declared_effect.effects) {
513 return Err(format!(
514 "{}Word '{}': body produces effect '{}' but no matching effect is declared.\n\
515 Hint: Add '| Yield <type>' to the word's stack effect declaration.",
516 line_info, word.name, inferred
517 ));
518 }
519 }
520
521 for declared in &declared_effect.effects {
524 if !self.effect_matches_any(declared, &inferred_effects) {
525 return Err(format!(
526 "{}Word '{}': declares effect '{}' but body doesn't produce it.\n\
527 Hint: Remove the effect declaration or ensure the body uses yield.",
528 line_info, word.name, declared
529 ));
530 }
531 }
532
533 let aux_stack = self.current_aux_stack.borrow().clone();
535 if aux_stack != StackType::Empty {
536 return Err(format!(
537 "{}Word '{}': aux stack is not empty at word return.\n\
538 Remaining aux stack: {}\n\
539 Every >aux must be matched by a corresponding aux> before the word returns.",
540 line_info, word.name, aux_stack
541 ));
542 }
543
544 *self.current_word.borrow_mut() = None;
546
547 Ok(())
548 }
549
550 fn infer_statements_from(
557 &self,
558 statements: &[Statement],
559 start_stack: &StackType,
560 capture_stmt_types: bool,
561 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
562 let mut current_stack = start_stack.clone();
563 let mut accumulated_subst = Subst::empty();
564 let mut accumulated_effects: Vec<SideEffect> = Vec::new();
565 let mut skip_next = false;
566
567 for (i, stmt) in statements.iter().enumerate() {
568 if skip_next {
570 skip_next = false;
571 continue;
572 }
573
574 if let Statement::IntLiteral(n) = stmt
577 && let Some(Statement::WordCall {
578 name: next_word, ..
579 }) = statements.get(i + 1)
580 {
581 if next_word == "pick" {
582 let (new_stack, subst) = self.handle_literal_pick(*n, current_stack.clone())?;
583 current_stack = new_stack;
584 accumulated_subst = accumulated_subst.compose(&subst);
585 skip_next = true; continue;
587 } else if next_word == "roll" {
588 let (new_stack, subst) = self.handle_literal_roll(*n, current_stack.clone())?;
589 current_stack = new_stack;
590 accumulated_subst = accumulated_subst.compose(&subst);
591 skip_next = true; continue;
593 }
594 }
595
596 let saved_expected_type = if matches!(stmt, Statement::Quotation { .. }) {
599 let saved = self.expected_quotation_type.borrow().clone();
601
602 if let Some(Statement::WordCall {
604 name: next_word, ..
605 }) = statements.get(i + 1)
606 {
607 if let Some(next_effect) = self.lookup_word_effect(next_word) {
609 if let Some((_rest, quot_type)) = next_effect.inputs.clone().pop()
612 && matches!(quot_type, Type::Quotation(_))
613 {
614 *self.expected_quotation_type.borrow_mut() = Some(quot_type);
615 }
616 }
617 }
618 Some(saved)
619 } else {
620 None
621 };
622
623 if capture_stmt_types && let Some((word_name, _)) = self.current_word.borrow().as_ref()
627 {
628 self.capture_statement_type(word_name, i, ¤t_stack);
629 }
630
631 let (new_stack, subst, effects) = self.infer_statement(stmt, current_stack)?;
632 current_stack = new_stack;
633 accumulated_subst = accumulated_subst.compose(&subst);
634
635 for effect in effects {
637 if !accumulated_effects.contains(&effect) {
638 accumulated_effects.push(effect);
639 }
640 }
641
642 if let Some(saved) = saved_expected_type {
644 *self.expected_quotation_type.borrow_mut() = saved;
645 }
646 }
647
648 Ok((current_stack, accumulated_subst, accumulated_effects))
649 }
650
651 fn handle_literal_pick(
662 &self,
663 n: i64,
664 current_stack: StackType,
665 ) -> Result<(StackType, Subst), String> {
666 if n < 0 {
667 return Err(format!("pick: index must be non-negative, got {}", n));
668 }
669
670 let type_at_n = self.get_type_at_position(¤t_stack, n as usize, "pick")?;
672
673 Ok((current_stack.push(type_at_n), Subst::empty()))
675 }
676
677 fn handle_literal_roll(
688 &self,
689 n: i64,
690 current_stack: StackType,
691 ) -> Result<(StackType, Subst), String> {
692 if n < 0 {
693 return Err(format!("roll: index must be non-negative, got {}", n));
694 }
695
696 self.rotate_type_to_top(current_stack, n as usize)
701 }
702
703 fn get_type_at_position(&self, stack: &StackType, n: usize, op: &str) -> Result<Type, String> {
705 let mut current = stack;
706 let mut pos = 0;
707
708 loop {
709 match current {
710 StackType::Cons { rest, top } => {
711 if pos == n {
712 return Ok(top.clone());
713 }
714 pos += 1;
715 current = rest;
716 }
717 StackType::RowVar(name) => {
718 let fresh_type = Type::Var(self.fresh_var(&format!("{}_{}", op, name)));
728 return Ok(fresh_type);
729 }
730 StackType::Empty => {
731 return Err(format!(
732 "{}{}: stack underflow - position {} requested but stack has only {} concrete items",
733 self.line_prefix(),
734 op,
735 n,
736 pos
737 ));
738 }
739 }
740 }
741 }
742
743 fn rotate_type_to_top(&self, stack: StackType, n: usize) -> Result<(StackType, Subst), String> {
745 if n == 0 {
746 return Ok((stack, Subst::empty()));
748 }
749
750 let mut types_above: Vec<Type> = Vec::new();
752 let mut current = stack;
753 let mut pos = 0;
754
755 loop {
757 match current {
758 StackType::Cons { rest, top } => {
759 if pos == n {
760 let mut result = *rest;
763 for ty in types_above.into_iter().rev() {
765 result = result.push(ty);
766 }
767 result = result.push(top);
769 return Ok((result, Subst::empty()));
770 }
771 types_above.push(top);
772 pos += 1;
773 current = *rest;
774 }
775 StackType::RowVar(name) => {
776 let fresh_type = Type::Var(self.fresh_var(&format!("roll_{}", name)));
789
790 let mut result = StackType::RowVar(name.clone());
792 for ty in types_above.into_iter().rev() {
793 result = result.push(ty);
794 }
795 result = result.push(fresh_type);
796 return Ok((result, Subst::empty()));
797 }
798 StackType::Empty => {
799 return Err(format!(
800 "{}roll: stack underflow - position {} requested but stack has only {} items",
801 self.line_prefix(),
802 n,
803 pos
804 ));
805 }
806 }
807 }
808 }
809
810 fn infer_statements(&self, statements: &[Statement]) -> Result<Effect, String> {
814 let start = StackType::RowVar("input".to_string());
815 let (result, subst, effects) = self.infer_statements_from(statements, &start, false)?;
817
818 let normalized_start = subst.apply_stack(&start);
821 let normalized_result = subst.apply_stack(&result);
822
823 Ok(Effect::with_effects(
824 normalized_start,
825 normalized_result,
826 effects,
827 ))
828 }
829
830 fn infer_match(
832 &self,
833 arms: &[crate::ast::MatchArm],
834 match_span: &Option<crate::ast::Span>,
835 current_stack: StackType,
836 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
837 if arms.is_empty() {
838 return Err("match expression must have at least one arm".to_string());
839 }
840
841 let (stack_after_match, _matched_type) =
843 self.pop_type(¤t_stack, "match expression")?;
844
845 let mut arm_results: Vec<StackType> = Vec::new();
847 let mut combined_subst = Subst::empty();
848 let mut merged_effects: Vec<SideEffect> = Vec::new();
849
850 let aux_before_match = self.current_aux_stack.borrow().clone();
852 let mut aux_after_arms: Vec<StackType> = Vec::new();
853
854 for arm in arms {
855 *self.current_aux_stack.borrow_mut() = aux_before_match.clone();
857
858 let variant_name = match &arm.pattern {
860 crate::ast::Pattern::Variant(name) => name.as_str(),
861 crate::ast::Pattern::VariantWithBindings { name, .. } => name.as_str(),
862 };
863
864 let (_union_name, variant_info) = self
866 .find_variant(variant_name)
867 .ok_or_else(|| format!("Unknown variant '{}' in match pattern", variant_name))?;
868
869 let arm_stack = self.push_variant_fields(
871 &stack_after_match,
872 &arm.pattern,
873 variant_info,
874 variant_name,
875 )?;
876
877 let (arm_result, arm_subst, arm_effects) =
880 self.infer_statements_from(&arm.body, &arm_stack, false)?;
881
882 combined_subst = combined_subst.compose(&arm_subst);
883 arm_results.push(arm_result);
884 aux_after_arms.push(self.current_aux_stack.borrow().clone());
885
886 for effect in arm_effects {
888 if !merged_effects.contains(&effect) {
889 merged_effects.push(effect);
890 }
891 }
892 }
893
894 if aux_after_arms.len() > 1 {
896 let first_aux = &aux_after_arms[0];
897 for (i, arm_aux) in aux_after_arms.iter().enumerate().skip(1) {
898 if arm_aux != first_aux {
899 let match_line = match_span.as_ref().map(|s| s.line + 1).unwrap_or(0);
900 return Err(format!(
901 "at line {}: match arms have incompatible aux stack effects:\n\
902 \x20 arm 0 aux: {}\n\
903 \x20 arm {} aux: {}\n\
904 \x20 All match arms must leave the aux stack in the same state.",
905 match_line, first_aux, i, arm_aux
906 ));
907 }
908 }
909 }
910 if let Some(aux) = aux_after_arms.into_iter().next() {
912 *self.current_aux_stack.borrow_mut() = aux;
913 }
914
915 let mut final_result = arm_results[0].clone();
917 for (i, arm_result) in arm_results.iter().enumerate().skip(1) {
918 let match_line = match_span.as_ref().map(|s| s.line + 1).unwrap_or(0);
920 let arm0_line = arms[0].span.as_ref().map(|s| s.line + 1).unwrap_or(0);
921 let arm_i_line = arms[i].span.as_ref().map(|s| s.line + 1).unwrap_or(0);
922
923 let arm_subst = unify_stacks(&final_result, arm_result).map_err(|e| {
924 if match_line > 0 && arm0_line > 0 && arm_i_line > 0 {
925 format!(
926 "at line {}: match arms have incompatible stack effects:\n\
927 \x20 arm 0 (line {}) produces: {}\n\
928 \x20 arm {} (line {}) produces: {}\n\
929 \x20 All match arms must produce the same stack shape.\n\
930 \x20 Error: {}",
931 match_line, arm0_line, final_result, i, arm_i_line, arm_result, e
932 )
933 } else {
934 format!(
935 "match arms have incompatible stack effects:\n\
936 \x20 arm 0 produces: {}\n\
937 \x20 arm {} produces: {}\n\
938 \x20 All match arms must produce the same stack shape.\n\
939 \x20 Error: {}",
940 final_result, i, arm_result, e
941 )
942 }
943 })?;
944 combined_subst = combined_subst.compose(&arm_subst);
945 final_result = arm_subst.apply_stack(&final_result);
946 }
947
948 Ok((final_result, combined_subst, merged_effects))
949 }
950
951 fn push_variant_fields(
953 &self,
954 stack: &StackType,
955 pattern: &crate::ast::Pattern,
956 variant_info: &VariantInfo,
957 variant_name: &str,
958 ) -> Result<StackType, String> {
959 let mut arm_stack = stack.clone();
960 match pattern {
961 crate::ast::Pattern::Variant(_) => {
962 for field in &variant_info.fields {
964 arm_stack = arm_stack.push(field.field_type.clone());
965 }
966 }
967 crate::ast::Pattern::VariantWithBindings { bindings, .. } => {
968 for binding in bindings {
970 let field = variant_info
971 .fields
972 .iter()
973 .find(|f| &f.name == binding)
974 .ok_or_else(|| {
975 let available: Vec<_> = variant_info
976 .fields
977 .iter()
978 .map(|f| f.name.as_str())
979 .collect();
980 format!(
981 "Unknown field '{}' in pattern for variant '{}'.\n\
982 Available fields: {}",
983 binding,
984 variant_name,
985 available.join(", ")
986 )
987 })?;
988 arm_stack = arm_stack.push(field.field_type.clone());
989 }
990 }
991 }
992 Ok(arm_stack)
993 }
994
995 fn is_divergent_branch(&self, statements: &[Statement]) -> bool {
1016 let Some((current_word_name, _)) = self.current_word.borrow().as_ref().cloned() else {
1017 return false;
1018 };
1019 let Some(Statement::WordCall { name, .. }) = statements.last() else {
1020 return false;
1021 };
1022
1023 if name == ¤t_word_name {
1025 return true;
1026 }
1027
1028 if let Some(ref graph) = self.call_graph
1030 && graph.are_mutually_recursive(¤t_word_name, name)
1031 {
1032 return true;
1033 }
1034
1035 false
1036 }
1037
1038 fn infer_if(
1040 &self,
1041 then_branch: &[Statement],
1042 else_branch: &Option<Vec<Statement>>,
1043 if_span: &Option<crate::ast::Span>,
1044 current_stack: StackType,
1045 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1046 let (stack_after_cond, cond_type) = self.pop_type(¤t_stack, "if condition")?;
1048
1049 let cond_subst = unify_stacks(
1051 &StackType::singleton(Type::Bool),
1052 &StackType::singleton(cond_type),
1053 )
1054 .map_err(|e| format!("if condition must be Bool: {}", e))?;
1055
1056 let stack_after_cond = cond_subst.apply_stack(&stack_after_cond);
1057
1058 let then_diverges = self.is_divergent_branch(then_branch);
1060 let else_diverges = else_branch
1061 .as_ref()
1062 .map(|stmts| self.is_divergent_branch(stmts))
1063 .unwrap_or(false);
1064
1065 let aux_before_branches = self.current_aux_stack.borrow().clone();
1067
1068 let (then_result, then_subst, then_effects) =
1071 self.infer_statements_from(then_branch, &stack_after_cond, false)?;
1072 let aux_after_then = self.current_aux_stack.borrow().clone();
1073
1074 *self.current_aux_stack.borrow_mut() = aux_before_branches.clone();
1076
1077 let (else_result, else_subst, else_effects) = if let Some(else_stmts) = else_branch {
1079 self.infer_statements_from(else_stmts, &stack_after_cond, false)?
1080 } else {
1081 (stack_after_cond.clone(), Subst::empty(), vec![])
1082 };
1083 let aux_after_else = self.current_aux_stack.borrow().clone();
1084
1085 if !then_diverges && !else_diverges && aux_after_then != aux_after_else {
1088 let if_line = if_span.as_ref().map(|s| s.line + 1).unwrap_or(0);
1089 return Err(format!(
1090 "at line {}: if/else branches have incompatible aux stack effects:\n\
1091 \x20 then branch aux: {}\n\
1092 \x20 else branch aux: {}\n\
1093 \x20 Both branches must leave the aux stack in the same state.",
1094 if_line, aux_after_then, aux_after_else
1095 ));
1096 }
1097
1098 if then_diverges && !else_diverges {
1100 *self.current_aux_stack.borrow_mut() = aux_after_else;
1101 } else {
1102 *self.current_aux_stack.borrow_mut() = aux_after_then;
1103 }
1104
1105 let mut merged_effects = then_effects;
1107 for effect in else_effects {
1108 if !merged_effects.contains(&effect) {
1109 merged_effects.push(effect);
1110 }
1111 }
1112
1113 let (result, branch_subst) = if then_diverges && !else_diverges {
1119 (else_result, Subst::empty())
1121 } else if else_diverges && !then_diverges {
1122 (then_result, Subst::empty())
1124 } else {
1125 let if_line = if_span.as_ref().map(|s| s.line + 1).unwrap_or(0);
1127 let branch_subst = unify_stacks(&then_result, &else_result).map_err(|e| {
1128 if if_line > 0 {
1129 format!(
1130 "at line {}: if/else branches have incompatible stack effects:\n\
1131 \x20 then branch produces: {}\n\
1132 \x20 else branch produces: {}\n\
1133 \x20 Both branches of an if/else must produce the same stack shape.\n\
1134 \x20 Hint: Make sure both branches push/pop the same number of values.\n\
1135 \x20 Error: {}",
1136 if_line, then_result, else_result, e
1137 )
1138 } else {
1139 format!(
1140 "if/else branches have incompatible stack effects:\n\
1141 \x20 then branch produces: {}\n\
1142 \x20 else branch produces: {}\n\
1143 \x20 Both branches of an if/else must produce the same stack shape.\n\
1144 \x20 Hint: Make sure both branches push/pop the same number of values.\n\
1145 \x20 Error: {}",
1146 then_result, else_result, e
1147 )
1148 }
1149 })?;
1150 (branch_subst.apply_stack(&then_result), branch_subst)
1151 };
1152
1153 let total_subst = cond_subst
1155 .compose(&then_subst)
1156 .compose(&else_subst)
1157 .compose(&branch_subst);
1158 Ok((result, total_subst, merged_effects))
1159 }
1160
1161 fn infer_quotation(
1164 &self,
1165 id: usize,
1166 body: &[Statement],
1167 current_stack: StackType,
1168 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1169 let expected_for_this_quotation = self.expected_quotation_type.borrow().clone();
1172 *self.expected_quotation_type.borrow_mut() = None;
1173
1174 let saved_aux = self.current_aux_stack.borrow().clone();
1179 *self.current_aux_stack.borrow_mut() = StackType::Empty;
1180 let saved_in_quotation = self.in_quotation_scope.get();
1181 self.in_quotation_scope.set(true);
1182
1183 let body_effect = self.infer_statements(body)?;
1185
1186 let quot_aux = self.current_aux_stack.borrow().clone();
1188 if quot_aux != StackType::Empty {
1189 return Err(format!(
1190 "Quotation has unbalanced aux stack.\n\
1191 Remaining aux stack: {}\n\
1192 Every >aux must be matched by a corresponding aux> within the quotation.",
1193 quot_aux
1194 ));
1195 }
1196
1197 *self.current_aux_stack.borrow_mut() = saved_aux;
1199 self.in_quotation_scope.set(saved_in_quotation);
1200
1201 *self.expected_quotation_type.borrow_mut() = expected_for_this_quotation;
1203
1204 let quot_type = self.analyze_captures(&body_effect, ¤t_stack)?;
1206
1207 self.quotation_types
1209 .borrow_mut()
1210 .insert(id, quot_type.clone());
1211
1212 let result_stack = match "_type {
1214 Type::Quotation(_) => {
1215 current_stack.push(quot_type)
1217 }
1218 Type::Closure { captures, .. } => {
1219 let mut stack = current_stack.clone();
1221 for _ in 0..captures.len() {
1222 let (new_stack, _value) = self.pop_type(&stack, "closure capture")?;
1223 stack = new_stack;
1224 }
1225 stack.push(quot_type)
1226 }
1227 _ => unreachable!("analyze_captures only returns Quotation or Closure"),
1228 };
1229
1230 Ok((result_stack, Subst::empty(), vec![]))
1234 }
1235
1236 fn infer_word_call(
1238 &self,
1239 name: &str,
1240 span: &Option<crate::ast::Span>,
1241 current_stack: StackType,
1242 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1243 let is_sugar = matches!(
1246 name,
1247 "+" | "-" | "*" | "/" | "%" | "=" | "<" | ">" | "<=" | ">=" | "<>"
1248 );
1249 if is_sugar {
1250 if let Some(resolved) = self.resolve_arithmetic_sugar(name, ¤t_stack) {
1251 if let Some(s) = span {
1253 self.resolved_sugar
1254 .borrow_mut()
1255 .insert((s.line, s.column), resolved.clone());
1256 }
1257 return self.infer_word_call(&resolved, span, current_stack);
1259 }
1260 let line_prefix = self.line_prefix();
1262 let (top_desc, second_desc) = {
1263 let top = current_stack.clone().pop().map(|(_, t)| format!("{}", t));
1264 let second = current_stack
1265 .clone()
1266 .pop()
1267 .and_then(|(r, _)| r.pop().map(|(_, t)| format!("{}", t)));
1268 (
1269 top.unwrap_or_else(|| "empty".to_string()),
1270 second.unwrap_or_else(|| "empty".to_string()),
1271 )
1272 };
1273 let (type_options, suggestion) = match name {
1274 "+" => (
1275 "Int+Int, Float+Float, or String+String",
1276 "Use `i.+`, `f.+`, or `string.concat`.",
1277 ),
1278 "=" => (
1279 "Int+Int, Float+Float, or String+String (equality)",
1280 "Use `i.=`, `f.=`, or `string.equal?`.",
1281 ),
1282 "%" => (
1283 "Int+Int only — float modulo is not supported",
1284 "Use `i.%` for integer modulo.",
1285 ),
1286 _ => (
1287 "Int+Int or Float+Float",
1288 "Use the `i.` or `f.` prefixed variant.",
1289 ),
1290 };
1291 return Err(format!(
1292 "{}`{}` requires matching types ({}), got ({}, {}). {}",
1293 line_prefix, name, type_options, second_desc, top_desc, suggestion,
1294 ));
1295 }
1296
1297 if name == ">aux" {
1299 return self.infer_to_aux(span, current_stack);
1300 }
1301 if name == "aux>" {
1302 return self.infer_from_aux(span, current_stack);
1303 }
1304
1305 if name == "call" {
1308 return self.infer_call(span, current_stack);
1309 }
1310
1311 if name == "dip" {
1313 return self.infer_dip(span, current_stack);
1314 }
1315 if name == "keep" {
1316 return self.infer_keep(span, current_stack);
1317 }
1318 if name == "bi" {
1319 return self.infer_bi(span, current_stack);
1320 }
1321
1322 let effect = self
1324 .lookup_word_effect(name)
1325 .ok_or_else(|| format!("Unknown word: '{}'", name))?;
1326
1327 let fresh_effect = self.freshen_effect(&effect);
1329
1330 let adjusted_stack = if name == "strand.spawn" {
1332 self.adjust_stack_for_spawn(current_stack, &fresh_effect)?
1333 } else {
1334 current_stack
1335 };
1336
1337 let (result_stack, subst) = self.apply_effect(&fresh_effect, adjusted_stack, name, span)?;
1339
1340 let propagated_effects = fresh_effect.effects.clone();
1344
1345 Ok((result_stack, subst, propagated_effects))
1346 }
1347
1348 fn infer_to_aux(
1350 &self,
1351 _span: &Option<crate::ast::Span>,
1352 current_stack: StackType,
1353 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1354 if self.in_quotation_scope.get() {
1355 return Err(">aux is not supported inside quotations.\n\
1356 Quotations are compiled as separate functions without aux stack slots.\n\
1357 Extract the quotation body into a named word if you need aux."
1358 .to_string());
1359 }
1360 let (rest, top_type) = self.pop_type(¤t_stack, ">aux")?;
1361
1362 let mut aux = self.current_aux_stack.borrow_mut();
1364 *aux = aux.clone().push(top_type);
1365
1366 let depth = Self::stack_depth(&aux);
1368 if let Some((word_name, _)) = self.current_word.borrow().as_ref() {
1369 let mut depths = self.aux_max_depths.borrow_mut();
1370 let entry = depths.entry(word_name.clone()).or_insert(0);
1371 if depth > *entry {
1372 *entry = depth;
1373 }
1374 }
1375
1376 Ok((rest, Subst::empty(), vec![]))
1377 }
1378
1379 fn infer_from_aux(
1381 &self,
1382 _span: &Option<crate::ast::Span>,
1383 current_stack: StackType,
1384 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1385 if self.in_quotation_scope.get() {
1386 return Err("aux> is not supported inside quotations.\n\
1387 Quotations are compiled as separate functions without aux stack slots.\n\
1388 Extract the quotation body into a named word if you need aux."
1389 .to_string());
1390 }
1391 let mut aux = self.current_aux_stack.borrow_mut();
1392 match aux.clone().pop() {
1393 Some((rest, top_type)) => {
1394 *aux = rest;
1395 Ok((current_stack.push(top_type), Subst::empty(), vec![]))
1396 }
1397 None => {
1398 let line_info = self.line_prefix();
1399 Err(format!(
1400 "{}aux>: aux stack is empty. Every aux> must be paired with a preceding >aux.",
1401 line_info
1402 ))
1403 }
1404 }
1405 }
1406
1407 fn infer_call(
1413 &self,
1414 span: &Option<crate::ast::Span>,
1415 current_stack: StackType,
1416 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1417 let line_prefix = self.line_prefix();
1419 let (remaining_stack, quot_type) = current_stack.clone().pop().ok_or_else(|| {
1420 format!(
1421 "{}call: stack underflow - expected quotation on stack",
1422 line_prefix
1423 )
1424 })?;
1425
1426 let quot_effect = match "_type {
1428 Type::Quotation(effect) => (**effect).clone(),
1429 Type::Closure { effect, .. } => (**effect).clone(),
1430 Type::Var(_) => {
1431 let effect = self
1434 .lookup_word_effect("call")
1435 .ok_or_else(|| "Unknown word: 'call'".to_string())?;
1436 let fresh_effect = self.freshen_effect(&effect);
1437 let (result_stack, subst) =
1438 self.apply_effect(&fresh_effect, current_stack, "call", span)?;
1439 return Ok((result_stack, subst, vec![]));
1440 }
1441 _ => {
1442 return Err(format!(
1443 "call: expected quotation or closure on stack, got {}",
1444 quot_type
1445 ));
1446 }
1447 };
1448
1449 if quot_effect.has_yield() {
1451 return Err("Cannot call quotation with Yield effect directly.\n\
1452 Quotations that yield values must be wrapped with `strand.weave`.\n\
1453 Example: `[ yielding-code ] strand.weave` instead of `[ yielding-code ] call`"
1454 .to_string());
1455 }
1456
1457 let fresh_effect = self.freshen_effect("_effect);
1459
1460 let (result_stack, subst) =
1462 self.apply_effect(&fresh_effect, remaining_stack, "call", span)?;
1463
1464 let propagated_effects = fresh_effect.effects.clone();
1466
1467 Ok((result_stack, subst, propagated_effects))
1468 }
1469
1470 fn resolve_arithmetic_sugar(&self, name: &str, stack: &StackType) -> Option<String> {
1473 let is_binary = matches!(
1475 name,
1476 "+" | "-" | "*" | "/" | "%" | "=" | "<" | ">" | "<=" | ">=" | "<>"
1477 );
1478 if !is_binary {
1479 return None;
1480 }
1481
1482 let (rest, top) = stack.clone().pop()?;
1484 let (_, second) = rest.pop()?;
1485
1486 match (name, &second, &top) {
1487 ("+", Type::Int, Type::Int) => Some("i.+".to_string()),
1489 ("-", Type::Int, Type::Int) => Some("i.-".to_string()),
1490 ("*", Type::Int, Type::Int) => Some("i.*".to_string()),
1491 ("/", Type::Int, Type::Int) => Some("i./".to_string()),
1492 ("%", Type::Int, Type::Int) => Some("i.%".to_string()),
1493 ("=", Type::Int, Type::Int) => Some("i.=".to_string()),
1494 ("<", Type::Int, Type::Int) => Some("i.<".to_string()),
1495 (">", Type::Int, Type::Int) => Some("i.>".to_string()),
1496 ("<=", Type::Int, Type::Int) => Some("i.<=".to_string()),
1497 (">=", Type::Int, Type::Int) => Some("i.>=".to_string()),
1498 ("<>", Type::Int, Type::Int) => Some("i.<>".to_string()),
1499
1500 ("+", Type::Float, Type::Float) => Some("f.+".to_string()),
1502 ("-", Type::Float, Type::Float) => Some("f.-".to_string()),
1503 ("*", Type::Float, Type::Float) => Some("f.*".to_string()),
1504 ("/", Type::Float, Type::Float) => Some("f./".to_string()),
1505 ("=", Type::Float, Type::Float) => Some("f.=".to_string()),
1506 ("<", Type::Float, Type::Float) => Some("f.<".to_string()),
1507 (">", Type::Float, Type::Float) => Some("f.>".to_string()),
1508 ("<=", Type::Float, Type::Float) => Some("f.<=".to_string()),
1509 (">=", Type::Float, Type::Float) => Some("f.>=".to_string()),
1510 ("<>", Type::Float, Type::Float) => Some("f.<>".to_string()),
1511
1512 ("+", Type::String, Type::String) => Some("string.concat".to_string()),
1514 ("=", Type::String, Type::String) => Some("string.equal?".to_string()),
1515
1516 _ => None,
1520 }
1521 }
1522
1523 fn infer_dip(
1528 &self,
1529 span: &Option<crate::ast::Span>,
1530 current_stack: StackType,
1531 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1532 let line_prefix = self.line_prefix();
1533
1534 let (stack_after_quot, quot_type) = current_stack.clone().pop().ok_or_else(|| {
1536 format!(
1537 "{}dip: stack underflow - expected quotation on stack",
1538 line_prefix
1539 )
1540 })?;
1541
1542 let quot_effect = match "_type {
1544 Type::Quotation(effect) => (**effect).clone(),
1545 Type::Closure { effect, .. } => (**effect).clone(),
1546 Type::Var(_) => {
1547 let effect = self
1549 .lookup_word_effect("dip")
1550 .ok_or_else(|| "Unknown word: 'dip'".to_string())?;
1551 let fresh_effect = self.freshen_effect(&effect);
1552 let (result_stack, subst) =
1553 self.apply_effect(&fresh_effect, current_stack, "dip", span)?;
1554 return Ok((result_stack, subst, vec![]));
1555 }
1556 _ => {
1557 return Err(format!(
1558 "{}dip: expected quotation or closure on top of stack, got {}",
1559 line_prefix, quot_type
1560 ));
1561 }
1562 };
1563
1564 if quot_effect.has_yield() {
1565 return Err("dip: quotation must not have Yield effects.\n\
1566 Use strand.weave for quotations that yield."
1567 .to_string());
1568 }
1569
1570 let (rest_stack, preserved_type) = stack_after_quot.clone().pop().ok_or_else(|| {
1572 format!(
1573 "{}dip: stack underflow - expected a value below the quotation",
1574 line_prefix
1575 )
1576 })?;
1577
1578 let fresh_effect = self.freshen_effect("_effect);
1580 let (result_stack, subst) =
1581 self.apply_effect(&fresh_effect, rest_stack, "dip (quotation)", span)?;
1582
1583 let resolved_preserved = subst.apply_type(&preserved_type);
1586 let result_stack = result_stack.push(resolved_preserved);
1587
1588 let propagated_effects = fresh_effect.effects.clone();
1589 Ok((result_stack, subst, propagated_effects))
1590 }
1591
1592 fn infer_keep(
1597 &self,
1598 span: &Option<crate::ast::Span>,
1599 current_stack: StackType,
1600 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1601 let line_prefix = self.line_prefix();
1602
1603 let (stack_after_quot, quot_type) = current_stack.clone().pop().ok_or_else(|| {
1605 format!(
1606 "{}keep: stack underflow - expected quotation on stack",
1607 line_prefix
1608 )
1609 })?;
1610
1611 let quot_effect = match "_type {
1613 Type::Quotation(effect) => (**effect).clone(),
1614 Type::Closure { effect, .. } => (**effect).clone(),
1615 Type::Var(_) => {
1616 let effect = self
1617 .lookup_word_effect("keep")
1618 .ok_or_else(|| "Unknown word: 'keep'".to_string())?;
1619 let fresh_effect = self.freshen_effect(&effect);
1620 let (result_stack, subst) =
1621 self.apply_effect(&fresh_effect, current_stack, "keep", span)?;
1622 return Ok((result_stack, subst, vec![]));
1623 }
1624 _ => {
1625 return Err(format!(
1626 "{}keep: expected quotation or closure on top of stack, got {}",
1627 line_prefix, quot_type
1628 ));
1629 }
1630 };
1631
1632 if quot_effect.has_yield() {
1633 return Err("keep: quotation must not have Yield effects.\n\
1634 Use strand.weave for quotations that yield."
1635 .to_string());
1636 }
1637
1638 let (_rest_stack, preserved_type) = stack_after_quot.clone().pop().ok_or_else(|| {
1640 format!(
1641 "{}keep: stack underflow - expected a value below the quotation",
1642 line_prefix
1643 )
1644 })?;
1645
1646 let fresh_effect = self.freshen_effect("_effect);
1649 let (result_stack, subst) =
1650 self.apply_effect(&fresh_effect, stack_after_quot, "keep (quotation)", span)?;
1651
1652 let resolved_preserved = subst.apply_type(&preserved_type);
1655 let result_stack = result_stack.push(resolved_preserved);
1656
1657 let propagated_effects = fresh_effect.effects.clone();
1658 Ok((result_stack, subst, propagated_effects))
1659 }
1660
1661 fn infer_bi(
1666 &self,
1667 span: &Option<crate::ast::Span>,
1668 current_stack: StackType,
1669 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1670 let line_prefix = self.line_prefix();
1671
1672 let (stack1, quot2_type) = current_stack.clone().pop().ok_or_else(|| {
1674 format!(
1675 "{}bi: stack underflow - expected second quotation on stack",
1676 line_prefix
1677 )
1678 })?;
1679
1680 let (stack2, quot1_type) = stack1.clone().pop().ok_or_else(|| {
1682 format!(
1683 "{}bi: stack underflow - expected first quotation on stack",
1684 line_prefix
1685 )
1686 })?;
1687
1688 let quot1_effect = match "1_type {
1690 Type::Quotation(effect) => (**effect).clone(),
1691 Type::Closure { effect, .. } => (**effect).clone(),
1692 Type::Var(_) => {
1693 let effect = self
1694 .lookup_word_effect("bi")
1695 .ok_or_else(|| "Unknown word: 'bi'".to_string())?;
1696 let fresh_effect = self.freshen_effect(&effect);
1697 let (result_stack, subst) =
1698 self.apply_effect(&fresh_effect, current_stack, "bi", span)?;
1699 return Ok((result_stack, subst, vec![]));
1700 }
1701 _ => {
1702 return Err(format!(
1703 "{}bi: expected quotation or closure as first quotation, got {}",
1704 line_prefix, quot1_type
1705 ));
1706 }
1707 };
1708
1709 let quot2_effect = match "2_type {
1710 Type::Quotation(effect) => (**effect).clone(),
1711 Type::Closure { effect, .. } => (**effect).clone(),
1712 Type::Var(_) => {
1713 let effect = self
1714 .lookup_word_effect("bi")
1715 .ok_or_else(|| "Unknown word: 'bi'".to_string())?;
1716 let fresh_effect = self.freshen_effect(&effect);
1717 let (result_stack, subst) =
1718 self.apply_effect(&fresh_effect, current_stack, "bi", span)?;
1719 return Ok((result_stack, subst, vec![]));
1720 }
1721 _ => {
1722 return Err(format!(
1723 "{}bi: expected quotation or closure as second quotation, got {}",
1724 line_prefix, quot2_type
1725 ));
1726 }
1727 };
1728
1729 if quot1_effect.has_yield() || quot2_effect.has_yield() {
1730 return Err("bi: quotations must not have Yield effects.\n\
1731 Use strand.weave for quotations that yield."
1732 .to_string());
1733 }
1734
1735 let (_rest, preserved_type) = stack2.clone().pop().ok_or_else(|| {
1738 format!(
1739 "{}bi: stack underflow - expected a value below the quotations",
1740 line_prefix
1741 )
1742 })?;
1743
1744 let fresh_effect1 = self.freshen_effect("1_effect);
1746 let (after_quot1, subst1) =
1747 self.apply_effect(&fresh_effect1, stack2, "bi (first quotation)", span)?;
1748
1749 let resolved_preserved = subst1.apply_type(&preserved_type);
1752 let with_x = after_quot1.push(resolved_preserved);
1753
1754 let fresh_effect2 = self.freshen_effect("2_effect);
1756 let (result_stack, subst2) =
1757 self.apply_effect(&fresh_effect2, with_x, "bi (second quotation)", span)?;
1758
1759 let subst = subst1.compose(&subst2);
1760
1761 let mut effects = fresh_effect1.effects.clone();
1762 for e in fresh_effect2.effects.clone() {
1763 if !effects.contains(&e) {
1764 effects.push(e);
1765 }
1766 }
1767
1768 Ok((result_stack, subst, effects))
1769 }
1770
1771 fn infer_statement(
1774 &self,
1775 statement: &Statement,
1776 current_stack: StackType,
1777 ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1778 match statement {
1779 Statement::IntLiteral(_) => Ok((current_stack.push(Type::Int), Subst::empty(), vec![])),
1780 Statement::BoolLiteral(_) => {
1781 Ok((current_stack.push(Type::Bool), Subst::empty(), vec![]))
1782 }
1783 Statement::StringLiteral(_) => {
1784 Ok((current_stack.push(Type::String), Subst::empty(), vec![]))
1785 }
1786 Statement::FloatLiteral(_) => {
1787 Ok((current_stack.push(Type::Float), Subst::empty(), vec![]))
1788 }
1789 Statement::Symbol(_) => Ok((current_stack.push(Type::Symbol), Subst::empty(), vec![])),
1790 Statement::Match { arms, span } => self.infer_match(arms, span, current_stack),
1791 Statement::WordCall { name, span } => self.infer_word_call(name, span, current_stack),
1792 Statement::If {
1793 then_branch,
1794 else_branch,
1795 span,
1796 } => self.infer_if(then_branch, else_branch, span, current_stack),
1797 Statement::Quotation { id, body, .. } => self.infer_quotation(*id, body, current_stack),
1798 }
1799 }
1800
1801 fn lookup_word_effect(&self, name: &str) -> Option<Effect> {
1803 if let Some(effect) = builtin_signature(name) {
1805 return Some(effect);
1806 }
1807
1808 self.env.get(name).cloned()
1810 }
1811
1812 fn apply_effect(
1817 &self,
1818 effect: &Effect,
1819 current_stack: StackType,
1820 operation: &str,
1821 span: &Option<crate::ast::Span>,
1822 ) -> Result<(StackType, Subst), String> {
1823 let effect_concrete = Self::count_concrete_types(&effect.inputs);
1831 let stack_concrete = Self::count_concrete_types(¤t_stack);
1832
1833 if let Some(row_var_name) = Self::get_row_var_base(¤t_stack) {
1834 let is_rigid = row_var_name == "rest";
1847
1848 if is_rigid && effect_concrete > stack_concrete {
1849 let word_name = self
1850 .current_word
1851 .borrow()
1852 .as_ref()
1853 .map(|(n, _)| n.clone())
1854 .unwrap_or_else(|| "unknown".to_string());
1855 return Err(format!(
1856 "{}In '{}': {}: stack underflow - requires {} value(s), only {} provided",
1857 self.line_prefix(),
1858 word_name,
1859 operation,
1860 effect_concrete,
1861 stack_concrete
1862 ));
1863 }
1864 }
1865
1866 let subst = unify_stacks(&effect.inputs, ¤t_stack).map_err(|e| {
1868 let line_info = span
1869 .as_ref()
1870 .map(|s| format_line_prefix(s.line))
1871 .unwrap_or_default();
1872 format!(
1873 "{}{}: stack type mismatch. Expected {}, got {}: {}",
1874 line_info, operation, effect.inputs, current_stack, e
1875 )
1876 })?;
1877
1878 let result_stack = subst.apply_stack(&effect.outputs);
1880
1881 Ok((result_stack, subst))
1882 }
1883
1884 fn count_concrete_types(stack: &StackType) -> usize {
1886 let mut count = 0;
1887 let mut current = stack;
1888 while let StackType::Cons { rest, top: _ } = current {
1889 count += 1;
1890 current = rest;
1891 }
1892 count
1893 }
1894
1895 fn get_row_var_base(stack: &StackType) -> Option<String> {
1897 let mut current = stack;
1898 while let StackType::Cons { rest, top: _ } = current {
1899 current = rest;
1900 }
1901 match current {
1902 StackType::RowVar(name) => Some(name.clone()),
1903 _ => None,
1904 }
1905 }
1906
1907 fn adjust_stack_for_spawn(
1912 &self,
1913 current_stack: StackType,
1914 spawn_effect: &Effect,
1915 ) -> Result<StackType, String> {
1916 let expected_quot_type = match &spawn_effect.inputs {
1919 StackType::Cons { top, rest: _ } => {
1920 if !matches!(top, Type::Quotation(_)) {
1921 return Ok(current_stack); }
1923 top
1924 }
1925 _ => return Ok(current_stack),
1926 };
1927
1928 let (rest_stack, actual_type) = match ¤t_stack {
1930 StackType::Cons { rest, top } => (rest.as_ref().clone(), top),
1931 _ => return Ok(current_stack), };
1933
1934 if let Type::Quotation(actual_effect) = actual_type {
1936 if !matches!(actual_effect.inputs, StackType::Empty) {
1938 let expected_effect = match expected_quot_type {
1940 Type::Quotation(eff) => eff.as_ref(),
1941 _ => return Ok(current_stack),
1942 };
1943
1944 let captures = calculate_captures(actual_effect, expected_effect)?;
1946
1947 let closure_type = Type::Closure {
1949 effect: Box::new(expected_effect.clone()),
1950 captures: captures.clone(),
1951 };
1952
1953 let mut adjusted_stack = rest_stack;
1956 for _ in &captures {
1957 adjusted_stack = match adjusted_stack {
1958 StackType::Cons { rest, .. } => rest.as_ref().clone(),
1959 _ => {
1960 return Err(format!(
1961 "strand.spawn: not enough values on stack to capture. Need {} values",
1962 captures.len()
1963 ));
1964 }
1965 };
1966 }
1967
1968 return Ok(adjusted_stack.push(closure_type));
1970 }
1971 }
1972
1973 Ok(current_stack)
1974 }
1975
1976 fn analyze_captures(
2002 &self,
2003 body_effect: &Effect,
2004 _current_stack: &StackType,
2005 ) -> Result<Type, String> {
2006 let expected = self.expected_quotation_type.borrow().clone();
2008
2009 match expected {
2010 Some(Type::Closure { effect, .. }) => {
2011 let captures = calculate_captures(body_effect, &effect)?;
2013 Ok(Type::Closure { effect, captures })
2014 }
2015 Some(Type::Quotation(expected_effect)) => {
2016 let expected_is_empty = matches!(expected_effect.inputs, StackType::Empty);
2022 let body_needs_inputs = !matches!(body_effect.inputs, StackType::Empty);
2023
2024 if expected_is_empty && body_needs_inputs {
2025 let captures = calculate_captures(body_effect, &expected_effect)?;
2028 Ok(Type::Closure {
2029 effect: expected_effect,
2030 captures,
2031 })
2032 } else {
2033 let body_quot = Type::Quotation(Box::new(body_effect.clone()));
2039 let expected_quot = Type::Quotation(expected_effect.clone());
2040 unify_types(&body_quot, &expected_quot).map_err(|e| {
2041 format!(
2042 "quotation effect mismatch: expected {}, got {}: {}",
2043 expected_effect, body_effect, e
2044 )
2045 })?;
2046
2047 Ok(Type::Quotation(expected_effect))
2049 }
2050 }
2051 _ => {
2052 Ok(Type::Quotation(Box::new(body_effect.clone())))
2054 }
2055 }
2056 }
2057
2058 fn effect_matches_any(&self, inferred: &SideEffect, declared: &[SideEffect]) -> bool {
2062 declared.iter().any(|decl| match (inferred, decl) {
2063 (SideEffect::Yield(_), SideEffect::Yield(_)) => true,
2064 })
2065 }
2066
2067 fn pop_type(&self, stack: &StackType, context: &str) -> Result<(StackType, Type), String> {
2069 match stack {
2070 StackType::Cons { rest, top } => Ok(((**rest).clone(), top.clone())),
2071 StackType::Empty => Err(format!(
2072 "{}: stack underflow - expected value on stack but stack is empty",
2073 context
2074 )),
2075 StackType::RowVar(_) => {
2076 Err(format!(
2080 "{}: cannot pop from polymorphic stack without more type information",
2081 context
2082 ))
2083 }
2084 }
2085 }
2086}
2087
2088impl Default for TypeChecker {
2089 fn default() -> Self {
2090 Self::new()
2091 }
2092}
2093
2094#[cfg(test)]
2095mod tests {
2096 use super::*;
2097
2098 #[test]
2099 fn test_simple_literal() {
2100 let program = Program {
2101 includes: vec![],
2102 unions: vec![],
2103 words: vec![WordDef {
2104 name: "test".to_string(),
2105 effect: Some(Effect::new(
2106 StackType::Empty,
2107 StackType::singleton(Type::Int),
2108 )),
2109 body: vec![Statement::IntLiteral(42)],
2110 source: None,
2111 allowed_lints: vec![],
2112 }],
2113 };
2114
2115 let mut checker = TypeChecker::new();
2116 assert!(checker.check_program(&program).is_ok());
2117 }
2118
2119 #[test]
2120 fn test_simple_operation() {
2121 let program = Program {
2123 includes: vec![],
2124 unions: vec![],
2125 words: vec![WordDef {
2126 name: "test".to_string(),
2127 effect: Some(Effect::new(
2128 StackType::Empty.push(Type::Int).push(Type::Int),
2129 StackType::singleton(Type::Int),
2130 )),
2131 body: vec![Statement::WordCall {
2132 name: "i.add".to_string(),
2133 span: None,
2134 }],
2135 source: None,
2136 allowed_lints: vec![],
2137 }],
2138 };
2139
2140 let mut checker = TypeChecker::new();
2141 assert!(checker.check_program(&program).is_ok());
2142 }
2143
2144 #[test]
2145 fn test_type_mismatch() {
2146 let program = Program {
2148 includes: vec![],
2149 unions: vec![],
2150 words: vec![WordDef {
2151 name: "test".to_string(),
2152 effect: Some(Effect::new(
2153 StackType::singleton(Type::String),
2154 StackType::Empty,
2155 )),
2156 body: vec![
2157 Statement::IntLiteral(42), Statement::WordCall {
2159 name: "io.write-line".to_string(),
2160 span: None,
2161 },
2162 ],
2163 source: None,
2164 allowed_lints: vec![],
2165 }],
2166 };
2167
2168 let mut checker = TypeChecker::new();
2169 let result = checker.check_program(&program);
2170 assert!(result.is_err());
2171 assert!(result.unwrap_err().contains("Type mismatch"));
2172 }
2173
2174 #[test]
2175 fn test_polymorphic_dup() {
2176 let program = Program {
2178 includes: vec![],
2179 unions: vec![],
2180 words: vec![WordDef {
2181 name: "my-dup".to_string(),
2182 effect: Some(Effect::new(
2183 StackType::singleton(Type::Int),
2184 StackType::Empty.push(Type::Int).push(Type::Int),
2185 )),
2186 body: vec![Statement::WordCall {
2187 name: "dup".to_string(),
2188 span: None,
2189 }],
2190 source: None,
2191 allowed_lints: vec![],
2192 }],
2193 };
2194
2195 let mut checker = TypeChecker::new();
2196 assert!(checker.check_program(&program).is_ok());
2197 }
2198
2199 #[test]
2200 fn test_conditional_branches() {
2201 let program = Program {
2204 includes: vec![],
2205 unions: vec![],
2206 words: vec![WordDef {
2207 name: "test".to_string(),
2208 effect: Some(Effect::new(
2209 StackType::Empty.push(Type::Int).push(Type::Int),
2210 StackType::singleton(Type::String),
2211 )),
2212 body: vec![
2213 Statement::WordCall {
2214 name: "i.>".to_string(),
2215 span: None,
2216 },
2217 Statement::If {
2218 then_branch: vec![Statement::StringLiteral("greater".to_string())],
2219 else_branch: Some(vec![Statement::StringLiteral(
2220 "not greater".to_string(),
2221 )]),
2222 span: None,
2223 },
2224 ],
2225 source: None,
2226 allowed_lints: vec![],
2227 }],
2228 };
2229
2230 let mut checker = TypeChecker::new();
2231 assert!(checker.check_program(&program).is_ok());
2232 }
2233
2234 #[test]
2235 fn test_mismatched_branches() {
2236 let program = Program {
2239 includes: vec![],
2240 unions: vec![],
2241 words: vec![WordDef {
2242 name: "test".to_string(),
2243 effect: Some(Effect::new(
2244 StackType::Empty,
2245 StackType::singleton(Type::Int),
2246 )),
2247 body: vec![
2248 Statement::BoolLiteral(true),
2249 Statement::If {
2250 then_branch: vec![Statement::IntLiteral(42)],
2251 else_branch: Some(vec![Statement::StringLiteral("string".to_string())]),
2252 span: None,
2253 },
2254 ],
2255 source: None,
2256 allowed_lints: vec![],
2257 }],
2258 };
2259
2260 let mut checker = TypeChecker::new();
2261 let result = checker.check_program(&program);
2262 assert!(result.is_err());
2263 assert!(result.unwrap_err().contains("incompatible"));
2264 }
2265
2266 #[test]
2267 fn test_user_defined_word_call() {
2268 let program = Program {
2271 includes: vec![],
2272 unions: vec![],
2273 words: vec![
2274 WordDef {
2275 name: "helper".to_string(),
2276 effect: Some(Effect::new(
2277 StackType::singleton(Type::Int),
2278 StackType::singleton(Type::String),
2279 )),
2280 body: vec![Statement::WordCall {
2281 name: "int->string".to_string(),
2282 span: None,
2283 }],
2284 source: None,
2285 allowed_lints: vec![],
2286 },
2287 WordDef {
2288 name: "main".to_string(),
2289 effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2290 body: vec![
2291 Statement::IntLiteral(42),
2292 Statement::WordCall {
2293 name: "helper".to_string(),
2294 span: None,
2295 },
2296 Statement::WordCall {
2297 name: "io.write-line".to_string(),
2298 span: None,
2299 },
2300 ],
2301 source: None,
2302 allowed_lints: vec![],
2303 },
2304 ],
2305 };
2306
2307 let mut checker = TypeChecker::new();
2308 assert!(checker.check_program(&program).is_ok());
2309 }
2310
2311 #[test]
2312 fn test_arithmetic_chain() {
2313 let program = Program {
2316 includes: vec![],
2317 unions: vec![],
2318 words: vec![WordDef {
2319 name: "test".to_string(),
2320 effect: Some(Effect::new(
2321 StackType::Empty
2322 .push(Type::Int)
2323 .push(Type::Int)
2324 .push(Type::Int),
2325 StackType::singleton(Type::Int),
2326 )),
2327 body: vec![
2328 Statement::WordCall {
2329 name: "i.add".to_string(),
2330 span: None,
2331 },
2332 Statement::WordCall {
2333 name: "i.multiply".to_string(),
2334 span: None,
2335 },
2336 ],
2337 source: None,
2338 allowed_lints: vec![],
2339 }],
2340 };
2341
2342 let mut checker = TypeChecker::new();
2343 assert!(checker.check_program(&program).is_ok());
2344 }
2345
2346 #[test]
2347 fn test_write_line_type_error() {
2348 let program = Program {
2350 includes: vec![],
2351 unions: vec![],
2352 words: vec![WordDef {
2353 name: "test".to_string(),
2354 effect: Some(Effect::new(
2355 StackType::singleton(Type::Int),
2356 StackType::Empty,
2357 )),
2358 body: vec![Statement::WordCall {
2359 name: "io.write-line".to_string(),
2360 span: None,
2361 }],
2362 source: None,
2363 allowed_lints: vec![],
2364 }],
2365 };
2366
2367 let mut checker = TypeChecker::new();
2368 let result = checker.check_program(&program);
2369 assert!(result.is_err());
2370 assert!(result.unwrap_err().contains("Type mismatch"));
2371 }
2372
2373 #[test]
2374 fn test_stack_underflow_drop() {
2375 let program = Program {
2377 includes: vec![],
2378 unions: vec![],
2379 words: vec![WordDef {
2380 name: "test".to_string(),
2381 effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2382 body: vec![Statement::WordCall {
2383 name: "drop".to_string(),
2384 span: None,
2385 }],
2386 source: None,
2387 allowed_lints: vec![],
2388 }],
2389 };
2390
2391 let mut checker = TypeChecker::new();
2392 let result = checker.check_program(&program);
2393 assert!(result.is_err());
2394 assert!(result.unwrap_err().contains("mismatch"));
2395 }
2396
2397 #[test]
2398 fn test_stack_underflow_add() {
2399 let program = Program {
2401 includes: vec![],
2402 unions: vec![],
2403 words: vec![WordDef {
2404 name: "test".to_string(),
2405 effect: Some(Effect::new(
2406 StackType::singleton(Type::Int),
2407 StackType::singleton(Type::Int),
2408 )),
2409 body: vec![Statement::WordCall {
2410 name: "i.add".to_string(),
2411 span: None,
2412 }],
2413 source: None,
2414 allowed_lints: vec![],
2415 }],
2416 };
2417
2418 let mut checker = TypeChecker::new();
2419 let result = checker.check_program(&program);
2420 assert!(result.is_err());
2421 assert!(result.unwrap_err().contains("mismatch"));
2422 }
2423
2424 #[test]
2427 fn test_stack_underflow_rot_issue_169() {
2428 let program = Program {
2432 includes: vec![],
2433 unions: vec![],
2434 words: vec![WordDef {
2435 name: "test".to_string(),
2436 effect: Some(Effect::new(
2437 StackType::RowVar("rest".to_string()),
2438 StackType::RowVar("rest".to_string()),
2439 )),
2440 body: vec![
2441 Statement::IntLiteral(3),
2442 Statement::IntLiteral(4),
2443 Statement::WordCall {
2444 name: "rot".to_string(),
2445 span: None,
2446 },
2447 ],
2448 source: None,
2449 allowed_lints: vec![],
2450 }],
2451 };
2452
2453 let mut checker = TypeChecker::new();
2454 let result = checker.check_program(&program);
2455 assert!(result.is_err(), "rot with 2 values should fail");
2456 let err = result.unwrap_err();
2457 assert!(
2458 err.contains("stack underflow") || err.contains("requires 3"),
2459 "Error should mention underflow: {}",
2460 err
2461 );
2462 }
2463
2464 #[test]
2465 fn test_csp_operations() {
2466 let program = Program {
2473 includes: vec![],
2474 unions: vec![],
2475 words: vec![WordDef {
2476 name: "test".to_string(),
2477 effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2478 body: vec![
2479 Statement::WordCall {
2480 name: "chan.make".to_string(),
2481 span: None,
2482 },
2483 Statement::IntLiteral(42),
2484 Statement::WordCall {
2485 name: "swap".to_string(),
2486 span: None,
2487 },
2488 Statement::WordCall {
2489 name: "chan.send".to_string(),
2490 span: None,
2491 },
2492 Statement::WordCall {
2493 name: "drop".to_string(),
2494 span: None,
2495 },
2496 ],
2497 source: None,
2498 allowed_lints: vec![],
2499 }],
2500 };
2501
2502 let mut checker = TypeChecker::new();
2503 assert!(checker.check_program(&program).is_ok());
2504 }
2505
2506 #[test]
2507 fn test_complex_stack_shuffling() {
2508 let program = Program {
2511 includes: vec![],
2512 unions: vec![],
2513 words: vec![WordDef {
2514 name: "test".to_string(),
2515 effect: Some(Effect::new(
2516 StackType::Empty
2517 .push(Type::Int)
2518 .push(Type::Int)
2519 .push(Type::Int),
2520 StackType::singleton(Type::Int),
2521 )),
2522 body: vec![
2523 Statement::WordCall {
2524 name: "rot".to_string(),
2525 span: None,
2526 },
2527 Statement::WordCall {
2528 name: "i.add".to_string(),
2529 span: None,
2530 },
2531 Statement::WordCall {
2532 name: "i.add".to_string(),
2533 span: None,
2534 },
2535 ],
2536 source: None,
2537 allowed_lints: vec![],
2538 }],
2539 };
2540
2541 let mut checker = TypeChecker::new();
2542 assert!(checker.check_program(&program).is_ok());
2543 }
2544
2545 #[test]
2546 fn test_empty_program() {
2547 let program = Program {
2549 includes: vec![],
2550 unions: vec![],
2551 words: vec![],
2552 };
2553
2554 let mut checker = TypeChecker::new();
2555 assert!(checker.check_program(&program).is_ok());
2556 }
2557
2558 #[test]
2559 fn test_word_without_effect_declaration() {
2560 let program = Program {
2562 includes: vec![],
2563 unions: vec![],
2564 words: vec![WordDef {
2565 name: "helper".to_string(),
2566 effect: None,
2567 body: vec![Statement::IntLiteral(42)],
2568 source: None,
2569 allowed_lints: vec![],
2570 }],
2571 };
2572
2573 let mut checker = TypeChecker::new();
2574 let result = checker.check_program(&program);
2575 assert!(result.is_err());
2576 assert!(
2577 result
2578 .unwrap_err()
2579 .contains("missing a stack effect declaration")
2580 );
2581 }
2582
2583 #[test]
2584 fn test_nested_conditionals() {
2585 let program = Program {
2594 includes: vec![],
2595 unions: vec![],
2596 words: vec![WordDef {
2597 name: "test".to_string(),
2598 effect: Some(Effect::new(
2599 StackType::Empty
2600 .push(Type::Int)
2601 .push(Type::Int)
2602 .push(Type::Int)
2603 .push(Type::Int),
2604 StackType::singleton(Type::String),
2605 )),
2606 body: vec![
2607 Statement::WordCall {
2608 name: "i.>".to_string(),
2609 span: None,
2610 },
2611 Statement::If {
2612 then_branch: vec![
2613 Statement::WordCall {
2614 name: "i.>".to_string(),
2615 span: None,
2616 },
2617 Statement::If {
2618 then_branch: vec![Statement::StringLiteral(
2619 "both true".to_string(),
2620 )],
2621 else_branch: Some(vec![Statement::StringLiteral(
2622 "first true".to_string(),
2623 )]),
2624 span: None,
2625 },
2626 ],
2627 else_branch: Some(vec![
2628 Statement::WordCall {
2629 name: "drop".to_string(),
2630 span: None,
2631 },
2632 Statement::WordCall {
2633 name: "drop".to_string(),
2634 span: None,
2635 },
2636 Statement::StringLiteral("first false".to_string()),
2637 ]),
2638 span: None,
2639 },
2640 ],
2641 source: None,
2642 allowed_lints: vec![],
2643 }],
2644 };
2645
2646 let mut checker = TypeChecker::new();
2647 match checker.check_program(&program) {
2648 Ok(_) => {}
2649 Err(e) => panic!("Type check failed: {}", e),
2650 }
2651 }
2652
2653 #[test]
2654 fn test_conditional_without_else() {
2655 let program = Program {
2659 includes: vec![],
2660 unions: vec![],
2661 words: vec![WordDef {
2662 name: "test".to_string(),
2663 effect: Some(Effect::new(
2664 StackType::Empty.push(Type::Int).push(Type::Int),
2665 StackType::singleton(Type::Int),
2666 )),
2667 body: vec![
2668 Statement::WordCall {
2669 name: "i.>".to_string(),
2670 span: None,
2671 },
2672 Statement::If {
2673 then_branch: vec![Statement::IntLiteral(100)],
2674 else_branch: None, span: None,
2676 },
2677 ],
2678 source: None,
2679 allowed_lints: vec![],
2680 }],
2681 };
2682
2683 let mut checker = TypeChecker::new();
2684 let result = checker.check_program(&program);
2685 assert!(result.is_err());
2687 }
2688
2689 #[test]
2690 fn test_multiple_word_chain() {
2691 let program = Program {
2695 includes: vec![],
2696 unions: vec![],
2697 words: vec![
2698 WordDef {
2699 name: "helper1".to_string(),
2700 effect: Some(Effect::new(
2701 StackType::singleton(Type::Int),
2702 StackType::singleton(Type::String),
2703 )),
2704 body: vec![Statement::WordCall {
2705 name: "int->string".to_string(),
2706 span: None,
2707 }],
2708 source: None,
2709 allowed_lints: vec![],
2710 },
2711 WordDef {
2712 name: "helper2".to_string(),
2713 effect: Some(Effect::new(
2714 StackType::singleton(Type::String),
2715 StackType::Empty,
2716 )),
2717 body: vec![Statement::WordCall {
2718 name: "io.write-line".to_string(),
2719 span: None,
2720 }],
2721 source: None,
2722 allowed_lints: vec![],
2723 },
2724 WordDef {
2725 name: "main".to_string(),
2726 effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2727 body: vec![
2728 Statement::IntLiteral(42),
2729 Statement::WordCall {
2730 name: "helper1".to_string(),
2731 span: None,
2732 },
2733 Statement::WordCall {
2734 name: "helper2".to_string(),
2735 span: None,
2736 },
2737 ],
2738 source: None,
2739 allowed_lints: vec![],
2740 },
2741 ],
2742 };
2743
2744 let mut checker = TypeChecker::new();
2745 assert!(checker.check_program(&program).is_ok());
2746 }
2747
2748 #[test]
2749 fn test_all_stack_ops() {
2750 let program = Program {
2753 includes: vec![],
2754 unions: vec![],
2755 words: vec![WordDef {
2756 name: "test".to_string(),
2757 effect: Some(Effect::new(
2758 StackType::Empty
2759 .push(Type::Int)
2760 .push(Type::Int)
2761 .push(Type::Int),
2762 StackType::Empty
2763 .push(Type::Int)
2764 .push(Type::Int)
2765 .push(Type::Int)
2766 .push(Type::Int),
2767 )),
2768 body: vec![
2769 Statement::WordCall {
2770 name: "over".to_string(),
2771 span: None,
2772 },
2773 Statement::WordCall {
2774 name: "nip".to_string(),
2775 span: None,
2776 },
2777 Statement::WordCall {
2778 name: "tuck".to_string(),
2779 span: None,
2780 },
2781 ],
2782 source: None,
2783 allowed_lints: vec![],
2784 }],
2785 };
2786
2787 let mut checker = TypeChecker::new();
2788 assert!(checker.check_program(&program).is_ok());
2789 }
2790
2791 #[test]
2792 fn test_mixed_types_complex() {
2793 let program = Program {
2802 includes: vec![],
2803 unions: vec![],
2804 words: vec![WordDef {
2805 name: "test".to_string(),
2806 effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2807 body: vec![
2808 Statement::IntLiteral(42),
2809 Statement::WordCall {
2810 name: "int->string".to_string(),
2811 span: None,
2812 },
2813 Statement::IntLiteral(100),
2814 Statement::IntLiteral(200),
2815 Statement::WordCall {
2816 name: "i.>".to_string(),
2817 span: None,
2818 },
2819 Statement::If {
2820 then_branch: vec![Statement::WordCall {
2821 name: "io.write-line".to_string(),
2822 span: None,
2823 }],
2824 else_branch: Some(vec![Statement::WordCall {
2825 name: "io.write-line".to_string(),
2826 span: None,
2827 }]),
2828 span: None,
2829 },
2830 ],
2831 source: None,
2832 allowed_lints: vec![],
2833 }],
2834 };
2835
2836 let mut checker = TypeChecker::new();
2837 assert!(checker.check_program(&program).is_ok());
2838 }
2839
2840 #[test]
2841 fn test_string_literal() {
2842 let program = Program {
2844 includes: vec![],
2845 unions: vec![],
2846 words: vec![WordDef {
2847 name: "test".to_string(),
2848 effect: Some(Effect::new(
2849 StackType::Empty,
2850 StackType::singleton(Type::String),
2851 )),
2852 body: vec![Statement::StringLiteral("hello".to_string())],
2853 source: None,
2854 allowed_lints: vec![],
2855 }],
2856 };
2857
2858 let mut checker = TypeChecker::new();
2859 assert!(checker.check_program(&program).is_ok());
2860 }
2861
2862 #[test]
2863 fn test_bool_literal() {
2864 let program = Program {
2867 includes: vec![],
2868 unions: vec![],
2869 words: vec![WordDef {
2870 name: "test".to_string(),
2871 effect: Some(Effect::new(
2872 StackType::Empty,
2873 StackType::singleton(Type::Bool),
2874 )),
2875 body: vec![Statement::BoolLiteral(true)],
2876 source: None,
2877 allowed_lints: vec![],
2878 }],
2879 };
2880
2881 let mut checker = TypeChecker::new();
2882 assert!(checker.check_program(&program).is_ok());
2883 }
2884
2885 #[test]
2886 fn test_type_error_in_nested_conditional() {
2887 let program = Program {
2894 includes: vec![],
2895 unions: vec![],
2896 words: vec![WordDef {
2897 name: "test".to_string(),
2898 effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2899 body: vec![
2900 Statement::IntLiteral(10),
2901 Statement::IntLiteral(20),
2902 Statement::WordCall {
2903 name: "i.>".to_string(),
2904 span: None,
2905 },
2906 Statement::If {
2907 then_branch: vec![
2908 Statement::IntLiteral(42),
2909 Statement::WordCall {
2910 name: "io.write-line".to_string(),
2911 span: None,
2912 },
2913 ],
2914 else_branch: Some(vec![
2915 Statement::StringLiteral("ok".to_string()),
2916 Statement::WordCall {
2917 name: "io.write-line".to_string(),
2918 span: None,
2919 },
2920 ]),
2921 span: None,
2922 },
2923 ],
2924 source: None,
2925 allowed_lints: vec![],
2926 }],
2927 };
2928
2929 let mut checker = TypeChecker::new();
2930 let result = checker.check_program(&program);
2931 assert!(result.is_err());
2932 assert!(result.unwrap_err().contains("Type mismatch"));
2933 }
2934
2935 #[test]
2936 fn test_read_line_operation() {
2937 let program = Program {
2940 includes: vec![],
2941 unions: vec![],
2942 words: vec![WordDef {
2943 name: "test".to_string(),
2944 effect: Some(Effect::new(
2945 StackType::Empty,
2946 StackType::from_vec(vec![Type::String, Type::Bool]),
2947 )),
2948 body: vec![Statement::WordCall {
2949 name: "io.read-line".to_string(),
2950 span: None,
2951 }],
2952 source: None,
2953 allowed_lints: vec![],
2954 }],
2955 };
2956
2957 let mut checker = TypeChecker::new();
2958 assert!(checker.check_program(&program).is_ok());
2959 }
2960
2961 #[test]
2962 fn test_comparison_operations() {
2963 let program = Program {
2968 includes: vec![],
2969 unions: vec![],
2970 words: vec![WordDef {
2971 name: "test".to_string(),
2972 effect: Some(Effect::new(
2973 StackType::Empty.push(Type::Int).push(Type::Int),
2974 StackType::singleton(Type::Bool),
2975 )),
2976 body: vec![Statement::WordCall {
2977 name: "i.<=".to_string(),
2978 span: None,
2979 }],
2980 source: None,
2981 allowed_lints: vec![],
2982 }],
2983 };
2984
2985 let mut checker = TypeChecker::new();
2986 assert!(checker.check_program(&program).is_ok());
2987 }
2988
2989 #[test]
2990 fn test_recursive_word_definitions() {
2991 let program = Program {
2997 includes: vec![],
2998 unions: vec![],
2999 words: vec![
3000 WordDef {
3001 name: "is-even".to_string(),
3002 effect: Some(Effect::new(
3003 StackType::singleton(Type::Int),
3004 StackType::singleton(Type::Int),
3005 )),
3006 body: vec![
3007 Statement::WordCall {
3008 name: "dup".to_string(),
3009 span: None,
3010 },
3011 Statement::IntLiteral(0),
3012 Statement::WordCall {
3013 name: "i.=".to_string(),
3014 span: None,
3015 },
3016 Statement::If {
3017 then_branch: vec![
3018 Statement::WordCall {
3019 name: "drop".to_string(),
3020 span: None,
3021 },
3022 Statement::IntLiteral(1),
3023 ],
3024 else_branch: Some(vec![
3025 Statement::IntLiteral(1),
3026 Statement::WordCall {
3027 name: "i.subtract".to_string(),
3028 span: None,
3029 },
3030 Statement::WordCall {
3031 name: "is-odd".to_string(),
3032 span: None,
3033 },
3034 ]),
3035 span: None,
3036 },
3037 ],
3038 source: None,
3039 allowed_lints: vec![],
3040 },
3041 WordDef {
3042 name: "is-odd".to_string(),
3043 effect: Some(Effect::new(
3044 StackType::singleton(Type::Int),
3045 StackType::singleton(Type::Int),
3046 )),
3047 body: vec![
3048 Statement::WordCall {
3049 name: "dup".to_string(),
3050 span: None,
3051 },
3052 Statement::IntLiteral(0),
3053 Statement::WordCall {
3054 name: "i.=".to_string(),
3055 span: None,
3056 },
3057 Statement::If {
3058 then_branch: vec![
3059 Statement::WordCall {
3060 name: "drop".to_string(),
3061 span: None,
3062 },
3063 Statement::IntLiteral(0),
3064 ],
3065 else_branch: Some(vec![
3066 Statement::IntLiteral(1),
3067 Statement::WordCall {
3068 name: "i.subtract".to_string(),
3069 span: None,
3070 },
3071 Statement::WordCall {
3072 name: "is-even".to_string(),
3073 span: None,
3074 },
3075 ]),
3076 span: None,
3077 },
3078 ],
3079 source: None,
3080 allowed_lints: vec![],
3081 },
3082 ],
3083 };
3084
3085 let mut checker = TypeChecker::new();
3086 assert!(checker.check_program(&program).is_ok());
3087 }
3088
3089 #[test]
3090 fn test_word_calling_word_with_row_polymorphism() {
3091 let program = Program {
3096 includes: vec![],
3097 unions: vec![],
3098 words: vec![
3099 WordDef {
3100 name: "apply-twice".to_string(),
3101 effect: Some(Effect::new(
3102 StackType::singleton(Type::Int),
3103 StackType::singleton(Type::Int),
3104 )),
3105 body: vec![
3106 Statement::WordCall {
3107 name: "dup".to_string(),
3108 span: None,
3109 },
3110 Statement::WordCall {
3111 name: "i.add".to_string(),
3112 span: None,
3113 },
3114 ],
3115 source: None,
3116 allowed_lints: vec![],
3117 },
3118 WordDef {
3119 name: "quad".to_string(),
3120 effect: Some(Effect::new(
3121 StackType::singleton(Type::Int),
3122 StackType::singleton(Type::Int),
3123 )),
3124 body: vec![
3125 Statement::WordCall {
3126 name: "apply-twice".to_string(),
3127 span: None,
3128 },
3129 Statement::WordCall {
3130 name: "apply-twice".to_string(),
3131 span: None,
3132 },
3133 ],
3134 source: None,
3135 allowed_lints: vec![],
3136 },
3137 ],
3138 };
3139
3140 let mut checker = TypeChecker::new();
3141 assert!(checker.check_program(&program).is_ok());
3142 }
3143
3144 #[test]
3145 fn test_deep_stack_types() {
3146 let mut stack_type = StackType::Empty;
3150 for _ in 0..10 {
3151 stack_type = stack_type.push(Type::Int);
3152 }
3153
3154 let program = Program {
3155 includes: vec![],
3156 unions: vec![],
3157 words: vec![WordDef {
3158 name: "test".to_string(),
3159 effect: Some(Effect::new(stack_type, StackType::singleton(Type::Int))),
3160 body: vec![
3161 Statement::WordCall {
3162 name: "i.add".to_string(),
3163 span: None,
3164 },
3165 Statement::WordCall {
3166 name: "i.add".to_string(),
3167 span: None,
3168 },
3169 Statement::WordCall {
3170 name: "i.add".to_string(),
3171 span: None,
3172 },
3173 Statement::WordCall {
3174 name: "i.add".to_string(),
3175 span: None,
3176 },
3177 Statement::WordCall {
3178 name: "i.add".to_string(),
3179 span: None,
3180 },
3181 Statement::WordCall {
3182 name: "i.add".to_string(),
3183 span: None,
3184 },
3185 Statement::WordCall {
3186 name: "i.add".to_string(),
3187 span: None,
3188 },
3189 Statement::WordCall {
3190 name: "i.add".to_string(),
3191 span: None,
3192 },
3193 Statement::WordCall {
3194 name: "i.add".to_string(),
3195 span: None,
3196 },
3197 ],
3198 source: None,
3199 allowed_lints: vec![],
3200 }],
3201 };
3202
3203 let mut checker = TypeChecker::new();
3204 assert!(checker.check_program(&program).is_ok());
3205 }
3206
3207 #[test]
3208 fn test_simple_quotation() {
3209 let program = Program {
3213 includes: vec![],
3214 unions: vec![],
3215 words: vec![WordDef {
3216 name: "test".to_string(),
3217 effect: Some(Effect::new(
3218 StackType::Empty,
3219 StackType::singleton(Type::Quotation(Box::new(Effect::new(
3220 StackType::RowVar("input".to_string()).push(Type::Int),
3221 StackType::RowVar("input".to_string()).push(Type::Int),
3222 )))),
3223 )),
3224 body: vec![Statement::Quotation {
3225 span: None,
3226 id: 0,
3227 body: vec![
3228 Statement::IntLiteral(1),
3229 Statement::WordCall {
3230 name: "i.add".to_string(),
3231 span: None,
3232 },
3233 ],
3234 }],
3235 source: None,
3236 allowed_lints: vec![],
3237 }],
3238 };
3239
3240 let mut checker = TypeChecker::new();
3241 match checker.check_program(&program) {
3242 Ok(_) => {}
3243 Err(e) => panic!("Type check failed: {}", e),
3244 }
3245 }
3246
3247 #[test]
3248 fn test_empty_quotation() {
3249 let program = Program {
3253 includes: vec![],
3254 unions: vec![],
3255 words: vec![WordDef {
3256 name: "test".to_string(),
3257 effect: Some(Effect::new(
3258 StackType::Empty,
3259 StackType::singleton(Type::Quotation(Box::new(Effect::new(
3260 StackType::RowVar("input".to_string()),
3261 StackType::RowVar("input".to_string()),
3262 )))),
3263 )),
3264 body: vec![Statement::Quotation {
3265 span: None,
3266 id: 1,
3267 body: vec![],
3268 }],
3269 source: None,
3270 allowed_lints: vec![],
3271 }],
3272 };
3273
3274 let mut checker = TypeChecker::new();
3275 assert!(checker.check_program(&program).is_ok());
3276 }
3277
3278 #[test]
3279 fn test_nested_quotation() {
3280 let inner_quot_type = Type::Quotation(Box::new(Effect::new(
3284 StackType::RowVar("input".to_string()).push(Type::Int),
3285 StackType::RowVar("input".to_string()).push(Type::Int),
3286 )));
3287
3288 let outer_quot_type = Type::Quotation(Box::new(Effect::new(
3289 StackType::RowVar("input".to_string()),
3290 StackType::RowVar("input".to_string()).push(inner_quot_type.clone()),
3291 )));
3292
3293 let program = Program {
3294 includes: vec![],
3295 unions: vec![],
3296 words: vec![WordDef {
3297 name: "test".to_string(),
3298 effect: Some(Effect::new(
3299 StackType::Empty,
3300 StackType::singleton(outer_quot_type),
3301 )),
3302 body: vec![Statement::Quotation {
3303 span: None,
3304 id: 2,
3305 body: vec![Statement::Quotation {
3306 span: None,
3307 id: 3,
3308 body: vec![
3309 Statement::IntLiteral(1),
3310 Statement::WordCall {
3311 name: "i.add".to_string(),
3312 span: None,
3313 },
3314 ],
3315 }],
3316 }],
3317 source: None,
3318 allowed_lints: vec![],
3319 }],
3320 };
3321
3322 let mut checker = TypeChecker::new();
3323 assert!(checker.check_program(&program).is_ok());
3324 }
3325
3326 #[test]
3327 fn test_invalid_field_type_error() {
3328 use crate::ast::{UnionDef, UnionField, UnionVariant};
3329
3330 let program = Program {
3331 includes: vec![],
3332 unions: vec![UnionDef {
3333 name: "Message".to_string(),
3334 variants: vec![UnionVariant {
3335 name: "Get".to_string(),
3336 fields: vec![UnionField {
3337 name: "chan".to_string(),
3338 type_name: "InvalidType".to_string(),
3339 }],
3340 source: None,
3341 }],
3342 source: None,
3343 }],
3344 words: vec![],
3345 };
3346
3347 let mut checker = TypeChecker::new();
3348 let result = checker.check_program(&program);
3349 assert!(result.is_err());
3350 let err = result.unwrap_err();
3351 assert!(err.contains("Unknown type 'InvalidType'"));
3352 assert!(err.contains("chan"));
3353 assert!(err.contains("Get"));
3354 assert!(err.contains("Message"));
3355 }
3356
3357 #[test]
3358 fn test_roll_inside_conditional_with_concrete_stack() {
3359 let program = Program {
3367 includes: vec![],
3368 unions: vec![],
3369 words: vec![WordDef {
3370 name: "test".to_string(),
3371 effect: Some(Effect::new(
3372 StackType::Empty
3373 .push(Type::Int)
3374 .push(Type::Int)
3375 .push(Type::Int)
3376 .push(Type::Int),
3377 StackType::Empty
3378 .push(Type::Int)
3379 .push(Type::Int)
3380 .push(Type::Int)
3381 .push(Type::Int),
3382 )),
3383 body: vec![
3384 Statement::WordCall {
3385 name: "dup".to_string(),
3386 span: None,
3387 },
3388 Statement::IntLiteral(0),
3389 Statement::WordCall {
3390 name: "i.>".to_string(),
3391 span: None,
3392 },
3393 Statement::If {
3394 then_branch: vec![
3395 Statement::IntLiteral(3),
3396 Statement::WordCall {
3397 name: "roll".to_string(),
3398 span: None,
3399 },
3400 ],
3401 else_branch: Some(vec![
3402 Statement::WordCall {
3403 name: "rot".to_string(),
3404 span: None,
3405 },
3406 Statement::WordCall {
3407 name: "rot".to_string(),
3408 span: None,
3409 },
3410 ]),
3411 span: None,
3412 },
3413 ],
3414 source: None,
3415 allowed_lints: vec![],
3416 }],
3417 };
3418
3419 let mut checker = TypeChecker::new();
3420 match checker.check_program(&program) {
3422 Ok(_) => {}
3423 Err(e) => panic!("Type check failed: {}", e),
3424 }
3425 }
3426
3427 #[test]
3428 fn test_roll_inside_match_arm_with_concrete_stack() {
3429 use crate::ast::{MatchArm, Pattern, UnionDef, UnionVariant};
3432
3433 let union_def = UnionDef {
3435 name: "Result".to_string(),
3436 variants: vec![
3437 UnionVariant {
3438 name: "Ok".to_string(),
3439 fields: vec![],
3440 source: None,
3441 },
3442 UnionVariant {
3443 name: "Err".to_string(),
3444 fields: vec![],
3445 source: None,
3446 },
3447 ],
3448 source: None,
3449 };
3450
3451 let program = Program {
3457 includes: vec![],
3458 unions: vec![union_def],
3459 words: vec![WordDef {
3460 name: "test".to_string(),
3461 effect: Some(Effect::new(
3462 StackType::Empty
3463 .push(Type::Int)
3464 .push(Type::Int)
3465 .push(Type::Int)
3466 .push(Type::Int)
3467 .push(Type::Union("Result".to_string())),
3468 StackType::Empty
3469 .push(Type::Int)
3470 .push(Type::Int)
3471 .push(Type::Int)
3472 .push(Type::Int),
3473 )),
3474 body: vec![Statement::Match {
3475 arms: vec![
3476 MatchArm {
3477 pattern: Pattern::Variant("Ok".to_string()),
3478 body: vec![
3479 Statement::IntLiteral(3),
3480 Statement::WordCall {
3481 name: "roll".to_string(),
3482 span: None,
3483 },
3484 ],
3485 span: None,
3486 },
3487 MatchArm {
3488 pattern: Pattern::Variant("Err".to_string()),
3489 body: vec![
3490 Statement::WordCall {
3491 name: "rot".to_string(),
3492 span: None,
3493 },
3494 Statement::WordCall {
3495 name: "rot".to_string(),
3496 span: None,
3497 },
3498 ],
3499 span: None,
3500 },
3501 ],
3502 span: None,
3503 }],
3504 source: None,
3505 allowed_lints: vec![],
3506 }],
3507 };
3508
3509 let mut checker = TypeChecker::new();
3510 match checker.check_program(&program) {
3511 Ok(_) => {}
3512 Err(e) => panic!("Type check failed: {}", e),
3513 }
3514 }
3515
3516 #[test]
3517 fn test_roll_with_row_polymorphic_input() {
3518 let program = Program {
3522 includes: vec![],
3523 unions: vec![],
3524 words: vec![WordDef {
3525 name: "test".to_string(),
3526 effect: Some(Effect::new(
3527 StackType::Empty
3528 .push(Type::Var("T".to_string()))
3529 .push(Type::Var("U".to_string()))
3530 .push(Type::Var("V".to_string()))
3531 .push(Type::Var("W".to_string())),
3532 StackType::Empty
3533 .push(Type::Var("U".to_string()))
3534 .push(Type::Var("V".to_string()))
3535 .push(Type::Var("W".to_string()))
3536 .push(Type::Var("T".to_string())),
3537 )),
3538 body: vec![
3539 Statement::IntLiteral(3),
3540 Statement::WordCall {
3541 name: "roll".to_string(),
3542 span: None,
3543 },
3544 ],
3545 source: None,
3546 allowed_lints: vec![],
3547 }],
3548 };
3549
3550 let mut checker = TypeChecker::new();
3551 let result = checker.check_program(&program);
3552 assert!(result.is_ok(), "roll test failed: {:?}", result.err());
3553 }
3554
3555 #[test]
3556 fn test_pick_with_row_polymorphic_input() {
3557 let program = Program {
3561 includes: vec![],
3562 unions: vec![],
3563 words: vec![WordDef {
3564 name: "test".to_string(),
3565 effect: Some(Effect::new(
3566 StackType::Empty
3567 .push(Type::Var("T".to_string()))
3568 .push(Type::Var("U".to_string()))
3569 .push(Type::Var("V".to_string())),
3570 StackType::Empty
3571 .push(Type::Var("T".to_string()))
3572 .push(Type::Var("U".to_string()))
3573 .push(Type::Var("V".to_string()))
3574 .push(Type::Var("T".to_string())),
3575 )),
3576 body: vec![
3577 Statement::IntLiteral(2),
3578 Statement::WordCall {
3579 name: "pick".to_string(),
3580 span: None,
3581 },
3582 ],
3583 source: None,
3584 allowed_lints: vec![],
3585 }],
3586 };
3587
3588 let mut checker = TypeChecker::new();
3589 assert!(checker.check_program(&program).is_ok());
3590 }
3591
3592 #[test]
3593 fn test_valid_union_reference_in_field() {
3594 use crate::ast::{UnionDef, UnionField, UnionVariant};
3595
3596 let program = Program {
3597 includes: vec![],
3598 unions: vec![
3599 UnionDef {
3600 name: "Inner".to_string(),
3601 variants: vec![UnionVariant {
3602 name: "Val".to_string(),
3603 fields: vec![UnionField {
3604 name: "x".to_string(),
3605 type_name: "Int".to_string(),
3606 }],
3607 source: None,
3608 }],
3609 source: None,
3610 },
3611 UnionDef {
3612 name: "Outer".to_string(),
3613 variants: vec![UnionVariant {
3614 name: "Wrap".to_string(),
3615 fields: vec![UnionField {
3616 name: "inner".to_string(),
3617 type_name: "Inner".to_string(), }],
3619 source: None,
3620 }],
3621 source: None,
3622 },
3623 ],
3624 words: vec![],
3625 };
3626
3627 let mut checker = TypeChecker::new();
3628 assert!(
3629 checker.check_program(&program).is_ok(),
3630 "Union reference in field should be valid"
3631 );
3632 }
3633
3634 #[test]
3635 fn test_divergent_recursive_tail_call() {
3636 let program = Program {
3658 includes: vec![],
3659 unions: vec![],
3660 words: vec![WordDef {
3661 name: "store-loop".to_string(),
3662 effect: Some(Effect::new(
3663 StackType::singleton(Type::Channel), StackType::Empty,
3665 )),
3666 body: vec![
3667 Statement::WordCall {
3669 name: "dup".to_string(),
3670 span: None,
3671 },
3672 Statement::WordCall {
3674 name: "chan.receive".to_string(),
3675 span: None,
3676 },
3677 Statement::WordCall {
3679 name: "not".to_string(),
3680 span: None,
3681 },
3682 Statement::If {
3684 then_branch: vec![
3685 Statement::WordCall {
3687 name: "drop".to_string(),
3688 span: None,
3689 },
3690 Statement::WordCall {
3692 name: "store-loop".to_string(), span: None,
3694 },
3695 ],
3696 else_branch: None, span: None,
3698 },
3699 Statement::WordCall {
3701 name: "drop".to_string(),
3702 span: None,
3703 },
3704 Statement::WordCall {
3705 name: "drop".to_string(),
3706 span: None,
3707 },
3708 ],
3709 source: None,
3710 allowed_lints: vec![],
3711 }],
3712 };
3713
3714 let mut checker = TypeChecker::new();
3715 let result = checker.check_program(&program);
3716 assert!(
3717 result.is_ok(),
3718 "Divergent recursive tail call should be accepted: {:?}",
3719 result.err()
3720 );
3721 }
3722
3723 #[test]
3724 fn test_divergent_else_branch() {
3725 let program = Program {
3738 includes: vec![],
3739 unions: vec![],
3740 words: vec![WordDef {
3741 name: "process-loop".to_string(),
3742 effect: Some(Effect::new(
3743 StackType::singleton(Type::Channel), StackType::Empty,
3745 )),
3746 body: vec![
3747 Statement::WordCall {
3748 name: "dup".to_string(),
3749 span: None,
3750 },
3751 Statement::WordCall {
3752 name: "chan.receive".to_string(),
3753 span: None,
3754 },
3755 Statement::If {
3756 then_branch: vec![
3757 Statement::WordCall {
3759 name: "drop".to_string(),
3760 span: None,
3761 },
3762 Statement::WordCall {
3763 name: "drop".to_string(),
3764 span: None,
3765 },
3766 ],
3767 else_branch: Some(vec![
3768 Statement::WordCall {
3770 name: "drop".to_string(),
3771 span: None,
3772 },
3773 Statement::WordCall {
3774 name: "process-loop".to_string(), span: None,
3776 },
3777 ]),
3778 span: None,
3779 },
3780 ],
3781 source: None,
3782 allowed_lints: vec![],
3783 }],
3784 };
3785
3786 let mut checker = TypeChecker::new();
3787 let result = checker.check_program(&program);
3788 assert!(
3789 result.is_ok(),
3790 "Divergent else branch should be accepted: {:?}",
3791 result.err()
3792 );
3793 }
3794
3795 #[test]
3796 fn test_non_tail_call_recursion_not_divergent() {
3797 let program = Program {
3814 includes: vec![],
3815 unions: vec![],
3816 words: vec![WordDef {
3817 name: "bad-loop".to_string(),
3818 effect: Some(Effect::new(
3819 StackType::singleton(Type::Int),
3820 StackType::singleton(Type::Int),
3821 )),
3822 body: vec![
3823 Statement::WordCall {
3824 name: "dup".to_string(),
3825 span: None,
3826 },
3827 Statement::IntLiteral(0),
3828 Statement::WordCall {
3829 name: "i.>".to_string(),
3830 span: None,
3831 },
3832 Statement::If {
3833 then_branch: vec![
3834 Statement::IntLiteral(1),
3835 Statement::WordCall {
3836 name: "i.subtract".to_string(),
3837 span: None,
3838 },
3839 Statement::WordCall {
3840 name: "bad-loop".to_string(), span: None,
3842 },
3843 Statement::IntLiteral(1),
3844 Statement::WordCall {
3845 name: "i.add".to_string(), span: None,
3847 },
3848 ],
3849 else_branch: None,
3850 span: None,
3851 },
3852 ],
3853 source: None,
3854 allowed_lints: vec![],
3855 }],
3856 };
3857
3858 let mut checker = TypeChecker::new();
3859 let result = checker.check_program(&program);
3864 assert!(
3865 result.is_ok(),
3866 "Non-tail recursion should type check normally: {:?}",
3867 result.err()
3868 );
3869 }
3870
3871 #[test]
3872 fn test_call_yield_quotation_error() {
3873 let program = Program {
3877 includes: vec![],
3878 unions: vec![],
3879 words: vec![WordDef {
3880 name: "bad".to_string(),
3881 effect: Some(Effect::new(
3882 StackType::singleton(Type::Var("Ctx".to_string())),
3883 StackType::singleton(Type::Var("Ctx".to_string())),
3884 )),
3885 body: vec![
3886 Statement::IntLiteral(42),
3888 Statement::Quotation {
3889 span: None,
3890 id: 0,
3891 body: vec![Statement::WordCall {
3892 name: "yield".to_string(),
3893 span: None,
3894 }],
3895 },
3896 Statement::WordCall {
3897 name: "call".to_string(),
3898 span: None,
3899 },
3900 ],
3901 source: None,
3902 allowed_lints: vec![],
3903 }],
3904 };
3905
3906 let mut checker = TypeChecker::new();
3907 let result = checker.check_program(&program);
3908 assert!(
3909 result.is_err(),
3910 "Calling yield quotation directly should fail"
3911 );
3912 let err = result.unwrap_err();
3913 assert!(
3914 err.contains("Yield") || err.contains("strand.weave"),
3915 "Error should mention Yield or strand.weave: {}",
3916 err
3917 );
3918 }
3919
3920 #[test]
3921 fn test_strand_weave_yield_quotation_ok() {
3922 let program = Program {
3925 includes: vec![],
3926 unions: vec![],
3927 words: vec![WordDef {
3928 name: "good".to_string(),
3929 effect: Some(Effect::new(
3930 StackType::Empty,
3931 StackType::Empty
3932 .push(Type::Int)
3933 .push(Type::Var("Handle".to_string())),
3934 )),
3935 body: vec![
3936 Statement::IntLiteral(42),
3937 Statement::Quotation {
3938 span: None,
3939 id: 0,
3940 body: vec![Statement::WordCall {
3941 name: "yield".to_string(),
3942 span: None,
3943 }],
3944 },
3945 Statement::WordCall {
3946 name: "strand.weave".to_string(),
3947 span: None,
3948 },
3949 ],
3950 source: None,
3951 allowed_lints: vec![],
3952 }],
3953 };
3954
3955 let mut checker = TypeChecker::new();
3956 let result = checker.check_program(&program);
3957 assert!(
3958 result.is_ok(),
3959 "strand.weave on yield quotation should pass: {:?}",
3960 result.err()
3961 );
3962 }
3963
3964 #[test]
3965 fn test_call_pure_quotation_ok() {
3966 let program = Program {
3969 includes: vec![],
3970 unions: vec![],
3971 words: vec![WordDef {
3972 name: "ok".to_string(),
3973 effect: Some(Effect::new(
3974 StackType::singleton(Type::Int),
3975 StackType::singleton(Type::Int),
3976 )),
3977 body: vec![
3978 Statement::Quotation {
3979 span: None,
3980 id: 0,
3981 body: vec![
3982 Statement::IntLiteral(1),
3983 Statement::WordCall {
3984 name: "i.add".to_string(),
3985 span: None,
3986 },
3987 ],
3988 },
3989 Statement::WordCall {
3990 name: "call".to_string(),
3991 span: None,
3992 },
3993 ],
3994 source: None,
3995 allowed_lints: vec![],
3996 }],
3997 };
3998
3999 let mut checker = TypeChecker::new();
4000 let result = checker.check_program(&program);
4001 assert!(
4002 result.is_ok(),
4003 "Calling pure quotation should pass: {:?}",
4004 result.err()
4005 );
4006 }
4007
4008 #[test]
4014 fn test_pollution_extra_push() {
4015 let program = Program {
4019 includes: vec![],
4020 unions: vec![],
4021 words: vec![WordDef {
4022 name: "test".to_string(),
4023 effect: Some(Effect::new(
4024 StackType::singleton(Type::Int),
4025 StackType::singleton(Type::Int),
4026 )),
4027 body: vec![Statement::IntLiteral(42)],
4028 source: None,
4029 allowed_lints: vec![],
4030 }],
4031 };
4032
4033 let mut checker = TypeChecker::new();
4034 let result = checker.check_program(&program);
4035 assert!(
4036 result.is_err(),
4037 "Should reject: declares ( Int -- Int ) but leaves 2 values on stack"
4038 );
4039 }
4040
4041 #[test]
4042 fn test_pollution_extra_dup() {
4043 let program = Program {
4046 includes: vec![],
4047 unions: vec![],
4048 words: vec![WordDef {
4049 name: "test".to_string(),
4050 effect: Some(Effect::new(
4051 StackType::singleton(Type::Int),
4052 StackType::singleton(Type::Int),
4053 )),
4054 body: vec![Statement::WordCall {
4055 name: "dup".to_string(),
4056 span: None,
4057 }],
4058 source: None,
4059 allowed_lints: vec![],
4060 }],
4061 };
4062
4063 let mut checker = TypeChecker::new();
4064 let result = checker.check_program(&program);
4065 assert!(
4066 result.is_err(),
4067 "Should reject: declares ( Int -- Int ) but dup produces 2 values"
4068 );
4069 }
4070
4071 #[test]
4072 fn test_pollution_consumes_extra() {
4073 let program = Program {
4076 includes: vec![],
4077 unions: vec![],
4078 words: vec![WordDef {
4079 name: "test".to_string(),
4080 effect: Some(Effect::new(
4081 StackType::singleton(Type::Int),
4082 StackType::singleton(Type::Int),
4083 )),
4084 body: vec![
4085 Statement::WordCall {
4086 name: "drop".to_string(),
4087 span: None,
4088 },
4089 Statement::WordCall {
4090 name: "drop".to_string(),
4091 span: None,
4092 },
4093 Statement::IntLiteral(42),
4094 ],
4095 source: None,
4096 allowed_lints: vec![],
4097 }],
4098 };
4099
4100 let mut checker = TypeChecker::new();
4101 let result = checker.check_program(&program);
4102 assert!(
4103 result.is_err(),
4104 "Should reject: declares ( Int -- Int ) but consumes 2 values"
4105 );
4106 }
4107
4108 #[test]
4109 fn test_pollution_in_then_branch() {
4110 let program = Program {
4114 includes: vec![],
4115 unions: vec![],
4116 words: vec![WordDef {
4117 name: "test".to_string(),
4118 effect: Some(Effect::new(
4119 StackType::singleton(Type::Bool),
4120 StackType::singleton(Type::Int),
4121 )),
4122 body: vec![Statement::If {
4123 then_branch: vec![
4124 Statement::IntLiteral(1),
4125 Statement::IntLiteral(2), ],
4127 else_branch: Some(vec![Statement::IntLiteral(3)]),
4128 span: None,
4129 }],
4130 source: None,
4131 allowed_lints: vec![],
4132 }],
4133 };
4134
4135 let mut checker = TypeChecker::new();
4136 let result = checker.check_program(&program);
4137 assert!(
4138 result.is_err(),
4139 "Should reject: then branch pushes 2 values, else pushes 1"
4140 );
4141 }
4142
4143 #[test]
4144 fn test_pollution_in_else_branch() {
4145 let program = Program {
4149 includes: vec![],
4150 unions: vec![],
4151 words: vec![WordDef {
4152 name: "test".to_string(),
4153 effect: Some(Effect::new(
4154 StackType::singleton(Type::Bool),
4155 StackType::singleton(Type::Int),
4156 )),
4157 body: vec![Statement::If {
4158 then_branch: vec![Statement::IntLiteral(1)],
4159 else_branch: Some(vec![
4160 Statement::IntLiteral(2),
4161 Statement::IntLiteral(3), ]),
4163 span: None,
4164 }],
4165 source: None,
4166 allowed_lints: vec![],
4167 }],
4168 };
4169
4170 let mut checker = TypeChecker::new();
4171 let result = checker.check_program(&program);
4172 assert!(
4173 result.is_err(),
4174 "Should reject: then branch pushes 1 value, else pushes 2"
4175 );
4176 }
4177
4178 #[test]
4179 fn test_pollution_both_branches_extra() {
4180 let program = Program {
4184 includes: vec![],
4185 unions: vec![],
4186 words: vec![WordDef {
4187 name: "test".to_string(),
4188 effect: Some(Effect::new(
4189 StackType::singleton(Type::Bool),
4190 StackType::singleton(Type::Int),
4191 )),
4192 body: vec![Statement::If {
4193 then_branch: vec![Statement::IntLiteral(1), Statement::IntLiteral(2)],
4194 else_branch: Some(vec![Statement::IntLiteral(3), Statement::IntLiteral(4)]),
4195 span: None,
4196 }],
4197 source: None,
4198 allowed_lints: vec![],
4199 }],
4200 };
4201
4202 let mut checker = TypeChecker::new();
4203 let result = checker.check_program(&program);
4204 assert!(
4205 result.is_err(),
4206 "Should reject: both branches push 2 values, but declared output is 1"
4207 );
4208 }
4209
4210 #[test]
4211 fn test_pollution_branch_consumes_extra() {
4212 let program = Program {
4216 includes: vec![],
4217 unions: vec![],
4218 words: vec![WordDef {
4219 name: "test".to_string(),
4220 effect: Some(Effect::new(
4221 StackType::Empty.push(Type::Bool).push(Type::Int),
4222 StackType::singleton(Type::Int),
4223 )),
4224 body: vec![Statement::If {
4225 then_branch: vec![
4226 Statement::WordCall {
4227 name: "drop".to_string(),
4228 span: None,
4229 },
4230 Statement::WordCall {
4231 name: "drop".to_string(),
4232 span: None,
4233 },
4234 Statement::IntLiteral(1),
4235 ],
4236 else_branch: Some(vec![]),
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: then branch consumes Bool (should only have Int after if)"
4249 );
4250 }
4251
4252 #[test]
4253 fn test_pollution_quotation_wrong_arity_output() {
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::singleton(Type::Int),
4264 StackType::singleton(Type::Int),
4265 )),
4266 body: vec![
4267 Statement::Quotation {
4268 span: None,
4269 id: 0,
4270 body: vec![Statement::WordCall {
4271 name: "dup".to_string(),
4272 span: None,
4273 }],
4274 },
4275 Statement::WordCall {
4276 name: "call".to_string(),
4277 span: None,
4278 },
4279 ],
4280 source: None,
4281 allowed_lints: vec![],
4282 }],
4283 };
4284
4285 let mut checker = TypeChecker::new();
4286 let result = checker.check_program(&program);
4287 assert!(
4288 result.is_err(),
4289 "Should reject: quotation [dup] produces 2 values, declared output is 1"
4290 );
4291 }
4292
4293 #[test]
4294 fn test_pollution_quotation_wrong_arity_input() {
4295 let program = Program {
4299 includes: vec![],
4300 unions: vec![],
4301 words: vec![WordDef {
4302 name: "test".to_string(),
4303 effect: Some(Effect::new(
4304 StackType::singleton(Type::Int),
4305 StackType::singleton(Type::Int),
4306 )),
4307 body: vec![
4308 Statement::Quotation {
4309 span: None,
4310 id: 0,
4311 body: vec![
4312 Statement::WordCall {
4313 name: "drop".to_string(),
4314 span: None,
4315 },
4316 Statement::WordCall {
4317 name: "drop".to_string(),
4318 span: None,
4319 },
4320 Statement::IntLiteral(42),
4321 ],
4322 },
4323 Statement::WordCall {
4324 name: "call".to_string(),
4325 span: None,
4326 },
4327 ],
4328 source: None,
4329 allowed_lints: vec![],
4330 }],
4331 };
4332
4333 let mut checker = TypeChecker::new();
4334 let result = checker.check_program(&program);
4335 assert!(
4336 result.is_err(),
4337 "Should reject: quotation [drop drop 42] consumes 2 values, only 1 available"
4338 );
4339 }
4340
4341 #[test]
4342 fn test_missing_effect_provides_helpful_error() {
4343 let program = Program {
4346 includes: vec![],
4347 unions: vec![],
4348 words: vec![WordDef {
4349 name: "myword".to_string(),
4350 effect: None, body: vec![Statement::IntLiteral(42)],
4352 source: None,
4353 allowed_lints: vec![],
4354 }],
4355 };
4356
4357 let mut checker = TypeChecker::new();
4358 let result = checker.check_program(&program);
4359 assert!(result.is_err());
4360 let err = result.unwrap_err();
4361 assert!(err.contains("myword"), "Error should mention word name");
4362 assert!(
4363 err.contains("stack effect"),
4364 "Error should mention stack effect"
4365 );
4366 }
4367
4368 #[test]
4369 fn test_valid_effect_exact_match() {
4370 let program = Program {
4373 includes: vec![],
4374 unions: vec![],
4375 words: vec![WordDef {
4376 name: "test".to_string(),
4377 effect: Some(Effect::new(
4378 StackType::Empty.push(Type::Int).push(Type::Int),
4379 StackType::singleton(Type::Int),
4380 )),
4381 body: vec![Statement::WordCall {
4382 name: "i.add".to_string(),
4383 span: None,
4384 }],
4385 source: None,
4386 allowed_lints: vec![],
4387 }],
4388 };
4389
4390 let mut checker = TypeChecker::new();
4391 let result = checker.check_program(&program);
4392 assert!(result.is_ok(), "Should accept: effect matches exactly");
4393 }
4394
4395 #[test]
4396 fn test_valid_polymorphic_passthrough() {
4397 let program = Program {
4400 includes: vec![],
4401 unions: vec![],
4402 words: vec![WordDef {
4403 name: "test".to_string(),
4404 effect: Some(Effect::new(
4405 StackType::Cons {
4406 rest: Box::new(StackType::RowVar("rest".to_string())),
4407 top: Type::Var("a".to_string()),
4408 },
4409 StackType::Cons {
4410 rest: Box::new(StackType::RowVar("rest".to_string())),
4411 top: Type::Var("a".to_string()),
4412 },
4413 )),
4414 body: vec![], source: None,
4416 allowed_lints: vec![],
4417 }],
4418 };
4419
4420 let mut checker = TypeChecker::new();
4421 let result = checker.check_program(&program);
4422 assert!(result.is_ok(), "Should accept: polymorphic identity");
4423 }
4424
4425 #[test]
4431 fn test_closure_basic_capture() {
4432 let program = Program {
4438 includes: vec![],
4439 unions: vec![],
4440 words: vec![WordDef {
4441 name: "make-adder".to_string(),
4442 effect: Some(Effect::new(
4443 StackType::singleton(Type::Int),
4444 StackType::singleton(Type::Closure {
4445 effect: Box::new(Effect::new(
4446 StackType::RowVar("r".to_string()).push(Type::Int),
4447 StackType::RowVar("r".to_string()).push(Type::Int),
4448 )),
4449 captures: vec![Type::Int], }),
4451 )),
4452 body: vec![Statement::Quotation {
4453 span: None,
4454 id: 0,
4455 body: vec![Statement::WordCall {
4456 name: "i.add".to_string(),
4457 span: None,
4458 }],
4459 }],
4460 source: None,
4461 allowed_lints: vec![],
4462 }],
4463 };
4464
4465 let mut checker = TypeChecker::new();
4466 let result = checker.check_program(&program);
4467 assert!(
4468 result.is_ok(),
4469 "Basic closure capture should work: {:?}",
4470 result.err()
4471 );
4472 }
4473
4474 #[test]
4475 fn test_closure_nested_two_levels() {
4476 let program = Program {
4481 includes: vec![],
4482 unions: vec![],
4483 words: vec![WordDef {
4484 name: "outer".to_string(),
4485 effect: Some(Effect::new(
4486 StackType::Empty,
4487 StackType::singleton(Type::Quotation(Box::new(Effect::new(
4488 StackType::RowVar("r".to_string()),
4489 StackType::RowVar("r".to_string()).push(Type::Quotation(Box::new(
4490 Effect::new(
4491 StackType::RowVar("s".to_string()).push(Type::Int),
4492 StackType::RowVar("s".to_string()).push(Type::Int),
4493 ),
4494 ))),
4495 )))),
4496 )),
4497 body: vec![Statement::Quotation {
4498 span: None,
4499 id: 0,
4500 body: vec![Statement::Quotation {
4501 span: None,
4502 id: 1,
4503 body: vec![
4504 Statement::IntLiteral(1),
4505 Statement::WordCall {
4506 name: "i.add".to_string(),
4507 span: None,
4508 },
4509 ],
4510 }],
4511 }],
4512 source: None,
4513 allowed_lints: vec![],
4514 }],
4515 };
4516
4517 let mut checker = TypeChecker::new();
4518 let result = checker.check_program(&program);
4519 assert!(
4520 result.is_ok(),
4521 "Two-level nested quotations should work: {:?}",
4522 result.err()
4523 );
4524 }
4525
4526 #[test]
4527 fn test_closure_nested_three_levels() {
4528 let inner_effect = Effect::new(
4532 StackType::RowVar("a".to_string()).push(Type::Int),
4533 StackType::RowVar("a".to_string()).push(Type::Int),
4534 );
4535 let middle_effect = Effect::new(
4536 StackType::RowVar("b".to_string()),
4537 StackType::RowVar("b".to_string()).push(Type::Quotation(Box::new(inner_effect))),
4538 );
4539 let outer_effect = Effect::new(
4540 StackType::RowVar("c".to_string()),
4541 StackType::RowVar("c".to_string()).push(Type::Quotation(Box::new(middle_effect))),
4542 );
4543
4544 let program = Program {
4545 includes: vec![],
4546 unions: vec![],
4547 words: vec![WordDef {
4548 name: "deep".to_string(),
4549 effect: Some(Effect::new(
4550 StackType::Empty,
4551 StackType::singleton(Type::Quotation(Box::new(outer_effect))),
4552 )),
4553 body: vec![Statement::Quotation {
4554 span: None,
4555 id: 0,
4556 body: vec![Statement::Quotation {
4557 span: None,
4558 id: 1,
4559 body: vec![Statement::Quotation {
4560 span: None,
4561 id: 2,
4562 body: vec![
4563 Statement::IntLiteral(1),
4564 Statement::WordCall {
4565 name: "i.add".to_string(),
4566 span: None,
4567 },
4568 ],
4569 }],
4570 }],
4571 }],
4572 source: None,
4573 allowed_lints: vec![],
4574 }],
4575 };
4576
4577 let mut checker = TypeChecker::new();
4578 let result = checker.check_program(&program);
4579 assert!(
4580 result.is_ok(),
4581 "Three-level nested quotations should work: {:?}",
4582 result.err()
4583 );
4584 }
4585
4586 #[test]
4587 fn test_closure_use_after_creation() {
4588 let adder_type = Type::Closure {
4594 effect: Box::new(Effect::new(
4595 StackType::RowVar("r".to_string()).push(Type::Int),
4596 StackType::RowVar("r".to_string()).push(Type::Int),
4597 )),
4598 captures: vec![Type::Int],
4599 };
4600
4601 let program = Program {
4602 includes: vec![],
4603 unions: vec![],
4604 words: vec![
4605 WordDef {
4606 name: "make-adder".to_string(),
4607 effect: Some(Effect::new(
4608 StackType::singleton(Type::Int),
4609 StackType::singleton(adder_type.clone()),
4610 )),
4611 body: vec![Statement::Quotation {
4612 span: None,
4613 id: 0,
4614 body: vec![Statement::WordCall {
4615 name: "i.add".to_string(),
4616 span: None,
4617 }],
4618 }],
4619 source: None,
4620 allowed_lints: vec![],
4621 },
4622 WordDef {
4623 name: "use-adder".to_string(),
4624 effect: Some(Effect::new(
4625 StackType::Empty,
4626 StackType::singleton(Type::Int),
4627 )),
4628 body: vec![
4629 Statement::IntLiteral(5),
4630 Statement::WordCall {
4631 name: "make-adder".to_string(),
4632 span: None,
4633 },
4634 Statement::IntLiteral(10),
4635 Statement::WordCall {
4636 name: "swap".to_string(),
4637 span: None,
4638 },
4639 Statement::WordCall {
4640 name: "call".to_string(),
4641 span: None,
4642 },
4643 ],
4644 source: None,
4645 allowed_lints: vec![],
4646 },
4647 ],
4648 };
4649
4650 let mut checker = TypeChecker::new();
4651 let result = checker.check_program(&program);
4652 assert!(
4653 result.is_ok(),
4654 "Closure usage after creation should work: {:?}",
4655 result.err()
4656 );
4657 }
4658
4659 #[test]
4660 fn test_closure_wrong_call_type() {
4661 let adder_type = Type::Closure {
4665 effect: Box::new(Effect::new(
4666 StackType::RowVar("r".to_string()).push(Type::Int),
4667 StackType::RowVar("r".to_string()).push(Type::Int),
4668 )),
4669 captures: vec![Type::Int],
4670 };
4671
4672 let program = Program {
4673 includes: vec![],
4674 unions: vec![],
4675 words: vec![
4676 WordDef {
4677 name: "make-adder".to_string(),
4678 effect: Some(Effect::new(
4679 StackType::singleton(Type::Int),
4680 StackType::singleton(adder_type.clone()),
4681 )),
4682 body: vec![Statement::Quotation {
4683 span: None,
4684 id: 0,
4685 body: vec![Statement::WordCall {
4686 name: "i.add".to_string(),
4687 span: None,
4688 }],
4689 }],
4690 source: None,
4691 allowed_lints: vec![],
4692 },
4693 WordDef {
4694 name: "bad-use".to_string(),
4695 effect: Some(Effect::new(
4696 StackType::Empty,
4697 StackType::singleton(Type::Int),
4698 )),
4699 body: vec![
4700 Statement::IntLiteral(5),
4701 Statement::WordCall {
4702 name: "make-adder".to_string(),
4703 span: None,
4704 },
4705 Statement::StringLiteral("hello".to_string()), Statement::WordCall {
4707 name: "swap".to_string(),
4708 span: None,
4709 },
4710 Statement::WordCall {
4711 name: "call".to_string(),
4712 span: None,
4713 },
4714 ],
4715 source: None,
4716 allowed_lints: vec![],
4717 },
4718 ],
4719 };
4720
4721 let mut checker = TypeChecker::new();
4722 let result = checker.check_program(&program);
4723 assert!(
4724 result.is_err(),
4725 "Calling Int closure with String should fail"
4726 );
4727 }
4728
4729 #[test]
4730 fn test_closure_multiple_captures() {
4731 let program = Program {
4738 includes: vec![],
4739 unions: vec![],
4740 words: vec![WordDef {
4741 name: "make-between".to_string(),
4742 effect: Some(Effect::new(
4743 StackType::Empty.push(Type::Int).push(Type::Int),
4744 StackType::singleton(Type::Quotation(Box::new(Effect::new(
4745 StackType::RowVar("r".to_string()).push(Type::Int),
4746 StackType::RowVar("r".to_string()).push(Type::Bool),
4747 )))),
4748 )),
4749 body: vec![Statement::Quotation {
4750 span: None,
4751 id: 0,
4752 body: vec![
4753 Statement::WordCall {
4755 name: "i.>=".to_string(),
4756 span: None,
4757 },
4758 ],
4760 }],
4761 source: None,
4762 allowed_lints: vec![],
4763 }],
4764 };
4765
4766 let mut checker = TypeChecker::new();
4767 let result = checker.check_program(&program);
4768 assert!(
4772 result.is_ok() || result.is_err(),
4773 "Multiple captures should be handled (pass or fail gracefully)"
4774 );
4775 }
4776
4777 #[test]
4778 fn test_quotation_type_preserved_through_word() {
4779 let quot_type = Type::Quotation(Box::new(Effect::new(
4782 StackType::RowVar("r".to_string()).push(Type::Int),
4783 StackType::RowVar("r".to_string()).push(Type::Int),
4784 )));
4785
4786 let program = Program {
4787 includes: vec![],
4788 unions: vec![],
4789 words: vec![WordDef {
4790 name: "identity-quot".to_string(),
4791 effect: Some(Effect::new(
4792 StackType::singleton(quot_type.clone()),
4793 StackType::singleton(quot_type.clone()),
4794 )),
4795 body: vec![], source: None,
4797 allowed_lints: vec![],
4798 }],
4799 };
4800
4801 let mut checker = TypeChecker::new();
4802 let result = checker.check_program(&program);
4803 assert!(
4804 result.is_ok(),
4805 "Quotation type should be preserved through identity word: {:?}",
4806 result.err()
4807 );
4808 }
4809
4810 #[test]
4811 fn test_closure_captures_value_for_inner_quotation() {
4812 let closure_effect = Effect::new(
4818 StackType::RowVar("r".to_string()).push(Type::Int),
4819 StackType::RowVar("r".to_string()).push(Type::Int),
4820 );
4821
4822 let program = Program {
4823 includes: vec![],
4824 unions: vec![],
4825 words: vec![WordDef {
4826 name: "make-inner-adder".to_string(),
4827 effect: Some(Effect::new(
4828 StackType::singleton(Type::Int),
4829 StackType::singleton(Type::Closure {
4830 effect: Box::new(closure_effect),
4831 captures: vec![Type::Int],
4832 }),
4833 )),
4834 body: vec![Statement::Quotation {
4835 span: None,
4836 id: 0,
4837 body: vec![
4838 Statement::WordCall {
4840 name: "i.add".to_string(),
4841 span: None,
4842 },
4843 ],
4844 }],
4845 source: None,
4846 allowed_lints: vec![],
4847 }],
4848 };
4849
4850 let mut checker = TypeChecker::new();
4851 let result = checker.check_program(&program);
4852 assert!(
4853 result.is_ok(),
4854 "Closure with capture for inner work should pass: {:?}",
4855 result.err()
4856 );
4857 }
4858
4859 #[test]
4860 fn test_union_type_mismatch_should_fail() {
4861 use crate::ast::{UnionDef, UnionField, UnionVariant};
4864
4865 let mut program = Program {
4866 includes: vec![],
4867 unions: vec![
4868 UnionDef {
4869 name: "UnionA".to_string(),
4870 variants: vec![UnionVariant {
4871 name: "AVal".to_string(),
4872 fields: vec![UnionField {
4873 name: "x".to_string(),
4874 type_name: "Int".to_string(),
4875 }],
4876 source: None,
4877 }],
4878 source: None,
4879 },
4880 UnionDef {
4881 name: "UnionB".to_string(),
4882 variants: vec![UnionVariant {
4883 name: "BVal".to_string(),
4884 fields: vec![UnionField {
4885 name: "y".to_string(),
4886 type_name: "Int".to_string(),
4887 }],
4888 source: None,
4889 }],
4890 source: None,
4891 },
4892 ],
4893 words: vec![
4894 WordDef {
4896 name: "takes-a".to_string(),
4897 effect: Some(Effect::new(
4898 StackType::RowVar("rest".to_string())
4899 .push(Type::Union("UnionA".to_string())),
4900 StackType::RowVar("rest".to_string()),
4901 )),
4902 body: vec![Statement::WordCall {
4903 name: "drop".to_string(),
4904 span: None,
4905 }],
4906 source: None,
4907 allowed_lints: vec![],
4908 },
4909 WordDef {
4912 name: "main".to_string(),
4913 effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
4914 body: vec![
4915 Statement::IntLiteral(99),
4916 Statement::WordCall {
4917 name: "Make-BVal".to_string(),
4918 span: None,
4919 },
4920 Statement::WordCall {
4921 name: "takes-a".to_string(),
4922 span: None,
4923 },
4924 ],
4925 source: None,
4926 allowed_lints: vec![],
4927 },
4928 ],
4929 };
4930
4931 program.generate_constructors().unwrap();
4933
4934 let mut checker = TypeChecker::new();
4935 let result = checker.check_program(&program);
4936
4937 assert!(
4939 result.is_err(),
4940 "Passing UnionB to function expecting UnionA should fail, but got: {:?}",
4941 result
4942 );
4943 let err = result.unwrap_err();
4944 assert!(
4945 err.contains("Union") || err.contains("mismatch"),
4946 "Error should mention union type mismatch, got: {}",
4947 err
4948 );
4949 }
4950
4951 fn make_word_call(name: &str) -> Statement {
4956 Statement::WordCall {
4957 name: name.to_string(),
4958 span: None,
4959 }
4960 }
4961
4962 #[test]
4963 fn test_aux_basic_round_trip() {
4964 let program = Program {
4966 includes: vec![],
4967 unions: vec![],
4968 words: vec![WordDef {
4969 name: "test".to_string(),
4970 effect: Some(Effect::new(
4971 StackType::singleton(Type::Int),
4972 StackType::singleton(Type::Int),
4973 )),
4974 body: vec![make_word_call(">aux"), make_word_call("aux>")],
4975 source: None,
4976 allowed_lints: vec![],
4977 }],
4978 };
4979
4980 let mut checker = TypeChecker::new();
4981 assert!(checker.check_program(&program).is_ok());
4982 }
4983
4984 #[test]
4985 fn test_aux_preserves_type() {
4986 let program = Program {
4988 includes: vec![],
4989 unions: vec![],
4990 words: vec![WordDef {
4991 name: "test".to_string(),
4992 effect: Some(Effect::new(
4993 StackType::singleton(Type::String),
4994 StackType::singleton(Type::String),
4995 )),
4996 body: vec![make_word_call(">aux"), make_word_call("aux>")],
4997 source: None,
4998 allowed_lints: vec![],
4999 }],
5000 };
5001
5002 let mut checker = TypeChecker::new();
5003 assert!(checker.check_program(&program).is_ok());
5004 }
5005
5006 #[test]
5007 fn test_aux_unbalanced_error() {
5008 let program = Program {
5010 includes: vec![],
5011 unions: vec![],
5012 words: vec![WordDef {
5013 name: "test".to_string(),
5014 effect: Some(Effect::new(
5015 StackType::singleton(Type::Int),
5016 StackType::Empty,
5017 )),
5018 body: vec![make_word_call(">aux")],
5019 source: None,
5020 allowed_lints: vec![],
5021 }],
5022 };
5023
5024 let mut checker = TypeChecker::new();
5025 let result = checker.check_program(&program);
5026 assert!(result.is_err());
5027 let err = result.unwrap_err();
5028 assert!(
5029 err.contains("aux stack is not empty"),
5030 "Expected aux stack balance error, got: {}",
5031 err
5032 );
5033 }
5034
5035 #[test]
5036 fn test_aux_pop_empty_error() {
5037 let program = Program {
5039 includes: vec![],
5040 unions: vec![],
5041 words: vec![WordDef {
5042 name: "test".to_string(),
5043 effect: Some(Effect::new(
5044 StackType::Empty,
5045 StackType::singleton(Type::Int),
5046 )),
5047 body: vec![make_word_call("aux>")],
5048 source: None,
5049 allowed_lints: vec![],
5050 }],
5051 };
5052
5053 let mut checker = TypeChecker::new();
5054 let result = checker.check_program(&program);
5055 assert!(result.is_err());
5056 let err = result.unwrap_err();
5057 assert!(
5058 err.contains("aux stack is empty"),
5059 "Expected aux empty error, got: {}",
5060 err
5061 );
5062 }
5063
5064 #[test]
5065 fn test_aux_multiple_values() {
5066 let program = Program {
5070 includes: vec![],
5071 unions: vec![],
5072 words: vec![WordDef {
5073 name: "test".to_string(),
5074 effect: Some(Effect::new(
5075 StackType::Empty.push(Type::Int).push(Type::String),
5076 StackType::Empty.push(Type::Int).push(Type::String),
5077 )),
5078 body: vec![
5079 make_word_call(">aux"),
5080 make_word_call(">aux"),
5081 make_word_call("aux>"),
5082 make_word_call("aux>"),
5083 ],
5084 source: None,
5085 allowed_lints: vec![],
5086 }],
5087 };
5088
5089 let mut checker = TypeChecker::new();
5090 assert!(checker.check_program(&program).is_ok());
5091 }
5092
5093 #[test]
5094 fn test_aux_max_depths_tracked() {
5095 let program = Program {
5097 includes: vec![],
5098 unions: vec![],
5099 words: vec![WordDef {
5100 name: "test".to_string(),
5101 effect: Some(Effect::new(
5102 StackType::singleton(Type::Int),
5103 StackType::singleton(Type::Int),
5104 )),
5105 body: vec![make_word_call(">aux"), make_word_call("aux>")],
5106 source: None,
5107 allowed_lints: vec![],
5108 }],
5109 };
5110
5111 let mut checker = TypeChecker::new();
5112 checker.check_program(&program).unwrap();
5113 let depths = checker.take_aux_max_depths();
5114 assert_eq!(depths.get("test"), Some(&1));
5115 }
5116
5117 #[test]
5118 fn test_aux_in_match_balanced() {
5119 use crate::ast::{MatchArm, Pattern, UnionDef, UnionVariant};
5121
5122 let union_def = UnionDef {
5123 name: "Choice".to_string(),
5124 variants: vec![
5125 UnionVariant {
5126 name: "Left".to_string(),
5127 fields: vec![],
5128 source: None,
5129 },
5130 UnionVariant {
5131 name: "Right".to_string(),
5132 fields: vec![],
5133 source: None,
5134 },
5135 ],
5136 source: None,
5137 };
5138
5139 let program = Program {
5142 includes: vec![],
5143 unions: vec![union_def],
5144 words: vec![WordDef {
5145 name: "test".to_string(),
5146 effect: Some(Effect::new(
5147 StackType::Empty
5148 .push(Type::Int)
5149 .push(Type::Union("Choice".to_string())),
5150 StackType::singleton(Type::Int),
5151 )),
5152 body: vec![
5153 make_word_call("swap"),
5154 make_word_call(">aux"),
5155 Statement::Match {
5156 arms: vec![
5157 MatchArm {
5158 pattern: Pattern::Variant("Left".to_string()),
5159 body: vec![make_word_call("aux>")],
5160 span: None,
5161 },
5162 MatchArm {
5163 pattern: Pattern::Variant("Right".to_string()),
5164 body: vec![make_word_call("aux>")],
5165 span: None,
5166 },
5167 ],
5168 span: None,
5169 },
5170 ],
5171 source: None,
5172 allowed_lints: vec![],
5173 }],
5174 };
5175
5176 let mut checker = TypeChecker::new();
5177 assert!(checker.check_program(&program).is_ok());
5178 }
5179
5180 #[test]
5181 fn test_aux_in_match_unbalanced_error() {
5182 use crate::ast::{MatchArm, Pattern, UnionDef, UnionVariant};
5184
5185 let union_def = UnionDef {
5186 name: "Choice".to_string(),
5187 variants: vec![
5188 UnionVariant {
5189 name: "Left".to_string(),
5190 fields: vec![],
5191 source: None,
5192 },
5193 UnionVariant {
5194 name: "Right".to_string(),
5195 fields: vec![],
5196 source: None,
5197 },
5198 ],
5199 source: None,
5200 };
5201
5202 let program = Program {
5205 includes: vec![],
5206 unions: vec![union_def],
5207 words: vec![WordDef {
5208 name: "test".to_string(),
5209 effect: Some(Effect::new(
5210 StackType::Empty
5211 .push(Type::Int)
5212 .push(Type::Union("Choice".to_string())),
5213 StackType::singleton(Type::Int),
5214 )),
5215 body: vec![
5216 make_word_call("swap"),
5217 make_word_call(">aux"),
5218 Statement::Match {
5219 arms: vec![
5220 MatchArm {
5221 pattern: Pattern::Variant("Left".to_string()),
5222 body: vec![make_word_call("aux>")],
5223 span: None,
5224 },
5225 MatchArm {
5226 pattern: Pattern::Variant("Right".to_string()),
5227 body: vec![],
5228 span: None,
5229 },
5230 ],
5231 span: None,
5232 },
5233 ],
5234 source: None,
5235 allowed_lints: vec![],
5236 }],
5237 };
5238
5239 let mut checker = TypeChecker::new();
5240 let result = checker.check_program(&program);
5241 assert!(result.is_err());
5242 let err = result.unwrap_err();
5243 assert!(
5244 err.contains("aux stack"),
5245 "Expected aux stack mismatch error, got: {}",
5246 err
5247 );
5248 }
5249
5250 #[test]
5251 fn test_aux_in_quotation_rejected() {
5252 let program = Program {
5254 includes: vec![],
5255 unions: vec![],
5256 words: vec![WordDef {
5257 name: "test".to_string(),
5258 effect: Some(Effect::new(
5259 StackType::singleton(Type::Int),
5260 StackType::singleton(Type::Quotation(Box::new(Effect::new(
5261 StackType::singleton(Type::Int),
5262 StackType::singleton(Type::Int),
5263 )))),
5264 )),
5265 body: vec![Statement::Quotation {
5266 span: None,
5267 id: 0,
5268 body: vec![make_word_call(">aux"), make_word_call("aux>")],
5269 }],
5270 source: None,
5271 allowed_lints: vec![],
5272 }],
5273 };
5274
5275 let mut checker = TypeChecker::new();
5276 let result = checker.check_program(&program);
5277 assert!(result.is_err());
5278 let err = result.unwrap_err();
5279 assert!(
5280 err.contains("not supported inside quotations"),
5281 "Expected quotation rejection error, got: {}",
5282 err
5283 );
5284 }
5285
5286 #[test]
5291 fn test_dip_basic() {
5292 let program = Program {
5295 includes: vec![],
5296 unions: vec![],
5297 words: vec![WordDef {
5298 name: "test".to_string(),
5299 effect: Some(Effect::new(
5300 StackType::Empty.push(Type::Int).push(Type::Int),
5301 StackType::Empty.push(Type::Int).push(Type::Int),
5302 )),
5303 body: vec![
5304 Statement::Quotation {
5305 id: 0,
5306 body: vec![
5307 Statement::IntLiteral(1),
5308 Statement::WordCall {
5309 name: "i.+".to_string(),
5310 span: None,
5311 },
5312 ],
5313 span: None,
5314 },
5315 Statement::WordCall {
5316 name: "dip".to_string(),
5317 span: None,
5318 },
5319 ],
5320 source: None,
5321 allowed_lints: vec![],
5322 }],
5323 };
5324
5325 let mut checker = TypeChecker::new();
5326 assert!(checker.check_program(&program).is_ok());
5327 }
5328
5329 #[test]
5330 fn test_dip_type_mismatch() {
5331 let program = Program {
5334 includes: vec![],
5335 unions: vec![],
5336 words: vec![WordDef {
5337 name: "test".to_string(),
5338 effect: Some(Effect::new(
5339 StackType::Empty.push(Type::String).push(Type::Int),
5340 StackType::Empty.push(Type::Int).push(Type::Int),
5341 )),
5342 body: vec![
5343 Statement::Quotation {
5344 id: 0,
5345 body: vec![
5346 Statement::IntLiteral(1),
5347 Statement::WordCall {
5348 name: "i.+".to_string(),
5349 span: None,
5350 },
5351 ],
5352 span: None,
5353 },
5354 Statement::WordCall {
5355 name: "dip".to_string(),
5356 span: None,
5357 },
5358 ],
5359 source: None,
5360 allowed_lints: vec![],
5361 }],
5362 };
5363
5364 let mut checker = TypeChecker::new();
5365 assert!(checker.check_program(&program).is_err());
5366 }
5367
5368 #[test]
5369 fn test_keep_basic() {
5370 let program = Program {
5373 includes: vec![],
5374 unions: vec![],
5375 words: vec![WordDef {
5376 name: "test".to_string(),
5377 effect: Some(Effect::new(
5378 StackType::singleton(Type::Int),
5379 StackType::Empty.push(Type::Int).push(Type::Int),
5380 )),
5381 body: vec![
5382 Statement::Quotation {
5383 id: 0,
5384 body: vec![
5385 Statement::WordCall {
5386 name: "dup".to_string(),
5387 span: None,
5388 },
5389 Statement::WordCall {
5390 name: "i.*".to_string(),
5391 span: None,
5392 },
5393 ],
5394 span: None,
5395 },
5396 Statement::WordCall {
5397 name: "keep".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_ok());
5408 }
5409
5410 #[test]
5411 fn test_bi_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::IntLiteral(2),
5428 Statement::WordCall {
5429 name: "i.*".to_string(),
5430 span: None,
5431 },
5432 ],
5433 span: None,
5434 },
5435 Statement::Quotation {
5436 id: 1,
5437 body: vec![
5438 Statement::IntLiteral(3),
5439 Statement::WordCall {
5440 name: "i.*".to_string(),
5441 span: None,
5442 },
5443 ],
5444 span: None,
5445 },
5446 Statement::WordCall {
5447 name: "bi".to_string(),
5448 span: None,
5449 },
5450 ],
5451 source: None,
5452 allowed_lints: vec![],
5453 }],
5454 };
5455
5456 let mut checker = TypeChecker::new();
5457 assert!(checker.check_program(&program).is_ok());
5458 }
5459
5460 #[test]
5461 fn test_keep_type_mismatch() {
5462 let program = Program {
5465 includes: vec![],
5466 unions: vec![],
5467 words: vec![WordDef {
5468 name: "test".to_string(),
5469 effect: Some(Effect::new(
5470 StackType::singleton(Type::String),
5471 StackType::Empty.push(Type::Int).push(Type::String),
5472 )),
5473 body: vec![
5474 Statement::Quotation {
5475 id: 0,
5476 body: vec![
5477 Statement::IntLiteral(1),
5478 Statement::WordCall {
5479 name: "i.+".to_string(),
5480 span: None,
5481 },
5482 ],
5483 span: None,
5484 },
5485 Statement::WordCall {
5486 name: "keep".to_string(),
5487 span: None,
5488 },
5489 ],
5490 source: None,
5491 allowed_lints: vec![],
5492 }],
5493 };
5494
5495 let mut checker = TypeChecker::new();
5496 assert!(checker.check_program(&program).is_err());
5497 }
5498
5499 #[test]
5500 fn test_bi_type_mismatch() {
5501 let program = Program {
5504 includes: vec![],
5505 unions: vec![],
5506 words: vec![WordDef {
5507 name: "test".to_string(),
5508 effect: Some(Effect::new(
5509 StackType::singleton(Type::String),
5510 StackType::Empty.push(Type::Int).push(Type::Int),
5511 )),
5512 body: vec![
5513 Statement::Quotation {
5514 id: 0,
5515 body: vec![Statement::WordCall {
5516 name: "string.length".to_string(),
5517 span: None,
5518 }],
5519 span: None,
5520 },
5521 Statement::Quotation {
5522 id: 1,
5523 body: vec![
5524 Statement::IntLiteral(1),
5525 Statement::WordCall {
5526 name: "i.+".to_string(),
5527 span: None,
5528 },
5529 ],
5530 span: None,
5531 },
5532 Statement::WordCall {
5533 name: "bi".to_string(),
5534 span: None,
5535 },
5536 ],
5537 source: None,
5538 allowed_lints: vec![],
5539 }],
5540 };
5541
5542 let mut checker = TypeChecker::new();
5543 assert!(checker.check_program(&program).is_err());
5544 }
5545
5546 #[test]
5547 fn test_dip_underflow() {
5548 let program = Program {
5551 includes: vec![],
5552 unions: vec![],
5553 words: vec![WordDef {
5554 name: "test".to_string(),
5555 effect: Some(Effect::new(
5556 StackType::Empty,
5557 StackType::singleton(Type::Int),
5558 )),
5559 body: vec![
5560 Statement::Quotation {
5561 id: 0,
5562 body: vec![Statement::IntLiteral(1)],
5563 span: None,
5564 },
5565 Statement::WordCall {
5566 name: "dip".to_string(),
5567 span: None,
5568 },
5569 ],
5570 source: None,
5571 allowed_lints: vec![],
5572 }],
5573 };
5574
5575 let mut checker = TypeChecker::new();
5576 let result = checker.check_program(&program);
5577 assert!(result.is_err());
5578 let err = result.unwrap_err();
5579 assert!(
5580 err.contains("stack underflow"),
5581 "Expected underflow error, got: {}",
5582 err
5583 );
5584 }
5585
5586 #[test]
5587 fn test_dip_preserves_type() {
5588 let program = Program {
5591 includes: vec![],
5592 unions: vec![],
5593 words: vec![WordDef {
5594 name: "test".to_string(),
5595 effect: Some(Effect::new(
5596 StackType::Empty.push(Type::Int).push(Type::String),
5597 StackType::Empty.push(Type::Int).push(Type::String),
5598 )),
5599 body: vec![
5600 Statement::Quotation {
5601 id: 0,
5602 body: vec![
5603 Statement::IntLiteral(1),
5604 Statement::WordCall {
5605 name: "i.+".to_string(),
5606 span: None,
5607 },
5608 ],
5609 span: None,
5610 },
5611 Statement::WordCall {
5612 name: "dip".to_string(),
5613 span: None,
5614 },
5615 ],
5616 source: None,
5617 allowed_lints: vec![],
5618 }],
5619 };
5620
5621 let mut checker = TypeChecker::new();
5622 assert!(checker.check_program(&program).is_ok());
5623 }
5624
5625 #[test]
5626 fn test_keep_underflow() {
5627 let program = Program {
5630 includes: vec![],
5631 unions: vec![],
5632 words: vec![WordDef {
5633 name: "test".to_string(),
5634 effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
5635 body: vec![
5636 Statement::Quotation {
5637 id: 0,
5638 body: vec![Statement::WordCall {
5639 name: "drop".to_string(),
5640 span: None,
5641 }],
5642 span: None,
5643 },
5644 Statement::WordCall {
5645 name: "keep".to_string(),
5646 span: None,
5647 },
5648 ],
5649 source: None,
5650 allowed_lints: vec![],
5651 }],
5652 };
5653
5654 let mut checker = TypeChecker::new();
5655 let result = checker.check_program(&program);
5656 assert!(result.is_err());
5657 let err = result.unwrap_err();
5658 assert!(
5659 err.contains("stack underflow") || err.contains("underflow"),
5660 "Expected underflow error, got: {}",
5661 err
5662 );
5663 }
5664
5665 #[test]
5666 fn test_bi_underflow() {
5667 let program = Program {
5670 includes: vec![],
5671 unions: vec![],
5672 words: vec![WordDef {
5673 name: "test".to_string(),
5674 effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
5675 body: vec![
5676 Statement::Quotation {
5677 id: 0,
5678 body: vec![Statement::IntLiteral(1)],
5679 span: None,
5680 },
5681 Statement::Quotation {
5682 id: 1,
5683 body: vec![Statement::IntLiteral(2)],
5684 span: None,
5685 },
5686 Statement::WordCall {
5687 name: "bi".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_polymorphic_quotations() {
5709 let program = Program {
5712 includes: vec![],
5713 unions: vec![],
5714 words: vec![WordDef {
5715 name: "test".to_string(),
5716 effect: Some(Effect::new(
5717 StackType::singleton(Type::Int),
5718 StackType::Empty.push(Type::Int).push(Type::String),
5719 )),
5720 body: vec![
5721 Statement::Quotation {
5722 id: 0,
5723 body: vec![
5724 Statement::IntLiteral(2),
5725 Statement::WordCall {
5726 name: "i.*".to_string(),
5727 span: None,
5728 },
5729 ],
5730 span: None,
5731 },
5732 Statement::Quotation {
5733 id: 1,
5734 body: vec![Statement::WordCall {
5735 name: "int->string".to_string(),
5736 span: None,
5737 }],
5738 span: None,
5739 },
5740 Statement::WordCall {
5741 name: "bi".to_string(),
5742 span: None,
5743 },
5744 ],
5745 source: None,
5746 allowed_lints: vec![],
5747 }],
5748 };
5749
5750 let mut checker = TypeChecker::new();
5751 assert!(checker.check_program(&program).is_ok());
5752 }
5753}