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        // Special handling for dataflow combinators
1247        if name == "dip" {
1248            return self.infer_dip(span, current_stack);
1249        }
1250        if name == "keep" {
1251            return self.infer_keep(span, current_stack);
1252        }
1253        if name == "bi" {
1254            return self.infer_bi(span, current_stack);
1255        }
1256
1257        // Look up word's effect
1258        let effect = self
1259            .lookup_word_effect(name)
1260            .ok_or_else(|| format!("Unknown word: '{}'", name))?;
1261
1262        // Freshen the effect to avoid variable name clashes
1263        let fresh_effect = self.freshen_effect(&effect);
1264
1265        // Special handling for strand.spawn: auto-convert Quotation to Closure if needed
1266        let adjusted_stack = if name == "strand.spawn" {
1267            self.adjust_stack_for_spawn(current_stack, &fresh_effect)?
1268        } else {
1269            current_stack
1270        };
1271
1272        // Apply the freshened effect to current stack
1273        let (result_stack, subst) = self.apply_effect(&fresh_effect, adjusted_stack, name, span)?;
1274
1275        // Propagate side effects from the called word
1276        // Note: strand.weave "handles" Yield effects (consumes them from the quotation)
1277        // strand.spawn requires pure quotations (checked separately)
1278        let propagated_effects = fresh_effect.effects.clone();
1279
1280        Ok((result_stack, subst, propagated_effects))
1281    }
1282
1283    /// Handle >aux: pop from main stack, push onto word-local aux stack (Issue #350)
1284    fn infer_to_aux(
1285        &self,
1286        _span: &Option<crate::ast::Span>,
1287        current_stack: StackType,
1288    ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1289        if self.in_quotation_scope.get() {
1290            return Err(">aux is not supported inside quotations.\n\
1291                 Quotations are compiled as separate functions without aux stack slots.\n\
1292                 Extract the quotation body into a named word if you need aux."
1293                .to_string());
1294        }
1295        let (rest, top_type) = self.pop_type(&current_stack, ">aux")?;
1296
1297        // Push onto aux stack
1298        let mut aux = self.current_aux_stack.borrow_mut();
1299        *aux = aux.clone().push(top_type);
1300
1301        // Track max depth for codegen alloca sizing
1302        let depth = Self::stack_depth(&aux);
1303        if let Some((word_name, _)) = self.current_word.borrow().as_ref() {
1304            let mut depths = self.aux_max_depths.borrow_mut();
1305            let entry = depths.entry(word_name.clone()).or_insert(0);
1306            if depth > *entry {
1307                *entry = depth;
1308            }
1309        }
1310
1311        Ok((rest, Subst::empty(), vec![]))
1312    }
1313
1314    /// Handle aux>: pop from aux stack, push onto main stack (Issue #350)
1315    fn infer_from_aux(
1316        &self,
1317        _span: &Option<crate::ast::Span>,
1318        current_stack: StackType,
1319    ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1320        if self.in_quotation_scope.get() {
1321            return Err("aux> is not supported inside quotations.\n\
1322                 Quotations are compiled as separate functions without aux stack slots.\n\
1323                 Extract the quotation body into a named word if you need aux."
1324                .to_string());
1325        }
1326        let mut aux = self.current_aux_stack.borrow_mut();
1327        match aux.clone().pop() {
1328            Some((rest, top_type)) => {
1329                *aux = rest;
1330                Ok((current_stack.push(top_type), Subst::empty(), vec![]))
1331            }
1332            None => {
1333                let line_info = self.line_prefix();
1334                Err(format!(
1335                    "{}aux>: aux stack is empty. Every aux> must be paired with a preceding >aux.",
1336                    line_info
1337                ))
1338            }
1339        }
1340    }
1341
1342    /// Special handling for `call` to properly propagate quotation effects (Issue #228)
1343    ///
1344    /// The generic `call` signature `( ..a Q -- ..b )` has independent row variables,
1345    /// which doesn't constrain the output based on the quotation's actual effect.
1346    /// This function extracts the quotation's effect and applies it properly.
1347    fn infer_call(
1348        &self,
1349        span: &Option<crate::ast::Span>,
1350        current_stack: StackType,
1351    ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1352        // Pop the quotation from the stack
1353        let line_prefix = self.line_prefix();
1354        let (remaining_stack, quot_type) = current_stack.clone().pop().ok_or_else(|| {
1355            format!(
1356                "{}call: stack underflow - expected quotation on stack",
1357                line_prefix
1358            )
1359        })?;
1360
1361        // Extract the quotation's effect
1362        let quot_effect = match &quot_type {
1363            Type::Quotation(effect) => (**effect).clone(),
1364            Type::Closure { effect, .. } => (**effect).clone(),
1365            Type::Var(_) => {
1366                // Type variable - fall back to polymorphic behavior
1367                // This happens when the quotation type isn't known yet
1368                let effect = self
1369                    .lookup_word_effect("call")
1370                    .ok_or_else(|| "Unknown word: 'call'".to_string())?;
1371                let fresh_effect = self.freshen_effect(&effect);
1372                let (result_stack, subst) =
1373                    self.apply_effect(&fresh_effect, current_stack, "call", span)?;
1374                return Ok((result_stack, subst, vec![]));
1375            }
1376            _ => {
1377                return Err(format!(
1378                    "call: expected quotation or closure on stack, got {}",
1379                    quot_type
1380                ));
1381            }
1382        };
1383
1384        // Check for Yield effects - quotations with Yield must use strand.weave
1385        if quot_effect.has_yield() {
1386            return Err("Cannot call quotation with Yield effect directly.\n\
1387                 Quotations that yield values must be wrapped with `strand.weave`.\n\
1388                 Example: `[ yielding-code ] strand.weave` instead of `[ yielding-code ] call`"
1389                .to_string());
1390        }
1391
1392        // Freshen the quotation's effect to avoid variable clashes
1393        let fresh_effect = self.freshen_effect(&quot_effect);
1394
1395        // Apply the quotation's effect to the remaining stack
1396        let (result_stack, subst) =
1397            self.apply_effect(&fresh_effect, remaining_stack, "call", span)?;
1398
1399        // Propagate side effects from the quotation
1400        let propagated_effects = fresh_effect.effects.clone();
1401
1402        Ok((result_stack, subst, propagated_effects))
1403    }
1404
1405    /// Infer the stack effect of `dip`: ( ..a x quot -- ..b x )
1406    ///
1407    /// Hide the value below the quotation, run the quotation on the rest
1408    /// of the stack, then restore the hidden value on top.
1409    fn infer_dip(
1410        &self,
1411        span: &Option<crate::ast::Span>,
1412        current_stack: StackType,
1413    ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1414        let line_prefix = self.line_prefix();
1415
1416        // Pop the quotation
1417        let (stack_after_quot, quot_type) = current_stack.clone().pop().ok_or_else(|| {
1418            format!(
1419                "{}dip: stack underflow - expected quotation on stack",
1420                line_prefix
1421            )
1422        })?;
1423
1424        // Extract the quotation's effect
1425        let quot_effect = match &quot_type {
1426            Type::Quotation(effect) => (**effect).clone(),
1427            Type::Closure { effect, .. } => (**effect).clone(),
1428            Type::Var(_) => {
1429                // Unknown quotation type — fall back to generic builtin signature
1430                let effect = self
1431                    .lookup_word_effect("dip")
1432                    .ok_or_else(|| "Unknown word: 'dip'".to_string())?;
1433                let fresh_effect = self.freshen_effect(&effect);
1434                let (result_stack, subst) =
1435                    self.apply_effect(&fresh_effect, current_stack, "dip", span)?;
1436                return Ok((result_stack, subst, vec![]));
1437            }
1438            _ => {
1439                return Err(format!(
1440                    "{}dip: expected quotation or closure on top of stack, got {}",
1441                    line_prefix, quot_type
1442                ));
1443            }
1444        };
1445
1446        if quot_effect.has_yield() {
1447            return Err("dip: quotation must not have Yield effects.\n\
1448                 Use strand.weave for quotations that yield."
1449                .to_string());
1450        }
1451
1452        // Pop the preserved value (below the quotation)
1453        let (rest_stack, preserved_type) = stack_after_quot.clone().pop().ok_or_else(|| {
1454            format!(
1455                "{}dip: stack underflow - expected a value below the quotation",
1456                line_prefix
1457            )
1458        })?;
1459
1460        // Freshen and apply the quotation's effect to the stack below the preserved value
1461        let fresh_effect = self.freshen_effect(&quot_effect);
1462        let (result_stack, subst) =
1463            self.apply_effect(&fresh_effect, rest_stack, "dip (quotation)", span)?;
1464
1465        // Push the preserved value back on top, applying substitution in case
1466        // preserved_type contains type variables resolved during unification
1467        let resolved_preserved = subst.apply_type(&preserved_type);
1468        let result_stack = result_stack.push(resolved_preserved);
1469
1470        let propagated_effects = fresh_effect.effects.clone();
1471        Ok((result_stack, subst, propagated_effects))
1472    }
1473
1474    /// Infer the stack effect of `keep`: ( ..a x quot -- ..b x )
1475    ///
1476    /// Run the quotation on the value (quotation receives x), then
1477    /// restore the original value on top. Like `over >aux call aux>`.
1478    fn infer_keep(
1479        &self,
1480        span: &Option<crate::ast::Span>,
1481        current_stack: StackType,
1482    ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1483        let line_prefix = self.line_prefix();
1484
1485        // Pop the quotation
1486        let (stack_after_quot, quot_type) = current_stack.clone().pop().ok_or_else(|| {
1487            format!(
1488                "{}keep: stack underflow - expected quotation on stack",
1489                line_prefix
1490            )
1491        })?;
1492
1493        // Extract the quotation's effect
1494        let quot_effect = match &quot_type {
1495            Type::Quotation(effect) => (**effect).clone(),
1496            Type::Closure { effect, .. } => (**effect).clone(),
1497            Type::Var(_) => {
1498                let effect = self
1499                    .lookup_word_effect("keep")
1500                    .ok_or_else(|| "Unknown word: 'keep'".to_string())?;
1501                let fresh_effect = self.freshen_effect(&effect);
1502                let (result_stack, subst) =
1503                    self.apply_effect(&fresh_effect, current_stack, "keep", span)?;
1504                return Ok((result_stack, subst, vec![]));
1505            }
1506            _ => {
1507                return Err(format!(
1508                    "{}keep: expected quotation or closure on top of stack, got {}",
1509                    line_prefix, quot_type
1510                ));
1511            }
1512        };
1513
1514        if quot_effect.has_yield() {
1515            return Err("keep: quotation must not have Yield effects.\n\
1516                 Use strand.weave for quotations that yield."
1517                .to_string());
1518        }
1519
1520        // Peek at the preserved value type (it stays, we just need its type)
1521        let (_rest_stack, preserved_type) = stack_after_quot.clone().pop().ok_or_else(|| {
1522            format!(
1523                "{}keep: stack underflow - expected a value below the quotation",
1524                line_prefix
1525            )
1526        })?;
1527
1528        // The quotation receives x on the stack (stack_after_quot still has x on top).
1529        // Apply the quotation's effect to the stack INCLUDING x.
1530        let fresh_effect = self.freshen_effect(&quot_effect);
1531        let (result_stack, subst) =
1532            self.apply_effect(&fresh_effect, stack_after_quot, "keep (quotation)", span)?;
1533
1534        // Push the preserved value back on top, applying substitution in case
1535        // preserved_type contains type variables resolved during unification
1536        let resolved_preserved = subst.apply_type(&preserved_type);
1537        let result_stack = result_stack.push(resolved_preserved);
1538
1539        let propagated_effects = fresh_effect.effects.clone();
1540        Ok((result_stack, subst, propagated_effects))
1541    }
1542
1543    /// Infer the stack effect of `bi`: ( ..a x quot1 quot2 -- ..c )
1544    ///
1545    /// Apply two quotations to the same value. First quotation receives x,
1546    /// then second quotation receives x on top of the first quotation's results.
1547    fn infer_bi(
1548        &self,
1549        span: &Option<crate::ast::Span>,
1550        current_stack: StackType,
1551    ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1552        let line_prefix = self.line_prefix();
1553
1554        // Pop quot2 (top)
1555        let (stack1, quot2_type) = current_stack.clone().pop().ok_or_else(|| {
1556            format!(
1557                "{}bi: stack underflow - expected second quotation on stack",
1558                line_prefix
1559            )
1560        })?;
1561
1562        // Pop quot1
1563        let (stack2, quot1_type) = stack1.clone().pop().ok_or_else(|| {
1564            format!(
1565                "{}bi: stack underflow - expected first quotation on stack",
1566                line_prefix
1567            )
1568        })?;
1569
1570        // Extract both quotation effects
1571        let quot1_effect = match &quot1_type {
1572            Type::Quotation(effect) => (**effect).clone(),
1573            Type::Closure { effect, .. } => (**effect).clone(),
1574            Type::Var(_) => {
1575                let effect = self
1576                    .lookup_word_effect("bi")
1577                    .ok_or_else(|| "Unknown word: 'bi'".to_string())?;
1578                let fresh_effect = self.freshen_effect(&effect);
1579                let (result_stack, subst) =
1580                    self.apply_effect(&fresh_effect, current_stack, "bi", span)?;
1581                return Ok((result_stack, subst, vec![]));
1582            }
1583            _ => {
1584                return Err(format!(
1585                    "{}bi: expected quotation or closure as first quotation, got {}",
1586                    line_prefix, quot1_type
1587                ));
1588            }
1589        };
1590
1591        let quot2_effect = match &quot2_type {
1592            Type::Quotation(effect) => (**effect).clone(),
1593            Type::Closure { effect, .. } => (**effect).clone(),
1594            Type::Var(_) => {
1595                let effect = self
1596                    .lookup_word_effect("bi")
1597                    .ok_or_else(|| "Unknown word: 'bi'".to_string())?;
1598                let fresh_effect = self.freshen_effect(&effect);
1599                let (result_stack, subst) =
1600                    self.apply_effect(&fresh_effect, current_stack, "bi", span)?;
1601                return Ok((result_stack, subst, vec![]));
1602            }
1603            _ => {
1604                return Err(format!(
1605                    "{}bi: expected quotation or closure as second quotation, got {}",
1606                    line_prefix, quot2_type
1607                ));
1608            }
1609        };
1610
1611        if quot1_effect.has_yield() || quot2_effect.has_yield() {
1612            return Err("bi: quotations must not have Yield effects.\n\
1613                 Use strand.weave for quotations that yield."
1614                .to_string());
1615        }
1616
1617        // stack2 has x on top (the value both quotations operate on)
1618        // Peek at x's type for the second application
1619        let (_rest, preserved_type) = stack2.clone().pop().ok_or_else(|| {
1620            format!(
1621                "{}bi: stack underflow - expected a value below the quotations",
1622                line_prefix
1623            )
1624        })?;
1625
1626        // Apply quot1 to stack including x
1627        let fresh_effect1 = self.freshen_effect(&quot1_effect);
1628        let (after_quot1, subst1) =
1629            self.apply_effect(&fresh_effect1, stack2, "bi (first quotation)", span)?;
1630
1631        // Push x again for quot2, applying subst1 in case preserved_type
1632        // contains type variables that were resolved during quot1's unification
1633        let resolved_preserved = subst1.apply_type(&preserved_type);
1634        let with_x = after_quot1.push(resolved_preserved);
1635
1636        // Apply quot2
1637        let fresh_effect2 = self.freshen_effect(&quot2_effect);
1638        let (result_stack, subst2) =
1639            self.apply_effect(&fresh_effect2, with_x, "bi (second quotation)", span)?;
1640
1641        let subst = subst1.compose(&subst2);
1642
1643        let mut effects = fresh_effect1.effects.clone();
1644        for e in fresh_effect2.effects.clone() {
1645            if !effects.contains(&e) {
1646                effects.push(e);
1647            }
1648        }
1649
1650        Ok((result_stack, subst, effects))
1651    }
1652
1653    /// Infer the resulting stack type after a statement
1654    /// Takes current stack, returns (new stack, substitution, side effects) after statement
1655    fn infer_statement(
1656        &self,
1657        statement: &Statement,
1658        current_stack: StackType,
1659    ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
1660        match statement {
1661            Statement::IntLiteral(_) => Ok((current_stack.push(Type::Int), Subst::empty(), vec![])),
1662            Statement::BoolLiteral(_) => {
1663                Ok((current_stack.push(Type::Bool), Subst::empty(), vec![]))
1664            }
1665            Statement::StringLiteral(_) => {
1666                Ok((current_stack.push(Type::String), Subst::empty(), vec![]))
1667            }
1668            Statement::FloatLiteral(_) => {
1669                Ok((current_stack.push(Type::Float), Subst::empty(), vec![]))
1670            }
1671            Statement::Symbol(_) => Ok((current_stack.push(Type::Symbol), Subst::empty(), vec![])),
1672            Statement::Match { arms, span } => self.infer_match(arms, span, current_stack),
1673            Statement::WordCall { name, span } => self.infer_word_call(name, span, current_stack),
1674            Statement::If {
1675                then_branch,
1676                else_branch,
1677                span,
1678            } => self.infer_if(then_branch, else_branch, span, current_stack),
1679            Statement::Quotation { id, body, .. } => self.infer_quotation(*id, body, current_stack),
1680        }
1681    }
1682
1683    /// Look up the effect of a word (built-in or user-defined)
1684    fn lookup_word_effect(&self, name: &str) -> Option<Effect> {
1685        // First check built-ins
1686        if let Some(effect) = builtin_signature(name) {
1687            return Some(effect);
1688        }
1689
1690        // Then check user-defined words
1691        self.env.get(name).cloned()
1692    }
1693
1694    /// Apply an effect to a stack
1695    /// Effect: (inputs -- outputs)
1696    /// Current stack must match inputs, result is outputs
1697    /// Returns (result_stack, substitution)
1698    fn apply_effect(
1699        &self,
1700        effect: &Effect,
1701        current_stack: StackType,
1702        operation: &str,
1703        span: &Option<crate::ast::Span>,
1704    ) -> Result<(StackType, Subst), String> {
1705        // Check for stack underflow: if the effect needs more concrete values than
1706        // the current stack provides, and the stack has a "rigid" row variable at its base,
1707        // this would be unsound (the row var could be Empty at runtime).
1708        // Bug #169: "phantom stack entries"
1709        //
1710        // We only check for "rigid" row variables (named "rest" from declared effects).
1711        // Row variables named "input" are from inference and CAN grow to discover requirements.
1712        let effect_concrete = Self::count_concrete_types(&effect.inputs);
1713        let stack_concrete = Self::count_concrete_types(&current_stack);
1714
1715        if let Some(row_var_name) = Self::get_row_var_base(&current_stack) {
1716            // Only check "rigid" row variables (from declared effects, not inference).
1717            //
1718            // Row variable naming convention (established in parser.rs:build_stack_type):
1719            // - "rest": Created by the parser for declared stack effects. When a word declares
1720            //   `( String Int -- String )`, the parser creates `( ..rest String Int -- ..rest String )`.
1721            //   This "rest" is rigid because the caller guarantees exactly these concrete types.
1722            // - "rest$N": Freshened versions created during type checking when calling other words.
1723            //   These represent the callee's stack context and can grow during unification.
1724            // - "input": Created for words without declared effects during inference.
1725            //   These are flexible and grow to discover the word's actual requirements.
1726            //
1727            // Only the original "rest" (exact match) should trigger underflow checking.
1728            let is_rigid = row_var_name == "rest";
1729
1730            if is_rigid && effect_concrete > stack_concrete {
1731                let word_name = self
1732                    .current_word
1733                    .borrow()
1734                    .as_ref()
1735                    .map(|(n, _)| n.clone())
1736                    .unwrap_or_else(|| "unknown".to_string());
1737                return Err(format!(
1738                    "{}In '{}': {}: stack underflow - requires {} value(s), only {} provided",
1739                    self.line_prefix(),
1740                    word_name,
1741                    operation,
1742                    effect_concrete,
1743                    stack_concrete
1744                ));
1745            }
1746        }
1747
1748        // Unify current stack with effect's input
1749        let subst = unify_stacks(&effect.inputs, &current_stack).map_err(|e| {
1750            let line_info = span
1751                .as_ref()
1752                .map(|s| format_line_prefix(s.line))
1753                .unwrap_or_default();
1754            format!(
1755                "{}{}: stack type mismatch. Expected {}, got {}: {}",
1756                line_info, operation, effect.inputs, current_stack, e
1757            )
1758        })?;
1759
1760        // Apply substitution to output
1761        let result_stack = subst.apply_stack(&effect.outputs);
1762
1763        Ok((result_stack, subst))
1764    }
1765
1766    /// Count the number of concrete (non-row-variable) types in a stack
1767    fn count_concrete_types(stack: &StackType) -> usize {
1768        let mut count = 0;
1769        let mut current = stack;
1770        while let StackType::Cons { rest, top: _ } = current {
1771            count += 1;
1772            current = rest;
1773        }
1774        count
1775    }
1776
1777    /// Get the row variable name at the base of a stack, if any
1778    fn get_row_var_base(stack: &StackType) -> Option<String> {
1779        let mut current = stack;
1780        while let StackType::Cons { rest, top: _ } = current {
1781            current = rest;
1782        }
1783        match current {
1784            StackType::RowVar(name) => Some(name.clone()),
1785            _ => None,
1786        }
1787    }
1788
1789    /// Adjust stack for strand.spawn operation by converting Quotation to Closure if needed
1790    ///
1791    /// strand.spawn expects Quotation(Empty -- Empty), but if we have Quotation(T... -- U...)
1792    /// with non-empty inputs, we auto-convert it to a Closure that captures those inputs.
1793    fn adjust_stack_for_spawn(
1794        &self,
1795        current_stack: StackType,
1796        spawn_effect: &Effect,
1797    ) -> Result<StackType, String> {
1798        // strand.spawn expects: ( ..a Quotation(Empty -- Empty) -- ..a Int )
1799        // Extract the expected quotation type from strand.spawn's effect
1800        let expected_quot_type = match &spawn_effect.inputs {
1801            StackType::Cons { top, rest: _ } => {
1802                if !matches!(top, Type::Quotation(_)) {
1803                    return Ok(current_stack); // Not a quotation, don't adjust
1804                }
1805                top
1806            }
1807            _ => return Ok(current_stack),
1808        };
1809
1810        // Check what's actually on the stack
1811        let (rest_stack, actual_type) = match &current_stack {
1812            StackType::Cons { rest, top } => (rest.as_ref().clone(), top),
1813            _ => return Ok(current_stack), // Empty stack, nothing to adjust
1814        };
1815
1816        // If top of stack is a Quotation with non-empty inputs, convert to Closure
1817        if let Type::Quotation(actual_effect) = actual_type {
1818            // Check if quotation needs inputs
1819            if !matches!(actual_effect.inputs, StackType::Empty) {
1820                // Extract expected effect from spawn's signature
1821                let expected_effect = match expected_quot_type {
1822                    Type::Quotation(eff) => eff.as_ref(),
1823                    _ => return Ok(current_stack),
1824                };
1825
1826                // Calculate what needs to be captured
1827                let captures = calculate_captures(actual_effect, expected_effect)?;
1828
1829                // Create a Closure type
1830                let closure_type = Type::Closure {
1831                    effect: Box::new(expected_effect.clone()),
1832                    captures: captures.clone(),
1833                };
1834
1835                // Pop the captured values from the stack
1836                // The values to capture are BELOW the quotation on the stack
1837                let mut adjusted_stack = rest_stack;
1838                for _ in &captures {
1839                    adjusted_stack = match adjusted_stack {
1840                        StackType::Cons { rest, .. } => rest.as_ref().clone(),
1841                        _ => {
1842                            return Err(format!(
1843                                "strand.spawn: not enough values on stack to capture. Need {} values",
1844                                captures.len()
1845                            ));
1846                        }
1847                    };
1848                }
1849
1850                // Push the Closure onto the adjusted stack
1851                return Ok(adjusted_stack.push(closure_type));
1852            }
1853        }
1854
1855        Ok(current_stack)
1856    }
1857
1858    /// Analyze quotation captures
1859    ///
1860    /// Determines whether a quotation should be stateless (Type::Quotation)
1861    /// or a closure (Type::Closure) based on the expected type from the word signature.
1862    ///
1863    /// Type-driven inference with automatic closure creation:
1864    ///   - If expected type is Closure[effect], calculate what to capture
1865    ///   - If expected type is Quotation[effect]:
1866    ///     - If body needs more inputs than expected effect, auto-create Closure
1867    ///     - Otherwise return stateless Quotation
1868    ///   - If no expected type, default to stateless (conservative)
1869    ///
1870    /// Example 1 (auto-create closure):
1871    ///   Expected: Quotation[-- ]          [spawn expects ( -- )]
1872    ///   Body: [ handle-connection ]       [needs ( Int -- )]
1873    ///   Body effect: ( Int -- )           [needs 1 Int]
1874    ///   Expected effect: ( -- )           [provides 0 inputs]
1875    ///   Result: Closure { effect: ( -- ), captures: [Int] }
1876    ///
1877    /// Example 2 (explicit closure):
1878    ///   Signature: ( Int -- Closure[Int -- Int] )
1879    ///   Body: [ add ]
1880    ///   Body effect: ( Int Int -- Int )  [add needs 2 Ints]
1881    ///   Expected effect: [Int -- Int]    [call site provides 1 Int]
1882    ///   Result: Closure { effect: [Int -- Int], captures: [Int] }
1883    fn analyze_captures(
1884        &self,
1885        body_effect: &Effect,
1886        _current_stack: &StackType,
1887    ) -> Result<Type, String> {
1888        // Check if there's an expected type from the word signature
1889        let expected = self.expected_quotation_type.borrow().clone();
1890
1891        match expected {
1892            Some(Type::Closure { effect, .. }) => {
1893                // User declared closure type - calculate captures
1894                let captures = calculate_captures(body_effect, &effect)?;
1895                Ok(Type::Closure { effect, captures })
1896            }
1897            Some(Type::Quotation(expected_effect)) => {
1898                // User declared quotation type - check if we need to auto-create closure
1899                // Auto-create closure only when:
1900                // 1. Expected effect has empty inputs (like spawn's ( -- ))
1901                // 2. Body effect has non-empty inputs (needs values to execute)
1902
1903                let expected_is_empty = matches!(expected_effect.inputs, StackType::Empty);
1904                let body_needs_inputs = !matches!(body_effect.inputs, StackType::Empty);
1905
1906                if expected_is_empty && body_needs_inputs {
1907                    // Body needs inputs but expected provides none
1908                    // Auto-create closure to capture the inputs
1909                    let captures = calculate_captures(body_effect, &expected_effect)?;
1910                    Ok(Type::Closure {
1911                        effect: expected_effect,
1912                        captures,
1913                    })
1914                } else {
1915                    // Verify the body effect is compatible with the expected effect
1916                    // by unifying the quotation types. This catches:
1917                    // - Stack pollution: body pushes values when expected is stack-neutral
1918                    // - Stack underflow: body consumes values when expected is stack-neutral
1919                    // - Wrong return type: body returns Int when Bool expected
1920                    let body_quot = Type::Quotation(Box::new(body_effect.clone()));
1921                    let expected_quot = Type::Quotation(expected_effect.clone());
1922                    unify_types(&body_quot, &expected_quot).map_err(|e| {
1923                        format!(
1924                            "quotation effect mismatch: expected {}, got {}: {}",
1925                            expected_effect, body_effect, e
1926                        )
1927                    })?;
1928
1929                    // Body is compatible with expected effect - stateless quotation
1930                    Ok(Type::Quotation(expected_effect))
1931                }
1932            }
1933            _ => {
1934                // No expected type - conservative default: stateless quotation
1935                Ok(Type::Quotation(Box::new(body_effect.clone())))
1936            }
1937        }
1938    }
1939
1940    /// Check if an inferred effect matches any of the declared effects
1941    /// Effects match by kind (e.g., Yield matches Yield, regardless of type parameters)
1942    /// Type parameters should unify, but for now we just check the effect kind
1943    fn effect_matches_any(&self, inferred: &SideEffect, declared: &[SideEffect]) -> bool {
1944        declared.iter().any(|decl| match (inferred, decl) {
1945            (SideEffect::Yield(_), SideEffect::Yield(_)) => true,
1946        })
1947    }
1948
1949    /// Pop a type from a stack type, returning (rest, top)
1950    fn pop_type(&self, stack: &StackType, context: &str) -> Result<(StackType, Type), String> {
1951        match stack {
1952            StackType::Cons { rest, top } => Ok(((**rest).clone(), top.clone())),
1953            StackType::Empty => Err(format!(
1954                "{}: stack underflow - expected value on stack but stack is empty",
1955                context
1956            )),
1957            StackType::RowVar(_) => {
1958                // Can't statically determine if row variable is empty
1959                // For now, assume it has at least one element
1960                // This is conservative - real implementation would track constraints
1961                Err(format!(
1962                    "{}: cannot pop from polymorphic stack without more type information",
1963                    context
1964                ))
1965            }
1966        }
1967    }
1968}
1969
1970impl Default for TypeChecker {
1971    fn default() -> Self {
1972        Self::new()
1973    }
1974}
1975
1976#[cfg(test)]
1977mod tests {
1978    use super::*;
1979
1980    #[test]
1981    fn test_simple_literal() {
1982        let program = Program {
1983            includes: vec![],
1984            unions: vec![],
1985            words: vec![WordDef {
1986                name: "test".to_string(),
1987                effect: Some(Effect::new(
1988                    StackType::Empty,
1989                    StackType::singleton(Type::Int),
1990                )),
1991                body: vec![Statement::IntLiteral(42)],
1992                source: None,
1993                allowed_lints: vec![],
1994            }],
1995        };
1996
1997        let mut checker = TypeChecker::new();
1998        assert!(checker.check_program(&program).is_ok());
1999    }
2000
2001    #[test]
2002    fn test_simple_operation() {
2003        // : test ( Int Int -- Int ) add ;
2004        let program = Program {
2005            includes: vec![],
2006            unions: vec![],
2007            words: vec![WordDef {
2008                name: "test".to_string(),
2009                effect: Some(Effect::new(
2010                    StackType::Empty.push(Type::Int).push(Type::Int),
2011                    StackType::singleton(Type::Int),
2012                )),
2013                body: vec![Statement::WordCall {
2014                    name: "i.add".to_string(),
2015                    span: None,
2016                }],
2017                source: None,
2018                allowed_lints: vec![],
2019            }],
2020        };
2021
2022        let mut checker = TypeChecker::new();
2023        assert!(checker.check_program(&program).is_ok());
2024    }
2025
2026    #[test]
2027    fn test_type_mismatch() {
2028        // : test ( String -- ) io.write-line ;  with body: 42
2029        let program = Program {
2030            includes: vec![],
2031            unions: vec![],
2032            words: vec![WordDef {
2033                name: "test".to_string(),
2034                effect: Some(Effect::new(
2035                    StackType::singleton(Type::String),
2036                    StackType::Empty,
2037                )),
2038                body: vec![
2039                    Statement::IntLiteral(42), // Pushes Int, not String!
2040                    Statement::WordCall {
2041                        name: "io.write-line".to_string(),
2042                        span: None,
2043                    },
2044                ],
2045                source: None,
2046                allowed_lints: vec![],
2047            }],
2048        };
2049
2050        let mut checker = TypeChecker::new();
2051        let result = checker.check_program(&program);
2052        assert!(result.is_err());
2053        assert!(result.unwrap_err().contains("Type mismatch"));
2054    }
2055
2056    #[test]
2057    fn test_polymorphic_dup() {
2058        // : my-dup ( Int -- Int Int ) dup ;
2059        let program = Program {
2060            includes: vec![],
2061            unions: vec![],
2062            words: vec![WordDef {
2063                name: "my-dup".to_string(),
2064                effect: Some(Effect::new(
2065                    StackType::singleton(Type::Int),
2066                    StackType::Empty.push(Type::Int).push(Type::Int),
2067                )),
2068                body: vec![Statement::WordCall {
2069                    name: "dup".to_string(),
2070                    span: None,
2071                }],
2072                source: None,
2073                allowed_lints: vec![],
2074            }],
2075        };
2076
2077        let mut checker = TypeChecker::new();
2078        assert!(checker.check_program(&program).is_ok());
2079    }
2080
2081    #[test]
2082    fn test_conditional_branches() {
2083        // : test ( Int Int -- String )
2084        //   > if "greater" else "not greater" then ;
2085        let program = Program {
2086            includes: vec![],
2087            unions: vec![],
2088            words: vec![WordDef {
2089                name: "test".to_string(),
2090                effect: Some(Effect::new(
2091                    StackType::Empty.push(Type::Int).push(Type::Int),
2092                    StackType::singleton(Type::String),
2093                )),
2094                body: vec![
2095                    Statement::WordCall {
2096                        name: "i.>".to_string(),
2097                        span: None,
2098                    },
2099                    Statement::If {
2100                        then_branch: vec![Statement::StringLiteral("greater".to_string())],
2101                        else_branch: Some(vec![Statement::StringLiteral(
2102                            "not greater".to_string(),
2103                        )]),
2104                        span: None,
2105                    },
2106                ],
2107                source: None,
2108                allowed_lints: vec![],
2109            }],
2110        };
2111
2112        let mut checker = TypeChecker::new();
2113        assert!(checker.check_program(&program).is_ok());
2114    }
2115
2116    #[test]
2117    fn test_mismatched_branches() {
2118        // : test ( -- Int )
2119        //   true if 42 else "string" then ;  // ERROR: incompatible types
2120        let program = Program {
2121            includes: vec![],
2122            unions: vec![],
2123            words: vec![WordDef {
2124                name: "test".to_string(),
2125                effect: Some(Effect::new(
2126                    StackType::Empty,
2127                    StackType::singleton(Type::Int),
2128                )),
2129                body: vec![
2130                    Statement::BoolLiteral(true),
2131                    Statement::If {
2132                        then_branch: vec![Statement::IntLiteral(42)],
2133                        else_branch: Some(vec![Statement::StringLiteral("string".to_string())]),
2134                        span: None,
2135                    },
2136                ],
2137                source: None,
2138                allowed_lints: vec![],
2139            }],
2140        };
2141
2142        let mut checker = TypeChecker::new();
2143        let result = checker.check_program(&program);
2144        assert!(result.is_err());
2145        assert!(result.unwrap_err().contains("incompatible"));
2146    }
2147
2148    #[test]
2149    fn test_user_defined_word_call() {
2150        // : helper ( Int -- String ) int->string ;
2151        // : main ( -- ) 42 helper io.write-line ;
2152        let program = Program {
2153            includes: vec![],
2154            unions: vec![],
2155            words: vec![
2156                WordDef {
2157                    name: "helper".to_string(),
2158                    effect: Some(Effect::new(
2159                        StackType::singleton(Type::Int),
2160                        StackType::singleton(Type::String),
2161                    )),
2162                    body: vec![Statement::WordCall {
2163                        name: "int->string".to_string(),
2164                        span: None,
2165                    }],
2166                    source: None,
2167                    allowed_lints: vec![],
2168                },
2169                WordDef {
2170                    name: "main".to_string(),
2171                    effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2172                    body: vec![
2173                        Statement::IntLiteral(42),
2174                        Statement::WordCall {
2175                            name: "helper".to_string(),
2176                            span: None,
2177                        },
2178                        Statement::WordCall {
2179                            name: "io.write-line".to_string(),
2180                            span: None,
2181                        },
2182                    ],
2183                    source: None,
2184                    allowed_lints: vec![],
2185                },
2186            ],
2187        };
2188
2189        let mut checker = TypeChecker::new();
2190        assert!(checker.check_program(&program).is_ok());
2191    }
2192
2193    #[test]
2194    fn test_arithmetic_chain() {
2195        // : test ( Int Int Int -- Int )
2196        //   add multiply ;
2197        let program = Program {
2198            includes: vec![],
2199            unions: vec![],
2200            words: vec![WordDef {
2201                name: "test".to_string(),
2202                effect: Some(Effect::new(
2203                    StackType::Empty
2204                        .push(Type::Int)
2205                        .push(Type::Int)
2206                        .push(Type::Int),
2207                    StackType::singleton(Type::Int),
2208                )),
2209                body: vec![
2210                    Statement::WordCall {
2211                        name: "i.add".to_string(),
2212                        span: None,
2213                    },
2214                    Statement::WordCall {
2215                        name: "i.multiply".to_string(),
2216                        span: None,
2217                    },
2218                ],
2219                source: None,
2220                allowed_lints: vec![],
2221            }],
2222        };
2223
2224        let mut checker = TypeChecker::new();
2225        assert!(checker.check_program(&program).is_ok());
2226    }
2227
2228    #[test]
2229    fn test_write_line_type_error() {
2230        // : test ( Int -- ) io.write-line ;  // ERROR: io.write-line expects String
2231        let program = Program {
2232            includes: vec![],
2233            unions: vec![],
2234            words: vec![WordDef {
2235                name: "test".to_string(),
2236                effect: Some(Effect::new(
2237                    StackType::singleton(Type::Int),
2238                    StackType::Empty,
2239                )),
2240                body: vec![Statement::WordCall {
2241                    name: "io.write-line".to_string(),
2242                    span: None,
2243                }],
2244                source: None,
2245                allowed_lints: vec![],
2246            }],
2247        };
2248
2249        let mut checker = TypeChecker::new();
2250        let result = checker.check_program(&program);
2251        assert!(result.is_err());
2252        assert!(result.unwrap_err().contains("Type mismatch"));
2253    }
2254
2255    #[test]
2256    fn test_stack_underflow_drop() {
2257        // : test ( -- ) drop ;  // ERROR: can't drop from empty stack
2258        let program = Program {
2259            includes: vec![],
2260            unions: vec![],
2261            words: vec![WordDef {
2262                name: "test".to_string(),
2263                effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2264                body: vec![Statement::WordCall {
2265                    name: "drop".to_string(),
2266                    span: None,
2267                }],
2268                source: None,
2269                allowed_lints: vec![],
2270            }],
2271        };
2272
2273        let mut checker = TypeChecker::new();
2274        let result = checker.check_program(&program);
2275        assert!(result.is_err());
2276        assert!(result.unwrap_err().contains("mismatch"));
2277    }
2278
2279    #[test]
2280    fn test_stack_underflow_add() {
2281        // : test ( Int -- Int ) add ;  // ERROR: add needs 2 values
2282        let program = Program {
2283            includes: vec![],
2284            unions: vec![],
2285            words: vec![WordDef {
2286                name: "test".to_string(),
2287                effect: Some(Effect::new(
2288                    StackType::singleton(Type::Int),
2289                    StackType::singleton(Type::Int),
2290                )),
2291                body: vec![Statement::WordCall {
2292                    name: "i.add".to_string(),
2293                    span: None,
2294                }],
2295                source: None,
2296                allowed_lints: vec![],
2297            }],
2298        };
2299
2300        let mut checker = TypeChecker::new();
2301        let result = checker.check_program(&program);
2302        assert!(result.is_err());
2303        assert!(result.unwrap_err().contains("mismatch"));
2304    }
2305
2306    /// Issue #169: rot with only 2 values should fail at compile time
2307    /// Previously this was silently accepted due to implicit row polymorphism
2308    #[test]
2309    fn test_stack_underflow_rot_issue_169() {
2310        // : test ( -- ) 3 4 rot ;  // ERROR: rot needs 3 values, only 2 provided
2311        // Note: The parser generates `( ..rest -- ..rest )` for `( -- )`, so we use RowVar("rest")
2312        // to match the actual parsing behavior. The "rest" row variable is rigid.
2313        let program = Program {
2314            includes: vec![],
2315            unions: vec![],
2316            words: vec![WordDef {
2317                name: "test".to_string(),
2318                effect: Some(Effect::new(
2319                    StackType::RowVar("rest".to_string()),
2320                    StackType::RowVar("rest".to_string()),
2321                )),
2322                body: vec![
2323                    Statement::IntLiteral(3),
2324                    Statement::IntLiteral(4),
2325                    Statement::WordCall {
2326                        name: "rot".to_string(),
2327                        span: None,
2328                    },
2329                ],
2330                source: None,
2331                allowed_lints: vec![],
2332            }],
2333        };
2334
2335        let mut checker = TypeChecker::new();
2336        let result = checker.check_program(&program);
2337        assert!(result.is_err(), "rot with 2 values should fail");
2338        let err = result.unwrap_err();
2339        assert!(
2340            err.contains("stack underflow") || err.contains("requires 3"),
2341            "Error should mention underflow: {}",
2342            err
2343        );
2344    }
2345
2346    #[test]
2347    fn test_csp_operations() {
2348        // : test ( -- )
2349        //   chan.make     # ( -- Channel )
2350        //   42 swap       # ( Channel Int -- Int Channel )
2351        //   chan.send     # ( Int Channel -- Bool )
2352        //   drop          # ( Bool -- )
2353        // ;
2354        let program = Program {
2355            includes: vec![],
2356            unions: vec![],
2357            words: vec![WordDef {
2358                name: "test".to_string(),
2359                effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2360                body: vec![
2361                    Statement::WordCall {
2362                        name: "chan.make".to_string(),
2363                        span: None,
2364                    },
2365                    Statement::IntLiteral(42),
2366                    Statement::WordCall {
2367                        name: "swap".to_string(),
2368                        span: None,
2369                    },
2370                    Statement::WordCall {
2371                        name: "chan.send".to_string(),
2372                        span: None,
2373                    },
2374                    Statement::WordCall {
2375                        name: "drop".to_string(),
2376                        span: None,
2377                    },
2378                ],
2379                source: None,
2380                allowed_lints: vec![],
2381            }],
2382        };
2383
2384        let mut checker = TypeChecker::new();
2385        assert!(checker.check_program(&program).is_ok());
2386    }
2387
2388    #[test]
2389    fn test_complex_stack_shuffling() {
2390        // : test ( Int Int Int -- Int )
2391        //   rot add add ;
2392        let program = Program {
2393            includes: vec![],
2394            unions: vec![],
2395            words: vec![WordDef {
2396                name: "test".to_string(),
2397                effect: Some(Effect::new(
2398                    StackType::Empty
2399                        .push(Type::Int)
2400                        .push(Type::Int)
2401                        .push(Type::Int),
2402                    StackType::singleton(Type::Int),
2403                )),
2404                body: vec![
2405                    Statement::WordCall {
2406                        name: "rot".to_string(),
2407                        span: None,
2408                    },
2409                    Statement::WordCall {
2410                        name: "i.add".to_string(),
2411                        span: None,
2412                    },
2413                    Statement::WordCall {
2414                        name: "i.add".to_string(),
2415                        span: None,
2416                    },
2417                ],
2418                source: None,
2419                allowed_lints: vec![],
2420            }],
2421        };
2422
2423        let mut checker = TypeChecker::new();
2424        assert!(checker.check_program(&program).is_ok());
2425    }
2426
2427    #[test]
2428    fn test_empty_program() {
2429        // Program with no words should be valid
2430        let program = Program {
2431            includes: vec![],
2432            unions: vec![],
2433            words: vec![],
2434        };
2435
2436        let mut checker = TypeChecker::new();
2437        assert!(checker.check_program(&program).is_ok());
2438    }
2439
2440    #[test]
2441    fn test_word_without_effect_declaration() {
2442        // : helper 42 ;  // No effect declaration - should error
2443        let program = Program {
2444            includes: vec![],
2445            unions: vec![],
2446            words: vec![WordDef {
2447                name: "helper".to_string(),
2448                effect: None,
2449                body: vec![Statement::IntLiteral(42)],
2450                source: None,
2451                allowed_lints: vec![],
2452            }],
2453        };
2454
2455        let mut checker = TypeChecker::new();
2456        let result = checker.check_program(&program);
2457        assert!(result.is_err());
2458        assert!(
2459            result
2460                .unwrap_err()
2461                .contains("missing a stack effect declaration")
2462        );
2463    }
2464
2465    #[test]
2466    fn test_nested_conditionals() {
2467        // : test ( Int Int Int Int -- String )
2468        //   > if
2469        //     > if "both true" else "first true" then
2470        //   else
2471        //     drop drop "first false"
2472        //   then ;
2473        // Note: Needs 4 Ints total (2 for each > comparison)
2474        // Else branch must drop unused Ints to match then branch's stack effect
2475        let program = Program {
2476            includes: vec![],
2477            unions: vec![],
2478            words: vec![WordDef {
2479                name: "test".to_string(),
2480                effect: Some(Effect::new(
2481                    StackType::Empty
2482                        .push(Type::Int)
2483                        .push(Type::Int)
2484                        .push(Type::Int)
2485                        .push(Type::Int),
2486                    StackType::singleton(Type::String),
2487                )),
2488                body: vec![
2489                    Statement::WordCall {
2490                        name: "i.>".to_string(),
2491                        span: None,
2492                    },
2493                    Statement::If {
2494                        then_branch: vec![
2495                            Statement::WordCall {
2496                                name: "i.>".to_string(),
2497                                span: None,
2498                            },
2499                            Statement::If {
2500                                then_branch: vec![Statement::StringLiteral(
2501                                    "both true".to_string(),
2502                                )],
2503                                else_branch: Some(vec![Statement::StringLiteral(
2504                                    "first true".to_string(),
2505                                )]),
2506                                span: None,
2507                            },
2508                        ],
2509                        else_branch: Some(vec![
2510                            Statement::WordCall {
2511                                name: "drop".to_string(),
2512                                span: None,
2513                            },
2514                            Statement::WordCall {
2515                                name: "drop".to_string(),
2516                                span: None,
2517                            },
2518                            Statement::StringLiteral("first false".to_string()),
2519                        ]),
2520                        span: None,
2521                    },
2522                ],
2523                source: None,
2524                allowed_lints: vec![],
2525            }],
2526        };
2527
2528        let mut checker = TypeChecker::new();
2529        match checker.check_program(&program) {
2530            Ok(_) => {}
2531            Err(e) => panic!("Type check failed: {}", e),
2532        }
2533    }
2534
2535    #[test]
2536    fn test_conditional_without_else() {
2537        // : test ( Int Int -- Int )
2538        //   > if 100 then ;
2539        // Both branches must leave same stack
2540        let program = Program {
2541            includes: vec![],
2542            unions: vec![],
2543            words: vec![WordDef {
2544                name: "test".to_string(),
2545                effect: Some(Effect::new(
2546                    StackType::Empty.push(Type::Int).push(Type::Int),
2547                    StackType::singleton(Type::Int),
2548                )),
2549                body: vec![
2550                    Statement::WordCall {
2551                        name: "i.>".to_string(),
2552                        span: None,
2553                    },
2554                    Statement::If {
2555                        then_branch: vec![Statement::IntLiteral(100)],
2556                        else_branch: None, // No else - should leave stack unchanged
2557                        span: None,
2558                    },
2559                ],
2560                source: None,
2561                allowed_lints: vec![],
2562            }],
2563        };
2564
2565        let mut checker = TypeChecker::new();
2566        let result = checker.check_program(&program);
2567        // This should fail because then pushes Int but else leaves stack empty
2568        assert!(result.is_err());
2569    }
2570
2571    #[test]
2572    fn test_multiple_word_chain() {
2573        // : helper1 ( Int -- String ) int->string ;
2574        // : helper2 ( String -- ) io.write-line ;
2575        // : main ( -- ) 42 helper1 helper2 ;
2576        let program = Program {
2577            includes: vec![],
2578            unions: vec![],
2579            words: vec![
2580                WordDef {
2581                    name: "helper1".to_string(),
2582                    effect: Some(Effect::new(
2583                        StackType::singleton(Type::Int),
2584                        StackType::singleton(Type::String),
2585                    )),
2586                    body: vec![Statement::WordCall {
2587                        name: "int->string".to_string(),
2588                        span: None,
2589                    }],
2590                    source: None,
2591                    allowed_lints: vec![],
2592                },
2593                WordDef {
2594                    name: "helper2".to_string(),
2595                    effect: Some(Effect::new(
2596                        StackType::singleton(Type::String),
2597                        StackType::Empty,
2598                    )),
2599                    body: vec![Statement::WordCall {
2600                        name: "io.write-line".to_string(),
2601                        span: None,
2602                    }],
2603                    source: None,
2604                    allowed_lints: vec![],
2605                },
2606                WordDef {
2607                    name: "main".to_string(),
2608                    effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2609                    body: vec![
2610                        Statement::IntLiteral(42),
2611                        Statement::WordCall {
2612                            name: "helper1".to_string(),
2613                            span: None,
2614                        },
2615                        Statement::WordCall {
2616                            name: "helper2".to_string(),
2617                            span: None,
2618                        },
2619                    ],
2620                    source: None,
2621                    allowed_lints: vec![],
2622                },
2623            ],
2624        };
2625
2626        let mut checker = TypeChecker::new();
2627        assert!(checker.check_program(&program).is_ok());
2628    }
2629
2630    #[test]
2631    fn test_all_stack_ops() {
2632        // : test ( Int Int Int -- Int Int Int Int )
2633        //   over nip tuck ;
2634        let program = Program {
2635            includes: vec![],
2636            unions: vec![],
2637            words: vec![WordDef {
2638                name: "test".to_string(),
2639                effect: Some(Effect::new(
2640                    StackType::Empty
2641                        .push(Type::Int)
2642                        .push(Type::Int)
2643                        .push(Type::Int),
2644                    StackType::Empty
2645                        .push(Type::Int)
2646                        .push(Type::Int)
2647                        .push(Type::Int)
2648                        .push(Type::Int),
2649                )),
2650                body: vec![
2651                    Statement::WordCall {
2652                        name: "over".to_string(),
2653                        span: None,
2654                    },
2655                    Statement::WordCall {
2656                        name: "nip".to_string(),
2657                        span: None,
2658                    },
2659                    Statement::WordCall {
2660                        name: "tuck".to_string(),
2661                        span: None,
2662                    },
2663                ],
2664                source: None,
2665                allowed_lints: vec![],
2666            }],
2667        };
2668
2669        let mut checker = TypeChecker::new();
2670        assert!(checker.check_program(&program).is_ok());
2671    }
2672
2673    #[test]
2674    fn test_mixed_types_complex() {
2675        // : test ( -- )
2676        //   42 int->string      # ( -- String )
2677        //   100 200 >           # ( String -- String Int )
2678        //   if                  # ( String -- String )
2679        //     io.write-line     # ( String -- )
2680        //   else
2681        //     io.write-line
2682        //   then ;
2683        let program = Program {
2684            includes: vec![],
2685            unions: vec![],
2686            words: vec![WordDef {
2687                name: "test".to_string(),
2688                effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2689                body: vec![
2690                    Statement::IntLiteral(42),
2691                    Statement::WordCall {
2692                        name: "int->string".to_string(),
2693                        span: None,
2694                    },
2695                    Statement::IntLiteral(100),
2696                    Statement::IntLiteral(200),
2697                    Statement::WordCall {
2698                        name: "i.>".to_string(),
2699                        span: None,
2700                    },
2701                    Statement::If {
2702                        then_branch: vec![Statement::WordCall {
2703                            name: "io.write-line".to_string(),
2704                            span: None,
2705                        }],
2706                        else_branch: Some(vec![Statement::WordCall {
2707                            name: "io.write-line".to_string(),
2708                            span: None,
2709                        }]),
2710                        span: None,
2711                    },
2712                ],
2713                source: None,
2714                allowed_lints: vec![],
2715            }],
2716        };
2717
2718        let mut checker = TypeChecker::new();
2719        assert!(checker.check_program(&program).is_ok());
2720    }
2721
2722    #[test]
2723    fn test_string_literal() {
2724        // : test ( -- String ) "hello" ;
2725        let program = Program {
2726            includes: vec![],
2727            unions: vec![],
2728            words: vec![WordDef {
2729                name: "test".to_string(),
2730                effect: Some(Effect::new(
2731                    StackType::Empty,
2732                    StackType::singleton(Type::String),
2733                )),
2734                body: vec![Statement::StringLiteral("hello".to_string())],
2735                source: None,
2736                allowed_lints: vec![],
2737            }],
2738        };
2739
2740        let mut checker = TypeChecker::new();
2741        assert!(checker.check_program(&program).is_ok());
2742    }
2743
2744    #[test]
2745    fn test_bool_literal() {
2746        // : test ( -- Bool ) true ;
2747        // Booleans are now properly typed as Bool
2748        let program = Program {
2749            includes: vec![],
2750            unions: vec![],
2751            words: vec![WordDef {
2752                name: "test".to_string(),
2753                effect: Some(Effect::new(
2754                    StackType::Empty,
2755                    StackType::singleton(Type::Bool),
2756                )),
2757                body: vec![Statement::BoolLiteral(true)],
2758                source: None,
2759                allowed_lints: vec![],
2760            }],
2761        };
2762
2763        let mut checker = TypeChecker::new();
2764        assert!(checker.check_program(&program).is_ok());
2765    }
2766
2767    #[test]
2768    fn test_type_error_in_nested_conditional() {
2769        // : test ( -- )
2770        //   10 20 i.> if
2771        //     42 io.write-line   # ERROR: io.write-line expects String, got Int
2772        //   else
2773        //     "ok" io.write-line
2774        //   then ;
2775        let program = Program {
2776            includes: vec![],
2777            unions: vec![],
2778            words: vec![WordDef {
2779                name: "test".to_string(),
2780                effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
2781                body: vec![
2782                    Statement::IntLiteral(10),
2783                    Statement::IntLiteral(20),
2784                    Statement::WordCall {
2785                        name: "i.>".to_string(),
2786                        span: None,
2787                    },
2788                    Statement::If {
2789                        then_branch: vec![
2790                            Statement::IntLiteral(42),
2791                            Statement::WordCall {
2792                                name: "io.write-line".to_string(),
2793                                span: None,
2794                            },
2795                        ],
2796                        else_branch: Some(vec![
2797                            Statement::StringLiteral("ok".to_string()),
2798                            Statement::WordCall {
2799                                name: "io.write-line".to_string(),
2800                                span: None,
2801                            },
2802                        ]),
2803                        span: None,
2804                    },
2805                ],
2806                source: None,
2807                allowed_lints: vec![],
2808            }],
2809        };
2810
2811        let mut checker = TypeChecker::new();
2812        let result = checker.check_program(&program);
2813        assert!(result.is_err());
2814        assert!(result.unwrap_err().contains("Type mismatch"));
2815    }
2816
2817    #[test]
2818    fn test_read_line_operation() {
2819        // : test ( -- String Bool ) io.read-line ;
2820        // io.read-line now returns ( -- String Bool ) for error handling
2821        let program = Program {
2822            includes: vec![],
2823            unions: vec![],
2824            words: vec![WordDef {
2825                name: "test".to_string(),
2826                effect: Some(Effect::new(
2827                    StackType::Empty,
2828                    StackType::from_vec(vec![Type::String, Type::Bool]),
2829                )),
2830                body: vec![Statement::WordCall {
2831                    name: "io.read-line".to_string(),
2832                    span: None,
2833                }],
2834                source: None,
2835                allowed_lints: vec![],
2836            }],
2837        };
2838
2839        let mut checker = TypeChecker::new();
2840        assert!(checker.check_program(&program).is_ok());
2841    }
2842
2843    #[test]
2844    fn test_comparison_operations() {
2845        // Test all comparison operators
2846        // : test ( Int Int -- Bool )
2847        //   i.<= ;
2848        // Simplified: just test that comparisons work and return Bool
2849        let program = Program {
2850            includes: vec![],
2851            unions: vec![],
2852            words: vec![WordDef {
2853                name: "test".to_string(),
2854                effect: Some(Effect::new(
2855                    StackType::Empty.push(Type::Int).push(Type::Int),
2856                    StackType::singleton(Type::Bool),
2857                )),
2858                body: vec![Statement::WordCall {
2859                    name: "i.<=".to_string(),
2860                    span: None,
2861                }],
2862                source: None,
2863                allowed_lints: vec![],
2864            }],
2865        };
2866
2867        let mut checker = TypeChecker::new();
2868        assert!(checker.check_program(&program).is_ok());
2869    }
2870
2871    #[test]
2872    fn test_recursive_word_definitions() {
2873        // Test mutually recursive words (type checking only, no runtime)
2874        // : is-even ( Int -- Int ) dup 0 = if drop 1 else 1 subtract is-odd then ;
2875        // : is-odd ( Int -- Int ) dup 0 = if drop 0 else 1 subtract is-even then ;
2876        //
2877        // Note: This tests that the checker can handle words that reference each other
2878        let program = Program {
2879            includes: vec![],
2880            unions: vec![],
2881            words: vec![
2882                WordDef {
2883                    name: "is-even".to_string(),
2884                    effect: Some(Effect::new(
2885                        StackType::singleton(Type::Int),
2886                        StackType::singleton(Type::Int),
2887                    )),
2888                    body: vec![
2889                        Statement::WordCall {
2890                            name: "dup".to_string(),
2891                            span: None,
2892                        },
2893                        Statement::IntLiteral(0),
2894                        Statement::WordCall {
2895                            name: "i.=".to_string(),
2896                            span: None,
2897                        },
2898                        Statement::If {
2899                            then_branch: vec![
2900                                Statement::WordCall {
2901                                    name: "drop".to_string(),
2902                                    span: None,
2903                                },
2904                                Statement::IntLiteral(1),
2905                            ],
2906                            else_branch: Some(vec![
2907                                Statement::IntLiteral(1),
2908                                Statement::WordCall {
2909                                    name: "i.subtract".to_string(),
2910                                    span: None,
2911                                },
2912                                Statement::WordCall {
2913                                    name: "is-odd".to_string(),
2914                                    span: None,
2915                                },
2916                            ]),
2917                            span: None,
2918                        },
2919                    ],
2920                    source: None,
2921                    allowed_lints: vec![],
2922                },
2923                WordDef {
2924                    name: "is-odd".to_string(),
2925                    effect: Some(Effect::new(
2926                        StackType::singleton(Type::Int),
2927                        StackType::singleton(Type::Int),
2928                    )),
2929                    body: vec![
2930                        Statement::WordCall {
2931                            name: "dup".to_string(),
2932                            span: None,
2933                        },
2934                        Statement::IntLiteral(0),
2935                        Statement::WordCall {
2936                            name: "i.=".to_string(),
2937                            span: None,
2938                        },
2939                        Statement::If {
2940                            then_branch: vec![
2941                                Statement::WordCall {
2942                                    name: "drop".to_string(),
2943                                    span: None,
2944                                },
2945                                Statement::IntLiteral(0),
2946                            ],
2947                            else_branch: Some(vec![
2948                                Statement::IntLiteral(1),
2949                                Statement::WordCall {
2950                                    name: "i.subtract".to_string(),
2951                                    span: None,
2952                                },
2953                                Statement::WordCall {
2954                                    name: "is-even".to_string(),
2955                                    span: None,
2956                                },
2957                            ]),
2958                            span: None,
2959                        },
2960                    ],
2961                    source: None,
2962                    allowed_lints: vec![],
2963                },
2964            ],
2965        };
2966
2967        let mut checker = TypeChecker::new();
2968        assert!(checker.check_program(&program).is_ok());
2969    }
2970
2971    #[test]
2972    fn test_word_calling_word_with_row_polymorphism() {
2973        // Test that row variables unify correctly through word calls
2974        // : apply-twice ( Int -- Int ) dup add ;
2975        // : quad ( Int -- Int ) apply-twice apply-twice ;
2976        // Should work: both use row polymorphism correctly
2977        let program = Program {
2978            includes: vec![],
2979            unions: vec![],
2980            words: vec![
2981                WordDef {
2982                    name: "apply-twice".to_string(),
2983                    effect: Some(Effect::new(
2984                        StackType::singleton(Type::Int),
2985                        StackType::singleton(Type::Int),
2986                    )),
2987                    body: vec![
2988                        Statement::WordCall {
2989                            name: "dup".to_string(),
2990                            span: None,
2991                        },
2992                        Statement::WordCall {
2993                            name: "i.add".to_string(),
2994                            span: None,
2995                        },
2996                    ],
2997                    source: None,
2998                    allowed_lints: vec![],
2999                },
3000                WordDef {
3001                    name: "quad".to_string(),
3002                    effect: Some(Effect::new(
3003                        StackType::singleton(Type::Int),
3004                        StackType::singleton(Type::Int),
3005                    )),
3006                    body: vec![
3007                        Statement::WordCall {
3008                            name: "apply-twice".to_string(),
3009                            span: None,
3010                        },
3011                        Statement::WordCall {
3012                            name: "apply-twice".to_string(),
3013                            span: None,
3014                        },
3015                    ],
3016                    source: None,
3017                    allowed_lints: vec![],
3018                },
3019            ],
3020        };
3021
3022        let mut checker = TypeChecker::new();
3023        assert!(checker.check_program(&program).is_ok());
3024    }
3025
3026    #[test]
3027    fn test_deep_stack_types() {
3028        // Test with many values on stack (10+ items)
3029        // : test ( Int Int Int Int Int Int Int Int Int Int -- Int )
3030        //   add add add add add add add add add ;
3031        let mut stack_type = StackType::Empty;
3032        for _ in 0..10 {
3033            stack_type = stack_type.push(Type::Int);
3034        }
3035
3036        let program = Program {
3037            includes: vec![],
3038            unions: vec![],
3039            words: vec![WordDef {
3040                name: "test".to_string(),
3041                effect: Some(Effect::new(stack_type, StackType::singleton(Type::Int))),
3042                body: vec![
3043                    Statement::WordCall {
3044                        name: "i.add".to_string(),
3045                        span: None,
3046                    },
3047                    Statement::WordCall {
3048                        name: "i.add".to_string(),
3049                        span: None,
3050                    },
3051                    Statement::WordCall {
3052                        name: "i.add".to_string(),
3053                        span: None,
3054                    },
3055                    Statement::WordCall {
3056                        name: "i.add".to_string(),
3057                        span: None,
3058                    },
3059                    Statement::WordCall {
3060                        name: "i.add".to_string(),
3061                        span: None,
3062                    },
3063                    Statement::WordCall {
3064                        name: "i.add".to_string(),
3065                        span: None,
3066                    },
3067                    Statement::WordCall {
3068                        name: "i.add".to_string(),
3069                        span: None,
3070                    },
3071                    Statement::WordCall {
3072                        name: "i.add".to_string(),
3073                        span: None,
3074                    },
3075                    Statement::WordCall {
3076                        name: "i.add".to_string(),
3077                        span: None,
3078                    },
3079                ],
3080                source: None,
3081                allowed_lints: vec![],
3082            }],
3083        };
3084
3085        let mut checker = TypeChecker::new();
3086        assert!(checker.check_program(&program).is_ok());
3087    }
3088
3089    #[test]
3090    fn test_simple_quotation() {
3091        // : test ( -- Quot )
3092        //   [ 1 add ] ;
3093        // Quotation type should be [ ..input Int -- ..input Int ] (row polymorphic)
3094        let program = Program {
3095            includes: vec![],
3096            unions: vec![],
3097            words: vec![WordDef {
3098                name: "test".to_string(),
3099                effect: Some(Effect::new(
3100                    StackType::Empty,
3101                    StackType::singleton(Type::Quotation(Box::new(Effect::new(
3102                        StackType::RowVar("input".to_string()).push(Type::Int),
3103                        StackType::RowVar("input".to_string()).push(Type::Int),
3104                    )))),
3105                )),
3106                body: vec![Statement::Quotation {
3107                    span: None,
3108                    id: 0,
3109                    body: vec![
3110                        Statement::IntLiteral(1),
3111                        Statement::WordCall {
3112                            name: "i.add".to_string(),
3113                            span: None,
3114                        },
3115                    ],
3116                }],
3117                source: None,
3118                allowed_lints: vec![],
3119            }],
3120        };
3121
3122        let mut checker = TypeChecker::new();
3123        match checker.check_program(&program) {
3124            Ok(_) => {}
3125            Err(e) => panic!("Type check failed: {}", e),
3126        }
3127    }
3128
3129    #[test]
3130    fn test_empty_quotation() {
3131        // : test ( -- Quot )
3132        //   [ ] ;
3133        // Empty quotation has effect ( ..input -- ..input ) (preserves stack)
3134        let program = Program {
3135            includes: vec![],
3136            unions: vec![],
3137            words: vec![WordDef {
3138                name: "test".to_string(),
3139                effect: Some(Effect::new(
3140                    StackType::Empty,
3141                    StackType::singleton(Type::Quotation(Box::new(Effect::new(
3142                        StackType::RowVar("input".to_string()),
3143                        StackType::RowVar("input".to_string()),
3144                    )))),
3145                )),
3146                body: vec![Statement::Quotation {
3147                    span: None,
3148                    id: 1,
3149                    body: vec![],
3150                }],
3151                source: None,
3152                allowed_lints: vec![],
3153            }],
3154        };
3155
3156        let mut checker = TypeChecker::new();
3157        assert!(checker.check_program(&program).is_ok());
3158    }
3159
3160    #[test]
3161    fn test_nested_quotation() {
3162        // : test ( -- Quot )
3163        //   [ [ 1 add ] ] ;
3164        // Outer quotation contains inner quotation (both row-polymorphic)
3165        let inner_quot_type = Type::Quotation(Box::new(Effect::new(
3166            StackType::RowVar("input".to_string()).push(Type::Int),
3167            StackType::RowVar("input".to_string()).push(Type::Int),
3168        )));
3169
3170        let outer_quot_type = Type::Quotation(Box::new(Effect::new(
3171            StackType::RowVar("input".to_string()),
3172            StackType::RowVar("input".to_string()).push(inner_quot_type.clone()),
3173        )));
3174
3175        let program = Program {
3176            includes: vec![],
3177            unions: vec![],
3178            words: vec![WordDef {
3179                name: "test".to_string(),
3180                effect: Some(Effect::new(
3181                    StackType::Empty,
3182                    StackType::singleton(outer_quot_type),
3183                )),
3184                body: vec![Statement::Quotation {
3185                    span: None,
3186                    id: 2,
3187                    body: vec![Statement::Quotation {
3188                        span: None,
3189                        id: 3,
3190                        body: vec![
3191                            Statement::IntLiteral(1),
3192                            Statement::WordCall {
3193                                name: "i.add".to_string(),
3194                                span: None,
3195                            },
3196                        ],
3197                    }],
3198                }],
3199                source: None,
3200                allowed_lints: vec![],
3201            }],
3202        };
3203
3204        let mut checker = TypeChecker::new();
3205        assert!(checker.check_program(&program).is_ok());
3206    }
3207
3208    #[test]
3209    fn test_invalid_field_type_error() {
3210        use crate::ast::{UnionDef, UnionField, UnionVariant};
3211
3212        let program = Program {
3213            includes: vec![],
3214            unions: vec![UnionDef {
3215                name: "Message".to_string(),
3216                variants: vec![UnionVariant {
3217                    name: "Get".to_string(),
3218                    fields: vec![UnionField {
3219                        name: "chan".to_string(),
3220                        type_name: "InvalidType".to_string(),
3221                    }],
3222                    source: None,
3223                }],
3224                source: None,
3225            }],
3226            words: vec![],
3227        };
3228
3229        let mut checker = TypeChecker::new();
3230        let result = checker.check_program(&program);
3231        assert!(result.is_err());
3232        let err = result.unwrap_err();
3233        assert!(err.contains("Unknown type 'InvalidType'"));
3234        assert!(err.contains("chan"));
3235        assert!(err.contains("Get"));
3236        assert!(err.contains("Message"));
3237    }
3238
3239    #[test]
3240    fn test_roll_inside_conditional_with_concrete_stack() {
3241        // Bug #93: n roll inside if/else should work when stack has enough concrete items
3242        // : test ( Int Int Int Int -- Int Int Int Int )
3243        //   dup 0 > if
3244        //     3 roll    # Works: 4 concrete items available
3245        //   else
3246        //     rot rot   # Alternative that also works
3247        //   then ;
3248        let program = Program {
3249            includes: vec![],
3250            unions: vec![],
3251            words: vec![WordDef {
3252                name: "test".to_string(),
3253                effect: Some(Effect::new(
3254                    StackType::Empty
3255                        .push(Type::Int)
3256                        .push(Type::Int)
3257                        .push(Type::Int)
3258                        .push(Type::Int),
3259                    StackType::Empty
3260                        .push(Type::Int)
3261                        .push(Type::Int)
3262                        .push(Type::Int)
3263                        .push(Type::Int),
3264                )),
3265                body: vec![
3266                    Statement::WordCall {
3267                        name: "dup".to_string(),
3268                        span: None,
3269                    },
3270                    Statement::IntLiteral(0),
3271                    Statement::WordCall {
3272                        name: "i.>".to_string(),
3273                        span: None,
3274                    },
3275                    Statement::If {
3276                        then_branch: vec![
3277                            Statement::IntLiteral(3),
3278                            Statement::WordCall {
3279                                name: "roll".to_string(),
3280                                span: None,
3281                            },
3282                        ],
3283                        else_branch: Some(vec![
3284                            Statement::WordCall {
3285                                name: "rot".to_string(),
3286                                span: None,
3287                            },
3288                            Statement::WordCall {
3289                                name: "rot".to_string(),
3290                                span: None,
3291                            },
3292                        ]),
3293                        span: None,
3294                    },
3295                ],
3296                source: None,
3297                allowed_lints: vec![],
3298            }],
3299        };
3300
3301        let mut checker = TypeChecker::new();
3302        // This should now work because both branches have 4 concrete items
3303        match checker.check_program(&program) {
3304            Ok(_) => {}
3305            Err(e) => panic!("Type check failed: {}", e),
3306        }
3307    }
3308
3309    #[test]
3310    fn test_roll_inside_match_arm_with_concrete_stack() {
3311        // Similar to bug #93 but for match arms: n roll inside match should work
3312        // when stack has enough concrete items from the match context
3313        use crate::ast::{MatchArm, Pattern, UnionDef, UnionVariant};
3314
3315        // Define a simple union: union Result = Ok | Err
3316        let union_def = UnionDef {
3317            name: "Result".to_string(),
3318            variants: vec![
3319                UnionVariant {
3320                    name: "Ok".to_string(),
3321                    fields: vec![],
3322                    source: None,
3323                },
3324                UnionVariant {
3325                    name: "Err".to_string(),
3326                    fields: vec![],
3327                    source: None,
3328                },
3329            ],
3330            source: None,
3331        };
3332
3333        // : test ( Int Int Int Int Result -- Int Int Int Int )
3334        //   match
3335        //     Ok => 3 roll
3336        //     Err => rot rot
3337        //   end ;
3338        let program = Program {
3339            includes: vec![],
3340            unions: vec![union_def],
3341            words: vec![WordDef {
3342                name: "test".to_string(),
3343                effect: Some(Effect::new(
3344                    StackType::Empty
3345                        .push(Type::Int)
3346                        .push(Type::Int)
3347                        .push(Type::Int)
3348                        .push(Type::Int)
3349                        .push(Type::Union("Result".to_string())),
3350                    StackType::Empty
3351                        .push(Type::Int)
3352                        .push(Type::Int)
3353                        .push(Type::Int)
3354                        .push(Type::Int),
3355                )),
3356                body: vec![Statement::Match {
3357                    arms: vec![
3358                        MatchArm {
3359                            pattern: Pattern::Variant("Ok".to_string()),
3360                            body: vec![
3361                                Statement::IntLiteral(3),
3362                                Statement::WordCall {
3363                                    name: "roll".to_string(),
3364                                    span: None,
3365                                },
3366                            ],
3367                            span: None,
3368                        },
3369                        MatchArm {
3370                            pattern: Pattern::Variant("Err".to_string()),
3371                            body: vec![
3372                                Statement::WordCall {
3373                                    name: "rot".to_string(),
3374                                    span: None,
3375                                },
3376                                Statement::WordCall {
3377                                    name: "rot".to_string(),
3378                                    span: None,
3379                                },
3380                            ],
3381                            span: None,
3382                        },
3383                    ],
3384                    span: None,
3385                }],
3386                source: None,
3387                allowed_lints: vec![],
3388            }],
3389        };
3390
3391        let mut checker = TypeChecker::new();
3392        match checker.check_program(&program) {
3393            Ok(_) => {}
3394            Err(e) => panic!("Type check failed: {}", e),
3395        }
3396    }
3397
3398    #[test]
3399    fn test_roll_with_row_polymorphic_input() {
3400        // roll reaching into row variable should work (needed for stdlib)
3401        // : test ( T U V W -- U V W T )
3402        //   3 roll ;   # Rotates: brings position 3 to top
3403        let program = Program {
3404            includes: vec![],
3405            unions: vec![],
3406            words: vec![WordDef {
3407                name: "test".to_string(),
3408                effect: Some(Effect::new(
3409                    StackType::Empty
3410                        .push(Type::Var("T".to_string()))
3411                        .push(Type::Var("U".to_string()))
3412                        .push(Type::Var("V".to_string()))
3413                        .push(Type::Var("W".to_string())),
3414                    StackType::Empty
3415                        .push(Type::Var("U".to_string()))
3416                        .push(Type::Var("V".to_string()))
3417                        .push(Type::Var("W".to_string()))
3418                        .push(Type::Var("T".to_string())),
3419                )),
3420                body: vec![
3421                    Statement::IntLiteral(3),
3422                    Statement::WordCall {
3423                        name: "roll".to_string(),
3424                        span: None,
3425                    },
3426                ],
3427                source: None,
3428                allowed_lints: vec![],
3429            }],
3430        };
3431
3432        let mut checker = TypeChecker::new();
3433        let result = checker.check_program(&program);
3434        assert!(result.is_ok(), "roll test failed: {:?}", result.err());
3435    }
3436
3437    #[test]
3438    fn test_pick_with_row_polymorphic_input() {
3439        // pick reaching into row variable should work (needed for stdlib)
3440        // : test ( T U V -- T U V T )
3441        //   2 pick ;   # Copies element at index 2 (0-indexed from top)
3442        let program = Program {
3443            includes: vec![],
3444            unions: vec![],
3445            words: vec![WordDef {
3446                name: "test".to_string(),
3447                effect: Some(Effect::new(
3448                    StackType::Empty
3449                        .push(Type::Var("T".to_string()))
3450                        .push(Type::Var("U".to_string()))
3451                        .push(Type::Var("V".to_string())),
3452                    StackType::Empty
3453                        .push(Type::Var("T".to_string()))
3454                        .push(Type::Var("U".to_string()))
3455                        .push(Type::Var("V".to_string()))
3456                        .push(Type::Var("T".to_string())),
3457                )),
3458                body: vec![
3459                    Statement::IntLiteral(2),
3460                    Statement::WordCall {
3461                        name: "pick".to_string(),
3462                        span: None,
3463                    },
3464                ],
3465                source: None,
3466                allowed_lints: vec![],
3467            }],
3468        };
3469
3470        let mut checker = TypeChecker::new();
3471        assert!(checker.check_program(&program).is_ok());
3472    }
3473
3474    #[test]
3475    fn test_valid_union_reference_in_field() {
3476        use crate::ast::{UnionDef, UnionField, UnionVariant};
3477
3478        let program = Program {
3479            includes: vec![],
3480            unions: vec![
3481                UnionDef {
3482                    name: "Inner".to_string(),
3483                    variants: vec![UnionVariant {
3484                        name: "Val".to_string(),
3485                        fields: vec![UnionField {
3486                            name: "x".to_string(),
3487                            type_name: "Int".to_string(),
3488                        }],
3489                        source: None,
3490                    }],
3491                    source: None,
3492                },
3493                UnionDef {
3494                    name: "Outer".to_string(),
3495                    variants: vec![UnionVariant {
3496                        name: "Wrap".to_string(),
3497                        fields: vec![UnionField {
3498                            name: "inner".to_string(),
3499                            type_name: "Inner".to_string(), // Reference to other union
3500                        }],
3501                        source: None,
3502                    }],
3503                    source: None,
3504                },
3505            ],
3506            words: vec![],
3507        };
3508
3509        let mut checker = TypeChecker::new();
3510        assert!(
3511            checker.check_program(&program).is_ok(),
3512            "Union reference in field should be valid"
3513        );
3514    }
3515
3516    #[test]
3517    fn test_divergent_recursive_tail_call() {
3518        // Test that recursive tail calls in if/else branches are recognized as divergent.
3519        // This pattern is common in actor loops:
3520        //
3521        // : store-loop ( Channel -- )
3522        //   dup           # ( chan chan )
3523        //   chan.receive  # ( chan value Bool )
3524        //   not if        # ( chan value )
3525        //     drop        # ( chan ) - drop value, keep chan for recursion
3526        //     store-loop  # diverges - never returns
3527        //   then
3528        //   # else: ( chan value ) - process msg normally
3529        //   drop drop     # ( )
3530        // ;
3531        //
3532        // The then branch ends with a recursive call (store-loop), so it diverges.
3533        // The else branch (implicit empty) continues with the stack after the if.
3534        // Without divergent branch detection, this would fail because:
3535        //   - then branch produces: () (after drop store-loop)
3536        //   - else branch produces: (chan value)
3537        // But since then diverges, we should use else's type.
3538
3539        let program = Program {
3540            includes: vec![],
3541            unions: vec![],
3542            words: vec![WordDef {
3543                name: "store-loop".to_string(),
3544                effect: Some(Effect::new(
3545                    StackType::singleton(Type::Channel), // ( Channel -- )
3546                    StackType::Empty,
3547                )),
3548                body: vec![
3549                    // dup -> ( chan chan )
3550                    Statement::WordCall {
3551                        name: "dup".to_string(),
3552                        span: None,
3553                    },
3554                    // chan.receive -> ( chan value Bool )
3555                    Statement::WordCall {
3556                        name: "chan.receive".to_string(),
3557                        span: None,
3558                    },
3559                    // not -> ( chan value Bool )
3560                    Statement::WordCall {
3561                        name: "not".to_string(),
3562                        span: None,
3563                    },
3564                    // if drop store-loop then
3565                    Statement::If {
3566                        then_branch: vec![
3567                            // drop value -> ( chan )
3568                            Statement::WordCall {
3569                                name: "drop".to_string(),
3570                                span: None,
3571                            },
3572                            // store-loop -> diverges
3573                            Statement::WordCall {
3574                                name: "store-loop".to_string(), // recursive tail call
3575                                span: None,
3576                            },
3577                        ],
3578                        else_branch: None, // implicit else continues with ( chan value )
3579                        span: None,
3580                    },
3581                    // After if: ( chan value ) - drop both
3582                    Statement::WordCall {
3583                        name: "drop".to_string(),
3584                        span: None,
3585                    },
3586                    Statement::WordCall {
3587                        name: "drop".to_string(),
3588                        span: None,
3589                    },
3590                ],
3591                source: None,
3592                allowed_lints: vec![],
3593            }],
3594        };
3595
3596        let mut checker = TypeChecker::new();
3597        let result = checker.check_program(&program);
3598        assert!(
3599            result.is_ok(),
3600            "Divergent recursive tail call should be accepted: {:?}",
3601            result.err()
3602        );
3603    }
3604
3605    #[test]
3606    fn test_divergent_else_branch() {
3607        // Test that divergence detection works for else branches too.
3608        //
3609        // : process-loop ( Channel -- )
3610        //   dup chan.receive   # ( chan value Bool )
3611        //   if                 # ( chan value )
3612        //     drop drop        # normal exit: ( )
3613        //   else
3614        //     drop             # ( chan )
3615        //     process-loop     # diverges - retry on failure
3616        //   then
3617        // ;
3618
3619        let program = Program {
3620            includes: vec![],
3621            unions: vec![],
3622            words: vec![WordDef {
3623                name: "process-loop".to_string(),
3624                effect: Some(Effect::new(
3625                    StackType::singleton(Type::Channel), // ( Channel -- )
3626                    StackType::Empty,
3627                )),
3628                body: vec![
3629                    Statement::WordCall {
3630                        name: "dup".to_string(),
3631                        span: None,
3632                    },
3633                    Statement::WordCall {
3634                        name: "chan.receive".to_string(),
3635                        span: None,
3636                    },
3637                    Statement::If {
3638                        then_branch: vec![
3639                            // success: drop value and chan
3640                            Statement::WordCall {
3641                                name: "drop".to_string(),
3642                                span: None,
3643                            },
3644                            Statement::WordCall {
3645                                name: "drop".to_string(),
3646                                span: None,
3647                            },
3648                        ],
3649                        else_branch: Some(vec![
3650                            // failure: drop value, keep chan, recurse
3651                            Statement::WordCall {
3652                                name: "drop".to_string(),
3653                                span: None,
3654                            },
3655                            Statement::WordCall {
3656                                name: "process-loop".to_string(), // recursive tail call
3657                                span: None,
3658                            },
3659                        ]),
3660                        span: None,
3661                    },
3662                ],
3663                source: None,
3664                allowed_lints: vec![],
3665            }],
3666        };
3667
3668        let mut checker = TypeChecker::new();
3669        let result = checker.check_program(&program);
3670        assert!(
3671            result.is_ok(),
3672            "Divergent else branch should be accepted: {:?}",
3673            result.err()
3674        );
3675    }
3676
3677    #[test]
3678    fn test_non_tail_call_recursion_not_divergent() {
3679        // Test that recursion NOT in tail position is not treated as divergent.
3680        // This should fail type checking because after the recursive call,
3681        // there's more code that changes the stack.
3682        //
3683        // : bad-loop ( Int -- Int )
3684        //   dup 0 i.> if
3685        //     1 i.subtract bad-loop  # recursive call
3686        //     1 i.add                # more code after - not tail position!
3687        //   then
3688        // ;
3689        //
3690        // This should fail because:
3691        // - then branch: recurse then add 1 -> stack changes after recursion
3692        // - else branch (implicit): stack is ( Int )
3693        // Without proper handling, this could incorrectly pass.
3694
3695        let program = Program {
3696            includes: vec![],
3697            unions: vec![],
3698            words: vec![WordDef {
3699                name: "bad-loop".to_string(),
3700                effect: Some(Effect::new(
3701                    StackType::singleton(Type::Int),
3702                    StackType::singleton(Type::Int),
3703                )),
3704                body: vec![
3705                    Statement::WordCall {
3706                        name: "dup".to_string(),
3707                        span: None,
3708                    },
3709                    Statement::IntLiteral(0),
3710                    Statement::WordCall {
3711                        name: "i.>".to_string(),
3712                        span: None,
3713                    },
3714                    Statement::If {
3715                        then_branch: vec![
3716                            Statement::IntLiteral(1),
3717                            Statement::WordCall {
3718                                name: "i.subtract".to_string(),
3719                                span: None,
3720                            },
3721                            Statement::WordCall {
3722                                name: "bad-loop".to_string(), // NOT in tail position
3723                                span: None,
3724                            },
3725                            Statement::IntLiteral(1),
3726                            Statement::WordCall {
3727                                name: "i.add".to_string(), // code after recursion
3728                                span: None,
3729                            },
3730                        ],
3731                        else_branch: None,
3732                        span: None,
3733                    },
3734                ],
3735                source: None,
3736                allowed_lints: vec![],
3737            }],
3738        };
3739
3740        let mut checker = TypeChecker::new();
3741        // This should pass because the branches ARE compatible:
3742        // - then: produces Int (after bad-loop returns Int, then add 1)
3743        // - else: produces Int (from the dup at start)
3744        // The key is that bad-loop is NOT in tail position, so it's not divergent.
3745        let result = checker.check_program(&program);
3746        assert!(
3747            result.is_ok(),
3748            "Non-tail recursion should type check normally: {:?}",
3749            result.err()
3750        );
3751    }
3752
3753    #[test]
3754    fn test_call_yield_quotation_error() {
3755        // Phase 2c: Calling a quotation with Yield effect directly should error.
3756        // : bad ( Ctx -- Ctx ) [ yield ] call ;
3757        // This is wrong because yield quotations must be wrapped with strand.weave.
3758        let program = Program {
3759            includes: vec![],
3760            unions: vec![],
3761            words: vec![WordDef {
3762                name: "bad".to_string(),
3763                effect: Some(Effect::new(
3764                    StackType::singleton(Type::Var("Ctx".to_string())),
3765                    StackType::singleton(Type::Var("Ctx".to_string())),
3766                )),
3767                body: vec![
3768                    // Push a dummy value that will be yielded
3769                    Statement::IntLiteral(42),
3770                    Statement::Quotation {
3771                        span: None,
3772                        id: 0,
3773                        body: vec![Statement::WordCall {
3774                            name: "yield".to_string(),
3775                            span: None,
3776                        }],
3777                    },
3778                    Statement::WordCall {
3779                        name: "call".to_string(),
3780                        span: None,
3781                    },
3782                ],
3783                source: None,
3784                allowed_lints: vec![],
3785            }],
3786        };
3787
3788        let mut checker = TypeChecker::new();
3789        let result = checker.check_program(&program);
3790        assert!(
3791            result.is_err(),
3792            "Calling yield quotation directly should fail"
3793        );
3794        let err = result.unwrap_err();
3795        assert!(
3796            err.contains("Yield") || err.contains("strand.weave"),
3797            "Error should mention Yield or strand.weave: {}",
3798            err
3799        );
3800    }
3801
3802    #[test]
3803    fn test_strand_weave_yield_quotation_ok() {
3804        // Phase 2c: Using strand.weave on a Yield quotation is correct.
3805        // : good ( -- Int Handle ) 42 [ yield ] strand.weave ;
3806        let program = Program {
3807            includes: vec![],
3808            unions: vec![],
3809            words: vec![WordDef {
3810                name: "good".to_string(),
3811                effect: Some(Effect::new(
3812                    StackType::Empty,
3813                    StackType::Empty
3814                        .push(Type::Int)
3815                        .push(Type::Var("Handle".to_string())),
3816                )),
3817                body: vec![
3818                    Statement::IntLiteral(42),
3819                    Statement::Quotation {
3820                        span: None,
3821                        id: 0,
3822                        body: vec![Statement::WordCall {
3823                            name: "yield".to_string(),
3824                            span: None,
3825                        }],
3826                    },
3827                    Statement::WordCall {
3828                        name: "strand.weave".to_string(),
3829                        span: None,
3830                    },
3831                ],
3832                source: None,
3833                allowed_lints: vec![],
3834            }],
3835        };
3836
3837        let mut checker = TypeChecker::new();
3838        let result = checker.check_program(&program);
3839        assert!(
3840            result.is_ok(),
3841            "strand.weave on yield quotation should pass: {:?}",
3842            result.err()
3843        );
3844    }
3845
3846    #[test]
3847    fn test_call_pure_quotation_ok() {
3848        // Phase 2c: Calling a pure quotation (no Yield) is fine.
3849        // : ok ( Int -- Int ) [ 1 i.add ] call ;
3850        let program = Program {
3851            includes: vec![],
3852            unions: vec![],
3853            words: vec![WordDef {
3854                name: "ok".to_string(),
3855                effect: Some(Effect::new(
3856                    StackType::singleton(Type::Int),
3857                    StackType::singleton(Type::Int),
3858                )),
3859                body: vec![
3860                    Statement::Quotation {
3861                        span: None,
3862                        id: 0,
3863                        body: vec![
3864                            Statement::IntLiteral(1),
3865                            Statement::WordCall {
3866                                name: "i.add".to_string(),
3867                                span: None,
3868                            },
3869                        ],
3870                    },
3871                    Statement::WordCall {
3872                        name: "call".to_string(),
3873                        span: None,
3874                    },
3875                ],
3876                source: None,
3877                allowed_lints: vec![],
3878            }],
3879        };
3880
3881        let mut checker = TypeChecker::new();
3882        let result = checker.check_program(&program);
3883        assert!(
3884            result.is_ok(),
3885            "Calling pure quotation should pass: {:?}",
3886            result.err()
3887        );
3888    }
3889
3890    // ==========================================================================
3891    // Stack Pollution Detection Tests (Issue #228)
3892    // These tests verify the type checker catches stack effect mismatches
3893    // ==========================================================================
3894
3895    #[test]
3896    fn test_pollution_extra_push() {
3897        // : test ( Int -- Int ) 42 ;
3898        // Declares consuming 1 Int, producing 1 Int
3899        // But body pushes 42 on top of input, leaving 2 values
3900        let program = Program {
3901            includes: vec![],
3902            unions: vec![],
3903            words: vec![WordDef {
3904                name: "test".to_string(),
3905                effect: Some(Effect::new(
3906                    StackType::singleton(Type::Int),
3907                    StackType::singleton(Type::Int),
3908                )),
3909                body: vec![Statement::IntLiteral(42)],
3910                source: None,
3911                allowed_lints: vec![],
3912            }],
3913        };
3914
3915        let mut checker = TypeChecker::new();
3916        let result = checker.check_program(&program);
3917        assert!(
3918            result.is_err(),
3919            "Should reject: declares ( Int -- Int ) but leaves 2 values on stack"
3920        );
3921    }
3922
3923    #[test]
3924    fn test_pollution_extra_dup() {
3925        // : test ( Int -- Int ) dup ;
3926        // Declares producing 1 Int, but dup produces 2
3927        let program = Program {
3928            includes: vec![],
3929            unions: vec![],
3930            words: vec![WordDef {
3931                name: "test".to_string(),
3932                effect: Some(Effect::new(
3933                    StackType::singleton(Type::Int),
3934                    StackType::singleton(Type::Int),
3935                )),
3936                body: vec![Statement::WordCall {
3937                    name: "dup".to_string(),
3938                    span: None,
3939                }],
3940                source: None,
3941                allowed_lints: vec![],
3942            }],
3943        };
3944
3945        let mut checker = TypeChecker::new();
3946        let result = checker.check_program(&program);
3947        assert!(
3948            result.is_err(),
3949            "Should reject: declares ( Int -- Int ) but dup produces 2 values"
3950        );
3951    }
3952
3953    #[test]
3954    fn test_pollution_consumes_extra() {
3955        // : test ( Int -- Int ) drop drop 42 ;
3956        // Declares consuming 1 Int, but body drops twice
3957        let program = Program {
3958            includes: vec![],
3959            unions: vec![],
3960            words: vec![WordDef {
3961                name: "test".to_string(),
3962                effect: Some(Effect::new(
3963                    StackType::singleton(Type::Int),
3964                    StackType::singleton(Type::Int),
3965                )),
3966                body: vec![
3967                    Statement::WordCall {
3968                        name: "drop".to_string(),
3969                        span: None,
3970                    },
3971                    Statement::WordCall {
3972                        name: "drop".to_string(),
3973                        span: None,
3974                    },
3975                    Statement::IntLiteral(42),
3976                ],
3977                source: None,
3978                allowed_lints: vec![],
3979            }],
3980        };
3981
3982        let mut checker = TypeChecker::new();
3983        let result = checker.check_program(&program);
3984        assert!(
3985            result.is_err(),
3986            "Should reject: declares ( Int -- Int ) but consumes 2 values"
3987        );
3988    }
3989
3990    #[test]
3991    fn test_pollution_in_then_branch() {
3992        // : test ( Bool -- Int )
3993        //   if 1 2 else 3 then ;
3994        // Then branch pushes 2 values, else pushes 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::singleton(Type::Bool),
4002                    StackType::singleton(Type::Int),
4003                )),
4004                body: vec![Statement::If {
4005                    then_branch: vec![
4006                        Statement::IntLiteral(1),
4007                        Statement::IntLiteral(2), // Extra value!
4008                    ],
4009                    else_branch: Some(vec![Statement::IntLiteral(3)]),
4010                    span: None,
4011                }],
4012                source: None,
4013                allowed_lints: vec![],
4014            }],
4015        };
4016
4017        let mut checker = TypeChecker::new();
4018        let result = checker.check_program(&program);
4019        assert!(
4020            result.is_err(),
4021            "Should reject: then branch pushes 2 values, else pushes 1"
4022        );
4023    }
4024
4025    #[test]
4026    fn test_pollution_in_else_branch() {
4027        // : test ( Bool -- Int )
4028        //   if 1 else 2 3 then ;
4029        // Then branch pushes 1, else pushes 2 values
4030        let program = Program {
4031            includes: vec![],
4032            unions: vec![],
4033            words: vec![WordDef {
4034                name: "test".to_string(),
4035                effect: Some(Effect::new(
4036                    StackType::singleton(Type::Bool),
4037                    StackType::singleton(Type::Int),
4038                )),
4039                body: vec![Statement::If {
4040                    then_branch: vec![Statement::IntLiteral(1)],
4041                    else_branch: Some(vec![
4042                        Statement::IntLiteral(2),
4043                        Statement::IntLiteral(3), // Extra value!
4044                    ]),
4045                    span: None,
4046                }],
4047                source: None,
4048                allowed_lints: vec![],
4049            }],
4050        };
4051
4052        let mut checker = TypeChecker::new();
4053        let result = checker.check_program(&program);
4054        assert!(
4055            result.is_err(),
4056            "Should reject: then branch pushes 1 value, else pushes 2"
4057        );
4058    }
4059
4060    #[test]
4061    fn test_pollution_both_branches_extra() {
4062        // : test ( Bool -- Int )
4063        //   if 1 2 else 3 4 then ;
4064        // Both branches push 2 values but declared output is 1
4065        let program = Program {
4066            includes: vec![],
4067            unions: vec![],
4068            words: vec![WordDef {
4069                name: "test".to_string(),
4070                effect: Some(Effect::new(
4071                    StackType::singleton(Type::Bool),
4072                    StackType::singleton(Type::Int),
4073                )),
4074                body: vec![Statement::If {
4075                    then_branch: vec![Statement::IntLiteral(1), Statement::IntLiteral(2)],
4076                    else_branch: Some(vec![Statement::IntLiteral(3), Statement::IntLiteral(4)]),
4077                    span: None,
4078                }],
4079                source: None,
4080                allowed_lints: vec![],
4081            }],
4082        };
4083
4084        let mut checker = TypeChecker::new();
4085        let result = checker.check_program(&program);
4086        assert!(
4087            result.is_err(),
4088            "Should reject: both branches push 2 values, but declared output is 1"
4089        );
4090    }
4091
4092    #[test]
4093    fn test_pollution_branch_consumes_extra() {
4094        // : test ( Bool Int -- Int )
4095        //   if drop drop 1 else then ;
4096        // Then branch consumes more than available from declared inputs
4097        let program = Program {
4098            includes: vec![],
4099            unions: vec![],
4100            words: vec![WordDef {
4101                name: "test".to_string(),
4102                effect: Some(Effect::new(
4103                    StackType::Empty.push(Type::Bool).push(Type::Int),
4104                    StackType::singleton(Type::Int),
4105                )),
4106                body: vec![Statement::If {
4107                    then_branch: vec![
4108                        Statement::WordCall {
4109                            name: "drop".to_string(),
4110                            span: None,
4111                        },
4112                        Statement::WordCall {
4113                            name: "drop".to_string(),
4114                            span: None,
4115                        },
4116                        Statement::IntLiteral(1),
4117                    ],
4118                    else_branch: Some(vec![]),
4119                    span: None,
4120                }],
4121                source: None,
4122                allowed_lints: vec![],
4123            }],
4124        };
4125
4126        let mut checker = TypeChecker::new();
4127        let result = checker.check_program(&program);
4128        assert!(
4129            result.is_err(),
4130            "Should reject: then branch consumes Bool (should only have Int after if)"
4131        );
4132    }
4133
4134    #[test]
4135    fn test_pollution_quotation_wrong_arity_output() {
4136        // : test ( Int -- Int )
4137        //   [ dup ] call ;
4138        // Quotation produces 2 values, but word declares 1 output
4139        let program = Program {
4140            includes: vec![],
4141            unions: vec![],
4142            words: vec![WordDef {
4143                name: "test".to_string(),
4144                effect: Some(Effect::new(
4145                    StackType::singleton(Type::Int),
4146                    StackType::singleton(Type::Int),
4147                )),
4148                body: vec![
4149                    Statement::Quotation {
4150                        span: None,
4151                        id: 0,
4152                        body: vec![Statement::WordCall {
4153                            name: "dup".to_string(),
4154                            span: None,
4155                        }],
4156                    },
4157                    Statement::WordCall {
4158                        name: "call".to_string(),
4159                        span: None,
4160                    },
4161                ],
4162                source: None,
4163                allowed_lints: vec![],
4164            }],
4165        };
4166
4167        let mut checker = TypeChecker::new();
4168        let result = checker.check_program(&program);
4169        assert!(
4170            result.is_err(),
4171            "Should reject: quotation [dup] produces 2 values, declared output is 1"
4172        );
4173    }
4174
4175    #[test]
4176    fn test_pollution_quotation_wrong_arity_input() {
4177        // : test ( Int -- Int )
4178        //   [ drop drop 42 ] call ;
4179        // Quotation consumes 2 values, but only 1 available
4180        let program = Program {
4181            includes: vec![],
4182            unions: vec![],
4183            words: vec![WordDef {
4184                name: "test".to_string(),
4185                effect: Some(Effect::new(
4186                    StackType::singleton(Type::Int),
4187                    StackType::singleton(Type::Int),
4188                )),
4189                body: vec![
4190                    Statement::Quotation {
4191                        span: None,
4192                        id: 0,
4193                        body: vec![
4194                            Statement::WordCall {
4195                                name: "drop".to_string(),
4196                                span: None,
4197                            },
4198                            Statement::WordCall {
4199                                name: "drop".to_string(),
4200                                span: None,
4201                            },
4202                            Statement::IntLiteral(42),
4203                        ],
4204                    },
4205                    Statement::WordCall {
4206                        name: "call".to_string(),
4207                        span: None,
4208                    },
4209                ],
4210                source: None,
4211                allowed_lints: vec![],
4212            }],
4213        };
4214
4215        let mut checker = TypeChecker::new();
4216        let result = checker.check_program(&program);
4217        assert!(
4218            result.is_err(),
4219            "Should reject: quotation [drop drop 42] consumes 2 values, only 1 available"
4220        );
4221    }
4222
4223    #[test]
4224    fn test_missing_effect_provides_helpful_error() {
4225        // : myword 42 ;
4226        // No effect annotation - should error with helpful message including word name
4227        let program = Program {
4228            includes: vec![],
4229            unions: vec![],
4230            words: vec![WordDef {
4231                name: "myword".to_string(),
4232                effect: None, // No annotation
4233                body: vec![Statement::IntLiteral(42)],
4234                source: None,
4235                allowed_lints: vec![],
4236            }],
4237        };
4238
4239        let mut checker = TypeChecker::new();
4240        let result = checker.check_program(&program);
4241        assert!(result.is_err());
4242        let err = result.unwrap_err();
4243        assert!(err.contains("myword"), "Error should mention word name");
4244        assert!(
4245            err.contains("stack effect"),
4246            "Error should mention stack effect"
4247        );
4248    }
4249
4250    #[test]
4251    fn test_valid_effect_exact_match() {
4252        // : test ( Int Int -- Int ) i.+ ;
4253        // Exact match - consumes 2, produces 1
4254        let program = Program {
4255            includes: vec![],
4256            unions: vec![],
4257            words: vec![WordDef {
4258                name: "test".to_string(),
4259                effect: Some(Effect::new(
4260                    StackType::Empty.push(Type::Int).push(Type::Int),
4261                    StackType::singleton(Type::Int),
4262                )),
4263                body: vec![Statement::WordCall {
4264                    name: "i.add".to_string(),
4265                    span: None,
4266                }],
4267                source: None,
4268                allowed_lints: vec![],
4269            }],
4270        };
4271
4272        let mut checker = TypeChecker::new();
4273        let result = checker.check_program(&program);
4274        assert!(result.is_ok(), "Should accept: effect matches exactly");
4275    }
4276
4277    #[test]
4278    fn test_valid_polymorphic_passthrough() {
4279        // : test ( a -- a ) ;
4280        // Identity function - row polymorphism allows this
4281        let program = Program {
4282            includes: vec![],
4283            unions: vec![],
4284            words: vec![WordDef {
4285                name: "test".to_string(),
4286                effect: Some(Effect::new(
4287                    StackType::Cons {
4288                        rest: Box::new(StackType::RowVar("rest".to_string())),
4289                        top: Type::Var("a".to_string()),
4290                    },
4291                    StackType::Cons {
4292                        rest: Box::new(StackType::RowVar("rest".to_string())),
4293                        top: Type::Var("a".to_string()),
4294                    },
4295                )),
4296                body: vec![], // Empty body - just pass through
4297                source: None,
4298                allowed_lints: vec![],
4299            }],
4300        };
4301
4302        let mut checker = TypeChecker::new();
4303        let result = checker.check_program(&program);
4304        assert!(result.is_ok(), "Should accept: polymorphic identity");
4305    }
4306
4307    // ==========================================================================
4308    // Closure Nesting Tests (Issue #230)
4309    // Tests for deep closure nesting, transitive captures, and edge cases
4310    // ==========================================================================
4311
4312    #[test]
4313    fn test_closure_basic_capture() {
4314        // : make-adder ( Int -- Closure )
4315        //   [ i.+ ] ;
4316        // The quotation needs 2 Ints (for i.+) but caller will only provide 1
4317        // So it captures 1 Int from the creation site
4318        // Must declare as Closure type to trigger capture analysis
4319        let program = Program {
4320            includes: vec![],
4321            unions: vec![],
4322            words: vec![WordDef {
4323                name: "make-adder".to_string(),
4324                effect: Some(Effect::new(
4325                    StackType::singleton(Type::Int),
4326                    StackType::singleton(Type::Closure {
4327                        effect: Box::new(Effect::new(
4328                            StackType::RowVar("r".to_string()).push(Type::Int),
4329                            StackType::RowVar("r".to_string()).push(Type::Int),
4330                        )),
4331                        captures: vec![Type::Int], // Captures 1 Int
4332                    }),
4333                )),
4334                body: vec![Statement::Quotation {
4335                    span: None,
4336                    id: 0,
4337                    body: vec![Statement::WordCall {
4338                        name: "i.add".to_string(),
4339                        span: None,
4340                    }],
4341                }],
4342                source: None,
4343                allowed_lints: vec![],
4344            }],
4345        };
4346
4347        let mut checker = TypeChecker::new();
4348        let result = checker.check_program(&program);
4349        assert!(
4350            result.is_ok(),
4351            "Basic closure capture should work: {:?}",
4352            result.err()
4353        );
4354    }
4355
4356    #[test]
4357    fn test_closure_nested_two_levels() {
4358        // : outer ( -- Quot )
4359        //   [ [ 1 i.+ ] ] ;
4360        // Outer quotation: no inputs, just returns inner quotation
4361        // Inner quotation: pushes 1 then adds (needs 1 Int from caller)
4362        let program = Program {
4363            includes: vec![],
4364            unions: vec![],
4365            words: vec![WordDef {
4366                name: "outer".to_string(),
4367                effect: Some(Effect::new(
4368                    StackType::Empty,
4369                    StackType::singleton(Type::Quotation(Box::new(Effect::new(
4370                        StackType::RowVar("r".to_string()),
4371                        StackType::RowVar("r".to_string()).push(Type::Quotation(Box::new(
4372                            Effect::new(
4373                                StackType::RowVar("s".to_string()).push(Type::Int),
4374                                StackType::RowVar("s".to_string()).push(Type::Int),
4375                            ),
4376                        ))),
4377                    )))),
4378                )),
4379                body: vec![Statement::Quotation {
4380                    span: None,
4381                    id: 0,
4382                    body: vec![Statement::Quotation {
4383                        span: None,
4384                        id: 1,
4385                        body: vec![
4386                            Statement::IntLiteral(1),
4387                            Statement::WordCall {
4388                                name: "i.add".to_string(),
4389                                span: None,
4390                            },
4391                        ],
4392                    }],
4393                }],
4394                source: None,
4395                allowed_lints: vec![],
4396            }],
4397        };
4398
4399        let mut checker = TypeChecker::new();
4400        let result = checker.check_program(&program);
4401        assert!(
4402            result.is_ok(),
4403            "Two-level nested quotations should work: {:?}",
4404            result.err()
4405        );
4406    }
4407
4408    #[test]
4409    fn test_closure_nested_three_levels() {
4410        // : deep ( -- Quot )
4411        //   [ [ [ 1 i.+ ] ] ] ;
4412        // Three levels of nesting, innermost does actual work
4413        let inner_effect = Effect::new(
4414            StackType::RowVar("a".to_string()).push(Type::Int),
4415            StackType::RowVar("a".to_string()).push(Type::Int),
4416        );
4417        let middle_effect = Effect::new(
4418            StackType::RowVar("b".to_string()),
4419            StackType::RowVar("b".to_string()).push(Type::Quotation(Box::new(inner_effect))),
4420        );
4421        let outer_effect = Effect::new(
4422            StackType::RowVar("c".to_string()),
4423            StackType::RowVar("c".to_string()).push(Type::Quotation(Box::new(middle_effect))),
4424        );
4425
4426        let program = Program {
4427            includes: vec![],
4428            unions: vec![],
4429            words: vec![WordDef {
4430                name: "deep".to_string(),
4431                effect: Some(Effect::new(
4432                    StackType::Empty,
4433                    StackType::singleton(Type::Quotation(Box::new(outer_effect))),
4434                )),
4435                body: vec![Statement::Quotation {
4436                    span: None,
4437                    id: 0,
4438                    body: vec![Statement::Quotation {
4439                        span: None,
4440                        id: 1,
4441                        body: vec![Statement::Quotation {
4442                            span: None,
4443                            id: 2,
4444                            body: vec![
4445                                Statement::IntLiteral(1),
4446                                Statement::WordCall {
4447                                    name: "i.add".to_string(),
4448                                    span: None,
4449                                },
4450                            ],
4451                        }],
4452                    }],
4453                }],
4454                source: None,
4455                allowed_lints: vec![],
4456            }],
4457        };
4458
4459        let mut checker = TypeChecker::new();
4460        let result = checker.check_program(&program);
4461        assert!(
4462            result.is_ok(),
4463            "Three-level nested quotations should work: {:?}",
4464            result.err()
4465        );
4466    }
4467
4468    #[test]
4469    fn test_closure_use_after_creation() {
4470        // : use-adder ( -- Int )
4471        //   5 make-adder   // Creates closure capturing 5
4472        //   10 swap call ; // Calls closure with 10, should return 15
4473        //
4474        // Tests that closure is properly typed when called later
4475        let adder_type = Type::Closure {
4476            effect: Box::new(Effect::new(
4477                StackType::RowVar("r".to_string()).push(Type::Int),
4478                StackType::RowVar("r".to_string()).push(Type::Int),
4479            )),
4480            captures: vec![Type::Int],
4481        };
4482
4483        let program = Program {
4484            includes: vec![],
4485            unions: vec![],
4486            words: vec![
4487                WordDef {
4488                    name: "make-adder".to_string(),
4489                    effect: Some(Effect::new(
4490                        StackType::singleton(Type::Int),
4491                        StackType::singleton(adder_type.clone()),
4492                    )),
4493                    body: vec![Statement::Quotation {
4494                        span: None,
4495                        id: 0,
4496                        body: vec![Statement::WordCall {
4497                            name: "i.add".to_string(),
4498                            span: None,
4499                        }],
4500                    }],
4501                    source: None,
4502                    allowed_lints: vec![],
4503                },
4504                WordDef {
4505                    name: "use-adder".to_string(),
4506                    effect: Some(Effect::new(
4507                        StackType::Empty,
4508                        StackType::singleton(Type::Int),
4509                    )),
4510                    body: vec![
4511                        Statement::IntLiteral(5),
4512                        Statement::WordCall {
4513                            name: "make-adder".to_string(),
4514                            span: None,
4515                        },
4516                        Statement::IntLiteral(10),
4517                        Statement::WordCall {
4518                            name: "swap".to_string(),
4519                            span: None,
4520                        },
4521                        Statement::WordCall {
4522                            name: "call".to_string(),
4523                            span: None,
4524                        },
4525                    ],
4526                    source: None,
4527                    allowed_lints: vec![],
4528                },
4529            ],
4530        };
4531
4532        let mut checker = TypeChecker::new();
4533        let result = checker.check_program(&program);
4534        assert!(
4535            result.is_ok(),
4536            "Closure usage after creation should work: {:?}",
4537            result.err()
4538        );
4539    }
4540
4541    #[test]
4542    fn test_closure_wrong_call_type() {
4543        // : bad-use ( -- Int )
4544        //   5 make-adder   // Creates Int -> Int closure
4545        //   "hello" swap call ; // Tries to call with String - should fail!
4546        let adder_type = Type::Closure {
4547            effect: Box::new(Effect::new(
4548                StackType::RowVar("r".to_string()).push(Type::Int),
4549                StackType::RowVar("r".to_string()).push(Type::Int),
4550            )),
4551            captures: vec![Type::Int],
4552        };
4553
4554        let program = Program {
4555            includes: vec![],
4556            unions: vec![],
4557            words: vec![
4558                WordDef {
4559                    name: "make-adder".to_string(),
4560                    effect: Some(Effect::new(
4561                        StackType::singleton(Type::Int),
4562                        StackType::singleton(adder_type.clone()),
4563                    )),
4564                    body: vec![Statement::Quotation {
4565                        span: None,
4566                        id: 0,
4567                        body: vec![Statement::WordCall {
4568                            name: "i.add".to_string(),
4569                            span: None,
4570                        }],
4571                    }],
4572                    source: None,
4573                    allowed_lints: vec![],
4574                },
4575                WordDef {
4576                    name: "bad-use".to_string(),
4577                    effect: Some(Effect::new(
4578                        StackType::Empty,
4579                        StackType::singleton(Type::Int),
4580                    )),
4581                    body: vec![
4582                        Statement::IntLiteral(5),
4583                        Statement::WordCall {
4584                            name: "make-adder".to_string(),
4585                            span: None,
4586                        },
4587                        Statement::StringLiteral("hello".to_string()), // Wrong type!
4588                        Statement::WordCall {
4589                            name: "swap".to_string(),
4590                            span: None,
4591                        },
4592                        Statement::WordCall {
4593                            name: "call".to_string(),
4594                            span: None,
4595                        },
4596                    ],
4597                    source: None,
4598                    allowed_lints: vec![],
4599                },
4600            ],
4601        };
4602
4603        let mut checker = TypeChecker::new();
4604        let result = checker.check_program(&program);
4605        assert!(
4606            result.is_err(),
4607            "Calling Int closure with String should fail"
4608        );
4609    }
4610
4611    #[test]
4612    fn test_closure_multiple_captures() {
4613        // : make-between ( Int Int -- Quot )
4614        //   [ dup rot i.>= swap rot i.<= and ] ;
4615        // Captures both min and max, checks if value is between them
4616        // Body needs: value min max (3 Ints)
4617        // Caller provides: value (1 Int)
4618        // Captures: min max (2 Ints)
4619        let program = Program {
4620            includes: vec![],
4621            unions: vec![],
4622            words: vec![WordDef {
4623                name: "make-between".to_string(),
4624                effect: Some(Effect::new(
4625                    StackType::Empty.push(Type::Int).push(Type::Int),
4626                    StackType::singleton(Type::Quotation(Box::new(Effect::new(
4627                        StackType::RowVar("r".to_string()).push(Type::Int),
4628                        StackType::RowVar("r".to_string()).push(Type::Bool),
4629                    )))),
4630                )),
4631                body: vec![Statement::Quotation {
4632                    span: None,
4633                    id: 0,
4634                    body: vec![
4635                        // Simplified: just do a comparison that uses all 3 values
4636                        Statement::WordCall {
4637                            name: "i.>=".to_string(),
4638                            span: None,
4639                        },
4640                        // Note: This doesn't match the comment but tests multi-capture
4641                    ],
4642                }],
4643                source: None,
4644                allowed_lints: vec![],
4645            }],
4646        };
4647
4648        let mut checker = TypeChecker::new();
4649        let result = checker.check_program(&program);
4650        // This should work - the quotation body uses values from stack
4651        // The exact behavior depends on how captures are inferred
4652        // For now, we're testing that it doesn't crash
4653        assert!(
4654            result.is_ok() || result.is_err(),
4655            "Multiple captures should be handled (pass or fail gracefully)"
4656        );
4657    }
4658
4659    #[test]
4660    fn test_quotation_type_preserved_through_word() {
4661        // : identity-quot ( Quot -- Quot ) ;
4662        // Tests that quotation types are preserved when passed through words
4663        let quot_type = Type::Quotation(Box::new(Effect::new(
4664            StackType::RowVar("r".to_string()).push(Type::Int),
4665            StackType::RowVar("r".to_string()).push(Type::Int),
4666        )));
4667
4668        let program = Program {
4669            includes: vec![],
4670            unions: vec![],
4671            words: vec![WordDef {
4672                name: "identity-quot".to_string(),
4673                effect: Some(Effect::new(
4674                    StackType::singleton(quot_type.clone()),
4675                    StackType::singleton(quot_type.clone()),
4676                )),
4677                body: vec![], // Identity - just return what's on stack
4678                source: None,
4679                allowed_lints: vec![],
4680            }],
4681        };
4682
4683        let mut checker = TypeChecker::new();
4684        let result = checker.check_program(&program);
4685        assert!(
4686            result.is_ok(),
4687            "Quotation type should be preserved through identity word: {:?}",
4688            result.err()
4689        );
4690    }
4691
4692    #[test]
4693    fn test_closure_captures_value_for_inner_quotation() {
4694        // : make-inner-adder ( Int -- Closure )
4695        //   [ [ i.+ ] swap call ] ;
4696        // The closure captures an Int
4697        // When called, it creates an inner quotation and calls it with the captured value
4698        // This tests that closures can work with nested quotations
4699        let closure_effect = Effect::new(
4700            StackType::RowVar("r".to_string()).push(Type::Int),
4701            StackType::RowVar("r".to_string()).push(Type::Int),
4702        );
4703
4704        let program = Program {
4705            includes: vec![],
4706            unions: vec![],
4707            words: vec![WordDef {
4708                name: "make-inner-adder".to_string(),
4709                effect: Some(Effect::new(
4710                    StackType::singleton(Type::Int),
4711                    StackType::singleton(Type::Closure {
4712                        effect: Box::new(closure_effect),
4713                        captures: vec![Type::Int],
4714                    }),
4715                )),
4716                body: vec![Statement::Quotation {
4717                    span: None,
4718                    id: 0,
4719                    body: vec![
4720                        // The captured Int and the caller's Int are on stack
4721                        Statement::WordCall {
4722                            name: "i.add".to_string(),
4723                            span: None,
4724                        },
4725                    ],
4726                }],
4727                source: None,
4728                allowed_lints: vec![],
4729            }],
4730        };
4731
4732        let mut checker = TypeChecker::new();
4733        let result = checker.check_program(&program);
4734        assert!(
4735            result.is_ok(),
4736            "Closure with capture for inner work should pass: {:?}",
4737            result.err()
4738        );
4739    }
4740
4741    #[test]
4742    fn test_union_type_mismatch_should_fail() {
4743        // RFC #345: Different union types should not unify
4744        // This tests that passing union B to a function expecting union A fails
4745        use crate::ast::{UnionDef, UnionField, UnionVariant};
4746
4747        let mut program = Program {
4748            includes: vec![],
4749            unions: vec![
4750                UnionDef {
4751                    name: "UnionA".to_string(),
4752                    variants: vec![UnionVariant {
4753                        name: "AVal".to_string(),
4754                        fields: vec![UnionField {
4755                            name: "x".to_string(),
4756                            type_name: "Int".to_string(),
4757                        }],
4758                        source: None,
4759                    }],
4760                    source: None,
4761                },
4762                UnionDef {
4763                    name: "UnionB".to_string(),
4764                    variants: vec![UnionVariant {
4765                        name: "BVal".to_string(),
4766                        fields: vec![UnionField {
4767                            name: "y".to_string(),
4768                            type_name: "Int".to_string(),
4769                        }],
4770                        source: None,
4771                    }],
4772                    source: None,
4773                },
4774            ],
4775            words: vec![
4776                // : takes-a ( UnionA -- ) drop ;
4777                WordDef {
4778                    name: "takes-a".to_string(),
4779                    effect: Some(Effect::new(
4780                        StackType::RowVar("rest".to_string())
4781                            .push(Type::Union("UnionA".to_string())),
4782                        StackType::RowVar("rest".to_string()),
4783                    )),
4784                    body: vec![Statement::WordCall {
4785                        name: "drop".to_string(),
4786                        span: None,
4787                    }],
4788                    source: None,
4789                    allowed_lints: vec![],
4790                },
4791                // : main ( -- ) 99 Make-BVal takes-a ;
4792                // This should FAIL: Make-BVal returns UnionB, takes-a expects UnionA
4793                WordDef {
4794                    name: "main".to_string(),
4795                    effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
4796                    body: vec![
4797                        Statement::IntLiteral(99),
4798                        Statement::WordCall {
4799                            name: "Make-BVal".to_string(),
4800                            span: None,
4801                        },
4802                        Statement::WordCall {
4803                            name: "takes-a".to_string(),
4804                            span: None,
4805                        },
4806                    ],
4807                    source: None,
4808                    allowed_lints: vec![],
4809                },
4810            ],
4811        };
4812
4813        // Generate constructors (Make-AVal, Make-BVal)
4814        program.generate_constructors().unwrap();
4815
4816        let mut checker = TypeChecker::new();
4817        let result = checker.check_program(&program);
4818
4819        // This SHOULD fail because UnionB != UnionA
4820        assert!(
4821            result.is_err(),
4822            "Passing UnionB to function expecting UnionA should fail, but got: {:?}",
4823            result
4824        );
4825        let err = result.unwrap_err();
4826        assert!(
4827            err.contains("Union") || err.contains("mismatch"),
4828            "Error should mention union type mismatch, got: {}",
4829            err
4830        );
4831    }
4832
4833    // =========================================================================
4834    // Aux stack tests (Issue #350)
4835    // =========================================================================
4836
4837    fn make_word_call(name: &str) -> Statement {
4838        Statement::WordCall {
4839            name: name.to_string(),
4840            span: None,
4841        }
4842    }
4843
4844    #[test]
4845    fn test_aux_basic_round_trip() {
4846        // : test ( Int -- Int ) >aux aux> ;
4847        let program = Program {
4848            includes: vec![],
4849            unions: vec![],
4850            words: vec![WordDef {
4851                name: "test".to_string(),
4852                effect: Some(Effect::new(
4853                    StackType::singleton(Type::Int),
4854                    StackType::singleton(Type::Int),
4855                )),
4856                body: vec![make_word_call(">aux"), make_word_call("aux>")],
4857                source: None,
4858                allowed_lints: vec![],
4859            }],
4860        };
4861
4862        let mut checker = TypeChecker::new();
4863        assert!(checker.check_program(&program).is_ok());
4864    }
4865
4866    #[test]
4867    fn test_aux_preserves_type() {
4868        // : test ( String -- String ) >aux aux> ;
4869        let program = Program {
4870            includes: vec![],
4871            unions: vec![],
4872            words: vec![WordDef {
4873                name: "test".to_string(),
4874                effect: Some(Effect::new(
4875                    StackType::singleton(Type::String),
4876                    StackType::singleton(Type::String),
4877                )),
4878                body: vec![make_word_call(">aux"), make_word_call("aux>")],
4879                source: None,
4880                allowed_lints: vec![],
4881            }],
4882        };
4883
4884        let mut checker = TypeChecker::new();
4885        assert!(checker.check_program(&program).is_ok());
4886    }
4887
4888    #[test]
4889    fn test_aux_unbalanced_error() {
4890        // : test ( Int -- ) >aux ;  -- ERROR: aux not empty at return
4891        let program = Program {
4892            includes: vec![],
4893            unions: vec![],
4894            words: vec![WordDef {
4895                name: "test".to_string(),
4896                effect: Some(Effect::new(
4897                    StackType::singleton(Type::Int),
4898                    StackType::Empty,
4899                )),
4900                body: vec![make_word_call(">aux")],
4901                source: None,
4902                allowed_lints: vec![],
4903            }],
4904        };
4905
4906        let mut checker = TypeChecker::new();
4907        let result = checker.check_program(&program);
4908        assert!(result.is_err());
4909        let err = result.unwrap_err();
4910        assert!(
4911            err.contains("aux stack is not empty"),
4912            "Expected aux stack balance error, got: {}",
4913            err
4914        );
4915    }
4916
4917    #[test]
4918    fn test_aux_pop_empty_error() {
4919        // : test ( -- Int ) aux> ;  -- ERROR: aux is empty
4920        let program = Program {
4921            includes: vec![],
4922            unions: vec![],
4923            words: vec![WordDef {
4924                name: "test".to_string(),
4925                effect: Some(Effect::new(
4926                    StackType::Empty,
4927                    StackType::singleton(Type::Int),
4928                )),
4929                body: vec![make_word_call("aux>")],
4930                source: None,
4931                allowed_lints: vec![],
4932            }],
4933        };
4934
4935        let mut checker = TypeChecker::new();
4936        let result = checker.check_program(&program);
4937        assert!(result.is_err());
4938        let err = result.unwrap_err();
4939        assert!(
4940            err.contains("aux stack is empty"),
4941            "Expected aux empty error, got: {}",
4942            err
4943        );
4944    }
4945
4946    #[test]
4947    fn test_aux_multiple_values() {
4948        // >aux >aux aux> aux> is identity (LIFO preserves order)
4949        // Input: ( Int String ), >aux pops String, >aux pops Int
4950        // aux> pushes Int, aux> pushes String → output: ( Int String )
4951        let program = Program {
4952            includes: vec![],
4953            unions: vec![],
4954            words: vec![WordDef {
4955                name: "test".to_string(),
4956                effect: Some(Effect::new(
4957                    StackType::Empty.push(Type::Int).push(Type::String),
4958                    StackType::Empty.push(Type::Int).push(Type::String),
4959                )),
4960                body: vec![
4961                    make_word_call(">aux"),
4962                    make_word_call(">aux"),
4963                    make_word_call("aux>"),
4964                    make_word_call("aux>"),
4965                ],
4966                source: None,
4967                allowed_lints: vec![],
4968            }],
4969        };
4970
4971        let mut checker = TypeChecker::new();
4972        assert!(checker.check_program(&program).is_ok());
4973    }
4974
4975    #[test]
4976    fn test_aux_max_depths_tracked() {
4977        // : test ( Int -- Int ) >aux aux> ;
4978        let program = Program {
4979            includes: vec![],
4980            unions: vec![],
4981            words: vec![WordDef {
4982                name: "test".to_string(),
4983                effect: Some(Effect::new(
4984                    StackType::singleton(Type::Int),
4985                    StackType::singleton(Type::Int),
4986                )),
4987                body: vec![make_word_call(">aux"), make_word_call("aux>")],
4988                source: None,
4989                allowed_lints: vec![],
4990            }],
4991        };
4992
4993        let mut checker = TypeChecker::new();
4994        checker.check_program(&program).unwrap();
4995        let depths = checker.take_aux_max_depths();
4996        assert_eq!(depths.get("test"), Some(&1));
4997    }
4998
4999    #[test]
5000    fn test_aux_in_match_balanced() {
5001        // Aux used symmetrically in match arms: each arm does >aux aux>
5002        use crate::ast::{MatchArm, Pattern, UnionDef, UnionVariant};
5003
5004        let union_def = UnionDef {
5005            name: "Choice".to_string(),
5006            variants: vec![
5007                UnionVariant {
5008                    name: "Left".to_string(),
5009                    fields: vec![],
5010                    source: None,
5011                },
5012                UnionVariant {
5013                    name: "Right".to_string(),
5014                    fields: vec![],
5015                    source: None,
5016                },
5017            ],
5018            source: None,
5019        };
5020
5021        // : test ( Int Choice -- Int )
5022        //   swap >aux match Left => aux> end Right => aux> end ;
5023        let program = Program {
5024            includes: vec![],
5025            unions: vec![union_def],
5026            words: vec![WordDef {
5027                name: "test".to_string(),
5028                effect: Some(Effect::new(
5029                    StackType::Empty
5030                        .push(Type::Int)
5031                        .push(Type::Union("Choice".to_string())),
5032                    StackType::singleton(Type::Int),
5033                )),
5034                body: vec![
5035                    make_word_call("swap"),
5036                    make_word_call(">aux"),
5037                    Statement::Match {
5038                        arms: vec![
5039                            MatchArm {
5040                                pattern: Pattern::Variant("Left".to_string()),
5041                                body: vec![make_word_call("aux>")],
5042                                span: None,
5043                            },
5044                            MatchArm {
5045                                pattern: Pattern::Variant("Right".to_string()),
5046                                body: vec![make_word_call("aux>")],
5047                                span: None,
5048                            },
5049                        ],
5050                        span: None,
5051                    },
5052                ],
5053                source: None,
5054                allowed_lints: vec![],
5055            }],
5056        };
5057
5058        let mut checker = TypeChecker::new();
5059        assert!(checker.check_program(&program).is_ok());
5060    }
5061
5062    #[test]
5063    fn test_aux_in_match_unbalanced_error() {
5064        // Match arms with different aux states should error
5065        use crate::ast::{MatchArm, Pattern, UnionDef, UnionVariant};
5066
5067        let union_def = UnionDef {
5068            name: "Choice".to_string(),
5069            variants: vec![
5070                UnionVariant {
5071                    name: "Left".to_string(),
5072                    fields: vec![],
5073                    source: None,
5074                },
5075                UnionVariant {
5076                    name: "Right".to_string(),
5077                    fields: vec![],
5078                    source: None,
5079                },
5080            ],
5081            source: None,
5082        };
5083
5084        // : test ( Int Choice -- Int )
5085        //   swap >aux match Left => aux> end Right => end ;  -- ERROR: unbalanced
5086        let program = Program {
5087            includes: vec![],
5088            unions: vec![union_def],
5089            words: vec![WordDef {
5090                name: "test".to_string(),
5091                effect: Some(Effect::new(
5092                    StackType::Empty
5093                        .push(Type::Int)
5094                        .push(Type::Union("Choice".to_string())),
5095                    StackType::singleton(Type::Int),
5096                )),
5097                body: vec![
5098                    make_word_call("swap"),
5099                    make_word_call(">aux"),
5100                    Statement::Match {
5101                        arms: vec![
5102                            MatchArm {
5103                                pattern: Pattern::Variant("Left".to_string()),
5104                                body: vec![make_word_call("aux>")],
5105                                span: None,
5106                            },
5107                            MatchArm {
5108                                pattern: Pattern::Variant("Right".to_string()),
5109                                body: vec![],
5110                                span: None,
5111                            },
5112                        ],
5113                        span: None,
5114                    },
5115                ],
5116                source: None,
5117                allowed_lints: vec![],
5118            }],
5119        };
5120
5121        let mut checker = TypeChecker::new();
5122        let result = checker.check_program(&program);
5123        assert!(result.is_err());
5124        let err = result.unwrap_err();
5125        assert!(
5126            err.contains("aux stack"),
5127            "Expected aux stack mismatch error, got: {}",
5128            err
5129        );
5130    }
5131
5132    #[test]
5133    fn test_aux_in_quotation_rejected() {
5134        // >aux inside a quotation should be rejected (codegen limitation)
5135        let program = Program {
5136            includes: vec![],
5137            unions: vec![],
5138            words: vec![WordDef {
5139                name: "test".to_string(),
5140                effect: Some(Effect::new(
5141                    StackType::singleton(Type::Int),
5142                    StackType::singleton(Type::Quotation(Box::new(Effect::new(
5143                        StackType::singleton(Type::Int),
5144                        StackType::singleton(Type::Int),
5145                    )))),
5146                )),
5147                body: vec![Statement::Quotation {
5148                    span: None,
5149                    id: 0,
5150                    body: vec![make_word_call(">aux"), make_word_call("aux>")],
5151                }],
5152                source: None,
5153                allowed_lints: vec![],
5154            }],
5155        };
5156
5157        let mut checker = TypeChecker::new();
5158        let result = checker.check_program(&program);
5159        assert!(result.is_err());
5160        let err = result.unwrap_err();
5161        assert!(
5162            err.contains("not supported inside quotations"),
5163            "Expected quotation rejection error, got: {}",
5164            err
5165        );
5166    }
5167
5168    // =========================================================================
5169    // Dataflow combinator tests
5170    // =========================================================================
5171
5172    #[test]
5173    fn test_dip_basic() {
5174        // : test ( Int Int -- Int Int )  [ 1 i.+ ] dip ;
5175        // Increments value below top, preserving top
5176        let program = Program {
5177            includes: vec![],
5178            unions: vec![],
5179            words: vec![WordDef {
5180                name: "test".to_string(),
5181                effect: Some(Effect::new(
5182                    StackType::Empty.push(Type::Int).push(Type::Int),
5183                    StackType::Empty.push(Type::Int).push(Type::Int),
5184                )),
5185                body: vec![
5186                    Statement::Quotation {
5187                        id: 0,
5188                        body: vec![
5189                            Statement::IntLiteral(1),
5190                            Statement::WordCall {
5191                                name: "i.+".to_string(),
5192                                span: None,
5193                            },
5194                        ],
5195                        span: None,
5196                    },
5197                    Statement::WordCall {
5198                        name: "dip".to_string(),
5199                        span: None,
5200                    },
5201                ],
5202                source: None,
5203                allowed_lints: vec![],
5204            }],
5205        };
5206
5207        let mut checker = TypeChecker::new();
5208        assert!(checker.check_program(&program).is_ok());
5209    }
5210
5211    #[test]
5212    fn test_dip_type_mismatch() {
5213        // : test ( String Int -- ?? )  [ 1 i.+ ] dip ;
5214        // Should fail: quotation expects Int but gets String
5215        let program = Program {
5216            includes: vec![],
5217            unions: vec![],
5218            words: vec![WordDef {
5219                name: "test".to_string(),
5220                effect: Some(Effect::new(
5221                    StackType::Empty.push(Type::String).push(Type::Int),
5222                    StackType::Empty.push(Type::Int).push(Type::Int),
5223                )),
5224                body: vec![
5225                    Statement::Quotation {
5226                        id: 0,
5227                        body: vec![
5228                            Statement::IntLiteral(1),
5229                            Statement::WordCall {
5230                                name: "i.+".to_string(),
5231                                span: None,
5232                            },
5233                        ],
5234                        span: None,
5235                    },
5236                    Statement::WordCall {
5237                        name: "dip".to_string(),
5238                        span: None,
5239                    },
5240                ],
5241                source: None,
5242                allowed_lints: vec![],
5243            }],
5244        };
5245
5246        let mut checker = TypeChecker::new();
5247        assert!(checker.check_program(&program).is_err());
5248    }
5249
5250    #[test]
5251    fn test_keep_basic() {
5252        // : test ( Int -- Int Int )  [ dup i.* ] keep ;
5253        // Squares and keeps original
5254        let program = Program {
5255            includes: vec![],
5256            unions: vec![],
5257            words: vec![WordDef {
5258                name: "test".to_string(),
5259                effect: Some(Effect::new(
5260                    StackType::singleton(Type::Int),
5261                    StackType::Empty.push(Type::Int).push(Type::Int),
5262                )),
5263                body: vec![
5264                    Statement::Quotation {
5265                        id: 0,
5266                        body: vec![
5267                            Statement::WordCall {
5268                                name: "dup".to_string(),
5269                                span: None,
5270                            },
5271                            Statement::WordCall {
5272                                name: "i.*".to_string(),
5273                                span: None,
5274                            },
5275                        ],
5276                        span: None,
5277                    },
5278                    Statement::WordCall {
5279                        name: "keep".to_string(),
5280                        span: None,
5281                    },
5282                ],
5283                source: None,
5284                allowed_lints: vec![],
5285            }],
5286        };
5287
5288        let mut checker = TypeChecker::new();
5289        assert!(checker.check_program(&program).is_ok());
5290    }
5291
5292    #[test]
5293    fn test_bi_basic() {
5294        // : test ( Int -- Int Int )  [ 2 i.* ] [ 3 i.* ] bi ;
5295        // Double and triple
5296        let program = Program {
5297            includes: vec![],
5298            unions: vec![],
5299            words: vec![WordDef {
5300                name: "test".to_string(),
5301                effect: Some(Effect::new(
5302                    StackType::singleton(Type::Int),
5303                    StackType::Empty.push(Type::Int).push(Type::Int),
5304                )),
5305                body: vec![
5306                    Statement::Quotation {
5307                        id: 0,
5308                        body: vec![
5309                            Statement::IntLiteral(2),
5310                            Statement::WordCall {
5311                                name: "i.*".to_string(),
5312                                span: None,
5313                            },
5314                        ],
5315                        span: None,
5316                    },
5317                    Statement::Quotation {
5318                        id: 1,
5319                        body: vec![
5320                            Statement::IntLiteral(3),
5321                            Statement::WordCall {
5322                                name: "i.*".to_string(),
5323                                span: None,
5324                            },
5325                        ],
5326                        span: None,
5327                    },
5328                    Statement::WordCall {
5329                        name: "bi".to_string(),
5330                        span: None,
5331                    },
5332                ],
5333                source: None,
5334                allowed_lints: vec![],
5335            }],
5336        };
5337
5338        let mut checker = TypeChecker::new();
5339        assert!(checker.check_program(&program).is_ok());
5340    }
5341
5342    #[test]
5343    fn test_keep_type_mismatch() {
5344        // : test ( String -- ?? )  [ 1 i.+ ] keep ;
5345        // Should fail: quotation expects Int but gets String
5346        let program = Program {
5347            includes: vec![],
5348            unions: vec![],
5349            words: vec![WordDef {
5350                name: "test".to_string(),
5351                effect: Some(Effect::new(
5352                    StackType::singleton(Type::String),
5353                    StackType::Empty.push(Type::Int).push(Type::String),
5354                )),
5355                body: vec![
5356                    Statement::Quotation {
5357                        id: 0,
5358                        body: vec![
5359                            Statement::IntLiteral(1),
5360                            Statement::WordCall {
5361                                name: "i.+".to_string(),
5362                                span: None,
5363                            },
5364                        ],
5365                        span: None,
5366                    },
5367                    Statement::WordCall {
5368                        name: "keep".to_string(),
5369                        span: None,
5370                    },
5371                ],
5372                source: None,
5373                allowed_lints: vec![],
5374            }],
5375        };
5376
5377        let mut checker = TypeChecker::new();
5378        assert!(checker.check_program(&program).is_err());
5379    }
5380
5381    #[test]
5382    fn test_bi_type_mismatch() {
5383        // : test ( String -- ?? )  [ string.length ] [ 1 i.+ ] bi ;
5384        // Should fail: second quotation expects Int but value is String
5385        let program = Program {
5386            includes: vec![],
5387            unions: vec![],
5388            words: vec![WordDef {
5389                name: "test".to_string(),
5390                effect: Some(Effect::new(
5391                    StackType::singleton(Type::String),
5392                    StackType::Empty.push(Type::Int).push(Type::Int),
5393                )),
5394                body: vec![
5395                    Statement::Quotation {
5396                        id: 0,
5397                        body: vec![Statement::WordCall {
5398                            name: "string.length".to_string(),
5399                            span: None,
5400                        }],
5401                        span: None,
5402                    },
5403                    Statement::Quotation {
5404                        id: 1,
5405                        body: vec![
5406                            Statement::IntLiteral(1),
5407                            Statement::WordCall {
5408                                name: "i.+".to_string(),
5409                                span: None,
5410                            },
5411                        ],
5412                        span: None,
5413                    },
5414                    Statement::WordCall {
5415                        name: "bi".to_string(),
5416                        span: None,
5417                    },
5418                ],
5419                source: None,
5420                allowed_lints: vec![],
5421            }],
5422        };
5423
5424        let mut checker = TypeChecker::new();
5425        assert!(checker.check_program(&program).is_err());
5426    }
5427
5428    #[test]
5429    fn test_dip_underflow() {
5430        // : test ( -- ?? )  [ 1 ] dip ;
5431        // Should fail: dip needs a value below the quotation
5432        let program = Program {
5433            includes: vec![],
5434            unions: vec![],
5435            words: vec![WordDef {
5436                name: "test".to_string(),
5437                effect: Some(Effect::new(
5438                    StackType::Empty,
5439                    StackType::singleton(Type::Int),
5440                )),
5441                body: vec![
5442                    Statement::Quotation {
5443                        id: 0,
5444                        body: vec![Statement::IntLiteral(1)],
5445                        span: None,
5446                    },
5447                    Statement::WordCall {
5448                        name: "dip".to_string(),
5449                        span: None,
5450                    },
5451                ],
5452                source: None,
5453                allowed_lints: vec![],
5454            }],
5455        };
5456
5457        let mut checker = TypeChecker::new();
5458        let result = checker.check_program(&program);
5459        assert!(result.is_err());
5460        let err = result.unwrap_err();
5461        assert!(
5462            err.contains("stack underflow"),
5463            "Expected underflow error, got: {}",
5464            err
5465        );
5466    }
5467
5468    #[test]
5469    fn test_dip_preserves_type() {
5470        // : test ( Int String -- Int String )  [ 1 i.+ ] dip ;
5471        // The String on top is preserved, Int below is incremented
5472        let program = Program {
5473            includes: vec![],
5474            unions: vec![],
5475            words: vec![WordDef {
5476                name: "test".to_string(),
5477                effect: Some(Effect::new(
5478                    StackType::Empty.push(Type::Int).push(Type::String),
5479                    StackType::Empty.push(Type::Int).push(Type::String),
5480                )),
5481                body: vec![
5482                    Statement::Quotation {
5483                        id: 0,
5484                        body: vec![
5485                            Statement::IntLiteral(1),
5486                            Statement::WordCall {
5487                                name: "i.+".to_string(),
5488                                span: None,
5489                            },
5490                        ],
5491                        span: None,
5492                    },
5493                    Statement::WordCall {
5494                        name: "dip".to_string(),
5495                        span: None,
5496                    },
5497                ],
5498                source: None,
5499                allowed_lints: vec![],
5500            }],
5501        };
5502
5503        let mut checker = TypeChecker::new();
5504        assert!(checker.check_program(&program).is_ok());
5505    }
5506
5507    #[test]
5508    fn test_keep_underflow() {
5509        // : test ( -- ?? )  [ drop ] keep ;
5510        // Should fail: keep needs a value below the quotation
5511        let program = Program {
5512            includes: vec![],
5513            unions: vec![],
5514            words: vec![WordDef {
5515                name: "test".to_string(),
5516                effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
5517                body: vec![
5518                    Statement::Quotation {
5519                        id: 0,
5520                        body: vec![Statement::WordCall {
5521                            name: "drop".to_string(),
5522                            span: None,
5523                        }],
5524                        span: None,
5525                    },
5526                    Statement::WordCall {
5527                        name: "keep".to_string(),
5528                        span: None,
5529                    },
5530                ],
5531                source: None,
5532                allowed_lints: vec![],
5533            }],
5534        };
5535
5536        let mut checker = TypeChecker::new();
5537        let result = checker.check_program(&program);
5538        assert!(result.is_err());
5539        let err = result.unwrap_err();
5540        assert!(
5541            err.contains("stack underflow") || err.contains("underflow"),
5542            "Expected underflow error, got: {}",
5543            err
5544        );
5545    }
5546
5547    #[test]
5548    fn test_bi_underflow() {
5549        // : test ( -- ?? )  [ 1 ] [ 2 ] bi ;
5550        // Should fail: bi needs a value below the two quotations
5551        let program = Program {
5552            includes: vec![],
5553            unions: vec![],
5554            words: vec![WordDef {
5555                name: "test".to_string(),
5556                effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
5557                body: vec![
5558                    Statement::Quotation {
5559                        id: 0,
5560                        body: vec![Statement::IntLiteral(1)],
5561                        span: None,
5562                    },
5563                    Statement::Quotation {
5564                        id: 1,
5565                        body: vec![Statement::IntLiteral(2)],
5566                        span: None,
5567                    },
5568                    Statement::WordCall {
5569                        name: "bi".to_string(),
5570                        span: None,
5571                    },
5572                ],
5573                source: None,
5574                allowed_lints: vec![],
5575            }],
5576        };
5577
5578        let mut checker = TypeChecker::new();
5579        let result = checker.check_program(&program);
5580        assert!(result.is_err());
5581        let err = result.unwrap_err();
5582        assert!(
5583            err.contains("stack underflow") || err.contains("underflow"),
5584            "Expected underflow error, got: {}",
5585            err
5586        );
5587    }
5588
5589    #[test]
5590    fn test_bi_polymorphic_quotations() {
5591        // : test ( Int -- Int String )  [ 2 i.* ] [ int->string ] bi ;
5592        // Two quotations with different output types — verifies independent typing
5593        let program = Program {
5594            includes: vec![],
5595            unions: vec![],
5596            words: vec![WordDef {
5597                name: "test".to_string(),
5598                effect: Some(Effect::new(
5599                    StackType::singleton(Type::Int),
5600                    StackType::Empty.push(Type::Int).push(Type::String),
5601                )),
5602                body: vec![
5603                    Statement::Quotation {
5604                        id: 0,
5605                        body: vec![
5606                            Statement::IntLiteral(2),
5607                            Statement::WordCall {
5608                                name: "i.*".to_string(),
5609                                span: None,
5610                            },
5611                        ],
5612                        span: None,
5613                    },
5614                    Statement::Quotation {
5615                        id: 1,
5616                        body: vec![Statement::WordCall {
5617                            name: "int->string".to_string(),
5618                            span: None,
5619                        }],
5620                        span: None,
5621                    },
5622                    Statement::WordCall {
5623                        name: "bi".to_string(),
5624                        span: None,
5625                    },
5626                ],
5627                source: None,
5628                allowed_lints: vec![],
5629            }],
5630        };
5631
5632        let mut checker = TypeChecker::new();
5633        assert!(checker.check_program(&program).is_ok());
5634    }
5635}