Skip to main content

seqc/typechecker/
driver.rs

1//! Top-level driver: check_program / check_word / statement iteration.
2
3use crate::ast::{Program, Statement, WordDef};
4use crate::types::{
5    Effect, SideEffect, StackType, Type, UnionTypeInfo, VariantFieldInfo, VariantInfo,
6};
7use crate::unification::{Subst, unify_stacks};
8
9use super::{TypeChecker, format_line_prefix, validate_main_effect};
10
11impl TypeChecker {
12    pub fn check_program(&mut self, program: &Program) -> Result<(), String> {
13        // First pass: register all union definitions
14        for union_def in &program.unions {
15            let variants = union_def
16                .variants
17                .iter()
18                .map(|v| VariantInfo {
19                    name: v.name.clone(),
20                    fields: v
21                        .fields
22                        .iter()
23                        .map(|f| VariantFieldInfo {
24                            name: f.name.clone(),
25                            field_type: self.parse_type_name(&f.type_name),
26                        })
27                        .collect(),
28                })
29                .collect();
30
31            self.unions.insert(
32                union_def.name.clone(),
33                UnionTypeInfo {
34                    name: union_def.name.clone(),
35                    variants,
36                },
37            );
38        }
39
40        // Validate field types in unions reference known types
41        self.validate_union_field_types(program)?;
42
43        // Second pass: collect all word signatures
44        // All words must have explicit stack effect declarations (v2.0 requirement)
45        for word in &program.words {
46            if let Some(effect) = &word.effect {
47                // RFC #345: Validate that all types in the effect are known
48                // This catches cases where an uppercase identifier was parsed as a type variable
49                // but should have been a union type (e.g., from an include)
50                self.validate_effect_types(effect, &word.name)?;
51                self.env.insert(word.name.clone(), effect.clone());
52            } else {
53                return Err(format!(
54                    "Word '{}' is missing a stack effect declaration.\n\
55                     All words must declare their stack effect, e.g.: : {} ( -- ) ... ;",
56                    word.name, word.name
57                ));
58            }
59        }
60
61        // Validate main's signature (Issue #355).
62        // Only `( -- )` and `( -- Int )` are allowed.
63        if let Some(main_effect) = self.env.get("main") {
64            validate_main_effect(main_effect)?;
65        }
66
67        // Third pass: type check each word body
68        for word in &program.words {
69            self.check_word(word)?;
70        }
71
72        Ok(())
73    }
74
75    /// Type check a word definition
76    pub(super) fn check_word(&self, word: &WordDef) -> Result<(), String> {
77        // Track current word for detecting recursive tail calls (divergent branches)
78        let line = word.source.as_ref().map(|s| s.start_line);
79        *self.current_word.borrow_mut() = Some((word.name.clone(), line));
80
81        // Reset aux stack for this word (Issue #350)
82        *self.current_aux_stack.borrow_mut() = StackType::Empty;
83
84        // All words must have declared effects (enforced in check_program)
85        let declared_effect = word.effect.as_ref().expect("word must have effect");
86
87        // Check if the word's output type is a quotation or closure
88        // If so, store it as the expected type for capture inference
89        if let Some((_rest, top_type)) = declared_effect.outputs.clone().pop()
90            && matches!(top_type, Type::Quotation(_) | Type::Closure { .. })
91        {
92            *self.expected_quotation_type.borrow_mut() = Some(top_type);
93        }
94
95        // Infer the result stack and effects starting from declared input
96        let (result_stack, _subst, inferred_effects) =
97            self.infer_statements_from(&word.body, &declared_effect.inputs, true)?;
98
99        // Clear expected type after checking
100        *self.expected_quotation_type.borrow_mut() = None;
101
102        // Verify result matches declared output
103        let line_info = line.map(format_line_prefix).unwrap_or_default();
104        unify_stacks(&declared_effect.outputs, &result_stack).map_err(|e| {
105            format!(
106                "{}Word '{}': declared output stack ({}) doesn't match inferred ({}): {}",
107                line_info, word.name, declared_effect.outputs, result_stack, e
108            )
109        })?;
110
111        // Verify computational effects match (bidirectional)
112        // 1. Check that each inferred effect has a matching declared effect (by kind)
113        // Type variables in effects are matched by kind (Yield matches Yield)
114        for inferred in &inferred_effects {
115            if !self.effect_matches_any(inferred, &declared_effect.effects) {
116                return Err(format!(
117                    "{}Word '{}': body produces effect '{}' but no matching effect is declared.\n\
118                     Hint: Add '| Yield <type>' to the word's stack effect declaration.",
119                    line_info, word.name, inferred
120                ));
121            }
122        }
123
124        // 2. Check that each declared effect is actually produced (effect soundness)
125        // This prevents declaring effects that don't occur
126        for declared in &declared_effect.effects {
127            if !self.effect_matches_any(declared, &inferred_effects) {
128                return Err(format!(
129                    "{}Word '{}': declares effect '{}' but body doesn't produce it.\n\
130                     Hint: Remove the effect declaration or ensure the body uses yield.",
131                    line_info, word.name, declared
132                ));
133            }
134        }
135
136        // Verify aux stack is empty at word boundary (Issue #350)
137        let aux_stack = self.current_aux_stack.borrow().clone();
138        if aux_stack != StackType::Empty {
139            return Err(format!(
140                "{}Word '{}': aux stack is not empty at word return.\n\
141                 Remaining aux stack: {}\n\
142                 Every >aux must be matched by a corresponding aux> before the word returns.",
143                line_info, word.name, aux_stack
144            ));
145        }
146
147        // Clear current word
148        *self.current_word.borrow_mut() = None;
149
150        Ok(())
151    }
152
153    /// Infer the resulting stack type from a sequence of statements
154    /// starting from a given input stack
155    /// Returns (final_stack, substitution, accumulated_effects)
156    ///
157    /// `capture_stmt_types`: If true, capture statement type info for codegen optimization.
158    /// Should only be true for top-level word bodies, not for nested branches/loops.
159    pub(super) fn infer_statements_from(
160        &self,
161        statements: &[Statement],
162        start_stack: &StackType,
163        capture_stmt_types: bool,
164    ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
165        let mut current_stack = start_stack.clone();
166        let mut accumulated_subst = Subst::empty();
167        let mut accumulated_effects: Vec<SideEffect> = Vec::new();
168        let mut skip_next = false;
169
170        for (i, stmt) in statements.iter().enumerate() {
171            // Skip this statement if we already handled it (e.g., pick/roll after literal)
172            if skip_next {
173                skip_next = false;
174                continue;
175            }
176
177            // Special case: IntLiteral followed by pick or roll
178            // Handle them as a fused operation with correct type semantics
179            if let Statement::IntLiteral(n) = stmt
180                && let Some(Statement::WordCall {
181                    name: next_word, ..
182                }) = statements.get(i + 1)
183            {
184                if next_word == "pick" {
185                    let (new_stack, subst) = self.handle_literal_pick(*n, current_stack.clone())?;
186                    current_stack = new_stack;
187                    accumulated_subst = accumulated_subst.compose(&subst);
188                    skip_next = true; // Skip the "pick" word
189                    continue;
190                } else if next_word == "roll" {
191                    let (new_stack, subst) = self.handle_literal_roll(*n, current_stack.clone())?;
192                    current_stack = new_stack;
193                    accumulated_subst = accumulated_subst.compose(&subst);
194                    skip_next = true; // Skip the "roll" word
195                    continue;
196                }
197            }
198
199            // Look ahead: if this is a quotation followed by a word that expects specific quotation type,
200            // set the expected type before checking the quotation
201            let saved_expected_type = if matches!(stmt, Statement::Quotation { .. }) {
202                // Save the current expected type
203                let saved = self.expected_quotation_type.borrow().clone();
204
205                // Try to set expected type based on lookahead
206                if let Some(Statement::WordCall {
207                    name: next_word, ..
208                }) = statements.get(i + 1)
209                {
210                    // Check if the next word expects a specific quotation type
211                    if let Some(next_effect) = self.lookup_word_effect(next_word) {
212                        // Extract the quotation type expected by the next word
213                        // For operations like spawn: ( ..a Quotation(-- ) -- ..a Int )
214                        if let Some((_rest, quot_type)) = next_effect.inputs.clone().pop()
215                            && matches!(quot_type, Type::Quotation(_))
216                        {
217                            *self.expected_quotation_type.borrow_mut() = Some(quot_type);
218                        }
219                    }
220                }
221                Some(saved)
222            } else {
223                None
224            };
225
226            // Capture statement type info for codegen optimization (Issue #186)
227            // Record the top-of-stack type BEFORE this statement for operations like dup
228            // Only capture for top-level word bodies, not nested branches/loops
229            if capture_stmt_types && let Some((word_name, _)) = self.current_word.borrow().as_ref()
230            {
231                self.capture_statement_type(word_name, i, &current_stack);
232            }
233
234            let (new_stack, subst, effects) = self.infer_statement(stmt, current_stack)?;
235            current_stack = new_stack;
236            accumulated_subst = accumulated_subst.compose(&subst);
237
238            // Accumulate side effects from this statement
239            for effect in effects {
240                if !accumulated_effects.contains(&effect) {
241                    accumulated_effects.push(effect);
242                }
243            }
244
245            // Restore expected type after checking quotation
246            if let Some(saved) = saved_expected_type {
247                *self.expected_quotation_type.borrow_mut() = saved;
248            }
249        }
250
251        Ok((current_stack, accumulated_subst, accumulated_effects))
252    }
253
254    /// Handle `n pick` where n is a literal integer
255    ///
256    /// pick(n) copies the value at position n to the top of the stack.
257    /// Position 0 is the top, 1 is below top, etc.
258    ///
259    /// Example: `2 pick` on stack ( A B C ) produces ( A B C A )
260    /// - Position 0: C (top)
261    /// - Position 1: B
262    /// - Position 2: A
263    /// - Result: copy A to top
264    pub(super) fn infer_statements(&self, statements: &[Statement]) -> Result<Effect, String> {
265        let start = StackType::RowVar("input".to_string());
266        // Don't capture statement types for quotation bodies - only top-level word bodies
267        let (result, subst, effects) = self.infer_statements_from(statements, &start, false)?;
268
269        // Apply the accumulated substitution to both start and result
270        // This ensures row variables are consistently named
271        let normalized_start = subst.apply_stack(&start);
272        let normalized_result = subst.apply_stack(&result);
273
274        Ok(Effect::with_effects(
275            normalized_start,
276            normalized_result,
277            effects,
278        ))
279    }
280
281    /// Infer the stack effect of a match expression
282    pub(super) fn infer_statement(
283        &self,
284        statement: &Statement,
285        current_stack: StackType,
286    ) -> Result<(StackType, Subst, Vec<SideEffect>), String> {
287        match statement {
288            Statement::IntLiteral(_) => Ok((current_stack.push(Type::Int), Subst::empty(), vec![])),
289            Statement::BoolLiteral(_) => {
290                Ok((current_stack.push(Type::Bool), Subst::empty(), vec![]))
291            }
292            Statement::StringLiteral(_) => {
293                Ok((current_stack.push(Type::String), Subst::empty(), vec![]))
294            }
295            Statement::FloatLiteral(_) => {
296                Ok((current_stack.push(Type::Float), Subst::empty(), vec![]))
297            }
298            Statement::Symbol(_) => Ok((current_stack.push(Type::Symbol), Subst::empty(), vec![])),
299            Statement::Match { arms, span } => self.infer_match(arms, span, current_stack),
300            Statement::WordCall { name, span } => self.infer_word_call(name, span, current_stack),
301            Statement::If {
302                then_branch,
303                else_branch,
304                span,
305            } => self.infer_if(then_branch, else_branch, span, current_stack),
306            Statement::Quotation { id, body, .. } => self.infer_quotation(*id, body, current_stack),
307        }
308    }
309
310    /// Look up the effect of a word (built-in or user-defined)
311    pub(super) fn effect_matches_any(
312        &self,
313        inferred: &SideEffect,
314        declared: &[SideEffect],
315    ) -> bool {
316        declared.iter().any(|decl| match (inferred, decl) {
317            (SideEffect::Yield(_), SideEffect::Yield(_)) => true,
318        })
319    }
320}