diff --git a/src/ast/compiler/type_checker/inference.rs b/src/ast/compiler/type_checker/inference.rs index eef343f..5fdbc5c 100644 --- a/src/ast/compiler/type_checker/inference.rs +++ b/src/ast/compiler/type_checker/inference.rs @@ -34,6 +34,11 @@ impl TypeChecker { match ty { StaticType::TypeVar(n) => { if let Some(resolved) = subst.get(&n) { + // Guard against self-referential substitutions (TypeVar(n) → TypeVar(n)) + // which would cause infinite recursion. + if *resolved == StaticType::TypeVar(n) { + return StaticType::TypeVar(n); + } // Follow the chain (handles transitive substitutions) Self::apply_subst(resolved.clone(), subst) } else { diff --git a/src/ast/compiler/type_checker/mod.rs b/src/ast/compiler/type_checker/mod.rs index a29536b..f0ef44b 100644 --- a/src/ast/compiler/type_checker/mod.rs +++ b/src/ast/compiler/type_checker/mod.rs @@ -18,8 +18,10 @@ use context::TypeContext; pub struct TypeChecker { pub(crate) root_types: Rc>>, - /// Monotonic counter for generating unique type variable IDs. - pub(crate) var_counter: Cell, + /// Shared monotonic counter for generating unique type variable IDs. + /// Points to the `Environment`'s counter so that IDs never collide + /// with TypeVars stored in `root_types` from earlier compilation passes. + pub(crate) var_counter: Rc>, /// Global substitution map: TypeVar ID → resolved StaticType. /// Shared across all scopes within a single type-checking pass. pub(crate) subst: std::cell::RefCell>, @@ -39,11 +41,12 @@ pub struct TypeChecker { impl TypeChecker { pub fn new( root_types: Rc>>, + var_counter: Rc>, compiler_hooks: Rc>>, ) -> Self { Self { root_types, - var_counter: Cell::new(0), + var_counter, subst: std::cell::RefCell::new(HashMap::new()), index_constraints: std::cell::RefCell::new(HashMap::new()), compiler_hooks, diff --git a/src/ast/environment.rs b/src/ast/environment.rs index 90f285a..912b375 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -5,7 +5,7 @@ use crate::ast::compiler::{CapturePass, TypeChecker, TypedNode}; use crate::ast::nodes::{AnalyzedPhase, BoundPhase, Symbol, SyntaxKind, SyntaxNode}; use crate::ast::parser::Parser; use crate::ast::vm::{GlobalStore, TracingObserver, VM}; -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::rc::Rc; @@ -71,6 +71,11 @@ pub struct Environment { pub function_registry: Rc>, pub typed_function_registry: Rc>, pub monomorph_cache: Rc>, + /// Shared monotonic counter for generating unique HM type-variable IDs. + /// Survives across multiple `compile()` / `run_script()` calls so that + /// TypeVar IDs created in one pass never collide with IDs stored in + /// `root_types` from earlier passes (prevents `Forall` self-substitution loops). + pub(crate) typevar_counter: Rc>, pub debug_mode: bool, pub optimization: bool, pub macro_registry: Rc>, @@ -106,6 +111,7 @@ struct RuntimeMacroEvaluator { root_scopes: Rc>>, root_slot_count: Rc>, root_types: Rc>>, + typevar_counter: Rc>, globals: GlobalStore, root_purity: Rc>>, compiler_hooks: Rc>>, @@ -129,7 +135,7 @@ impl MacroEvaluator for RuntimeMacroEvaluator { let (bound_ast, captures, _, _) = Binder::bind_root(initial_scopes, initial_slot_count, node, &mut diag)?; let bound_ast = CapturePass::apply(bound_ast, &captures); - let checker = TypeChecker::new(self.root_types.clone(), Rc::clone(&self.compiler_hooks)); + let checker = TypeChecker::new(self.root_types.clone(), Rc::clone(&self.typevar_counter), Rc::clone(&self.compiler_hooks)); let typed_ast = checker.check(&bound_ast, &[], &mut diag); if diag.has_errors() { @@ -162,6 +168,7 @@ impl Environment { function_registry: Rc::new(RefCell::new(HashMap::new())), typed_function_registry: Rc::new(RefCell::new(HashMap::new())), monomorph_cache: Rc::new(RefCell::new(HashMap::new())), + typevar_counter: Rc::new(Cell::new(0)), debug_mode: false, optimization: true, macro_registry: Rc::new(RefCell::new(MacroRegistry::new())), @@ -254,6 +261,7 @@ impl Environment { function_registry: Rc::new(RefCell::new(HashMap::new())), typed_function_registry: Rc::new(RefCell::new(HashMap::new())), monomorph_cache: Rc::new(RefCell::new(HashMap::new())), + typevar_counter: Rc::new(Cell::new(0)), debug_mode: false, optimization: true, macro_registry: Rc::new(RefCell::new(MacroRegistry::new())), @@ -321,6 +329,7 @@ impl Environment { root_scopes: self.root_scopes.clone(), root_slot_count: self.root_slot_count.clone(), root_types: self.root_types.clone(), + typevar_counter: Rc::clone(&self.typevar_counter), globals: self.global_store(), root_purity: self.root_purity.clone(), compiler_hooks: Rc::clone(&self.rtl.compiler_hooks), @@ -447,7 +456,7 @@ impl Environment { /// Type-checks a bound AST and collects doc comments. fn type_check(&self, bound_ast: &Node, diagnostics: &mut Diagnostics) -> TypedNode { - let checker = TypeChecker::new(self.root_types.clone(), Rc::clone(&self.rtl.compiler_hooks)); + let checker = TypeChecker::new(self.root_types.clone(), Rc::clone(&self.typevar_counter), Rc::clone(&self.rtl.compiler_hooks)); let typed = checker.check(bound_ast, &[], diagnostics); self.collect_doc_comments(&typed); typed @@ -766,13 +775,14 @@ impl Environment { let root_purity = self.root_purity.clone(); let optimization = self.optimization; let compiler_hooks = Rc::clone(&self.rtl.compiler_hooks); + let typevar_counter = Rc::clone(&self.typevar_counter); let compiler = Rc::new( move |func_template: Rc>, arg_types: &[StaticType]| -> Result<(Value, StaticType), String> { let mut diag = Diagnostics::new(); - let checker = TypeChecker::new(root_types.clone(), Rc::clone(&compiler_hooks)); + let checker = TypeChecker::new(root_types.clone(), Rc::clone(&typevar_counter), Rc::clone(&compiler_hooks)); // Monomorphization: re-type-check the function template with the concrete // call-site argument types, producing a specialized TypedNode. diff --git a/src/ast/rtl/prelude.myc b/src/ast/rtl/prelude.myc index c7ba7f4..99b08b2 100644 --- a/src/ast/rtl/prelude.myc +++ b/src/ast/rtl/prelude.myc @@ -1,8 +1,6 @@ ;; Myc System Library ;; Standard macros and core utilities. -(def func1 (fn [] (print "Hello World"))) - (macro while [cond body] `((fn [] (if ~cond (do ~body (again)) diff --git a/tests/repl.rs b/tests/repl.rs new file mode 100644 index 0000000..a31601d --- /dev/null +++ b/tests/repl.rs @@ -0,0 +1,57 @@ +use myc::ast::environment::Environment; +use myc::ast::types::Value; + +#[test] +fn repl_preserves_variable_bindings() { + let env = Environment::new(); + + let r1 = env.run_script("(def x 42)"); + assert!(r1.is_ok(), "def should succeed: {:?}", r1.err()); + + let r2 = env.run_script("(+ x 1)"); + assert_eq!(r2.unwrap(), Value::Int(43)); +} + +#[test] +fn repl_preserves_function_bindings() { + let env = Environment::new(); + + let r1 = env.run_script("(def double (fn [n] (* n 2)))"); + assert!(r1.is_ok(), "def fn should succeed: {:?}", r1.err()); + + let r2 = env.run_script("(double 21)"); + assert_eq!(r2.unwrap(), Value::Int(42)); +} + +#[test] +fn repl_function_uses_prior_variable() { + let env = Environment::new(); + + env.run_script("(def x 10)").unwrap(); + env.run_script("(def double (fn [n] (* n 2)))").unwrap(); + + let result = env.run_script("(double x)"); + assert_eq!(result.unwrap(), Value::Int(20)); +} + +#[test] +fn repl_reassign_variable() { + let env = Environment::new(); + + env.run_script("(def x 1)").unwrap(); + env.run_script("(assign x 99)").unwrap(); + + let result = env.run_script("x"); + assert_eq!(result.unwrap(), Value::Int(99)); +} + +#[test] +fn repl_macro_from_prior_call() { + let env = Environment::new(); + + env.run_script("(macro unless [c body] `(if ~c ... ~body))") + .unwrap(); + + let result = env.run_script(r#"(unless false "yes")"#); + assert_eq!(result.unwrap(), Value::Text("yes".into())); +}