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