Skip to main content

seqc/
typechecker.rs

1//! Enhanced type checker for Seq with full type tracking
2//!
3//! Uses row polymorphism and unification to verify stack effects.
4//! Based on cem2's type checker but simplified for Phase 8.5.
5
6use 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
16/// Format a line number as an error message prefix (e.g., "at line 42: ").
17/// Line numbers are 0-indexed internally, so we add 1 for display.
18fn format_line_prefix(line: usize) -> String {
19    format!("at line {}: ", line + 1)
20}
21
22pub struct TypeChecker {
23    /// Environment mapping word names to their effects
24    env: HashMap<String, Effect>,
25    /// Union type registry - maps union names to their type information
26    /// Contains variant names and field types for each union
27    unions: HashMap<String, UnionTypeInfo>,
28    /// Counter for generating fresh type variables
29    fresh_counter: std::cell::Cell<usize>,
30    /// Quotation types tracked during type checking
31    /// Maps quotation ID (from AST) to inferred type (Quotation or Closure)
32    /// This type map is used by codegen to generate appropriate code
33    quotation_types: std::cell::RefCell<HashMap<usize, Type>>,
34    /// Expected quotation/closure type (from word signature, if any)
35    /// Used during type-driven capture inference
36    expected_quotation_type: std::cell::RefCell<Option<Type>>,
37    /// Current word being type-checked (for detecting recursive tail calls)
38    /// Used to identify divergent branches in if/else expressions
39    /// Stores (name, line_number) for better error messages
40    current_word: std::cell::RefCell<Option<(String, Option<usize>)>>,
41    /// Per-statement type info for codegen optimization (Issue #186)
42    /// Maps (word_name, statement_index) -> concrete top-of-stack type before statement
43    /// Only stores trivially-copyable types (Int, Float, Bool) to enable optimizations
44    statement_top_types: std::cell::RefCell<HashMap<(String, usize), Type>>,
45    /// Call graph for detecting mutual recursion (Issue #229)
46    /// Used to improve divergent branch detection beyond direct recursion
47    call_graph: Option<CallGraph>,
48    /// Current aux stack type during word body checking (Issue #350)
49    /// Tracked per-word; reset to Empty at each word boundary.
50    current_aux_stack: std::cell::RefCell<StackType>,
51    /// Maximum aux stack depth per word, for codegen alloca sizing (Issue #350)
52    /// Maps word_name -> max_depth (number of %Value allocas needed)
53    aux_max_depths: std::cell::RefCell<HashMap<String, usize>>,
54    /// True when type-checking inside a quotation body (Issue #350)
55    /// Aux operations are not supported in quotations (codegen limitation).
56    in_quotation_scope: std::cell::Cell<bool>,
57}
58
59impl TypeChecker {
60    pub fn new() -> Self {
61        TypeChecker {
62            env: HashMap::new(),
63            unions: HashMap::new(),
64            fresh_counter: std::cell::Cell::new(0),
65            quotation_types: std::cell::RefCell::new(HashMap::new()),
66            expected_quotation_type: std::cell::RefCell::new(None),
67            current_word: std::cell::RefCell::new(None),
68            statement_top_types: std::cell::RefCell::new(HashMap::new()),
69            call_graph: None,
70            current_aux_stack: std::cell::RefCell::new(StackType::Empty),
71            aux_max_depths: std::cell::RefCell::new(HashMap::new()),
72            in_quotation_scope: std::cell::Cell::new(false),
73        }
74    }
75
76    /// Set the call graph for mutual recursion detection.
77    ///
78    /// When set, the type checker can detect divergent branches caused by
79    /// mutual recursion (e.g., even/odd pattern) in addition to direct recursion.
80    pub fn set_call_graph(&mut self, call_graph: CallGraph) {
81        self.call_graph = Some(call_graph);
82    }
83
84    /// Get line info prefix for error messages (e.g., "at line 42: " or "")
85    fn line_prefix(&self) -> String {
86        self.current_word
87            .borrow()
88            .as_ref()
89            .and_then(|(_, line)| line.map(format_line_prefix))
90            .unwrap_or_default()
91    }
92
93    /// Look up a union type by name
94    pub fn get_union(&self, name: &str) -> Option<&UnionTypeInfo> {
95        self.unions.get(name)
96    }
97
98    /// Get all registered union types
99    pub fn get_unions(&self) -> &HashMap<String, UnionTypeInfo> {
100        &self.unions
101    }
102
103    /// Find variant info by name across all unions
104    ///
105    /// Returns (union_name, variant_info) for the variant
106    fn find_variant(&self, variant_name: &str) -> Option<(&str, &VariantInfo)> {
107        for (union_name, union_info) in &self.unions {
108            for variant in &union_info.variants {
109                if variant.name == variant_name {
110                    return Some((union_name.as_str(), variant));
111                }
112            }
113        }
114        None
115    }
116
117    /// Register external word effects (e.g., from included modules or FFI).
118    ///
119    /// All external words must have explicit stack effects for type safety.
120    pub fn register_external_words(&mut self, words: &[(&str, &Effect)]) {
121        for (name, effect) in words {
122            self.env.insert(name.to_string(), (*effect).clone());
123        }
124    }
125
126    /// Register external union type names (e.g., from included modules).
127    ///
128    /// This allows field types in union definitions to reference types from includes.
129    /// We only register the name as a valid type; we don't need full variant info
130    /// since the actual union definition lives in the included file.
131    pub fn register_external_unions(&mut self, union_names: &[&str]) {
132        for name in union_names {
133            // Insert a placeholder union with no variants
134            // This makes is_valid_type_name() return true for this type
135            self.unions.insert(
136                name.to_string(),
137                UnionTypeInfo {
138                    name: name.to_string(),
139                    variants: vec![],
140                },
141            );
142        }
143    }
144
145    /// Extract the type map (quotation ID -> inferred type)
146    ///
147    /// This should be called after check_program() to get the inferred types
148    /// for all quotations in the program. The map is used by codegen to generate
149    /// appropriate code for Quotations vs Closures.
150    pub fn take_quotation_types(&self) -> HashMap<usize, Type> {
151        self.quotation_types.replace(HashMap::new())
152    }
153
154    /// Extract per-statement type info for codegen optimization (Issue #186)
155    /// Returns map of (word_name, statement_index) -> top-of-stack type
156    pub fn take_statement_top_types(&self) -> HashMap<(String, usize), Type> {
157        self.statement_top_types.replace(HashMap::new())
158    }
159
160    /// Extract per-word aux stack max depths for codegen alloca sizing (Issue #350)
161    pub fn take_aux_max_depths(&self) -> HashMap<String, usize> {
162        self.aux_max_depths.replace(HashMap::new())
163    }
164
165    /// Count the number of concrete types in a StackType (for aux depth tracking)
166    fn stack_depth(stack: &StackType) -> usize {
167        let mut depth = 0;
168        let mut current = stack;
169        while let StackType::Cons { rest, .. } = current {
170            depth += 1;
171            current = rest;
172        }
173        depth
174    }
175
176    /// Check if the top of the stack is a trivially-copyable type (Int, Float, Bool)
177    /// These types have no heap references and can be memcpy'd in codegen.
178    fn get_trivially_copyable_top(stack: &StackType) -> Option<Type> {
179        match stack {
180            StackType::Cons { top, .. } => match top {
181                Type::Int | Type::Float | Type::Bool => Some(top.clone()),
182                _ => None,
183            },
184            _ => None,
185        }
186    }
187
188    /// Record the top-of-stack type for a statement if it's trivially copyable (Issue #186)
189    fn capture_statement_type(&self, word_name: &str, stmt_index: usize, stack: &StackType) {
190        if let Some(top_type) = Self::get_trivially_copyable_top(stack) {
191            self.statement_top_types
192                .borrow_mut()
193                .insert((word_name.to_string(), stmt_index), top_type);
194        }
195    }
196
197    /// Generate a fresh variable name
198    fn fresh_var(&self, prefix: &str) -> String {
199        let n = self.fresh_counter.get();
200        self.fresh_counter.set(n + 1);
201        format!("{}${}", prefix, n)
202    }
203
204    /// Freshen all type and row variables in an effect
205    fn freshen_effect(&self, effect: &Effect) -> Effect {
206        let mut type_map = HashMap::new();
207        let mut row_map = HashMap::new();
208
209        let fresh_inputs = self.freshen_stack(&effect.inputs, &mut type_map, &mut row_map);
210        let fresh_outputs = self.freshen_stack(&effect.outputs, &mut type_map, &mut row_map);
211
212        // Freshen the side effects too
213        let fresh_effects = effect
214            .effects
215            .iter()
216            .map(|e| self.freshen_side_effect(e, &mut type_map, &mut row_map))
217            .collect();
218
219        Effect::with_effects(fresh_inputs, fresh_outputs, fresh_effects)
220    }
221
222    fn freshen_side_effect(
223        &self,
224        effect: &SideEffect,
225        type_map: &mut HashMap<String, String>,
226        row_map: &mut HashMap<String, String>,
227    ) -> SideEffect {
228        match effect {
229            SideEffect::Yield(ty) => {
230                SideEffect::Yield(Box::new(self.freshen_type(ty, type_map, row_map)))
231            }
232        }
233    }
234
235    fn freshen_stack(
236        &self,
237        stack: &StackType,
238        type_map: &mut HashMap<String, String>,
239        row_map: &mut HashMap<String, String>,
240    ) -> StackType {
241        match stack {
242            StackType::Empty => StackType::Empty,
243            StackType::RowVar(name) => {
244                let fresh_name = row_map
245                    .entry(name.clone())
246                    .or_insert_with(|| self.fresh_var(name));
247                StackType::RowVar(fresh_name.clone())
248            }
249            StackType::Cons { rest, top } => {
250                let fresh_rest = self.freshen_stack(rest, type_map, row_map);
251                let fresh_top = self.freshen_type(top, type_map, row_map);
252                StackType::Cons {
253                    rest: Box::new(fresh_rest),
254                    top: fresh_top,
255                }
256            }
257        }
258    }
259
260    fn freshen_type(
261        &self,
262        ty: &Type,
263        type_map: &mut HashMap<String, String>,
264        row_map: &mut HashMap<String, String>,
265    ) -> Type {
266        match ty {
267            Type::Int | Type::Float | Type::Bool | Type::String | Type::Symbol | Type::Channel => {
268                ty.clone()
269            }
270            Type::Var(name) => {
271                let fresh_name = type_map
272                    .entry(name.clone())
273                    .or_insert_with(|| self.fresh_var(name));
274                Type::Var(fresh_name.clone())
275            }
276            Type::Quotation(effect) => {
277                let fresh_inputs = self.freshen_stack(&effect.inputs, type_map, row_map);
278                let fresh_outputs = self.freshen_stack(&effect.outputs, type_map, row_map);
279                Type::Quotation(Box::new(Effect::new(fresh_inputs, fresh_outputs)))
280            }
281            Type::Closure { effect, captures } => {
282                let fresh_inputs = self.freshen_stack(&effect.inputs, type_map, row_map);
283                let fresh_outputs = self.freshen_stack(&effect.outputs, type_map, row_map);
284                let fresh_captures = captures
285                    .iter()
286                    .map(|t| self.freshen_type(t, type_map, row_map))
287                    .collect();
288                Type::Closure {
289                    effect: Box::new(Effect::new(fresh_inputs, fresh_outputs)),
290                    captures: fresh_captures,
291                }
292            }
293            // Union types are concrete named types - no freshening needed
294            Type::Union(name) => Type::Union(name.clone()),
295        }
296    }
297
298    /// Parse a type name string into a Type
299    ///
300    /// Supports: Int, Float, Bool, String, Channel, and union types
301    fn parse_type_name(&self, name: &str) -> Type {
302        match name {
303            "Int" => Type::Int,
304            "Float" => Type::Float,
305            "Bool" => Type::Bool,
306            "String" => Type::String,
307            "Channel" => Type::Channel,
308            // Any other name is assumed to be a union type reference
309            other => Type::Union(other.to_string()),
310        }
311    }
312
313    /// Check if a type name is a known valid type
314    ///
315    /// Returns true for built-in types (Int, Float, Bool, String, Channel) and
316    /// registered union type names
317    fn is_valid_type_name(&self, name: &str) -> bool {
318        matches!(name, "Int" | "Float" | "Bool" | "String" | "Channel")
319            || self.unions.contains_key(name)
320    }
321
322    /// Validate that all field types in union definitions reference known types
323    ///
324    /// Note: Field count validation happens earlier in generate_constructors()
325    fn validate_union_field_types(&self, program: &Program) -> Result<(), String> {
326        for union_def in &program.unions {
327            for variant in &union_def.variants {
328                for field in &variant.fields {
329                    if !self.is_valid_type_name(&field.type_name) {
330                        return Err(format!(
331                            "Unknown type '{}' in field '{}' of variant '{}' in union '{}'. \
332                             Valid types are: Int, Float, Bool, String, Channel, or a defined union name.",
333                            field.type_name, field.name, variant.name, union_def.name
334                        ));
335                    }
336                }
337            }
338        }
339        Ok(())
340    }
341
342    /// Validate that all types in a stack effect are known types
343    ///
344    /// RFC #345: This catches cases where an uppercase identifier was parsed as a type
345    /// variable but should have been a union type (e.g., from an include that wasn't
346    /// available when parsing). Type variables should be single uppercase letters (T, U, V).
347    fn validate_effect_types(&self, effect: &Effect, word_name: &str) -> Result<(), String> {
348        self.validate_stack_types(&effect.inputs, word_name)?;
349        self.validate_stack_types(&effect.outputs, word_name)?;
350        Ok(())
351    }
352
353    /// Validate types in a stack type
354    fn validate_stack_types(&self, stack: &StackType, word_name: &str) -> Result<(), String> {
355        match stack {
356            StackType::Empty | StackType::RowVar(_) => Ok(()),
357            StackType::Cons { rest, top } => {
358                self.validate_type(top, word_name)?;
359                self.validate_stack_types(rest, word_name)
360            }
361        }
362    }
363
364    /// Validate a single type
365    ///
366    /// Type variables are allowed - they're used for polymorphism.
367    /// Only Type::Union types are validated to ensure they're registered.
368    fn validate_type(&self, ty: &Type, word_name: &str) -> Result<(), String> {
369        match ty {
370            Type::Var(_) => {
371                // Type variables are always valid - they represent polymorphic types
372                // Examples: T, U, V, Ctx, Handle, Acc, etc.
373                // After fixup_union_types(), any union name that was mistakenly parsed
374                // as a type variable will have been converted to Type::Union
375                Ok(())
376            }
377            Type::Quotation(effect) => self.validate_effect_types(effect, word_name),
378            Type::Closure { effect, captures } => {
379                self.validate_effect_types(effect, word_name)?;
380                for cap in captures {
381                    self.validate_type(cap, word_name)?;
382                }
383                Ok(())
384            }
385            // Concrete types are always valid
386            Type::Int | Type::Float | Type::Bool | Type::String | Type::Symbol | Type::Channel => {
387                Ok(())
388            }
389            // Union types are valid if they're registered
390            Type::Union(name) => {
391                if !self.unions.contains_key(name) {
392                    return Err(format!(
393                        "In word '{}': Unknown union type '{}' in stack effect.\n\
394                         Make sure this union is defined before the word that uses it.",
395                        word_name, name
396                    ));
397                }
398                Ok(())
399            }
400        }
401    }
402
403    /// Type check a complete program
404    pub fn check_program(&mut self, program: &Program) -> Result<(), String> {
405        // First pass: register all union definitions
406        for union_def in &program.unions {
407            let variants = union_def
408                .variants
409                .iter()
410                .map(|v| VariantInfo {
411                    name: v.name.clone(),
412                    fields: v
413                        .fields
414                        .iter()
415                        .map(|f| VariantFieldInfo {
416                            name: f.name.clone(),
417                            field_type: self.parse_type_name(&f.type_name),
418                        })
419                        .collect(),
420                })
421                .collect();
422
423            self.unions.insert(
424                union_def.name.clone(),
425                UnionTypeInfo {
426                    name: union_def.name.clone(),
427                    variants,
428                },
429            );
430        }
431
432        // Validate field types in unions reference known types
433        self.validate_union_field_types(program)?;
434
435        // Second pass: collect all word signatures
436        // All words must have explicit stack effect declarations (v2.0 requirement)
437        for word in &program.words {
438            if let Some(effect) = &word.effect {
439                // RFC #345: Validate that all types in the effect are known
440                // This catches cases where an uppercase identifier was parsed as a type variable
441                // but should have been a union type (e.g., from an include)
442                self.validate_effect_types(effect, &word.name)?;
443                self.env.insert(word.name.clone(), effect.clone());
444            } else {
445                return Err(format!(
446                    "Word '{}' is missing a stack effect declaration.\n\
447                     All words must declare their stack effect, e.g.: : {} ( -- ) ... ;",
448                    word.name, word.name
449                ));
450            }
451        }
452
453        // Third pass: type check each word body
454        for word in &program.words {
455            self.check_word(word)?;
456        }
457
458        Ok(())
459    }
460
461    /// Type check a word definition
462    fn check_word(&self, word: &WordDef) -> Result<(), String> {
463        // Track current word for detecting recursive tail calls (divergent branches)
464        let line = word.source.as_ref().map(|s| s.start_line);
465        *self.current_word.borrow_mut() = Some((word.name.clone(), line));
466
467        // Reset aux stack for this word (Issue #350)
468        *self.current_aux_stack.borrow_mut() = StackType::Empty;
469
470        // All words must have declared effects (enforced in check_program)
471        let declared_effect = word.effect.as_ref().expect("word must have effect");
472
473        // Check if the word's output type is a quotation or closure
474        // If so, store it as the expected type for capture inference
475        if let Some((_rest, top_type)) = declared_effect.outputs.clone().pop()
476            && matches!(top_type, Type::Quotation(_) | Type::Closure { .. })
477        {
478            *self.expected_quotation_type.borrow_mut() = Some(top_type);
479        }
480
481        // Infer the result stack and effects starting from declared input
482        let (result_stack, _subst, inferred_effects) =
483            self.infer_statements_from(&word.body, &declared_effect.inputs, true)?;
484
485        // Clear expected type after checking
486        *self.expected_quotation_type.borrow_mut() = None;
487
488        // Verify result matches declared output
489        let line_info = line.map(format_line_prefix).unwrap_or_default();
490        unify_stacks(&declared_effect.outputs, &result_stack).map_err(|e| {
491            format!(
492                "{}Word '{}': declared output stack ({}) doesn't match inferred ({}): {}",
493                line_info, word.name, declared_effect.outputs, result_stack, e
494            )
495        })?;
496
497        // Verify computational effects match (bidirectional)
498        // 1. Check that each inferred effect has a matching declared effect (by kind)
499        // Type variables in effects are matched by kind (Yield matches Yield)
500        for inferred in &inferred_effects {
501            if !self.effect_matches_any(inferred, &declared_effect.effects) {
502                return Err(format!(
503                    "{}Word '{}': body produces effect '{}' but no matching effect is declared.\n\
504                     Hint: Add '| Yield <type>' to the word's stack effect declaration.",
505                    line_info, word.name, inferred
506                ));
507            }
508        }
509
510        // 2. Check that each declared effect is actually produced (effect soundness)
511        // This prevents declaring effects that don't occur
512        for declared in &declared_effect.effects {
513            if !self.effect_matches_any(declared, &inferred_effects) {
514                return Err(format!(
515                    "{}Word '{}': declares effect '{}' but body doesn't produce it.\n\
516                     Hint: Remove the effect declaration or ensure the body uses yield.",
517                    line_info, word.name, declared
518                ));
519            }
520        }
521
522        // Verify aux stack is empty at word boundary (Issue #350)
523        let aux_stack = self.current_aux_stack.borrow().clone();
524        if aux_stack != StackType::Empty {
525            return Err(format!(
526                "{}Word '{}': aux stack is not empty at word return.\n\
527                 Remaining aux stack: {}\n\
528                 Every >aux must be matched by a corresponding aux> before the word returns.",
529                line_info, word.name, aux_stack
530            ));
531        }
532
533        // Clear current word
534        *self.current_word.borrow_mut() = None;
535
536        Ok(())
537    }
538
539    /// Infer the resulting stack type from a sequence of statements
540    /// starting from a given input stack
541    /// Returns (final_stack, substitution, accumulated_effects)
542    ///
543    /// `capture_stmt_types`: If true, capture statement type info for codegen optimization.
544    /// Should only be true for top-level word bodies, not for nested branches/loops.
545    fn infer_statements_from(
546        &self,
547        statements: &[Statement],
548        start_stack: &StackType,
549        capture_stmt_types: bool,
550    ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
551        let mut current_stack = start_stack.clone();
552        let mut accumulated_subst = Subst::empty();
553        let mut accumulated_effects: Vec<SideEffect> = Vec::new();
554        let mut skip_next = false;
555
556        for (i, stmt) in statements.iter().enumerate() {
557            // Skip this statement if we already handled it (e.g., pick/roll after literal)
558            if skip_next {
559                skip_next = false;
560                continue;
561            }
562
563            // Special case: IntLiteral followed by pick or roll
564            // Handle them as a fused operation with correct type semantics
565            if let Statement::IntLiteral(n) = stmt
566                && let Some(Statement::WordCall {
567                    name: next_word, ..
568                }) = statements.get(i + 1)
569            {
570                if next_word == "pick" {
571                    let (new_stack, subst) = self.handle_literal_pick(*n, current_stack.clone())?;
572                    current_stack = new_stack;
573                    accumulated_subst = accumulated_subst.compose(&subst);
574                    skip_next = true; // Skip the "pick" word
575                    continue;
576                } else if next_word == "roll" {
577                    let (new_stack, subst) = self.handle_literal_roll(*n, current_stack.clone())?;
578                    current_stack = new_stack;
579                    accumulated_subst = accumulated_subst.compose(&subst);
580                    skip_next = true; // Skip the "roll" word
581                    continue;
582                }
583            }
584
585            // Look ahead: if this is a quotation followed by a word that expects specific quotation type,
586            // set the expected type before checking the quotation
587            let saved_expected_type = if matches!(stmt, Statement::Quotation { .. }) {
588                // Save the current expected type
589                let saved = self.expected_quotation_type.borrow().clone();
590
591                // Try to set expected type based on lookahead
592                if let Some(Statement::WordCall {
593                    name: next_word, ..
594                }) = statements.get(i + 1)
595                {
596                    // Check if the next word expects a specific quotation type
597                    if let Some(next_effect) = self.lookup_word_effect(next_word) {
598                        // Extract the quotation type expected by the next word
599                        // For operations like spawn: ( ..a Quotation(-- ) -- ..a Int )
600                        if let Some((_rest, quot_type)) = next_effect.inputs.clone().pop()
601                            && matches!(quot_type, Type::Quotation(_))
602                        {
603                            *self.expected_quotation_type.borrow_mut() = Some(quot_type);
604                        }
605                    }
606                }
607                Some(saved)
608            } else {
609                None
610            };
611
612            // Capture statement type info for codegen optimization (Issue #186)
613            // Record the top-of-stack type BEFORE this statement for operations like dup
614            // Only capture for top-level word bodies, not nested branches/loops
615            if capture_stmt_types && let Some((word_name, _)) = self.current_word.borrow().as_ref()
616            {
617                self.capture_statement_type(word_name, i, &current_stack);
618            }
619
620            let (new_stack, subst, effects) = self.infer_statement(stmt, current_stack)?;
621            current_stack = new_stack;
622            accumulated_subst = accumulated_subst.compose(&subst);
623
624            // Accumulate side effects from this statement
625            for effect in effects {
626                if !accumulated_effects.contains(&effect) {
627                    accumulated_effects.push(effect);
628                }
629            }
630
631            // Restore expected type after checking quotation
632            if let Some(saved) = saved_expected_type {
633                *self.expected_quotation_type.borrow_mut() = saved;
634            }
635        }
636
637        Ok((current_stack, accumulated_subst, accumulated_effects))
638    }
639
640    /// Handle `n pick` where n is a literal integer
641    ///
642    /// pick(n) copies the value at position n to the top of the stack.
643    /// Position 0 is the top, 1 is below top, etc.
644    ///
645    /// Example: `2 pick` on stack ( A B C ) produces ( A B C A )
646    /// - Position 0: C (top)
647    /// - Position 1: B
648    /// - Position 2: A
649    /// - Result: copy A to top
650    fn handle_literal_pick(
651        &self,
652        n: i64,
653        current_stack: StackType,
654    ) -> Result<(StackType, Subst), String> {
655        if n < 0 {
656            return Err(format!("pick: index must be non-negative, got {}", n));
657        }
658
659        // Get the type at position n
660        let type_at_n = self.get_type_at_position(&current_stack, n as usize, "pick")?;
661
662        // Push a copy of that type onto the stack
663        Ok((current_stack.push(type_at_n), Subst::empty()))
664    }
665
666    /// Handle `n roll` where n is a literal integer
667    ///
668    /// roll(n) moves the value at position n to the top of the stack,
669    /// shifting all items above it down by one position.
670    ///
671    /// Example: `2 roll` on stack ( A B C ) produces ( B C A )
672    /// - Position 0: C (top)
673    /// - Position 1: B
674    /// - Position 2: A
675    /// - Result: move A to top, B and C shift down
676    fn handle_literal_roll(
677        &self,
678        n: i64,
679        current_stack: StackType,
680    ) -> Result<(StackType, Subst), String> {
681        if n < 0 {
682            return Err(format!("roll: index must be non-negative, got {}", n));
683        }
684
685        // For roll, we need to:
686        // 1. Extract the type at position n
687        // 2. Remove it from that position
688        // 3. Push it on top
689        self.rotate_type_to_top(current_stack, n as usize)
690    }
691
692    /// Get the type at position n in the stack (0 = top)
693    fn get_type_at_position(&self, stack: &StackType, n: usize, op: &str) -> Result<Type, String> {
694        let mut current = stack;
695        let mut pos = 0;
696
697        loop {
698            match current {
699                StackType::Cons { rest, top } => {
700                    if pos == n {
701                        return Ok(top.clone());
702                    }
703                    pos += 1;
704                    current = rest;
705                }
706                StackType::RowVar(name) => {
707                    // We've hit a row variable before reaching position n
708                    // This means the type at position n is unknown statically.
709                    // Generate a fresh type variable to represent it.
710                    // This allows the code to type-check, with the actual type
711                    // determined by unification with how the value is used.
712                    //
713                    // Note: This works correctly even in conditional branches because
714                    // branches are now inferred from the actual stack (not abstractly),
715                    // so row variables only appear when the word itself has polymorphic inputs.
716                    let fresh_type = Type::Var(self.fresh_var(&format!("{}_{}", op, name)));
717                    return Ok(fresh_type);
718                }
719                StackType::Empty => {
720                    return Err(format!(
721                        "{}{}: stack underflow - position {} requested but stack has only {} concrete items",
722                        self.line_prefix(),
723                        op,
724                        n,
725                        pos
726                    ));
727                }
728            }
729        }
730    }
731
732    /// Remove the type at position n and push it on top (for roll)
733    fn rotate_type_to_top(&self, stack: StackType, n: usize) -> Result<(StackType, Subst), String> {
734        if n == 0 {
735            // roll(0) is a no-op
736            return Ok((stack, Subst::empty()));
737        }
738
739        // Collect all types from top to the target position
740        let mut types_above: Vec<Type> = Vec::new();
741        let mut current = stack;
742        let mut pos = 0;
743
744        // Pop items until we reach position n
745        loop {
746            match current {
747                StackType::Cons { rest, top } => {
748                    if pos == n {
749                        // Found the target - 'top' is what we want to move to the top
750                        // Rebuild the stack: rest, then types_above (reversed), then top
751                        let mut result = *rest;
752                        // Push types_above back in reverse order (bottom to top)
753                        for ty in types_above.into_iter().rev() {
754                            result = result.push(ty);
755                        }
756                        // Push the rotated type on top
757                        result = result.push(top);
758                        return Ok((result, Subst::empty()));
759                    }
760                    types_above.push(top);
761                    pos += 1;
762                    current = *rest;
763                }
764                StackType::RowVar(name) => {
765                    // Reached a row variable before position n
766                    // The type at position n is in the row variable.
767                    // Generate a fresh type variable to represent the moved value.
768                    //
769                    // Note: This preserves stack size correctly because we're moving
770                    // (not copying) a value. The row variable conceptually "loses"
771                    // an item which appears on top. Since we can't express "row minus one",
772                    // we generate a fresh type and trust unification to constrain it.
773                    //
774                    // This works correctly in conditional branches because branches are
775                    // now inferred from the actual stack (not abstractly), so row variables
776                    // only appear when the word itself has polymorphic inputs.
777                    let fresh_type = Type::Var(self.fresh_var(&format!("roll_{}", name)));
778
779                    // Reconstruct the stack with the rolled type on top
780                    let mut result = StackType::RowVar(name.clone());
781                    for ty in types_above.into_iter().rev() {
782                        result = result.push(ty);
783                    }
784                    result = result.push(fresh_type);
785                    return Ok((result, Subst::empty()));
786                }
787                StackType::Empty => {
788                    return Err(format!(
789                        "{}roll: stack underflow - position {} requested but stack has only {} items",
790                        self.line_prefix(),
791                        n,
792                        pos
793                    ));
794                }
795            }
796        }
797    }
798
799    /// Infer the stack effect of a sequence of statements
800    /// Returns an Effect with both inputs and outputs normalized by applying discovered substitutions
801    /// Also includes any computational side effects (Yield, etc.)
802    fn infer_statements(&self, statements: &[Statement]) -> Result<Effect, String> {
803        let start = StackType::RowVar("input".to_string());
804        // Don't capture statement types for quotation bodies - only top-level word bodies
805        let (result, subst, effects) = self.infer_statements_from(statements, &start, false)?;
806
807        // Apply the accumulated substitution to both start and result
808        // This ensures row variables are consistently named
809        let normalized_start = subst.apply_stack(&start);
810        let normalized_result = subst.apply_stack(&result);
811
812        Ok(Effect::with_effects(
813            normalized_start,
814            normalized_result,
815            effects,
816        ))
817    }
818
819    /// Infer the stack effect of a match expression
820    fn infer_match(
821        &self,
822        arms: &[crate::ast::MatchArm],
823        match_span: &Option<crate::ast::Span>,
824        current_stack: StackType,
825    ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
826        if arms.is_empty() {
827            return Err("match expression must have at least one arm".to_string());
828        }
829
830        // Pop the matched value from the stack
831        let (stack_after_match, _matched_type) =
832            self.pop_type(&current_stack, "match expression")?;
833
834        // Track all arm results for unification
835        let mut arm_results: Vec<StackType> = Vec::new();
836        let mut combined_subst = Subst::empty();
837        let mut merged_effects: Vec<SideEffect> = Vec::new();
838
839        // Save aux stack before match arms (Issue #350)
840        let aux_before_match = self.current_aux_stack.borrow().clone();
841        let mut aux_after_arms: Vec<StackType> = Vec::new();
842
843        for arm in arms {
844            // Restore aux stack before each arm (Issue #350)
845            *self.current_aux_stack.borrow_mut() = aux_before_match.clone();
846
847            // Get variant name from pattern
848            let variant_name = match &arm.pattern {
849                crate::ast::Pattern::Variant(name) => name.as_str(),
850                crate::ast::Pattern::VariantWithBindings { name, .. } => name.as_str(),
851            };
852
853            // Look up variant info
854            let (_union_name, variant_info) = self
855                .find_variant(variant_name)
856                .ok_or_else(|| format!("Unknown variant '{}' in match pattern", variant_name))?;
857
858            // Push fields onto the stack based on pattern type
859            let arm_stack = self.push_variant_fields(
860                &stack_after_match,
861                &arm.pattern,
862                variant_info,
863                variant_name,
864            )?;
865
866            // Type check the arm body directly from the actual stack
867            // Don't capture statement types for match arms - only top-level word bodies
868            let (arm_result, arm_subst, arm_effects) =
869                self.infer_statements_from(&arm.body, &arm_stack, false)?;
870
871            combined_subst = combined_subst.compose(&arm_subst);
872            arm_results.push(arm_result);
873            aux_after_arms.push(self.current_aux_stack.borrow().clone());
874
875            // Merge effects from this arm
876            for effect in arm_effects {
877                if !merged_effects.contains(&effect) {
878                    merged_effects.push(effect);
879                }
880            }
881        }
882
883        // Verify all arms produce the same aux stack (Issue #350)
884        if aux_after_arms.len() > 1 {
885            let first_aux = &aux_after_arms[0];
886            for (i, arm_aux) in aux_after_arms.iter().enumerate().skip(1) {
887                if arm_aux != first_aux {
888                    let match_line = match_span.as_ref().map(|s| s.line + 1).unwrap_or(0);
889                    return Err(format!(
890                        "at line {}: match arms have incompatible aux stack effects:\n\
891                         \x20 arm 0 aux: {}\n\
892                         \x20 arm {} aux: {}\n\
893                         \x20 All match arms must leave the aux stack in the same state.",
894                        match_line, first_aux, i, arm_aux
895                    ));
896                }
897            }
898        }
899        // Set aux to the first arm's result (all are verified equal)
900        if let Some(aux) = aux_after_arms.into_iter().next() {
901            *self.current_aux_stack.borrow_mut() = aux;
902        }
903
904        // Unify all arm results to ensure they're compatible
905        let mut final_result = arm_results[0].clone();
906        for (i, arm_result) in arm_results.iter().enumerate().skip(1) {
907            // Get line info for error reporting
908            let match_line = match_span.as_ref().map(|s| s.line + 1).unwrap_or(0);
909            let arm0_line = arms[0].span.as_ref().map(|s| s.line + 1).unwrap_or(0);
910            let arm_i_line = arms[i].span.as_ref().map(|s| s.line + 1).unwrap_or(0);
911
912            let arm_subst = unify_stacks(&final_result, arm_result).map_err(|e| {
913                if match_line > 0 && arm0_line > 0 && arm_i_line > 0 {
914                    format!(
915                        "at line {}: match arms have incompatible stack effects:\n\
916                         \x20 arm 0 (line {}) produces: {}\n\
917                         \x20 arm {} (line {}) produces: {}\n\
918                         \x20 All match arms must produce the same stack shape.\n\
919                         \x20 Error: {}",
920                        match_line, arm0_line, final_result, i, arm_i_line, arm_result, e
921                    )
922                } else {
923                    format!(
924                        "match arms have incompatible stack effects:\n\
925                         \x20 arm 0 produces: {}\n\
926                         \x20 arm {} produces: {}\n\
927                         \x20 All match arms must produce the same stack shape.\n\
928                         \x20 Error: {}",
929                        final_result, i, arm_result, e
930                    )
931                }
932            })?;
933            combined_subst = combined_subst.compose(&arm_subst);
934            final_result = arm_subst.apply_stack(&final_result);
935        }
936
937        Ok((final_result, combined_subst, merged_effects))
938    }
939
940    /// Push variant fields onto the stack based on the match pattern
941    fn push_variant_fields(
942        &self,
943        stack: &StackType,
944        pattern: &crate::ast::Pattern,
945        variant_info: &VariantInfo,
946        variant_name: &str,
947    ) -> Result<StackType, String> {
948        let mut arm_stack = stack.clone();
949        match pattern {
950            crate::ast::Pattern::Variant(_) => {
951                // Stack-based: push all fields in declaration order
952                for field in &variant_info.fields {
953                    arm_stack = arm_stack.push(field.field_type.clone());
954                }
955            }
956            crate::ast::Pattern::VariantWithBindings { bindings, .. } => {
957                // Named bindings: validate and push only bound fields
958                for binding in bindings {
959                    let field = variant_info
960                        .fields
961                        .iter()
962                        .find(|f| &f.name == binding)
963                        .ok_or_else(|| {
964                            let available: Vec<_> = variant_info
965                                .fields
966                                .iter()
967                                .map(|f| f.name.as_str())
968                                .collect();
969                            format!(
970                                "Unknown field '{}' in pattern for variant '{}'.\n\
971                                 Available fields: {}",
972                                binding,
973                                variant_name,
974                                available.join(", ")
975                            )
976                        })?;
977                    arm_stack = arm_stack.push(field.field_type.clone());
978                }
979            }
980        }
981        Ok(arm_stack)
982    }
983
984    /// Check if a branch ends with a recursive tail call to the current word
985    /// or to a mutually recursive word.
986    ///
987    /// Such branches are "divergent" - they never return to the if/else,
988    /// so their stack effect shouldn't constrain the other branch.
989    ///
990    /// # Detection Capabilities
991    ///
992    /// - Direct recursion: word calls itself
993    /// - Mutual recursion: word calls another word in the same SCC (when call graph is available)
994    ///
995    /// # Limitations
996    ///
997    /// This detection does NOT detect:
998    /// - Calls to known non-returning functions (panic, exit, infinite loops)
999    /// - Nested control flow with tail calls (if ... if ... recurse then then)
1000    ///
1001    /// These patterns will still require branch unification. Future enhancements
1002    /// could track known non-returning functions or support explicit divergence
1003    /// annotations (similar to Rust's `!` type).
1004    fn is_divergent_branch(&self, statements: &[Statement]) -> bool {
1005        let Some((current_word_name, _)) = self.current_word.borrow().as_ref().cloned() else {
1006            return false;
1007        };
1008        let Some(Statement::WordCall { name, .. }) = statements.last() else {
1009            return false;
1010        };
1011
1012        // Direct recursion: word calls itself
1013        if name == &current_word_name {
1014            return true;
1015        }
1016
1017        // Mutual recursion: word calls another word in the same SCC
1018        if let Some(ref graph) = self.call_graph
1019            && graph.are_mutually_recursive(&current_word_name, name)
1020        {
1021            return true;
1022        }
1023
1024        false
1025    }
1026
1027    /// Infer the stack effect of an if/else expression
1028    fn infer_if(
1029        &self,
1030        then_branch: &[Statement],
1031        else_branch: &Option<Vec<Statement>>,
1032        if_span: &Option<crate::ast::Span>,
1033        current_stack: StackType,
1034    ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1035        // Pop condition (must be Bool)
1036        let (stack_after_cond, cond_type) = self.pop_type(&current_stack, "if condition")?;
1037
1038        // Condition must be Bool
1039        let cond_subst = unify_stacks(
1040            &StackType::singleton(Type::Bool),
1041            &StackType::singleton(cond_type),
1042        )
1043        .map_err(|e| format!("if condition must be Bool: {}", e))?;
1044
1045        let stack_after_cond = cond_subst.apply_stack(&stack_after_cond);
1046
1047        // Check for divergent branches (recursive tail calls)
1048        let then_diverges = self.is_divergent_branch(then_branch);
1049        let else_diverges = else_branch
1050            .as_ref()
1051            .map(|stmts| self.is_divergent_branch(stmts))
1052            .unwrap_or(false);
1053
1054        // Save aux stack before branching (Issue #350)
1055        let aux_before_branches = self.current_aux_stack.borrow().clone();
1056
1057        // Infer branches directly from the actual stack
1058        // Don't capture statement types for if branches - only top-level word bodies
1059        let (then_result, then_subst, then_effects) =
1060            self.infer_statements_from(then_branch, &stack_after_cond, false)?;
1061        let aux_after_then = self.current_aux_stack.borrow().clone();
1062
1063        // Restore aux stack before checking else branch (Issue #350)
1064        *self.current_aux_stack.borrow_mut() = aux_before_branches.clone();
1065
1066        // Infer else branch (or use stack_after_cond if no else)
1067        let (else_result, else_subst, else_effects) = if let Some(else_stmts) = else_branch {
1068            self.infer_statements_from(else_stmts, &stack_after_cond, false)?
1069        } else {
1070            (stack_after_cond.clone(), Subst::empty(), vec![])
1071        };
1072        let aux_after_else = self.current_aux_stack.borrow().clone();
1073
1074        // Verify aux stacks match between branches (Issue #350)
1075        // Skip check if one branch diverges (never returns)
1076        if !then_diverges && !else_diverges && aux_after_then != aux_after_else {
1077            let if_line = if_span.as_ref().map(|s| s.line + 1).unwrap_or(0);
1078            return Err(format!(
1079                "at line {}: if/else branches have incompatible aux stack effects:\n\
1080                 \x20 then branch aux: {}\n\
1081                 \x20 else branch aux: {}\n\
1082                 \x20 Both branches must leave the aux stack in the same state.",
1083                if_line, aux_after_then, aux_after_else
1084            ));
1085        }
1086
1087        // Set aux to the non-divergent branch's result (or then if neither diverges)
1088        if then_diverges && !else_diverges {
1089            *self.current_aux_stack.borrow_mut() = aux_after_else;
1090        } else {
1091            *self.current_aux_stack.borrow_mut() = aux_after_then;
1092        }
1093
1094        // Merge effects from both branches (if either yields, the whole if yields)
1095        let mut merged_effects = then_effects;
1096        for effect in else_effects {
1097            if !merged_effects.contains(&effect) {
1098                merged_effects.push(effect);
1099            }
1100        }
1101
1102        // Handle divergent branches: if one branch diverges (never returns),
1103        // use the other branch's stack type without requiring unification.
1104        // This supports patterns like:
1105        //   chan.receive not if drop store-loop then
1106        // where the then branch recurses and the else branch continues.
1107        let (result, branch_subst) = if then_diverges && !else_diverges {
1108            // Then branch diverges, use else branch's type
1109            (else_result, Subst::empty())
1110        } else if else_diverges && !then_diverges {
1111            // Else branch diverges, use then branch's type
1112            (then_result, Subst::empty())
1113        } else {
1114            // Both branches must produce compatible stacks (normal case)
1115            let if_line = if_span.as_ref().map(|s| s.line + 1).unwrap_or(0);
1116            let branch_subst = unify_stacks(&then_result, &else_result).map_err(|e| {
1117                if if_line > 0 {
1118                    format!(
1119                        "at line {}: if/else branches have incompatible stack effects:\n\
1120                         \x20 then branch produces: {}\n\
1121                         \x20 else branch produces: {}\n\
1122                         \x20 Both branches of an if/else must produce the same stack shape.\n\
1123                         \x20 Hint: Make sure both branches push/pop the same number of values.\n\
1124                         \x20 Error: {}",
1125                        if_line, then_result, else_result, e
1126                    )
1127                } else {
1128                    format!(
1129                        "if/else branches have incompatible stack effects:\n\
1130                         \x20 then branch produces: {}\n\
1131                         \x20 else branch produces: {}\n\
1132                         \x20 Both branches of an if/else must produce the same stack shape.\n\
1133                         \x20 Hint: Make sure both branches push/pop the same number of values.\n\
1134                         \x20 Error: {}",
1135                        then_result, else_result, e
1136                    )
1137                }
1138            })?;
1139            (branch_subst.apply_stack(&then_result), branch_subst)
1140        };
1141
1142        // Propagate all substitutions
1143        let total_subst = cond_subst
1144            .compose(&then_subst)
1145            .compose(&else_subst)
1146            .compose(&branch_subst);
1147        Ok((result, total_subst, merged_effects))
1148    }
1149
1150    /// Infer the stack effect of a quotation
1151    /// Quotations capture effects in their type - they don't propagate effects to the outer scope
1152    fn infer_quotation(
1153        &self,
1154        id: usize,
1155        body: &[Statement],
1156        current_stack: StackType,
1157    ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1158        // Save and clear expected type so nested quotations don't inherit it
1159        // The expected type applies only to THIS quotation, not inner ones
1160        let expected_for_this_quotation = self.expected_quotation_type.borrow().clone();
1161        *self.expected_quotation_type.borrow_mut() = None;
1162
1163        // Save enclosing aux stack and mark quotation scope (Issue #350)
1164        // Aux operations are rejected inside quotations because quotations are
1165        // compiled as separate LLVM functions without their own aux slot allocas.
1166        // Users should extract quotation bodies into named words if they need aux.
1167        let saved_aux = self.current_aux_stack.borrow().clone();
1168        *self.current_aux_stack.borrow_mut() = StackType::Empty;
1169        let saved_in_quotation = self.in_quotation_scope.get();
1170        self.in_quotation_scope.set(true);
1171
1172        // Infer the effect of the quotation body (includes computational effects)
1173        let body_effect = self.infer_statements(body)?;
1174
1175        // Verify quotation's aux stack is balanced (Issue #350)
1176        let quot_aux = self.current_aux_stack.borrow().clone();
1177        if quot_aux != StackType::Empty {
1178            return Err(format!(
1179                "Quotation has unbalanced aux stack.\n\
1180                 Remaining aux stack: {}\n\
1181                 Every >aux must be matched by a corresponding aux> within the quotation.",
1182                quot_aux
1183            ));
1184        }
1185
1186        // Restore enclosing aux stack and quotation scope flag
1187        *self.current_aux_stack.borrow_mut() = saved_aux;
1188        self.in_quotation_scope.set(saved_in_quotation);
1189
1190        // Restore expected type for capture analysis of THIS quotation
1191        *self.expected_quotation_type.borrow_mut() = expected_for_this_quotation;
1192
1193        // Perform capture analysis
1194        let quot_type = self.analyze_captures(&body_effect, &current_stack)?;
1195
1196        // Record this quotation's type in the type map (for CodeGen to use later)
1197        self.quotation_types
1198            .borrow_mut()
1199            .insert(id, quot_type.clone());
1200
1201        // If this is a closure, we need to pop the captured values from the stack
1202        let result_stack = match &quot_type {
1203            Type::Quotation(_) => {
1204                // Stateless - no captures, just push quotation onto stack
1205                current_stack.push(quot_type)
1206            }
1207            Type::Closure { captures, .. } => {
1208                // Pop captured values from stack, then push closure
1209                let mut stack = current_stack.clone();
1210                for _ in 0..captures.len() {
1211                    let (new_stack, _value) = self.pop_type(&stack, "closure capture")?;
1212                    stack = new_stack;
1213                }
1214                stack.push(quot_type)
1215            }
1216            _ => unreachable!("analyze_captures only returns Quotation or Closure"),
1217        };
1218
1219        // Quotations don't propagate effects - they capture them in the quotation type
1220        // The effect annotation on the quotation type (e.g., [ ..a -- ..b | Yield Int ])
1221        // indicates what effects the quotation may produce when called
1222        Ok((result_stack, Subst::empty(), vec![]))
1223    }
1224
1225    /// Infer the stack effect of a word call
1226    fn infer_word_call(
1227        &self,
1228        name: &str,
1229        span: &Option<crate::ast::Span>,
1230        current_stack: StackType,
1231    ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1232        // Special handling for aux stack operations (Issue #350)
1233        if name == ">aux" {
1234            return self.infer_to_aux(span, current_stack);
1235        }
1236        if name == "aux>" {
1237            return self.infer_from_aux(span, current_stack);
1238        }
1239
1240        // Special handling for `call`: extract and apply the quotation's actual effect
1241        // This ensures stack pollution through quotations is caught (Issue #228)
1242        if name == "call" {
1243            return self.infer_call(span, current_stack);
1244        }
1245
1246        // Look up word's effect
1247        let effect = self
1248            .lookup_word_effect(name)
1249            .ok_or_else(|| format!("Unknown word: '{}'", name))?;
1250
1251        // Freshen the effect to avoid variable name clashes
1252        let fresh_effect = self.freshen_effect(&effect);
1253
1254        // Special handling for strand.spawn: auto-convert Quotation to Closure if needed
1255        let adjusted_stack = if name == "strand.spawn" {
1256            self.adjust_stack_for_spawn(current_stack, &fresh_effect)?
1257        } else {
1258            current_stack
1259        };
1260
1261        // Apply the freshened effect to current stack
1262        let (result_stack, subst) = self.apply_effect(&fresh_effect, adjusted_stack, name, span)?;
1263
1264        // Propagate side effects from the called word
1265        // Note: strand.weave "handles" Yield effects (consumes them from the quotation)
1266        // strand.spawn requires pure quotations (checked separately)
1267        let propagated_effects = fresh_effect.effects.clone();
1268
1269        Ok((result_stack, subst, propagated_effects))
1270    }
1271
1272    /// Handle >aux: pop from main stack, push onto word-local aux stack (Issue #350)
1273    fn infer_to_aux(
1274        &self,
1275        _span: &Option<crate::ast::Span>,
1276        current_stack: StackType,
1277    ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1278        if self.in_quotation_scope.get() {
1279            return Err(">aux is not supported inside quotations.\n\
1280                 Quotations are compiled as separate functions without aux stack slots.\n\
1281                 Extract the quotation body into a named word if you need aux."
1282                .to_string());
1283        }
1284        let (rest, top_type) = self.pop_type(&current_stack, ">aux")?;
1285
1286        // Push onto aux stack
1287        let mut aux = self.current_aux_stack.borrow_mut();
1288        *aux = aux.clone().push(top_type);
1289
1290        // Track max depth for codegen alloca sizing
1291        let depth = Self::stack_depth(&aux);
1292        if let Some((word_name, _)) = self.current_word.borrow().as_ref() {
1293            let mut depths = self.aux_max_depths.borrow_mut();
1294            let entry = depths.entry(word_name.clone()).or_insert(0);
1295            if depth > *entry {
1296                *entry = depth;
1297            }
1298        }
1299
1300        Ok((rest, Subst::empty(), vec![]))
1301    }
1302
1303    /// Handle aux>: pop from aux stack, push onto main stack (Issue #350)
1304    fn infer_from_aux(
1305        &self,
1306        _span: &Option<crate::ast::Span>,
1307        current_stack: StackType,
1308    ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1309        if self.in_quotation_scope.get() {
1310            return Err("aux> is not supported inside quotations.\n\
1311                 Quotations are compiled as separate functions without aux stack slots.\n\
1312                 Extract the quotation body into a named word if you need aux."
1313                .to_string());
1314        }
1315        let mut aux = self.current_aux_stack.borrow_mut();
1316        match aux.clone().pop() {
1317            Some((rest, top_type)) => {
1318                *aux = rest;
1319                Ok((current_stack.push(top_type), Subst::empty(), vec![]))
1320            }
1321            None => {
1322                let line_info = self.line_prefix();
1323                Err(format!(
1324                    "{}aux>: aux stack is empty. Every aux> must be paired with a preceding >aux.",
1325                    line_info
1326                ))
1327            }
1328        }
1329    }
1330
1331    /// Special handling for `call` to properly propagate quotation effects (Issue #228)
1332    ///
1333    /// The generic `call` signature `( ..a Q -- ..b )` has independent row variables,
1334    /// which doesn't constrain the output based on the quotation's actual effect.
1335    /// This function extracts the quotation's effect and applies it properly.
1336    fn infer_call(
1337        &self,
1338        span: &Option<crate::ast::Span>,
1339        current_stack: StackType,
1340    ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1341        // Pop the quotation from the stack
1342        let line_prefix = self.line_prefix();
1343        let (remaining_stack, quot_type) = current_stack.clone().pop().ok_or_else(|| {
1344            format!(
1345                "{}call: stack underflow - expected quotation on stack",
1346                line_prefix
1347            )
1348        })?;
1349
1350        // Extract the quotation's effect
1351        let quot_effect = match &quot_type {
1352            Type::Quotation(effect) => (**effect).clone(),
1353            Type::Closure { effect, .. } => (**effect).clone(),
1354            Type::Var(_) => {
1355                // Type variable - fall back to polymorphic behavior
1356                // This happens when the quotation type isn't known yet
1357                let effect = self
1358                    .lookup_word_effect("call")
1359                    .ok_or_else(|| "Unknown word: 'call'".to_string())?;
1360                let fresh_effect = self.freshen_effect(&effect);
1361                let (result_stack, subst) =
1362                    self.apply_effect(&fresh_effect, current_stack, "call", span)?;
1363                return Ok((result_stack, subst, vec![]));
1364            }
1365            _ => {
1366                return Err(format!(
1367                    "call: expected quotation or closure on stack, got {}",
1368                    quot_type
1369                ));
1370            }
1371        };
1372
1373        // Check for Yield effects - quotations with Yield must use strand.weave
1374        if quot_effect.has_yield() {
1375            return Err("Cannot call quotation with Yield effect directly.\n\
1376                 Quotations that yield values must be wrapped with `strand.weave`.\n\
1377                 Example: `[ yielding-code ] strand.weave` instead of `[ yielding-code ] call`"
1378                .to_string());
1379        }
1380
1381        // Freshen the quotation's effect to avoid variable clashes
1382        let fresh_effect = self.freshen_effect(&quot_effect);
1383
1384        // Apply the quotation's effect to the remaining stack
1385        let (result_stack, subst) =
1386            self.apply_effect(&fresh_effect, remaining_stack, "call", span)?;
1387
1388        // Propagate side effects from the quotation
1389        let propagated_effects = fresh_effect.effects.clone();
1390
1391        Ok((result_stack, subst, propagated_effects))
1392    }
1393
1394    /// Infer the resulting stack type after a statement
1395    /// Takes current stack, returns (new stack, substitution, side effects) after statement
1396    fn infer_statement(
1397        &self,
1398        statement: &Statement,
1399        current_stack: StackType,
1400    ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1401        match statement {
1402            Statement::IntLiteral(_) => Ok((current_stack.push(Type::Int), Subst::empty(), vec![])),
1403            Statement::BoolLiteral(_) => {
1404                Ok((current_stack.push(Type::Bool), Subst::empty(), vec![]))
1405            }
1406            Statement::StringLiteral(_) => {
1407                Ok((current_stack.push(Type::String), Subst::empty(), vec![]))
1408            }
1409            Statement::FloatLiteral(_) => {
1410                Ok((current_stack.push(Type::Float), Subst::empty(), vec![]))
1411            }
1412            Statement::Symbol(_) => Ok((current_stack.push(Type::Symbol), Subst::empty(), vec![])),
1413            Statement::Match { arms, span } => self.infer_match(arms, span, current_stack),
1414            Statement::WordCall { name, span } => self.infer_word_call(name, span, current_stack),
1415            Statement::If {
1416                then_branch,
1417                else_branch,
1418                span,
1419            } => self.infer_if(then_branch, else_branch, span, current_stack),
1420            Statement::Quotation { id, body, .. } => self.infer_quotation(*id, body, current_stack),
1421        }
1422    }
1423
1424    /// Look up the effect of a word (built-in or user-defined)
1425    fn lookup_word_effect(&self, name: &str) -> Option<Effect> {
1426        // First check built-ins
1427        if let Some(effect) = builtin_signature(name) {
1428            return Some(effect);
1429        }
1430
1431        // Then check user-defined words
1432        self.env.get(name).cloned()
1433    }
1434
1435    /// Apply an effect to a stack
1436    /// Effect: (inputs -- outputs)
1437    /// Current stack must match inputs, result is outputs
1438    /// Returns (result_stack, substitution)
1439    fn apply_effect(
1440        &self,
1441        effect: &Effect,
1442        current_stack: StackType,
1443        operation: &str,
1444        span: &Option<crate::ast::Span>,
1445    ) -> Result<(StackType, Subst), String> {
1446        // Check for stack underflow: if the effect needs more concrete values than
1447        // the current stack provides, and the stack has a "rigid" row variable at its base,
1448        // this would be unsound (the row var could be Empty at runtime).
1449        // Bug #169: "phantom stack entries"
1450        //
1451        // We only check for "rigid" row variables (named "rest" from declared effects).
1452        // Row variables named "input" are from inference and CAN grow to discover requirements.
1453        let effect_concrete = Self::count_concrete_types(&effect.inputs);
1454        let stack_concrete = Self::count_concrete_types(&current_stack);
1455
1456        if let Some(row_var_name) = Self::get_row_var_base(&current_stack) {
1457            // Only check "rigid" row variables (from declared effects, not inference).
1458            //
1459            // Row variable naming convention (established in parser.rs:build_stack_type):
1460            // - "rest": Created by the parser for declared stack effects. When a word declares
1461            //   `( String Int -- String )`, the parser creates `( ..rest String Int -- ..rest String )`.
1462            //   This "rest" is rigid because the caller guarantees exactly these concrete types.
1463            // - "rest$N": Freshened versions created during type checking when calling other words.
1464            //   These represent the callee's stack context and can grow during unification.
1465            // - "input": Created for words without declared effects during inference.
1466            //   These are flexible and grow to discover the word's actual requirements.
1467            //
1468            // Only the original "rest" (exact match) should trigger underflow checking.
1469            let is_rigid = row_var_name == "rest";
1470
1471            if is_rigid && effect_concrete > stack_concrete {
1472                let word_name = self
1473                    .current_word
1474                    .borrow()
1475                    .as_ref()
1476                    .map(|(n, _)| n.clone())
1477                    .unwrap_or_else(|| "unknown".to_string());
1478                return Err(format!(
1479                    "{}In '{}': {}: stack underflow - requires {} value(s), only {} provided",
1480                    self.line_prefix(),
1481                    word_name,
1482                    operation,
1483                    effect_concrete,
1484                    stack_concrete
1485                ));
1486            }
1487        }
1488
1489        // Unify current stack with effect's input
1490        let subst = unify_stacks(&effect.inputs, &current_stack).map_err(|e| {
1491            let line_info = span
1492                .as_ref()
1493                .map(|s| format_line_prefix(s.line))
1494                .unwrap_or_default();
1495            format!(
1496                "{}{}: stack type mismatch. Expected {}, got {}: {}",
1497                line_info, operation, effect.inputs, current_stack, e
1498            )
1499        })?;
1500
1501        // Apply substitution to output
1502        let result_stack = subst.apply_stack(&effect.outputs);
1503
1504        Ok((result_stack, subst))
1505    }
1506
1507    /// Count the number of concrete (non-row-variable) types in a stack
1508    fn count_concrete_types(stack: &StackType) -> usize {
1509        let mut count = 0;
1510        let mut current = stack;
1511        while let StackType::Cons { rest, top: _ } = current {
1512            count += 1;
1513            current = rest;
1514        }
1515        count
1516    }
1517
1518    /// Get the row variable name at the base of a stack, if any
1519    fn get_row_var_base(stack: &StackType) -> Option<String> {
1520        let mut current = stack;
1521        while let StackType::Cons { rest, top: _ } = current {
1522            current = rest;
1523        }
1524        match current {
1525            StackType::RowVar(name) => Some(name.clone()),
1526            _ => None,
1527        }
1528    }
1529
1530    /// Adjust stack for strand.spawn operation by converting Quotation to Closure if needed
1531    ///
1532    /// strand.spawn expects Quotation(Empty -- Empty), but if we have Quotation(T... -- U...)
1533    /// with non-empty inputs, we auto-convert it to a Closure that captures those inputs.
1534    fn adjust_stack_for_spawn(
1535        &self,
1536        current_stack: StackType,
1537        spawn_effect: &Effect,
1538    ) -> Result<StackType, String> {
1539        // strand.spawn expects: ( ..a Quotation(Empty -- Empty) -- ..a Int )
1540        // Extract the expected quotation type from strand.spawn's effect
1541        let expected_quot_type = match &spawn_effect.inputs {
1542            StackType::Cons { top, rest: _ } => {
1543                if !matches!(top, Type::Quotation(_)) {
1544                    return Ok(current_stack); // Not a quotation, don't adjust
1545                }
1546                top
1547            }
1548            _ => return Ok(current_stack),
1549        };
1550
1551        // Check what's actually on the stack
1552        let (rest_stack, actual_type) = match &current_stack {
1553            StackType::Cons { rest, top } => (rest.as_ref().clone(), top),
1554            _ => return Ok(current_stack), // Empty stack, nothing to adjust
1555        };
1556
1557        // If top of stack is a Quotation with non-empty inputs, convert to Closure
1558        if let Type::Quotation(actual_effect) = actual_type {
1559            // Check if quotation needs inputs
1560            if !matches!(actual_effect.inputs, StackType::Empty) {
1561                // Extract expected effect from spawn's signature
1562                let expected_effect = match expected_quot_type {
1563                    Type::Quotation(eff) => eff.as_ref(),
1564                    _ => return Ok(current_stack),
1565                };
1566
1567                // Calculate what needs to be captured
1568                let captures = calculate_captures(actual_effect, expected_effect)?;
1569
1570                // Create a Closure type
1571                let closure_type = Type::Closure {
1572                    effect: Box::new(expected_effect.clone()),
1573                    captures: captures.clone(),
1574                };
1575
1576                // Pop the captured values from the stack
1577                // The values to capture are BELOW the quotation on the stack
1578                let mut adjusted_stack = rest_stack;
1579                for _ in &captures {
1580                    adjusted_stack = match adjusted_stack {
1581                        StackType::Cons { rest, .. } => rest.as_ref().clone(),
1582                        _ => {
1583                            return Err(format!(
1584                                "strand.spawn: not enough values on stack to capture. Need {} values",
1585                                captures.len()
1586                            ));
1587                        }
1588                    };
1589                }
1590
1591                // Push the Closure onto the adjusted stack
1592                return Ok(adjusted_stack.push(closure_type));
1593            }
1594        }
1595
1596        Ok(current_stack)
1597    }
1598
1599    /// Analyze quotation captures
1600    ///
1601    /// Determines whether a quotation should be stateless (Type::Quotation)
1602    /// or a closure (Type::Closure) based on the expected type from the word signature.
1603    ///
1604    /// Type-driven inference with automatic closure creation:
1605    ///   - If expected type is Closure[effect], calculate what to capture
1606    ///   - If expected type is Quotation[effect]:
1607    ///     - If body needs more inputs than expected effect, auto-create Closure
1608    ///     - Otherwise return stateless Quotation
1609    ///   - If no expected type, default to stateless (conservative)
1610    ///
1611    /// Example 1 (auto-create closure):
1612    ///   Expected: Quotation[-- ]          [spawn expects ( -- )]
1613    ///   Body: [ handle-connection ]       [needs ( Int -- )]
1614    ///   Body effect: ( Int -- )           [needs 1 Int]
1615    ///   Expected effect: ( -- )           [provides 0 inputs]
1616    ///   Result: Closure { effect: ( -- ), captures: [Int] }
1617    ///
1618    /// Example 2 (explicit closure):
1619    ///   Signature: ( Int -- Closure[Int -- Int] )
1620    ///   Body: [ add ]
1621    ///   Body effect: ( Int Int -- Int )  [add needs 2 Ints]
1622    ///   Expected effect: [Int -- Int]    [call site provides 1 Int]
1623    ///   Result: Closure { effect: [Int -- Int], captures: [Int] }
1624    fn analyze_captures(
1625        &self,
1626        body_effect: &Effect,
1627        _current_stack: &StackType,
1628    ) -> Result<Type, String> {
1629        // Check if there's an expected type from the word signature
1630        let expected = self.expected_quotation_type.borrow().clone();
1631
1632        match expected {
1633            Some(Type::Closure { effect, .. }) => {
1634                // User declared closure type - calculate captures
1635                let captures = calculate_captures(body_effect, &effect)?;
1636                Ok(Type::Closure { effect, captures })
1637            }
1638            Some(Type::Quotation(expected_effect)) => {
1639                // User declared quotation type - check if we need to auto-create closure
1640                // Auto-create closure only when:
1641                // 1. Expected effect has empty inputs (like spawn's ( -- ))
1642                // 2. Body effect has non-empty inputs (needs values to execute)
1643
1644                let expected_is_empty = matches!(expected_effect.inputs, StackType::Empty);
1645                let body_needs_inputs = !matches!(body_effect.inputs, StackType::Empty);
1646
1647                if expected_is_empty && body_needs_inputs {
1648                    // Body needs inputs but expected provides none
1649                    // Auto-create closure to capture the inputs
1650                    let captures = calculate_captures(body_effect, &expected_effect)?;
1651                    Ok(Type::Closure {
1652                        effect: expected_effect,
1653                        captures,
1654                    })
1655                } else {
1656                    // Verify the body effect is compatible with the expected effect
1657                    // by unifying the quotation types. This catches:
1658                    // - Stack pollution: body pushes values when expected is stack-neutral
1659                    // - Stack underflow: body consumes values when expected is stack-neutral
1660                    // - Wrong return type: body returns Int when Bool expected
1661                    let body_quot = Type::Quotation(Box::new(body_effect.clone()));
1662                    let expected_quot = Type::Quotation(expected_effect.clone());
1663                    unify_types(&body_quot, &expected_quot).map_err(|e| {
1664                        format!(
1665                            "quotation effect mismatch: expected {}, got {}: {}",
1666                            expected_effect, body_effect, e
1667                        )
1668                    })?;
1669
1670                    // Body is compatible with expected effect - stateless quotation
1671                    Ok(Type::Quotation(expected_effect))
1672                }
1673            }
1674            _ => {
1675                // No expected type - conservative default: stateless quotation
1676                Ok(Type::Quotation(Box::new(body_effect.clone())))
1677            }
1678        }
1679    }
1680
1681    /// Check if an inferred effect matches any of the declared effects
1682    /// Effects match by kind (e.g., Yield matches Yield, regardless of type parameters)
1683    /// Type parameters should unify, but for now we just check the effect kind
1684    fn effect_matches_any(&self, inferred: &SideEffect, declared: &[SideEffect]) -> bool {
1685        declared.iter().any(|decl| match (inferred, decl) {
1686            (SideEffect::Yield(_), SideEffect::Yield(_)) => true,
1687        })
1688    }
1689
1690    /// Pop a type from a stack type, returning (rest, top)
1691    fn pop_type(&self, stack: &StackType, context: &str) -> Result<(StackType, Type), String> {
1692        match stack {
1693            StackType::Cons { rest, top } => Ok(((**rest).clone(), top.clone())),
1694            StackType::Empty => Err(format!(
1695                "{}: stack underflow - expected value on stack but stack is empty",
1696                context
1697            )),
1698            StackType::RowVar(_) => {
1699                // Can't statically determine if row variable is empty
1700                // For now, assume it has at least one element
1701                // This is conservative - real implementation would track constraints
1702                Err(format!(
1703                    "{}: cannot pop from polymorphic stack without more type information",
1704                    context
1705                ))
1706            }
1707        }
1708    }
1709}
1710
1711impl Default for TypeChecker {
1712    fn default() -> Self {
1713        Self::new()
1714    }
1715}
1716
1717#[cfg(test)]
1718mod tests {
1719    use super::*;
1720
1721    #[test]
1722    fn test_simple_literal() {
1723        let program = Program {
1724            includes: vec![],
1725            unions: vec![],
1726            words: vec![WordDef {
1727                name: "test".to_string(),
1728                effect: Some(Effect::new(
1729                    StackType::Empty,
1730                    StackType::singleton(Type::Int),
1731                )),
1732                body: vec![Statement::IntLiteral(42)],
1733                source: None,
1734                allowed_lints: vec![],
1735            }],
1736        };
1737
1738        let mut checker = TypeChecker::new();
1739        assert!(checker.check_program(&program).is_ok());
1740    }
1741
1742    #[test]
1743    fn test_simple_operation() {
1744        // : test ( Int Int -- Int ) add ;
1745        let program = Program {
1746            includes: vec![],
1747            unions: vec![],
1748            words: vec![WordDef {
1749                name: "test".to_string(),
1750                effect: Some(Effect::new(
1751                    StackType::Empty.push(Type::Int).push(Type::Int),
1752                    StackType::singleton(Type::Int),
1753                )),
1754                body: vec![Statement::WordCall {
1755                    name: "i.add".to_string(),
1756                    span: None,
1757                }],
1758                source: None,
1759                allowed_lints: vec![],
1760            }],
1761        };
1762
1763        let mut checker = TypeChecker::new();
1764        assert!(checker.check_program(&program).is_ok());
1765    }
1766
1767    #[test]
1768    fn test_type_mismatch() {
1769        // : test ( String -- ) io.write-line ;  with body: 42
1770        let program = Program {
1771            includes: vec![],
1772            unions: vec![],
1773            words: vec![WordDef {
1774                name: "test".to_string(),
1775                effect: Some(Effect::new(
1776                    StackType::singleton(Type::String),
1777                    StackType::Empty,
1778                )),
1779                body: vec![
1780                    Statement::IntLiteral(42), // Pushes Int, not String!
1781                    Statement::WordCall {
1782                        name: "io.write-line".to_string(),
1783                        span: None,
1784                    },
1785                ],
1786                source: None,
1787                allowed_lints: vec![],
1788            }],
1789        };
1790
1791        let mut checker = TypeChecker::new();
1792        let result = checker.check_program(&program);
1793        assert!(result.is_err());
1794        assert!(result.unwrap_err().contains("Type mismatch"));
1795    }
1796
1797    #[test]
1798    fn test_polymorphic_dup() {
1799        // : my-dup ( Int -- Int Int ) dup ;
1800        let program = Program {
1801            includes: vec![],
1802            unions: vec![],
1803            words: vec![WordDef {
1804                name: "my-dup".to_string(),
1805                effect: Some(Effect::new(
1806                    StackType::singleton(Type::Int),
1807                    StackType::Empty.push(Type::Int).push(Type::Int),
1808                )),
1809                body: vec![Statement::WordCall {
1810                    name: "dup".to_string(),
1811                    span: None,
1812                }],
1813                source: None,
1814                allowed_lints: vec![],
1815            }],
1816        };
1817
1818        let mut checker = TypeChecker::new();
1819        assert!(checker.check_program(&program).is_ok());
1820    }
1821
1822    #[test]
1823    fn test_conditional_branches() {
1824        // : test ( Int Int -- String )
1825        //   > if "greater" else "not greater" then ;
1826        let program = Program {
1827            includes: vec![],
1828            unions: vec![],
1829            words: vec![WordDef {
1830                name: "test".to_string(),
1831                effect: Some(Effect::new(
1832                    StackType::Empty.push(Type::Int).push(Type::Int),
1833                    StackType::singleton(Type::String),
1834                )),
1835                body: vec![
1836                    Statement::WordCall {
1837                        name: "i.>".to_string(),
1838                        span: None,
1839                    },
1840                    Statement::If {
1841                        then_branch: vec![Statement::StringLiteral("greater".to_string())],
1842                        else_branch: Some(vec![Statement::StringLiteral(
1843                            "not greater".to_string(),
1844                        )]),
1845                        span: None,
1846                    },
1847                ],
1848                source: None,
1849                allowed_lints: vec![],
1850            }],
1851        };
1852
1853        let mut checker = TypeChecker::new();
1854        assert!(checker.check_program(&program).is_ok());
1855    }
1856
1857    #[test]
1858    fn test_mismatched_branches() {
1859        // : test ( -- Int )
1860        //   true if 42 else "string" then ;  // ERROR: incompatible types
1861        let program = Program {
1862            includes: vec![],
1863            unions: vec![],
1864            words: vec![WordDef {
1865                name: "test".to_string(),
1866                effect: Some(Effect::new(
1867                    StackType::Empty,
1868                    StackType::singleton(Type::Int),
1869                )),
1870                body: vec![
1871                    Statement::BoolLiteral(true),
1872                    Statement::If {
1873                        then_branch: vec![Statement::IntLiteral(42)],
1874                        else_branch: Some(vec![Statement::StringLiteral("string".to_string())]),
1875                        span: None,
1876                    },
1877                ],
1878                source: None,
1879                allowed_lints: vec![],
1880            }],
1881        };
1882
1883        let mut checker = TypeChecker::new();
1884        let result = checker.check_program(&program);
1885        assert!(result.is_err());
1886        assert!(result.unwrap_err().contains("incompatible"));
1887    }
1888
1889    #[test]
1890    fn test_user_defined_word_call() {
1891        // : helper ( Int -- String ) int->string ;
1892        // : main ( -- ) 42 helper io.write-line ;
1893        let program = Program {
1894            includes: vec![],
1895            unions: vec![],
1896            words: vec![
1897                WordDef {
1898                    name: "helper".to_string(),
1899                    effect: Some(Effect::new(
1900                        StackType::singleton(Type::Int),
1901                        StackType::singleton(Type::String),
1902                    )),
1903                    body: vec![Statement::WordCall {
1904                        name: "int->string".to_string(),
1905                        span: None,
1906                    }],
1907                    source: None,
1908                    allowed_lints: vec![],
1909                },
1910                WordDef {
1911                    name: "main".to_string(),
1912                    effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
1913                    body: vec![
1914                        Statement::IntLiteral(42),
1915                        Statement::WordCall {
1916                            name: "helper".to_string(),
1917                            span: None,
1918                        },
1919                        Statement::WordCall {
1920                            name: "io.write-line".to_string(),
1921                            span: None,
1922                        },
1923                    ],
1924                    source: None,
1925                    allowed_lints: vec![],
1926                },
1927            ],
1928        };
1929
1930        let mut checker = TypeChecker::new();
1931        assert!(checker.check_program(&program).is_ok());
1932    }
1933
1934    #[test]
1935    fn test_arithmetic_chain() {
1936        // : test ( Int Int Int -- Int )
1937        //   add multiply ;
1938        let program = Program {
1939            includes: vec![],
1940            unions: vec![],
1941            words: vec![WordDef {
1942                name: "test".to_string(),
1943                effect: Some(Effect::new(
1944                    StackType::Empty
1945                        .push(Type::Int)
1946                        .push(Type::Int)
1947                        .push(Type::Int),
1948                    StackType::singleton(Type::Int),
1949                )),
1950                body: vec![
1951                    Statement::WordCall {
1952                        name: "i.add".to_string(),
1953                        span: None,
1954                    },
1955                    Statement::WordCall {
1956                        name: "i.multiply".to_string(),
1957                        span: None,
1958                    },
1959                ],
1960                source: None,
1961                allowed_lints: vec![],
1962            }],
1963        };
1964
1965        let mut checker = TypeChecker::new();
1966        assert!(checker.check_program(&program).is_ok());
1967    }
1968
1969    #[test]
1970    fn test_write_line_type_error() {
1971        // : test ( Int -- ) io.write-line ;  // ERROR: io.write-line expects String
1972        let program = Program {
1973            includes: vec![],
1974            unions: vec![],
1975            words: vec![WordDef {
1976                name: "test".to_string(),
1977                effect: Some(Effect::new(
1978                    StackType::singleton(Type::Int),
1979                    StackType::Empty,
1980                )),
1981                body: vec![Statement::WordCall {
1982                    name: "io.write-line".to_string(),
1983                    span: None,
1984                }],
1985                source: None,
1986                allowed_lints: vec![],
1987            }],
1988        };
1989
1990        let mut checker = TypeChecker::new();
1991        let result = checker.check_program(&program);
1992        assert!(result.is_err());
1993        assert!(result.unwrap_err().contains("Type mismatch"));
1994    }
1995
1996    #[test]
1997    fn test_stack_underflow_drop() {
1998        // : test ( -- ) drop ;  // ERROR: can't drop from empty stack
1999        let program = Program {
2000            includes: vec![],
2001            unions: vec![],
2002            words: vec![WordDef {
2003                name: "test".to_string(),
2004                effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2005                body: vec![Statement::WordCall {
2006                    name: "drop".to_string(),
2007                    span: None,
2008                }],
2009                source: None,
2010                allowed_lints: vec![],
2011            }],
2012        };
2013
2014        let mut checker = TypeChecker::new();
2015        let result = checker.check_program(&program);
2016        assert!(result.is_err());
2017        assert!(result.unwrap_err().contains("mismatch"));
2018    }
2019
2020    #[test]
2021    fn test_stack_underflow_add() {
2022        // : test ( Int -- Int ) add ;  // ERROR: add needs 2 values
2023        let program = Program {
2024            includes: vec![],
2025            unions: vec![],
2026            words: vec![WordDef {
2027                name: "test".to_string(),
2028                effect: Some(Effect::new(
2029                    StackType::singleton(Type::Int),
2030                    StackType::singleton(Type::Int),
2031                )),
2032                body: vec![Statement::WordCall {
2033                    name: "i.add".to_string(),
2034                    span: None,
2035                }],
2036                source: None,
2037                allowed_lints: vec![],
2038            }],
2039        };
2040
2041        let mut checker = TypeChecker::new();
2042        let result = checker.check_program(&program);
2043        assert!(result.is_err());
2044        assert!(result.unwrap_err().contains("mismatch"));
2045    }
2046
2047    /// Issue #169: rot with only 2 values should fail at compile time
2048    /// Previously this was silently accepted due to implicit row polymorphism
2049    #[test]
2050    fn test_stack_underflow_rot_issue_169() {
2051        // : test ( -- ) 3 4 rot ;  // ERROR: rot needs 3 values, only 2 provided
2052        // Note: The parser generates `( ..rest -- ..rest )` for `( -- )`, so we use RowVar("rest")
2053        // to match the actual parsing behavior. The "rest" row variable is rigid.
2054        let program = Program {
2055            includes: vec![],
2056            unions: vec![],
2057            words: vec![WordDef {
2058                name: "test".to_string(),
2059                effect: Some(Effect::new(
2060                    StackType::RowVar("rest".to_string()),
2061                    StackType::RowVar("rest".to_string()),
2062                )),
2063                body: vec![
2064                    Statement::IntLiteral(3),
2065                    Statement::IntLiteral(4),
2066                    Statement::WordCall {
2067                        name: "rot".to_string(),
2068                        span: None,
2069                    },
2070                ],
2071                source: None,
2072                allowed_lints: vec![],
2073            }],
2074        };
2075
2076        let mut checker = TypeChecker::new();
2077        let result = checker.check_program(&program);
2078        assert!(result.is_err(), "rot with 2 values should fail");
2079        let err = result.unwrap_err();
2080        assert!(
2081            err.contains("stack underflow") || err.contains("requires 3"),
2082            "Error should mention underflow: {}",
2083            err
2084        );
2085    }
2086
2087    #[test]
2088    fn test_csp_operations() {
2089        // : test ( -- )
2090        //   chan.make     # ( -- Channel )
2091        //   42 swap       # ( Channel Int -- Int Channel )
2092        //   chan.send     # ( Int Channel -- Bool )
2093        //   drop          # ( Bool -- )
2094        // ;
2095        let program = Program {
2096            includes: vec![],
2097            unions: vec![],
2098            words: vec![WordDef {
2099                name: "test".to_string(),
2100                effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2101                body: vec![
2102                    Statement::WordCall {
2103                        name: "chan.make".to_string(),
2104                        span: None,
2105                    },
2106                    Statement::IntLiteral(42),
2107                    Statement::WordCall {
2108                        name: "swap".to_string(),
2109                        span: None,
2110                    },
2111                    Statement::WordCall {
2112                        name: "chan.send".to_string(),
2113                        span: None,
2114                    },
2115                    Statement::WordCall {
2116                        name: "drop".to_string(),
2117                        span: None,
2118                    },
2119                ],
2120                source: None,
2121                allowed_lints: vec![],
2122            }],
2123        };
2124
2125        let mut checker = TypeChecker::new();
2126        assert!(checker.check_program(&program).is_ok());
2127    }
2128
2129    #[test]
2130    fn test_complex_stack_shuffling() {
2131        // : test ( Int Int Int -- Int )
2132        //   rot add add ;
2133        let program = Program {
2134            includes: vec![],
2135            unions: vec![],
2136            words: vec![WordDef {
2137                name: "test".to_string(),
2138                effect: Some(Effect::new(
2139                    StackType::Empty
2140                        .push(Type::Int)
2141                        .push(Type::Int)
2142                        .push(Type::Int),
2143                    StackType::singleton(Type::Int),
2144                )),
2145                body: vec![
2146                    Statement::WordCall {
2147                        name: "rot".to_string(),
2148                        span: None,
2149                    },
2150                    Statement::WordCall {
2151                        name: "i.add".to_string(),
2152                        span: None,
2153                    },
2154                    Statement::WordCall {
2155                        name: "i.add".to_string(),
2156                        span: None,
2157                    },
2158                ],
2159                source: None,
2160                allowed_lints: vec![],
2161            }],
2162        };
2163
2164        let mut checker = TypeChecker::new();
2165        assert!(checker.check_program(&program).is_ok());
2166    }
2167
2168    #[test]
2169    fn test_empty_program() {
2170        // Program with no words should be valid
2171        let program = Program {
2172            includes: vec![],
2173            unions: vec![],
2174            words: vec![],
2175        };
2176
2177        let mut checker = TypeChecker::new();
2178        assert!(checker.check_program(&program).is_ok());
2179    }
2180
2181    #[test]
2182    fn test_word_without_effect_declaration() {
2183        // : helper 42 ;  // No effect declaration - should error
2184        let program = Program {
2185            includes: vec![],
2186            unions: vec![],
2187            words: vec![WordDef {
2188                name: "helper".to_string(),
2189                effect: None,
2190                body: vec![Statement::IntLiteral(42)],
2191                source: None,
2192                allowed_lints: vec![],
2193            }],
2194        };
2195
2196        let mut checker = TypeChecker::new();
2197        let result = checker.check_program(&program);
2198        assert!(result.is_err());
2199        assert!(
2200            result
2201                .unwrap_err()
2202                .contains("missing a stack effect declaration")
2203        );
2204    }
2205
2206    #[test]
2207    fn test_nested_conditionals() {
2208        // : test ( Int Int Int Int -- String )
2209        //   > if
2210        //     > if "both true" else "first true" then
2211        //   else
2212        //     drop drop "first false"
2213        //   then ;
2214        // Note: Needs 4 Ints total (2 for each > comparison)
2215        // Else branch must drop unused Ints to match then branch's stack effect
2216        let program = Program {
2217            includes: vec![],
2218            unions: vec![],
2219            words: vec![WordDef {
2220                name: "test".to_string(),
2221                effect: Some(Effect::new(
2222                    StackType::Empty
2223                        .push(Type::Int)
2224                        .push(Type::Int)
2225                        .push(Type::Int)
2226                        .push(Type::Int),
2227                    StackType::singleton(Type::String),
2228                )),
2229                body: vec![
2230                    Statement::WordCall {
2231                        name: "i.>".to_string(),
2232                        span: None,
2233                    },
2234                    Statement::If {
2235                        then_branch: vec![
2236                            Statement::WordCall {
2237                                name: "i.>".to_string(),
2238                                span: None,
2239                            },
2240                            Statement::If {
2241                                then_branch: vec![Statement::StringLiteral(
2242                                    "both true".to_string(),
2243                                )],
2244                                else_branch: Some(vec![Statement::StringLiteral(
2245                                    "first true".to_string(),
2246                                )]),
2247                                span: None,
2248                            },
2249                        ],
2250                        else_branch: Some(vec![
2251                            Statement::WordCall {
2252                                name: "drop".to_string(),
2253                                span: None,
2254                            },
2255                            Statement::WordCall {
2256                                name: "drop".to_string(),
2257                                span: None,
2258                            },
2259                            Statement::StringLiteral("first false".to_string()),
2260                        ]),
2261                        span: None,
2262                    },
2263                ],
2264                source: None,
2265                allowed_lints: vec![],
2266            }],
2267        };
2268
2269        let mut checker = TypeChecker::new();
2270        match checker.check_program(&program) {
2271            Ok(_) => {}
2272            Err(e) => panic!("Type check failed: {}", e),
2273        }
2274    }
2275
2276    #[test]
2277    fn test_conditional_without_else() {
2278        // : test ( Int Int -- Int )
2279        //   > if 100 then ;
2280        // Both branches must leave same stack
2281        let program = Program {
2282            includes: vec![],
2283            unions: vec![],
2284            words: vec![WordDef {
2285                name: "test".to_string(),
2286                effect: Some(Effect::new(
2287                    StackType::Empty.push(Type::Int).push(Type::Int),
2288                    StackType::singleton(Type::Int),
2289                )),
2290                body: vec![
2291                    Statement::WordCall {
2292                        name: "i.>".to_string(),
2293                        span: None,
2294                    },
2295                    Statement::If {
2296                        then_branch: vec![Statement::IntLiteral(100)],
2297                        else_branch: None, // No else - should leave stack unchanged
2298                        span: None,
2299                    },
2300                ],
2301                source: None,
2302                allowed_lints: vec![],
2303            }],
2304        };
2305
2306        let mut checker = TypeChecker::new();
2307        let result = checker.check_program(&program);
2308        // This should fail because then pushes Int but else leaves stack empty
2309        assert!(result.is_err());
2310    }
2311
2312    #[test]
2313    fn test_multiple_word_chain() {
2314        // : helper1 ( Int -- String ) int->string ;
2315        // : helper2 ( String -- ) io.write-line ;
2316        // : main ( -- ) 42 helper1 helper2 ;
2317        let program = Program {
2318            includes: vec![],
2319            unions: vec![],
2320            words: vec![
2321                WordDef {
2322                    name: "helper1".to_string(),
2323                    effect: Some(Effect::new(
2324                        StackType::singleton(Type::Int),
2325                        StackType::singleton(Type::String),
2326                    )),
2327                    body: vec![Statement::WordCall {
2328                        name: "int->string".to_string(),
2329                        span: None,
2330                    }],
2331                    source: None,
2332                    allowed_lints: vec![],
2333                },
2334                WordDef {
2335                    name: "helper2".to_string(),
2336                    effect: Some(Effect::new(
2337                        StackType::singleton(Type::String),
2338                        StackType::Empty,
2339                    )),
2340                    body: vec![Statement::WordCall {
2341                        name: "io.write-line".to_string(),
2342                        span: None,
2343                    }],
2344                    source: None,
2345                    allowed_lints: vec![],
2346                },
2347                WordDef {
2348                    name: "main".to_string(),
2349                    effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2350                    body: vec![
2351                        Statement::IntLiteral(42),
2352                        Statement::WordCall {
2353                            name: "helper1".to_string(),
2354                            span: None,
2355                        },
2356                        Statement::WordCall {
2357                            name: "helper2".to_string(),
2358                            span: None,
2359                        },
2360                    ],
2361                    source: None,
2362                    allowed_lints: vec![],
2363                },
2364            ],
2365        };
2366
2367        let mut checker = TypeChecker::new();
2368        assert!(checker.check_program(&program).is_ok());
2369    }
2370
2371    #[test]
2372    fn test_all_stack_ops() {
2373        // : test ( Int Int Int -- Int Int Int Int )
2374        //   over nip tuck ;
2375        let program = Program {
2376            includes: vec![],
2377            unions: vec![],
2378            words: vec![WordDef {
2379                name: "test".to_string(),
2380                effect: Some(Effect::new(
2381                    StackType::Empty
2382                        .push(Type::Int)
2383                        .push(Type::Int)
2384                        .push(Type::Int),
2385                    StackType::Empty
2386                        .push(Type::Int)
2387                        .push(Type::Int)
2388                        .push(Type::Int)
2389                        .push(Type::Int),
2390                )),
2391                body: vec![
2392                    Statement::WordCall {
2393                        name: "over".to_string(),
2394                        span: None,
2395                    },
2396                    Statement::WordCall {
2397                        name: "nip".to_string(),
2398                        span: None,
2399                    },
2400                    Statement::WordCall {
2401                        name: "tuck".to_string(),
2402                        span: None,
2403                    },
2404                ],
2405                source: None,
2406                allowed_lints: vec![],
2407            }],
2408        };
2409
2410        let mut checker = TypeChecker::new();
2411        assert!(checker.check_program(&program).is_ok());
2412    }
2413
2414    #[test]
2415    fn test_mixed_types_complex() {
2416        // : test ( -- )
2417        //   42 int->string      # ( -- String )
2418        //   100 200 >           # ( String -- String Int )
2419        //   if                  # ( String -- String )
2420        //     io.write-line     # ( String -- )
2421        //   else
2422        //     io.write-line
2423        //   then ;
2424        let program = Program {
2425            includes: vec![],
2426            unions: vec![],
2427            words: vec![WordDef {
2428                name: "test".to_string(),
2429                effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2430                body: vec![
2431                    Statement::IntLiteral(42),
2432                    Statement::WordCall {
2433                        name: "int->string".to_string(),
2434                        span: None,
2435                    },
2436                    Statement::IntLiteral(100),
2437                    Statement::IntLiteral(200),
2438                    Statement::WordCall {
2439                        name: "i.>".to_string(),
2440                        span: None,
2441                    },
2442                    Statement::If {
2443                        then_branch: vec![Statement::WordCall {
2444                            name: "io.write-line".to_string(),
2445                            span: None,
2446                        }],
2447                        else_branch: Some(vec![Statement::WordCall {
2448                            name: "io.write-line".to_string(),
2449                            span: None,
2450                        }]),
2451                        span: None,
2452                    },
2453                ],
2454                source: None,
2455                allowed_lints: vec![],
2456            }],
2457        };
2458
2459        let mut checker = TypeChecker::new();
2460        assert!(checker.check_program(&program).is_ok());
2461    }
2462
2463    #[test]
2464    fn test_string_literal() {
2465        // : test ( -- String ) "hello" ;
2466        let program = Program {
2467            includes: vec![],
2468            unions: vec![],
2469            words: vec![WordDef {
2470                name: "test".to_string(),
2471                effect: Some(Effect::new(
2472                    StackType::Empty,
2473                    StackType::singleton(Type::String),
2474                )),
2475                body: vec![Statement::StringLiteral("hello".to_string())],
2476                source: None,
2477                allowed_lints: vec![],
2478            }],
2479        };
2480
2481        let mut checker = TypeChecker::new();
2482        assert!(checker.check_program(&program).is_ok());
2483    }
2484
2485    #[test]
2486    fn test_bool_literal() {
2487        // : test ( -- Bool ) true ;
2488        // Booleans are now properly typed as Bool
2489        let program = Program {
2490            includes: vec![],
2491            unions: vec![],
2492            words: vec![WordDef {
2493                name: "test".to_string(),
2494                effect: Some(Effect::new(
2495                    StackType::Empty,
2496                    StackType::singleton(Type::Bool),
2497                )),
2498                body: vec![Statement::BoolLiteral(true)],
2499                source: None,
2500                allowed_lints: vec![],
2501            }],
2502        };
2503
2504        let mut checker = TypeChecker::new();
2505        assert!(checker.check_program(&program).is_ok());
2506    }
2507
2508    #[test]
2509    fn test_type_error_in_nested_conditional() {
2510        // : test ( -- )
2511        //   10 20 i.> if
2512        //     42 io.write-line   # ERROR: io.write-line expects String, got Int
2513        //   else
2514        //     "ok" io.write-line
2515        //   then ;
2516        let program = Program {
2517            includes: vec![],
2518            unions: vec![],
2519            words: vec![WordDef {
2520                name: "test".to_string(),
2521                effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2522                body: vec![
2523                    Statement::IntLiteral(10),
2524                    Statement::IntLiteral(20),
2525                    Statement::WordCall {
2526                        name: "i.>".to_string(),
2527                        span: None,
2528                    },
2529                    Statement::If {
2530                        then_branch: vec![
2531                            Statement::IntLiteral(42),
2532                            Statement::WordCall {
2533                                name: "io.write-line".to_string(),
2534                                span: None,
2535                            },
2536                        ],
2537                        else_branch: Some(vec![
2538                            Statement::StringLiteral("ok".to_string()),
2539                            Statement::WordCall {
2540                                name: "io.write-line".to_string(),
2541                                span: None,
2542                            },
2543                        ]),
2544                        span: None,
2545                    },
2546                ],
2547                source: None,
2548                allowed_lints: vec![],
2549            }],
2550        };
2551
2552        let mut checker = TypeChecker::new();
2553        let result = checker.check_program(&program);
2554        assert!(result.is_err());
2555        assert!(result.unwrap_err().contains("Type mismatch"));
2556    }
2557
2558    #[test]
2559    fn test_read_line_operation() {
2560        // : test ( -- String Bool ) io.read-line ;
2561        // io.read-line now returns ( -- String Bool ) for error handling
2562        let program = Program {
2563            includes: vec![],
2564            unions: vec![],
2565            words: vec![WordDef {
2566                name: "test".to_string(),
2567                effect: Some(Effect::new(
2568                    StackType::Empty,
2569                    StackType::from_vec(vec![Type::String, Type::Bool]),
2570                )),
2571                body: vec![Statement::WordCall {
2572                    name: "io.read-line".to_string(),
2573                    span: None,
2574                }],
2575                source: None,
2576                allowed_lints: vec![],
2577            }],
2578        };
2579
2580        let mut checker = TypeChecker::new();
2581        assert!(checker.check_program(&program).is_ok());
2582    }
2583
2584    #[test]
2585    fn test_comparison_operations() {
2586        // Test all comparison operators
2587        // : test ( Int Int -- Bool )
2588        //   i.<= ;
2589        // Simplified: just test that comparisons work and return Bool
2590        let program = Program {
2591            includes: vec![],
2592            unions: vec![],
2593            words: vec![WordDef {
2594                name: "test".to_string(),
2595                effect: Some(Effect::new(
2596                    StackType::Empty.push(Type::Int).push(Type::Int),
2597                    StackType::singleton(Type::Bool),
2598                )),
2599                body: vec![Statement::WordCall {
2600                    name: "i.<=".to_string(),
2601                    span: None,
2602                }],
2603                source: None,
2604                allowed_lints: vec![],
2605            }],
2606        };
2607
2608        let mut checker = TypeChecker::new();
2609        assert!(checker.check_program(&program).is_ok());
2610    }
2611
2612    #[test]
2613    fn test_recursive_word_definitions() {
2614        // Test mutually recursive words (type checking only, no runtime)
2615        // : is-even ( Int -- Int ) dup 0 = if drop 1 else 1 subtract is-odd then ;
2616        // : is-odd ( Int -- Int ) dup 0 = if drop 0 else 1 subtract is-even then ;
2617        //
2618        // Note: This tests that the checker can handle words that reference each other
2619        let program = Program {
2620            includes: vec![],
2621            unions: vec![],
2622            words: vec![
2623                WordDef {
2624                    name: "is-even".to_string(),
2625                    effect: Some(Effect::new(
2626                        StackType::singleton(Type::Int),
2627                        StackType::singleton(Type::Int),
2628                    )),
2629                    body: vec![
2630                        Statement::WordCall {
2631                            name: "dup".to_string(),
2632                            span: None,
2633                        },
2634                        Statement::IntLiteral(0),
2635                        Statement::WordCall {
2636                            name: "i.=".to_string(),
2637                            span: None,
2638                        },
2639                        Statement::If {
2640                            then_branch: vec![
2641                                Statement::WordCall {
2642                                    name: "drop".to_string(),
2643                                    span: None,
2644                                },
2645                                Statement::IntLiteral(1),
2646                            ],
2647                            else_branch: Some(vec![
2648                                Statement::IntLiteral(1),
2649                                Statement::WordCall {
2650                                    name: "i.subtract".to_string(),
2651                                    span: None,
2652                                },
2653                                Statement::WordCall {
2654                                    name: "is-odd".to_string(),
2655                                    span: None,
2656                                },
2657                            ]),
2658                            span: None,
2659                        },
2660                    ],
2661                    source: None,
2662                    allowed_lints: vec![],
2663                },
2664                WordDef {
2665                    name: "is-odd".to_string(),
2666                    effect: Some(Effect::new(
2667                        StackType::singleton(Type::Int),
2668                        StackType::singleton(Type::Int),
2669                    )),
2670                    body: vec![
2671                        Statement::WordCall {
2672                            name: "dup".to_string(),
2673                            span: None,
2674                        },
2675                        Statement::IntLiteral(0),
2676                        Statement::WordCall {
2677                            name: "i.=".to_string(),
2678                            span: None,
2679                        },
2680                        Statement::If {
2681                            then_branch: vec![
2682                                Statement::WordCall {
2683                                    name: "drop".to_string(),
2684                                    span: None,
2685                                },
2686                                Statement::IntLiteral(0),
2687                            ],
2688                            else_branch: Some(vec![
2689                                Statement::IntLiteral(1),
2690                                Statement::WordCall {
2691                                    name: "i.subtract".to_string(),
2692                                    span: None,
2693                                },
2694                                Statement::WordCall {
2695                                    name: "is-even".to_string(),
2696                                    span: None,
2697                                },
2698                            ]),
2699                            span: None,
2700                        },
2701                    ],
2702                    source: None,
2703                    allowed_lints: vec![],
2704                },
2705            ],
2706        };
2707
2708        let mut checker = TypeChecker::new();
2709        assert!(checker.check_program(&program).is_ok());
2710    }
2711
2712    #[test]
2713    fn test_word_calling_word_with_row_polymorphism() {
2714        // Test that row variables unify correctly through word calls
2715        // : apply-twice ( Int -- Int ) dup add ;
2716        // : quad ( Int -- Int ) apply-twice apply-twice ;
2717        // Should work: both use row polymorphism correctly
2718        let program = Program {
2719            includes: vec![],
2720            unions: vec![],
2721            words: vec![
2722                WordDef {
2723                    name: "apply-twice".to_string(),
2724                    effect: Some(Effect::new(
2725                        StackType::singleton(Type::Int),
2726                        StackType::singleton(Type::Int),
2727                    )),
2728                    body: vec![
2729                        Statement::WordCall {
2730                            name: "dup".to_string(),
2731                            span: None,
2732                        },
2733                        Statement::WordCall {
2734                            name: "i.add".to_string(),
2735                            span: None,
2736                        },
2737                    ],
2738                    source: None,
2739                    allowed_lints: vec![],
2740                },
2741                WordDef {
2742                    name: "quad".to_string(),
2743                    effect: Some(Effect::new(
2744                        StackType::singleton(Type::Int),
2745                        StackType::singleton(Type::Int),
2746                    )),
2747                    body: vec![
2748                        Statement::WordCall {
2749                            name: "apply-twice".to_string(),
2750                            span: None,
2751                        },
2752                        Statement::WordCall {
2753                            name: "apply-twice".to_string(),
2754                            span: None,
2755                        },
2756                    ],
2757                    source: None,
2758                    allowed_lints: vec![],
2759                },
2760            ],
2761        };
2762
2763        let mut checker = TypeChecker::new();
2764        assert!(checker.check_program(&program).is_ok());
2765    }
2766
2767    #[test]
2768    fn test_deep_stack_types() {
2769        // Test with many values on stack (10+ items)
2770        // : test ( Int Int Int Int Int Int Int Int Int Int -- Int )
2771        //   add add add add add add add add add ;
2772        let mut stack_type = StackType::Empty;
2773        for _ in 0..10 {
2774            stack_type = stack_type.push(Type::Int);
2775        }
2776
2777        let program = Program {
2778            includes: vec![],
2779            unions: vec![],
2780            words: vec![WordDef {
2781                name: "test".to_string(),
2782                effect: Some(Effect::new(stack_type, StackType::singleton(Type::Int))),
2783                body: vec![
2784                    Statement::WordCall {
2785                        name: "i.add".to_string(),
2786                        span: None,
2787                    },
2788                    Statement::WordCall {
2789                        name: "i.add".to_string(),
2790                        span: None,
2791                    },
2792                    Statement::WordCall {
2793                        name: "i.add".to_string(),
2794                        span: None,
2795                    },
2796                    Statement::WordCall {
2797                        name: "i.add".to_string(),
2798                        span: None,
2799                    },
2800                    Statement::WordCall {
2801                        name: "i.add".to_string(),
2802                        span: None,
2803                    },
2804                    Statement::WordCall {
2805                        name: "i.add".to_string(),
2806                        span: None,
2807                    },
2808                    Statement::WordCall {
2809                        name: "i.add".to_string(),
2810                        span: None,
2811                    },
2812                    Statement::WordCall {
2813                        name: "i.add".to_string(),
2814                        span: None,
2815                    },
2816                    Statement::WordCall {
2817                        name: "i.add".to_string(),
2818                        span: None,
2819                    },
2820                ],
2821                source: None,
2822                allowed_lints: vec![],
2823            }],
2824        };
2825
2826        let mut checker = TypeChecker::new();
2827        assert!(checker.check_program(&program).is_ok());
2828    }
2829
2830    #[test]
2831    fn test_simple_quotation() {
2832        // : test ( -- Quot )
2833        //   [ 1 add ] ;
2834        // Quotation type should be [ ..input Int -- ..input Int ] (row polymorphic)
2835        let program = Program {
2836            includes: vec![],
2837            unions: vec![],
2838            words: vec![WordDef {
2839                name: "test".to_string(),
2840                effect: Some(Effect::new(
2841                    StackType::Empty,
2842                    StackType::singleton(Type::Quotation(Box::new(Effect::new(
2843                        StackType::RowVar("input".to_string()).push(Type::Int),
2844                        StackType::RowVar("input".to_string()).push(Type::Int),
2845                    )))),
2846                )),
2847                body: vec![Statement::Quotation {
2848                    span: None,
2849                    id: 0,
2850                    body: vec![
2851                        Statement::IntLiteral(1),
2852                        Statement::WordCall {
2853                            name: "i.add".to_string(),
2854                            span: None,
2855                        },
2856                    ],
2857                }],
2858                source: None,
2859                allowed_lints: vec![],
2860            }],
2861        };
2862
2863        let mut checker = TypeChecker::new();
2864        match checker.check_program(&program) {
2865            Ok(_) => {}
2866            Err(e) => panic!("Type check failed: {}", e),
2867        }
2868    }
2869
2870    #[test]
2871    fn test_empty_quotation() {
2872        // : test ( -- Quot )
2873        //   [ ] ;
2874        // Empty quotation has effect ( ..input -- ..input ) (preserves stack)
2875        let program = Program {
2876            includes: vec![],
2877            unions: vec![],
2878            words: vec![WordDef {
2879                name: "test".to_string(),
2880                effect: Some(Effect::new(
2881                    StackType::Empty,
2882                    StackType::singleton(Type::Quotation(Box::new(Effect::new(
2883                        StackType::RowVar("input".to_string()),
2884                        StackType::RowVar("input".to_string()),
2885                    )))),
2886                )),
2887                body: vec![Statement::Quotation {
2888                    span: None,
2889                    id: 1,
2890                    body: vec![],
2891                }],
2892                source: None,
2893                allowed_lints: vec![],
2894            }],
2895        };
2896
2897        let mut checker = TypeChecker::new();
2898        assert!(checker.check_program(&program).is_ok());
2899    }
2900
2901    #[test]
2902    fn test_nested_quotation() {
2903        // : test ( -- Quot )
2904        //   [ [ 1 add ] ] ;
2905        // Outer quotation contains inner quotation (both row-polymorphic)
2906        let inner_quot_type = Type::Quotation(Box::new(Effect::new(
2907            StackType::RowVar("input".to_string()).push(Type::Int),
2908            StackType::RowVar("input".to_string()).push(Type::Int),
2909        )));
2910
2911        let outer_quot_type = Type::Quotation(Box::new(Effect::new(
2912            StackType::RowVar("input".to_string()),
2913            StackType::RowVar("input".to_string()).push(inner_quot_type.clone()),
2914        )));
2915
2916        let program = Program {
2917            includes: vec![],
2918            unions: vec![],
2919            words: vec![WordDef {
2920                name: "test".to_string(),
2921                effect: Some(Effect::new(
2922                    StackType::Empty,
2923                    StackType::singleton(outer_quot_type),
2924                )),
2925                body: vec![Statement::Quotation {
2926                    span: None,
2927                    id: 2,
2928                    body: vec![Statement::Quotation {
2929                        span: None,
2930                        id: 3,
2931                        body: vec![
2932                            Statement::IntLiteral(1),
2933                            Statement::WordCall {
2934                                name: "i.add".to_string(),
2935                                span: None,
2936                            },
2937                        ],
2938                    }],
2939                }],
2940                source: None,
2941                allowed_lints: vec![],
2942            }],
2943        };
2944
2945        let mut checker = TypeChecker::new();
2946        assert!(checker.check_program(&program).is_ok());
2947    }
2948
2949    #[test]
2950    fn test_invalid_field_type_error() {
2951        use crate::ast::{UnionDef, UnionField, UnionVariant};
2952
2953        let program = Program {
2954            includes: vec![],
2955            unions: vec![UnionDef {
2956                name: "Message".to_string(),
2957                variants: vec![UnionVariant {
2958                    name: "Get".to_string(),
2959                    fields: vec![UnionField {
2960                        name: "chan".to_string(),
2961                        type_name: "InvalidType".to_string(),
2962                    }],
2963                    source: None,
2964                }],
2965                source: None,
2966            }],
2967            words: vec![],
2968        };
2969
2970        let mut checker = TypeChecker::new();
2971        let result = checker.check_program(&program);
2972        assert!(result.is_err());
2973        let err = result.unwrap_err();
2974        assert!(err.contains("Unknown type 'InvalidType'"));
2975        assert!(err.contains("chan"));
2976        assert!(err.contains("Get"));
2977        assert!(err.contains("Message"));
2978    }
2979
2980    #[test]
2981    fn test_roll_inside_conditional_with_concrete_stack() {
2982        // Bug #93: n roll inside if/else should work when stack has enough concrete items
2983        // : test ( Int Int Int Int -- Int Int Int Int )
2984        //   dup 0 > if
2985        //     3 roll    # Works: 4 concrete items available
2986        //   else
2987        //     rot rot   # Alternative that also works
2988        //   then ;
2989        let program = Program {
2990            includes: vec![],
2991            unions: vec![],
2992            words: vec![WordDef {
2993                name: "test".to_string(),
2994                effect: Some(Effect::new(
2995                    StackType::Empty
2996                        .push(Type::Int)
2997                        .push(Type::Int)
2998                        .push(Type::Int)
2999                        .push(Type::Int),
3000                    StackType::Empty
3001                        .push(Type::Int)
3002                        .push(Type::Int)
3003                        .push(Type::Int)
3004                        .push(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::IntLiteral(3),
3019                            Statement::WordCall {
3020                                name: "roll".to_string(),
3021                                span: None,
3022                            },
3023                        ],
3024                        else_branch: Some(vec![
3025                            Statement::WordCall {
3026                                name: "rot".to_string(),
3027                                span: None,
3028                            },
3029                            Statement::WordCall {
3030                                name: "rot".to_string(),
3031                                span: None,
3032                            },
3033                        ]),
3034                        span: None,
3035                    },
3036                ],
3037                source: None,
3038                allowed_lints: vec![],
3039            }],
3040        };
3041
3042        let mut checker = TypeChecker::new();
3043        // This should now work because both branches have 4 concrete items
3044        match checker.check_program(&program) {
3045            Ok(_) => {}
3046            Err(e) => panic!("Type check failed: {}", e),
3047        }
3048    }
3049
3050    #[test]
3051    fn test_roll_inside_match_arm_with_concrete_stack() {
3052        // Similar to bug #93 but for match arms: n roll inside match should work
3053        // when stack has enough concrete items from the match context
3054        use crate::ast::{MatchArm, Pattern, UnionDef, UnionVariant};
3055
3056        // Define a simple union: union Result = Ok | Err
3057        let union_def = UnionDef {
3058            name: "Result".to_string(),
3059            variants: vec![
3060                UnionVariant {
3061                    name: "Ok".to_string(),
3062                    fields: vec![],
3063                    source: None,
3064                },
3065                UnionVariant {
3066                    name: "Err".to_string(),
3067                    fields: vec![],
3068                    source: None,
3069                },
3070            ],
3071            source: None,
3072        };
3073
3074        // : test ( Int Int Int Int Result -- Int Int Int Int )
3075        //   match
3076        //     Ok => 3 roll
3077        //     Err => rot rot
3078        //   end ;
3079        let program = Program {
3080            includes: vec![],
3081            unions: vec![union_def],
3082            words: vec![WordDef {
3083                name: "test".to_string(),
3084                effect: Some(Effect::new(
3085                    StackType::Empty
3086                        .push(Type::Int)
3087                        .push(Type::Int)
3088                        .push(Type::Int)
3089                        .push(Type::Int)
3090                        .push(Type::Union("Result".to_string())),
3091                    StackType::Empty
3092                        .push(Type::Int)
3093                        .push(Type::Int)
3094                        .push(Type::Int)
3095                        .push(Type::Int),
3096                )),
3097                body: vec![Statement::Match {
3098                    arms: vec![
3099                        MatchArm {
3100                            pattern: Pattern::Variant("Ok".to_string()),
3101                            body: vec![
3102                                Statement::IntLiteral(3),
3103                                Statement::WordCall {
3104                                    name: "roll".to_string(),
3105                                    span: None,
3106                                },
3107                            ],
3108                            span: None,
3109                        },
3110                        MatchArm {
3111                            pattern: Pattern::Variant("Err".to_string()),
3112                            body: vec![
3113                                Statement::WordCall {
3114                                    name: "rot".to_string(),
3115                                    span: None,
3116                                },
3117                                Statement::WordCall {
3118                                    name: "rot".to_string(),
3119                                    span: None,
3120                                },
3121                            ],
3122                            span: None,
3123                        },
3124                    ],
3125                    span: None,
3126                }],
3127                source: None,
3128                allowed_lints: vec![],
3129            }],
3130        };
3131
3132        let mut checker = TypeChecker::new();
3133        match checker.check_program(&program) {
3134            Ok(_) => {}
3135            Err(e) => panic!("Type check failed: {}", e),
3136        }
3137    }
3138
3139    #[test]
3140    fn test_roll_with_row_polymorphic_input() {
3141        // roll reaching into row variable should work (needed for stdlib)
3142        // : test ( T U V W -- U V W T )
3143        //   3 roll ;   # Rotates: brings position 3 to top
3144        let program = Program {
3145            includes: vec![],
3146            unions: vec![],
3147            words: vec![WordDef {
3148                name: "test".to_string(),
3149                effect: Some(Effect::new(
3150                    StackType::Empty
3151                        .push(Type::Var("T".to_string()))
3152                        .push(Type::Var("U".to_string()))
3153                        .push(Type::Var("V".to_string()))
3154                        .push(Type::Var("W".to_string())),
3155                    StackType::Empty
3156                        .push(Type::Var("U".to_string()))
3157                        .push(Type::Var("V".to_string()))
3158                        .push(Type::Var("W".to_string()))
3159                        .push(Type::Var("T".to_string())),
3160                )),
3161                body: vec![
3162                    Statement::IntLiteral(3),
3163                    Statement::WordCall {
3164                        name: "roll".to_string(),
3165                        span: None,
3166                    },
3167                ],
3168                source: None,
3169                allowed_lints: vec![],
3170            }],
3171        };
3172
3173        let mut checker = TypeChecker::new();
3174        let result = checker.check_program(&program);
3175        assert!(result.is_ok(), "roll test failed: {:?}", result.err());
3176    }
3177
3178    #[test]
3179    fn test_pick_with_row_polymorphic_input() {
3180        // pick reaching into row variable should work (needed for stdlib)
3181        // : test ( T U V -- T U V T )
3182        //   2 pick ;   # Copies element at index 2 (0-indexed from top)
3183        let program = Program {
3184            includes: vec![],
3185            unions: vec![],
3186            words: vec![WordDef {
3187                name: "test".to_string(),
3188                effect: Some(Effect::new(
3189                    StackType::Empty
3190                        .push(Type::Var("T".to_string()))
3191                        .push(Type::Var("U".to_string()))
3192                        .push(Type::Var("V".to_string())),
3193                    StackType::Empty
3194                        .push(Type::Var("T".to_string()))
3195                        .push(Type::Var("U".to_string()))
3196                        .push(Type::Var("V".to_string()))
3197                        .push(Type::Var("T".to_string())),
3198                )),
3199                body: vec![
3200                    Statement::IntLiteral(2),
3201                    Statement::WordCall {
3202                        name: "pick".to_string(),
3203                        span: None,
3204                    },
3205                ],
3206                source: None,
3207                allowed_lints: vec![],
3208            }],
3209        };
3210
3211        let mut checker = TypeChecker::new();
3212        assert!(checker.check_program(&program).is_ok());
3213    }
3214
3215    #[test]
3216    fn test_valid_union_reference_in_field() {
3217        use crate::ast::{UnionDef, UnionField, UnionVariant};
3218
3219        let program = Program {
3220            includes: vec![],
3221            unions: vec![
3222                UnionDef {
3223                    name: "Inner".to_string(),
3224                    variants: vec![UnionVariant {
3225                        name: "Val".to_string(),
3226                        fields: vec![UnionField {
3227                            name: "x".to_string(),
3228                            type_name: "Int".to_string(),
3229                        }],
3230                        source: None,
3231                    }],
3232                    source: None,
3233                },
3234                UnionDef {
3235                    name: "Outer".to_string(),
3236                    variants: vec![UnionVariant {
3237                        name: "Wrap".to_string(),
3238                        fields: vec![UnionField {
3239                            name: "inner".to_string(),
3240                            type_name: "Inner".to_string(), // Reference to other union
3241                        }],
3242                        source: None,
3243                    }],
3244                    source: None,
3245                },
3246            ],
3247            words: vec![],
3248        };
3249
3250        let mut checker = TypeChecker::new();
3251        assert!(
3252            checker.check_program(&program).is_ok(),
3253            "Union reference in field should be valid"
3254        );
3255    }
3256
3257    #[test]
3258    fn test_divergent_recursive_tail_call() {
3259        // Test that recursive tail calls in if/else branches are recognized as divergent.
3260        // This pattern is common in actor loops:
3261        //
3262        // : store-loop ( Channel -- )
3263        //   dup           # ( chan chan )
3264        //   chan.receive  # ( chan value Bool )
3265        //   not if        # ( chan value )
3266        //     drop        # ( chan ) - drop value, keep chan for recursion
3267        //     store-loop  # diverges - never returns
3268        //   then
3269        //   # else: ( chan value ) - process msg normally
3270        //   drop drop     # ( )
3271        // ;
3272        //
3273        // The then branch ends with a recursive call (store-loop), so it diverges.
3274        // The else branch (implicit empty) continues with the stack after the if.
3275        // Without divergent branch detection, this would fail because:
3276        //   - then branch produces: () (after drop store-loop)
3277        //   - else branch produces: (chan value)
3278        // But since then diverges, we should use else's type.
3279
3280        let program = Program {
3281            includes: vec![],
3282            unions: vec![],
3283            words: vec![WordDef {
3284                name: "store-loop".to_string(),
3285                effect: Some(Effect::new(
3286                    StackType::singleton(Type::Channel), // ( Channel -- )
3287                    StackType::Empty,
3288                )),
3289                body: vec![
3290                    // dup -> ( chan chan )
3291                    Statement::WordCall {
3292                        name: "dup".to_string(),
3293                        span: None,
3294                    },
3295                    // chan.receive -> ( chan value Bool )
3296                    Statement::WordCall {
3297                        name: "chan.receive".to_string(),
3298                        span: None,
3299                    },
3300                    // not -> ( chan value Bool )
3301                    Statement::WordCall {
3302                        name: "not".to_string(),
3303                        span: None,
3304                    },
3305                    // if drop store-loop then
3306                    Statement::If {
3307                        then_branch: vec![
3308                            // drop value -> ( chan )
3309                            Statement::WordCall {
3310                                name: "drop".to_string(),
3311                                span: None,
3312                            },
3313                            // store-loop -> diverges
3314                            Statement::WordCall {
3315                                name: "store-loop".to_string(), // recursive tail call
3316                                span: None,
3317                            },
3318                        ],
3319                        else_branch: None, // implicit else continues with ( chan value )
3320                        span: None,
3321                    },
3322                    // After if: ( chan value ) - drop both
3323                    Statement::WordCall {
3324                        name: "drop".to_string(),
3325                        span: None,
3326                    },
3327                    Statement::WordCall {
3328                        name: "drop".to_string(),
3329                        span: None,
3330                    },
3331                ],
3332                source: None,
3333                allowed_lints: vec![],
3334            }],
3335        };
3336
3337        let mut checker = TypeChecker::new();
3338        let result = checker.check_program(&program);
3339        assert!(
3340            result.is_ok(),
3341            "Divergent recursive tail call should be accepted: {:?}",
3342            result.err()
3343        );
3344    }
3345
3346    #[test]
3347    fn test_divergent_else_branch() {
3348        // Test that divergence detection works for else branches too.
3349        //
3350        // : process-loop ( Channel -- )
3351        //   dup chan.receive   # ( chan value Bool )
3352        //   if                 # ( chan value )
3353        //     drop drop        # normal exit: ( )
3354        //   else
3355        //     drop             # ( chan )
3356        //     process-loop     # diverges - retry on failure
3357        //   then
3358        // ;
3359
3360        let program = Program {
3361            includes: vec![],
3362            unions: vec![],
3363            words: vec![WordDef {
3364                name: "process-loop".to_string(),
3365                effect: Some(Effect::new(
3366                    StackType::singleton(Type::Channel), // ( Channel -- )
3367                    StackType::Empty,
3368                )),
3369                body: vec![
3370                    Statement::WordCall {
3371                        name: "dup".to_string(),
3372                        span: None,
3373                    },
3374                    Statement::WordCall {
3375                        name: "chan.receive".to_string(),
3376                        span: None,
3377                    },
3378                    Statement::If {
3379                        then_branch: vec![
3380                            // success: drop value and chan
3381                            Statement::WordCall {
3382                                name: "drop".to_string(),
3383                                span: None,
3384                            },
3385                            Statement::WordCall {
3386                                name: "drop".to_string(),
3387                                span: None,
3388                            },
3389                        ],
3390                        else_branch: Some(vec![
3391                            // failure: drop value, keep chan, recurse
3392                            Statement::WordCall {
3393                                name: "drop".to_string(),
3394                                span: None,
3395                            },
3396                            Statement::WordCall {
3397                                name: "process-loop".to_string(), // recursive tail call
3398                                span: None,
3399                            },
3400                        ]),
3401                        span: None,
3402                    },
3403                ],
3404                source: None,
3405                allowed_lints: vec![],
3406            }],
3407        };
3408
3409        let mut checker = TypeChecker::new();
3410        let result = checker.check_program(&program);
3411        assert!(
3412            result.is_ok(),
3413            "Divergent else branch should be accepted: {:?}",
3414            result.err()
3415        );
3416    }
3417
3418    #[test]
3419    fn test_non_tail_call_recursion_not_divergent() {
3420        // Test that recursion NOT in tail position is not treated as divergent.
3421        // This should fail type checking because after the recursive call,
3422        // there's more code that changes the stack.
3423        //
3424        // : bad-loop ( Int -- Int )
3425        //   dup 0 i.> if
3426        //     1 i.subtract bad-loop  # recursive call
3427        //     1 i.add                # more code after - not tail position!
3428        //   then
3429        // ;
3430        //
3431        // This should fail because:
3432        // - then branch: recurse then add 1 -> stack changes after recursion
3433        // - else branch (implicit): stack is ( Int )
3434        // Without proper handling, this could incorrectly pass.
3435
3436        let program = Program {
3437            includes: vec![],
3438            unions: vec![],
3439            words: vec![WordDef {
3440                name: "bad-loop".to_string(),
3441                effect: Some(Effect::new(
3442                    StackType::singleton(Type::Int),
3443                    StackType::singleton(Type::Int),
3444                )),
3445                body: vec![
3446                    Statement::WordCall {
3447                        name: "dup".to_string(),
3448                        span: None,
3449                    },
3450                    Statement::IntLiteral(0),
3451                    Statement::WordCall {
3452                        name: "i.>".to_string(),
3453                        span: None,
3454                    },
3455                    Statement::If {
3456                        then_branch: vec![
3457                            Statement::IntLiteral(1),
3458                            Statement::WordCall {
3459                                name: "i.subtract".to_string(),
3460                                span: None,
3461                            },
3462                            Statement::WordCall {
3463                                name: "bad-loop".to_string(), // NOT in tail position
3464                                span: None,
3465                            },
3466                            Statement::IntLiteral(1),
3467                            Statement::WordCall {
3468                                name: "i.add".to_string(), // code after recursion
3469                                span: None,
3470                            },
3471                        ],
3472                        else_branch: None,
3473                        span: None,
3474                    },
3475                ],
3476                source: None,
3477                allowed_lints: vec![],
3478            }],
3479        };
3480
3481        let mut checker = TypeChecker::new();
3482        // This should pass because the branches ARE compatible:
3483        // - then: produces Int (after bad-loop returns Int, then add 1)
3484        // - else: produces Int (from the dup at start)
3485        // The key is that bad-loop is NOT in tail position, so it's not divergent.
3486        let result = checker.check_program(&program);
3487        assert!(
3488            result.is_ok(),
3489            "Non-tail recursion should type check normally: {:?}",
3490            result.err()
3491        );
3492    }
3493
3494    #[test]
3495    fn test_call_yield_quotation_error() {
3496        // Phase 2c: Calling a quotation with Yield effect directly should error.
3497        // : bad ( Ctx -- Ctx ) [ yield ] call ;
3498        // This is wrong because yield quotations must be wrapped with strand.weave.
3499        let program = Program {
3500            includes: vec![],
3501            unions: vec![],
3502            words: vec![WordDef {
3503                name: "bad".to_string(),
3504                effect: Some(Effect::new(
3505                    StackType::singleton(Type::Var("Ctx".to_string())),
3506                    StackType::singleton(Type::Var("Ctx".to_string())),
3507                )),
3508                body: vec![
3509                    // Push a dummy value that will be yielded
3510                    Statement::IntLiteral(42),
3511                    Statement::Quotation {
3512                        span: None,
3513                        id: 0,
3514                        body: vec![Statement::WordCall {
3515                            name: "yield".to_string(),
3516                            span: None,
3517                        }],
3518                    },
3519                    Statement::WordCall {
3520                        name: "call".to_string(),
3521                        span: None,
3522                    },
3523                ],
3524                source: None,
3525                allowed_lints: vec![],
3526            }],
3527        };
3528
3529        let mut checker = TypeChecker::new();
3530        let result = checker.check_program(&program);
3531        assert!(
3532            result.is_err(),
3533            "Calling yield quotation directly should fail"
3534        );
3535        let err = result.unwrap_err();
3536        assert!(
3537            err.contains("Yield") || err.contains("strand.weave"),
3538            "Error should mention Yield or strand.weave: {}",
3539            err
3540        );
3541    }
3542
3543    #[test]
3544    fn test_strand_weave_yield_quotation_ok() {
3545        // Phase 2c: Using strand.weave on a Yield quotation is correct.
3546        // : good ( -- Int Handle ) 42 [ yield ] strand.weave ;
3547        let program = Program {
3548            includes: vec![],
3549            unions: vec![],
3550            words: vec![WordDef {
3551                name: "good".to_string(),
3552                effect: Some(Effect::new(
3553                    StackType::Empty,
3554                    StackType::Empty
3555                        .push(Type::Int)
3556                        .push(Type::Var("Handle".to_string())),
3557                )),
3558                body: vec![
3559                    Statement::IntLiteral(42),
3560                    Statement::Quotation {
3561                        span: None,
3562                        id: 0,
3563                        body: vec![Statement::WordCall {
3564                            name: "yield".to_string(),
3565                            span: None,
3566                        }],
3567                    },
3568                    Statement::WordCall {
3569                        name: "strand.weave".to_string(),
3570                        span: None,
3571                    },
3572                ],
3573                source: None,
3574                allowed_lints: vec![],
3575            }],
3576        };
3577
3578        let mut checker = TypeChecker::new();
3579        let result = checker.check_program(&program);
3580        assert!(
3581            result.is_ok(),
3582            "strand.weave on yield quotation should pass: {:?}",
3583            result.err()
3584        );
3585    }
3586
3587    #[test]
3588    fn test_call_pure_quotation_ok() {
3589        // Phase 2c: Calling a pure quotation (no Yield) is fine.
3590        // : ok ( Int -- Int ) [ 1 i.add ] call ;
3591        let program = Program {
3592            includes: vec![],
3593            unions: vec![],
3594            words: vec![WordDef {
3595                name: "ok".to_string(),
3596                effect: Some(Effect::new(
3597                    StackType::singleton(Type::Int),
3598                    StackType::singleton(Type::Int),
3599                )),
3600                body: vec![
3601                    Statement::Quotation {
3602                        span: None,
3603                        id: 0,
3604                        body: vec![
3605                            Statement::IntLiteral(1),
3606                            Statement::WordCall {
3607                                name: "i.add".to_string(),
3608                                span: None,
3609                            },
3610                        ],
3611                    },
3612                    Statement::WordCall {
3613                        name: "call".to_string(),
3614                        span: None,
3615                    },
3616                ],
3617                source: None,
3618                allowed_lints: vec![],
3619            }],
3620        };
3621
3622        let mut checker = TypeChecker::new();
3623        let result = checker.check_program(&program);
3624        assert!(
3625            result.is_ok(),
3626            "Calling pure quotation should pass: {:?}",
3627            result.err()
3628        );
3629    }
3630
3631    // ==========================================================================
3632    // Stack Pollution Detection Tests (Issue #228)
3633    // These tests verify the type checker catches stack effect mismatches
3634    // ==========================================================================
3635
3636    #[test]
3637    fn test_pollution_extra_push() {
3638        // : test ( Int -- Int ) 42 ;
3639        // Declares consuming 1 Int, producing 1 Int
3640        // But body pushes 42 on top of input, leaving 2 values
3641        let program = Program {
3642            includes: vec![],
3643            unions: vec![],
3644            words: vec![WordDef {
3645                name: "test".to_string(),
3646                effect: Some(Effect::new(
3647                    StackType::singleton(Type::Int),
3648                    StackType::singleton(Type::Int),
3649                )),
3650                body: vec![Statement::IntLiteral(42)],
3651                source: None,
3652                allowed_lints: vec![],
3653            }],
3654        };
3655
3656        let mut checker = TypeChecker::new();
3657        let result = checker.check_program(&program);
3658        assert!(
3659            result.is_err(),
3660            "Should reject: declares ( Int -- Int ) but leaves 2 values on stack"
3661        );
3662    }
3663
3664    #[test]
3665    fn test_pollution_extra_dup() {
3666        // : test ( Int -- Int ) dup ;
3667        // Declares producing 1 Int, but dup produces 2
3668        let program = Program {
3669            includes: vec![],
3670            unions: vec![],
3671            words: vec![WordDef {
3672                name: "test".to_string(),
3673                effect: Some(Effect::new(
3674                    StackType::singleton(Type::Int),
3675                    StackType::singleton(Type::Int),
3676                )),
3677                body: vec![Statement::WordCall {
3678                    name: "dup".to_string(),
3679                    span: None,
3680                }],
3681                source: None,
3682                allowed_lints: vec![],
3683            }],
3684        };
3685
3686        let mut checker = TypeChecker::new();
3687        let result = checker.check_program(&program);
3688        assert!(
3689            result.is_err(),
3690            "Should reject: declares ( Int -- Int ) but dup produces 2 values"
3691        );
3692    }
3693
3694    #[test]
3695    fn test_pollution_consumes_extra() {
3696        // : test ( Int -- Int ) drop drop 42 ;
3697        // Declares consuming 1 Int, but body drops twice
3698        let program = Program {
3699            includes: vec![],
3700            unions: vec![],
3701            words: vec![WordDef {
3702                name: "test".to_string(),
3703                effect: Some(Effect::new(
3704                    StackType::singleton(Type::Int),
3705                    StackType::singleton(Type::Int),
3706                )),
3707                body: vec![
3708                    Statement::WordCall {
3709                        name: "drop".to_string(),
3710                        span: None,
3711                    },
3712                    Statement::WordCall {
3713                        name: "drop".to_string(),
3714                        span: None,
3715                    },
3716                    Statement::IntLiteral(42),
3717                ],
3718                source: None,
3719                allowed_lints: vec![],
3720            }],
3721        };
3722
3723        let mut checker = TypeChecker::new();
3724        let result = checker.check_program(&program);
3725        assert!(
3726            result.is_err(),
3727            "Should reject: declares ( Int -- Int ) but consumes 2 values"
3728        );
3729    }
3730
3731    #[test]
3732    fn test_pollution_in_then_branch() {
3733        // : test ( Bool -- Int )
3734        //   if 1 2 else 3 then ;
3735        // Then branch pushes 2 values, else pushes 1
3736        let program = Program {
3737            includes: vec![],
3738            unions: vec![],
3739            words: vec![WordDef {
3740                name: "test".to_string(),
3741                effect: Some(Effect::new(
3742                    StackType::singleton(Type::Bool),
3743                    StackType::singleton(Type::Int),
3744                )),
3745                body: vec![Statement::If {
3746                    then_branch: vec![
3747                        Statement::IntLiteral(1),
3748                        Statement::IntLiteral(2), // Extra value!
3749                    ],
3750                    else_branch: Some(vec![Statement::IntLiteral(3)]),
3751                    span: None,
3752                }],
3753                source: None,
3754                allowed_lints: vec![],
3755            }],
3756        };
3757
3758        let mut checker = TypeChecker::new();
3759        let result = checker.check_program(&program);
3760        assert!(
3761            result.is_err(),
3762            "Should reject: then branch pushes 2 values, else pushes 1"
3763        );
3764    }
3765
3766    #[test]
3767    fn test_pollution_in_else_branch() {
3768        // : test ( Bool -- Int )
3769        //   if 1 else 2 3 then ;
3770        // Then branch pushes 1, else pushes 2 values
3771        let program = Program {
3772            includes: vec![],
3773            unions: vec![],
3774            words: vec![WordDef {
3775                name: "test".to_string(),
3776                effect: Some(Effect::new(
3777                    StackType::singleton(Type::Bool),
3778                    StackType::singleton(Type::Int),
3779                )),
3780                body: vec![Statement::If {
3781                    then_branch: vec![Statement::IntLiteral(1)],
3782                    else_branch: Some(vec![
3783                        Statement::IntLiteral(2),
3784                        Statement::IntLiteral(3), // Extra value!
3785                    ]),
3786                    span: None,
3787                }],
3788                source: None,
3789                allowed_lints: vec![],
3790            }],
3791        };
3792
3793        let mut checker = TypeChecker::new();
3794        let result = checker.check_program(&program);
3795        assert!(
3796            result.is_err(),
3797            "Should reject: then branch pushes 1 value, else pushes 2"
3798        );
3799    }
3800
3801    #[test]
3802    fn test_pollution_both_branches_extra() {
3803        // : test ( Bool -- Int )
3804        //   if 1 2 else 3 4 then ;
3805        // Both branches push 2 values but declared output is 1
3806        let program = Program {
3807            includes: vec![],
3808            unions: vec![],
3809            words: vec![WordDef {
3810                name: "test".to_string(),
3811                effect: Some(Effect::new(
3812                    StackType::singleton(Type::Bool),
3813                    StackType::singleton(Type::Int),
3814                )),
3815                body: vec![Statement::If {
3816                    then_branch: vec![Statement::IntLiteral(1), Statement::IntLiteral(2)],
3817                    else_branch: Some(vec![Statement::IntLiteral(3), Statement::IntLiteral(4)]),
3818                    span: None,
3819                }],
3820                source: None,
3821                allowed_lints: vec![],
3822            }],
3823        };
3824
3825        let mut checker = TypeChecker::new();
3826        let result = checker.check_program(&program);
3827        assert!(
3828            result.is_err(),
3829            "Should reject: both branches push 2 values, but declared output is 1"
3830        );
3831    }
3832
3833    #[test]
3834    fn test_pollution_branch_consumes_extra() {
3835        // : test ( Bool Int -- Int )
3836        //   if drop drop 1 else then ;
3837        // Then branch consumes more than available from declared inputs
3838        let program = Program {
3839            includes: vec![],
3840            unions: vec![],
3841            words: vec![WordDef {
3842                name: "test".to_string(),
3843                effect: Some(Effect::new(
3844                    StackType::Empty.push(Type::Bool).push(Type::Int),
3845                    StackType::singleton(Type::Int),
3846                )),
3847                body: vec![Statement::If {
3848                    then_branch: vec![
3849                        Statement::WordCall {
3850                            name: "drop".to_string(),
3851                            span: None,
3852                        },
3853                        Statement::WordCall {
3854                            name: "drop".to_string(),
3855                            span: None,
3856                        },
3857                        Statement::IntLiteral(1),
3858                    ],
3859                    else_branch: Some(vec![]),
3860                    span: None,
3861                }],
3862                source: None,
3863                allowed_lints: vec![],
3864            }],
3865        };
3866
3867        let mut checker = TypeChecker::new();
3868        let result = checker.check_program(&program);
3869        assert!(
3870            result.is_err(),
3871            "Should reject: then branch consumes Bool (should only have Int after if)"
3872        );
3873    }
3874
3875    #[test]
3876    fn test_pollution_quotation_wrong_arity_output() {
3877        // : test ( Int -- Int )
3878        //   [ dup ] call ;
3879        // Quotation produces 2 values, but word declares 1 output
3880        let program = Program {
3881            includes: vec![],
3882            unions: vec![],
3883            words: vec![WordDef {
3884                name: "test".to_string(),
3885                effect: Some(Effect::new(
3886                    StackType::singleton(Type::Int),
3887                    StackType::singleton(Type::Int),
3888                )),
3889                body: vec![
3890                    Statement::Quotation {
3891                        span: None,
3892                        id: 0,
3893                        body: vec![Statement::WordCall {
3894                            name: "dup".to_string(),
3895                            span: None,
3896                        }],
3897                    },
3898                    Statement::WordCall {
3899                        name: "call".to_string(),
3900                        span: None,
3901                    },
3902                ],
3903                source: None,
3904                allowed_lints: vec![],
3905            }],
3906        };
3907
3908        let mut checker = TypeChecker::new();
3909        let result = checker.check_program(&program);
3910        assert!(
3911            result.is_err(),
3912            "Should reject: quotation [dup] produces 2 values, declared output is 1"
3913        );
3914    }
3915
3916    #[test]
3917    fn test_pollution_quotation_wrong_arity_input() {
3918        // : test ( Int -- Int )
3919        //   [ drop drop 42 ] call ;
3920        // Quotation consumes 2 values, but only 1 available
3921        let program = Program {
3922            includes: vec![],
3923            unions: vec![],
3924            words: vec![WordDef {
3925                name: "test".to_string(),
3926                effect: Some(Effect::new(
3927                    StackType::singleton(Type::Int),
3928                    StackType::singleton(Type::Int),
3929                )),
3930                body: vec![
3931                    Statement::Quotation {
3932                        span: None,
3933                        id: 0,
3934                        body: vec![
3935                            Statement::WordCall {
3936                                name: "drop".to_string(),
3937                                span: None,
3938                            },
3939                            Statement::WordCall {
3940                                name: "drop".to_string(),
3941                                span: None,
3942                            },
3943                            Statement::IntLiteral(42),
3944                        ],
3945                    },
3946                    Statement::WordCall {
3947                        name: "call".to_string(),
3948                        span: None,
3949                    },
3950                ],
3951                source: None,
3952                allowed_lints: vec![],
3953            }],
3954        };
3955
3956        let mut checker = TypeChecker::new();
3957        let result = checker.check_program(&program);
3958        assert!(
3959            result.is_err(),
3960            "Should reject: quotation [drop drop 42] consumes 2 values, only 1 available"
3961        );
3962    }
3963
3964    #[test]
3965    fn test_missing_effect_provides_helpful_error() {
3966        // : myword 42 ;
3967        // No effect annotation - should error with helpful message including word name
3968        let program = Program {
3969            includes: vec![],
3970            unions: vec![],
3971            words: vec![WordDef {
3972                name: "myword".to_string(),
3973                effect: None, // No annotation
3974                body: vec![Statement::IntLiteral(42)],
3975                source: None,
3976                allowed_lints: vec![],
3977            }],
3978        };
3979
3980        let mut checker = TypeChecker::new();
3981        let result = checker.check_program(&program);
3982        assert!(result.is_err());
3983        let err = result.unwrap_err();
3984        assert!(err.contains("myword"), "Error should mention word name");
3985        assert!(
3986            err.contains("stack effect"),
3987            "Error should mention stack effect"
3988        );
3989    }
3990
3991    #[test]
3992    fn test_valid_effect_exact_match() {
3993        // : test ( Int Int -- Int ) i.+ ;
3994        // Exact match - consumes 2, produces 1
3995        let program = Program {
3996            includes: vec![],
3997            unions: vec![],
3998            words: vec![WordDef {
3999                name: "test".to_string(),
4000                effect: Some(Effect::new(
4001                    StackType::Empty.push(Type::Int).push(Type::Int),
4002                    StackType::singleton(Type::Int),
4003                )),
4004                body: vec![Statement::WordCall {
4005                    name: "i.add".to_string(),
4006                    span: None,
4007                }],
4008                source: None,
4009                allowed_lints: vec![],
4010            }],
4011        };
4012
4013        let mut checker = TypeChecker::new();
4014        let result = checker.check_program(&program);
4015        assert!(result.is_ok(), "Should accept: effect matches exactly");
4016    }
4017
4018    #[test]
4019    fn test_valid_polymorphic_passthrough() {
4020        // : test ( a -- a ) ;
4021        // Identity function - row polymorphism allows this
4022        let program = Program {
4023            includes: vec![],
4024            unions: vec![],
4025            words: vec![WordDef {
4026                name: "test".to_string(),
4027                effect: Some(Effect::new(
4028                    StackType::Cons {
4029                        rest: Box::new(StackType::RowVar("rest".to_string())),
4030                        top: Type::Var("a".to_string()),
4031                    },
4032                    StackType::Cons {
4033                        rest: Box::new(StackType::RowVar("rest".to_string())),
4034                        top: Type::Var("a".to_string()),
4035                    },
4036                )),
4037                body: vec![], // Empty body - just pass through
4038                source: None,
4039                allowed_lints: vec![],
4040            }],
4041        };
4042
4043        let mut checker = TypeChecker::new();
4044        let result = checker.check_program(&program);
4045        assert!(result.is_ok(), "Should accept: polymorphic identity");
4046    }
4047
4048    // ==========================================================================
4049    // Closure Nesting Tests (Issue #230)
4050    // Tests for deep closure nesting, transitive captures, and edge cases
4051    // ==========================================================================
4052
4053    #[test]
4054    fn test_closure_basic_capture() {
4055        // : make-adder ( Int -- Closure )
4056        //   [ i.+ ] ;
4057        // The quotation needs 2 Ints (for i.+) but caller will only provide 1
4058        // So it captures 1 Int from the creation site
4059        // Must declare as Closure type to trigger capture analysis
4060        let program = Program {
4061            includes: vec![],
4062            unions: vec![],
4063            words: vec![WordDef {
4064                name: "make-adder".to_string(),
4065                effect: Some(Effect::new(
4066                    StackType::singleton(Type::Int),
4067                    StackType::singleton(Type::Closure {
4068                        effect: Box::new(Effect::new(
4069                            StackType::RowVar("r".to_string()).push(Type::Int),
4070                            StackType::RowVar("r".to_string()).push(Type::Int),
4071                        )),
4072                        captures: vec![Type::Int], // Captures 1 Int
4073                    }),
4074                )),
4075                body: vec![Statement::Quotation {
4076                    span: None,
4077                    id: 0,
4078                    body: vec![Statement::WordCall {
4079                        name: "i.add".to_string(),
4080                        span: None,
4081                    }],
4082                }],
4083                source: None,
4084                allowed_lints: vec![],
4085            }],
4086        };
4087
4088        let mut checker = TypeChecker::new();
4089        let result = checker.check_program(&program);
4090        assert!(
4091            result.is_ok(),
4092            "Basic closure capture should work: {:?}",
4093            result.err()
4094        );
4095    }
4096
4097    #[test]
4098    fn test_closure_nested_two_levels() {
4099        // : outer ( -- Quot )
4100        //   [ [ 1 i.+ ] ] ;
4101        // Outer quotation: no inputs, just returns inner quotation
4102        // Inner quotation: pushes 1 then adds (needs 1 Int from caller)
4103        let program = Program {
4104            includes: vec![],
4105            unions: vec![],
4106            words: vec![WordDef {
4107                name: "outer".to_string(),
4108                effect: Some(Effect::new(
4109                    StackType::Empty,
4110                    StackType::singleton(Type::Quotation(Box::new(Effect::new(
4111                        StackType::RowVar("r".to_string()),
4112                        StackType::RowVar("r".to_string()).push(Type::Quotation(Box::new(
4113                            Effect::new(
4114                                StackType::RowVar("s".to_string()).push(Type::Int),
4115                                StackType::RowVar("s".to_string()).push(Type::Int),
4116                            ),
4117                        ))),
4118                    )))),
4119                )),
4120                body: vec![Statement::Quotation {
4121                    span: None,
4122                    id: 0,
4123                    body: vec![Statement::Quotation {
4124                        span: None,
4125                        id: 1,
4126                        body: vec![
4127                            Statement::IntLiteral(1),
4128                            Statement::WordCall {
4129                                name: "i.add".to_string(),
4130                                span: None,
4131                            },
4132                        ],
4133                    }],
4134                }],
4135                source: None,
4136                allowed_lints: vec![],
4137            }],
4138        };
4139
4140        let mut checker = TypeChecker::new();
4141        let result = checker.check_program(&program);
4142        assert!(
4143            result.is_ok(),
4144            "Two-level nested quotations should work: {:?}",
4145            result.err()
4146        );
4147    }
4148
4149    #[test]
4150    fn test_closure_nested_three_levels() {
4151        // : deep ( -- Quot )
4152        //   [ [ [ 1 i.+ ] ] ] ;
4153        // Three levels of nesting, innermost does actual work
4154        let inner_effect = Effect::new(
4155            StackType::RowVar("a".to_string()).push(Type::Int),
4156            StackType::RowVar("a".to_string()).push(Type::Int),
4157        );
4158        let middle_effect = Effect::new(
4159            StackType::RowVar("b".to_string()),
4160            StackType::RowVar("b".to_string()).push(Type::Quotation(Box::new(inner_effect))),
4161        );
4162        let outer_effect = Effect::new(
4163            StackType::RowVar("c".to_string()),
4164            StackType::RowVar("c".to_string()).push(Type::Quotation(Box::new(middle_effect))),
4165        );
4166
4167        let program = Program {
4168            includes: vec![],
4169            unions: vec![],
4170            words: vec![WordDef {
4171                name: "deep".to_string(),
4172                effect: Some(Effect::new(
4173                    StackType::Empty,
4174                    StackType::singleton(Type::Quotation(Box::new(outer_effect))),
4175                )),
4176                body: vec![Statement::Quotation {
4177                    span: None,
4178                    id: 0,
4179                    body: vec![Statement::Quotation {
4180                        span: None,
4181                        id: 1,
4182                        body: vec![Statement::Quotation {
4183                            span: None,
4184                            id: 2,
4185                            body: vec![
4186                                Statement::IntLiteral(1),
4187                                Statement::WordCall {
4188                                    name: "i.add".to_string(),
4189                                    span: None,
4190                                },
4191                            ],
4192                        }],
4193                    }],
4194                }],
4195                source: None,
4196                allowed_lints: vec![],
4197            }],
4198        };
4199
4200        let mut checker = TypeChecker::new();
4201        let result = checker.check_program(&program);
4202        assert!(
4203            result.is_ok(),
4204            "Three-level nested quotations should work: {:?}",
4205            result.err()
4206        );
4207    }
4208
4209    #[test]
4210    fn test_closure_use_after_creation() {
4211        // : use-adder ( -- Int )
4212        //   5 make-adder   // Creates closure capturing 5
4213        //   10 swap call ; // Calls closure with 10, should return 15
4214        //
4215        // Tests that closure is properly typed when called later
4216        let adder_type = Type::Closure {
4217            effect: Box::new(Effect::new(
4218                StackType::RowVar("r".to_string()).push(Type::Int),
4219                StackType::RowVar("r".to_string()).push(Type::Int),
4220            )),
4221            captures: vec![Type::Int],
4222        };
4223
4224        let program = Program {
4225            includes: vec![],
4226            unions: vec![],
4227            words: vec![
4228                WordDef {
4229                    name: "make-adder".to_string(),
4230                    effect: Some(Effect::new(
4231                        StackType::singleton(Type::Int),
4232                        StackType::singleton(adder_type.clone()),
4233                    )),
4234                    body: vec![Statement::Quotation {
4235                        span: None,
4236                        id: 0,
4237                        body: vec![Statement::WordCall {
4238                            name: "i.add".to_string(),
4239                            span: None,
4240                        }],
4241                    }],
4242                    source: None,
4243                    allowed_lints: vec![],
4244                },
4245                WordDef {
4246                    name: "use-adder".to_string(),
4247                    effect: Some(Effect::new(
4248                        StackType::Empty,
4249                        StackType::singleton(Type::Int),
4250                    )),
4251                    body: vec![
4252                        Statement::IntLiteral(5),
4253                        Statement::WordCall {
4254                            name: "make-adder".to_string(),
4255                            span: None,
4256                        },
4257                        Statement::IntLiteral(10),
4258                        Statement::WordCall {
4259                            name: "swap".to_string(),
4260                            span: None,
4261                        },
4262                        Statement::WordCall {
4263                            name: "call".to_string(),
4264                            span: None,
4265                        },
4266                    ],
4267                    source: None,
4268                    allowed_lints: vec![],
4269                },
4270            ],
4271        };
4272
4273        let mut checker = TypeChecker::new();
4274        let result = checker.check_program(&program);
4275        assert!(
4276            result.is_ok(),
4277            "Closure usage after creation should work: {:?}",
4278            result.err()
4279        );
4280    }
4281
4282    #[test]
4283    fn test_closure_wrong_call_type() {
4284        // : bad-use ( -- Int )
4285        //   5 make-adder   // Creates Int -> Int closure
4286        //   "hello" swap call ; // Tries to call with String - should fail!
4287        let adder_type = Type::Closure {
4288            effect: Box::new(Effect::new(
4289                StackType::RowVar("r".to_string()).push(Type::Int),
4290                StackType::RowVar("r".to_string()).push(Type::Int),
4291            )),
4292            captures: vec![Type::Int],
4293        };
4294
4295        let program = Program {
4296            includes: vec![],
4297            unions: vec![],
4298            words: vec![
4299                WordDef {
4300                    name: "make-adder".to_string(),
4301                    effect: Some(Effect::new(
4302                        StackType::singleton(Type::Int),
4303                        StackType::singleton(adder_type.clone()),
4304                    )),
4305                    body: vec![Statement::Quotation {
4306                        span: None,
4307                        id: 0,
4308                        body: vec![Statement::WordCall {
4309                            name: "i.add".to_string(),
4310                            span: None,
4311                        }],
4312                    }],
4313                    source: None,
4314                    allowed_lints: vec![],
4315                },
4316                WordDef {
4317                    name: "bad-use".to_string(),
4318                    effect: Some(Effect::new(
4319                        StackType::Empty,
4320                        StackType::singleton(Type::Int),
4321                    )),
4322                    body: vec![
4323                        Statement::IntLiteral(5),
4324                        Statement::WordCall {
4325                            name: "make-adder".to_string(),
4326                            span: None,
4327                        },
4328                        Statement::StringLiteral("hello".to_string()), // Wrong type!
4329                        Statement::WordCall {
4330                            name: "swap".to_string(),
4331                            span: None,
4332                        },
4333                        Statement::WordCall {
4334                            name: "call".to_string(),
4335                            span: None,
4336                        },
4337                    ],
4338                    source: None,
4339                    allowed_lints: vec![],
4340                },
4341            ],
4342        };
4343
4344        let mut checker = TypeChecker::new();
4345        let result = checker.check_program(&program);
4346        assert!(
4347            result.is_err(),
4348            "Calling Int closure with String should fail"
4349        );
4350    }
4351
4352    #[test]
4353    fn test_closure_multiple_captures() {
4354        // : make-between ( Int Int -- Quot )
4355        //   [ dup rot i.>= swap rot i.<= and ] ;
4356        // Captures both min and max, checks if value is between them
4357        // Body needs: value min max (3 Ints)
4358        // Caller provides: value (1 Int)
4359        // Captures: min max (2 Ints)
4360        let program = Program {
4361            includes: vec![],
4362            unions: vec![],
4363            words: vec![WordDef {
4364                name: "make-between".to_string(),
4365                effect: Some(Effect::new(
4366                    StackType::Empty.push(Type::Int).push(Type::Int),
4367                    StackType::singleton(Type::Quotation(Box::new(Effect::new(
4368                        StackType::RowVar("r".to_string()).push(Type::Int),
4369                        StackType::RowVar("r".to_string()).push(Type::Bool),
4370                    )))),
4371                )),
4372                body: vec![Statement::Quotation {
4373                    span: None,
4374                    id: 0,
4375                    body: vec![
4376                        // Simplified: just do a comparison that uses all 3 values
4377                        Statement::WordCall {
4378                            name: "i.>=".to_string(),
4379                            span: None,
4380                        },
4381                        // Note: This doesn't match the comment but tests multi-capture
4382                    ],
4383                }],
4384                source: None,
4385                allowed_lints: vec![],
4386            }],
4387        };
4388
4389        let mut checker = TypeChecker::new();
4390        let result = checker.check_program(&program);
4391        // This should work - the quotation body uses values from stack
4392        // The exact behavior depends on how captures are inferred
4393        // For now, we're testing that it doesn't crash
4394        assert!(
4395            result.is_ok() || result.is_err(),
4396            "Multiple captures should be handled (pass or fail gracefully)"
4397        );
4398    }
4399
4400    #[test]
4401    fn test_quotation_type_preserved_through_word() {
4402        // : identity-quot ( Quot -- Quot ) ;
4403        // Tests that quotation types are preserved when passed through words
4404        let quot_type = Type::Quotation(Box::new(Effect::new(
4405            StackType::RowVar("r".to_string()).push(Type::Int),
4406            StackType::RowVar("r".to_string()).push(Type::Int),
4407        )));
4408
4409        let program = Program {
4410            includes: vec![],
4411            unions: vec![],
4412            words: vec![WordDef {
4413                name: "identity-quot".to_string(),
4414                effect: Some(Effect::new(
4415                    StackType::singleton(quot_type.clone()),
4416                    StackType::singleton(quot_type.clone()),
4417                )),
4418                body: vec![], // Identity - just return what's on stack
4419                source: None,
4420                allowed_lints: vec![],
4421            }],
4422        };
4423
4424        let mut checker = TypeChecker::new();
4425        let result = checker.check_program(&program);
4426        assert!(
4427            result.is_ok(),
4428            "Quotation type should be preserved through identity word: {:?}",
4429            result.err()
4430        );
4431    }
4432
4433    #[test]
4434    fn test_closure_captures_value_for_inner_quotation() {
4435        // : make-inner-adder ( Int -- Closure )
4436        //   [ [ i.+ ] swap call ] ;
4437        // The closure captures an Int
4438        // When called, it creates an inner quotation and calls it with the captured value
4439        // This tests that closures can work with nested quotations
4440        let closure_effect = Effect::new(
4441            StackType::RowVar("r".to_string()).push(Type::Int),
4442            StackType::RowVar("r".to_string()).push(Type::Int),
4443        );
4444
4445        let program = Program {
4446            includes: vec![],
4447            unions: vec![],
4448            words: vec![WordDef {
4449                name: "make-inner-adder".to_string(),
4450                effect: Some(Effect::new(
4451                    StackType::singleton(Type::Int),
4452                    StackType::singleton(Type::Closure {
4453                        effect: Box::new(closure_effect),
4454                        captures: vec![Type::Int],
4455                    }),
4456                )),
4457                body: vec![Statement::Quotation {
4458                    span: None,
4459                    id: 0,
4460                    body: vec![
4461                        // The captured Int and the caller's Int are on stack
4462                        Statement::WordCall {
4463                            name: "i.add".to_string(),
4464                            span: None,
4465                        },
4466                    ],
4467                }],
4468                source: None,
4469                allowed_lints: vec![],
4470            }],
4471        };
4472
4473        let mut checker = TypeChecker::new();
4474        let result = checker.check_program(&program);
4475        assert!(
4476            result.is_ok(),
4477            "Closure with capture for inner work should pass: {:?}",
4478            result.err()
4479        );
4480    }
4481
4482    #[test]
4483    fn test_union_type_mismatch_should_fail() {
4484        // RFC #345: Different union types should not unify
4485        // This tests that passing union B to a function expecting union A fails
4486        use crate::ast::{UnionDef, UnionField, UnionVariant};
4487
4488        let mut program = Program {
4489            includes: vec![],
4490            unions: vec![
4491                UnionDef {
4492                    name: "UnionA".to_string(),
4493                    variants: vec![UnionVariant {
4494                        name: "AVal".to_string(),
4495                        fields: vec![UnionField {
4496                            name: "x".to_string(),
4497                            type_name: "Int".to_string(),
4498                        }],
4499                        source: None,
4500                    }],
4501                    source: None,
4502                },
4503                UnionDef {
4504                    name: "UnionB".to_string(),
4505                    variants: vec![UnionVariant {
4506                        name: "BVal".to_string(),
4507                        fields: vec![UnionField {
4508                            name: "y".to_string(),
4509                            type_name: "Int".to_string(),
4510                        }],
4511                        source: None,
4512                    }],
4513                    source: None,
4514                },
4515            ],
4516            words: vec![
4517                // : takes-a ( UnionA -- ) drop ;
4518                WordDef {
4519                    name: "takes-a".to_string(),
4520                    effect: Some(Effect::new(
4521                        StackType::RowVar("rest".to_string())
4522                            .push(Type::Union("UnionA".to_string())),
4523                        StackType::RowVar("rest".to_string()),
4524                    )),
4525                    body: vec![Statement::WordCall {
4526                        name: "drop".to_string(),
4527                        span: None,
4528                    }],
4529                    source: None,
4530                    allowed_lints: vec![],
4531                },
4532                // : main ( -- ) 99 Make-BVal takes-a ;
4533                // This should FAIL: Make-BVal returns UnionB, takes-a expects UnionA
4534                WordDef {
4535                    name: "main".to_string(),
4536                    effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
4537                    body: vec![
4538                        Statement::IntLiteral(99),
4539                        Statement::WordCall {
4540                            name: "Make-BVal".to_string(),
4541                            span: None,
4542                        },
4543                        Statement::WordCall {
4544                            name: "takes-a".to_string(),
4545                            span: None,
4546                        },
4547                    ],
4548                    source: None,
4549                    allowed_lints: vec![],
4550                },
4551            ],
4552        };
4553
4554        // Generate constructors (Make-AVal, Make-BVal)
4555        program.generate_constructors().unwrap();
4556
4557        let mut checker = TypeChecker::new();
4558        let result = checker.check_program(&program);
4559
4560        // This SHOULD fail because UnionB != UnionA
4561        assert!(
4562            result.is_err(),
4563            "Passing UnionB to function expecting UnionA should fail, but got: {:?}",
4564            result
4565        );
4566        let err = result.unwrap_err();
4567        assert!(
4568            err.contains("Union") || err.contains("mismatch"),
4569            "Error should mention union type mismatch, got: {}",
4570            err
4571        );
4572    }
4573
4574    // =========================================================================
4575    // Aux stack tests (Issue #350)
4576    // =========================================================================
4577
4578    fn make_word_call(name: &str) -> Statement {
4579        Statement::WordCall {
4580            name: name.to_string(),
4581            span: None,
4582        }
4583    }
4584
4585    #[test]
4586    fn test_aux_basic_round_trip() {
4587        // : test ( Int -- Int ) >aux aux> ;
4588        let program = Program {
4589            includes: vec![],
4590            unions: vec![],
4591            words: vec![WordDef {
4592                name: "test".to_string(),
4593                effect: Some(Effect::new(
4594                    StackType::singleton(Type::Int),
4595                    StackType::singleton(Type::Int),
4596                )),
4597                body: vec![make_word_call(">aux"), make_word_call("aux>")],
4598                source: None,
4599                allowed_lints: vec![],
4600            }],
4601        };
4602
4603        let mut checker = TypeChecker::new();
4604        assert!(checker.check_program(&program).is_ok());
4605    }
4606
4607    #[test]
4608    fn test_aux_preserves_type() {
4609        // : test ( String -- String ) >aux aux> ;
4610        let program = Program {
4611            includes: vec![],
4612            unions: vec![],
4613            words: vec![WordDef {
4614                name: "test".to_string(),
4615                effect: Some(Effect::new(
4616                    StackType::singleton(Type::String),
4617                    StackType::singleton(Type::String),
4618                )),
4619                body: vec![make_word_call(">aux"), make_word_call("aux>")],
4620                source: None,
4621                allowed_lints: vec![],
4622            }],
4623        };
4624
4625        let mut checker = TypeChecker::new();
4626        assert!(checker.check_program(&program).is_ok());
4627    }
4628
4629    #[test]
4630    fn test_aux_unbalanced_error() {
4631        // : test ( Int -- ) >aux ;  -- ERROR: aux not empty at return
4632        let program = Program {
4633            includes: vec![],
4634            unions: vec![],
4635            words: vec![WordDef {
4636                name: "test".to_string(),
4637                effect: Some(Effect::new(
4638                    StackType::singleton(Type::Int),
4639                    StackType::Empty,
4640                )),
4641                body: vec![make_word_call(">aux")],
4642                source: None,
4643                allowed_lints: vec![],
4644            }],
4645        };
4646
4647        let mut checker = TypeChecker::new();
4648        let result = checker.check_program(&program);
4649        assert!(result.is_err());
4650        let err = result.unwrap_err();
4651        assert!(
4652            err.contains("aux stack is not empty"),
4653            "Expected aux stack balance error, got: {}",
4654            err
4655        );
4656    }
4657
4658    #[test]
4659    fn test_aux_pop_empty_error() {
4660        // : test ( -- Int ) aux> ;  -- ERROR: aux is empty
4661        let program = Program {
4662            includes: vec![],
4663            unions: vec![],
4664            words: vec![WordDef {
4665                name: "test".to_string(),
4666                effect: Some(Effect::new(
4667                    StackType::Empty,
4668                    StackType::singleton(Type::Int),
4669                )),
4670                body: vec![make_word_call("aux>")],
4671                source: None,
4672                allowed_lints: vec![],
4673            }],
4674        };
4675
4676        let mut checker = TypeChecker::new();
4677        let result = checker.check_program(&program);
4678        assert!(result.is_err());
4679        let err = result.unwrap_err();
4680        assert!(
4681            err.contains("aux stack is empty"),
4682            "Expected aux empty error, got: {}",
4683            err
4684        );
4685    }
4686
4687    #[test]
4688    fn test_aux_multiple_values() {
4689        // >aux >aux aux> aux> is identity (LIFO preserves order)
4690        // Input: ( Int String ), >aux pops String, >aux pops Int
4691        // aux> pushes Int, aux> pushes String → output: ( Int String )
4692        let program = Program {
4693            includes: vec![],
4694            unions: vec![],
4695            words: vec![WordDef {
4696                name: "test".to_string(),
4697                effect: Some(Effect::new(
4698                    StackType::Empty.push(Type::Int).push(Type::String),
4699                    StackType::Empty.push(Type::Int).push(Type::String),
4700                )),
4701                body: vec![
4702                    make_word_call(">aux"),
4703                    make_word_call(">aux"),
4704                    make_word_call("aux>"),
4705                    make_word_call("aux>"),
4706                ],
4707                source: None,
4708                allowed_lints: vec![],
4709            }],
4710        };
4711
4712        let mut checker = TypeChecker::new();
4713        assert!(checker.check_program(&program).is_ok());
4714    }
4715
4716    #[test]
4717    fn test_aux_max_depths_tracked() {
4718        // : test ( Int -- Int ) >aux aux> ;
4719        let program = Program {
4720            includes: vec![],
4721            unions: vec![],
4722            words: vec![WordDef {
4723                name: "test".to_string(),
4724                effect: Some(Effect::new(
4725                    StackType::singleton(Type::Int),
4726                    StackType::singleton(Type::Int),
4727                )),
4728                body: vec![make_word_call(">aux"), make_word_call("aux>")],
4729                source: None,
4730                allowed_lints: vec![],
4731            }],
4732        };
4733
4734        let mut checker = TypeChecker::new();
4735        checker.check_program(&program).unwrap();
4736        let depths = checker.take_aux_max_depths();
4737        assert_eq!(depths.get("test"), Some(&1));
4738    }
4739
4740    #[test]
4741    fn test_aux_in_match_balanced() {
4742        // Aux used symmetrically in match arms: each arm does >aux aux>
4743        use crate::ast::{MatchArm, Pattern, UnionDef, UnionVariant};
4744
4745        let union_def = UnionDef {
4746            name: "Choice".to_string(),
4747            variants: vec![
4748                UnionVariant {
4749                    name: "Left".to_string(),
4750                    fields: vec![],
4751                    source: None,
4752                },
4753                UnionVariant {
4754                    name: "Right".to_string(),
4755                    fields: vec![],
4756                    source: None,
4757                },
4758            ],
4759            source: None,
4760        };
4761
4762        // : test ( Int Choice -- Int )
4763        //   swap >aux match Left => aux> end Right => aux> end ;
4764        let program = Program {
4765            includes: vec![],
4766            unions: vec![union_def],
4767            words: vec![WordDef {
4768                name: "test".to_string(),
4769                effect: Some(Effect::new(
4770                    StackType::Empty
4771                        .push(Type::Int)
4772                        .push(Type::Union("Choice".to_string())),
4773                    StackType::singleton(Type::Int),
4774                )),
4775                body: vec![
4776                    make_word_call("swap"),
4777                    make_word_call(">aux"),
4778                    Statement::Match {
4779                        arms: vec![
4780                            MatchArm {
4781                                pattern: Pattern::Variant("Left".to_string()),
4782                                body: vec![make_word_call("aux>")],
4783                                span: None,
4784                            },
4785                            MatchArm {
4786                                pattern: Pattern::Variant("Right".to_string()),
4787                                body: vec![make_word_call("aux>")],
4788                                span: None,
4789                            },
4790                        ],
4791                        span: None,
4792                    },
4793                ],
4794                source: None,
4795                allowed_lints: vec![],
4796            }],
4797        };
4798
4799        let mut checker = TypeChecker::new();
4800        assert!(checker.check_program(&program).is_ok());
4801    }
4802
4803    #[test]
4804    fn test_aux_in_match_unbalanced_error() {
4805        // Match arms with different aux states should error
4806        use crate::ast::{MatchArm, Pattern, UnionDef, UnionVariant};
4807
4808        let union_def = UnionDef {
4809            name: "Choice".to_string(),
4810            variants: vec![
4811                UnionVariant {
4812                    name: "Left".to_string(),
4813                    fields: vec![],
4814                    source: None,
4815                },
4816                UnionVariant {
4817                    name: "Right".to_string(),
4818                    fields: vec![],
4819                    source: None,
4820                },
4821            ],
4822            source: None,
4823        };
4824
4825        // : test ( Int Choice -- Int )
4826        //   swap >aux match Left => aux> end Right => end ;  -- ERROR: unbalanced
4827        let program = Program {
4828            includes: vec![],
4829            unions: vec![union_def],
4830            words: vec![WordDef {
4831                name: "test".to_string(),
4832                effect: Some(Effect::new(
4833                    StackType::Empty
4834                        .push(Type::Int)
4835                        .push(Type::Union("Choice".to_string())),
4836                    StackType::singleton(Type::Int),
4837                )),
4838                body: vec![
4839                    make_word_call("swap"),
4840                    make_word_call(">aux"),
4841                    Statement::Match {
4842                        arms: vec![
4843                            MatchArm {
4844                                pattern: Pattern::Variant("Left".to_string()),
4845                                body: vec![make_word_call("aux>")],
4846                                span: None,
4847                            },
4848                            MatchArm {
4849                                pattern: Pattern::Variant("Right".to_string()),
4850                                body: vec![],
4851                                span: None,
4852                            },
4853                        ],
4854                        span: None,
4855                    },
4856                ],
4857                source: None,
4858                allowed_lints: vec![],
4859            }],
4860        };
4861
4862        let mut checker = TypeChecker::new();
4863        let result = checker.check_program(&program);
4864        assert!(result.is_err());
4865        let err = result.unwrap_err();
4866        assert!(
4867            err.contains("aux stack"),
4868            "Expected aux stack mismatch error, got: {}",
4869            err
4870        );
4871    }
4872
4873    #[test]
4874    fn test_aux_in_quotation_rejected() {
4875        // >aux inside a quotation should be rejected (codegen limitation)
4876        let program = Program {
4877            includes: vec![],
4878            unions: vec![],
4879            words: vec![WordDef {
4880                name: "test".to_string(),
4881                effect: Some(Effect::new(
4882                    StackType::singleton(Type::Int),
4883                    StackType::singleton(Type::Quotation(Box::new(Effect::new(
4884                        StackType::singleton(Type::Int),
4885                        StackType::singleton(Type::Int),
4886                    )))),
4887                )),
4888                body: vec![Statement::Quotation {
4889                    span: None,
4890                    id: 0,
4891                    body: vec![make_word_call(">aux"), make_word_call("aux>")],
4892                }],
4893                source: None,
4894                allowed_lints: vec![],
4895            }],
4896        };
4897
4898        let mut checker = TypeChecker::new();
4899        let result = checker.check_program(&program);
4900        assert!(result.is_err());
4901        let err = result.unwrap_err();
4902        assert!(
4903            err.contains("not supported inside quotations"),
4904            "Expected quotation rejection error, got: {}",
4905            err
4906        );
4907    }
4908}