fix: resolve all stress test bugs

- Record equality: add Record case to values_equal in interpreter
- Invalid escapes: error on unknown escape sequences in lexer
- Unknown effects: validate effect names in check_function with suggestions
- Circular types: add DFS cycle detection in check_type_cycles
- Parser: require | for enum variants, enabling proper type alias syntax

All 265 tests pass.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 02:45:52 -05:00
parent 07a35f1829
commit c81349d82c
6 changed files with 224 additions and 27 deletions

View File

@@ -2,48 +2,43 @@
This document summarizes issues found during systematic stress testing of the Lux parser, typechecker, and REPL. This document summarizes issues found during systematic stress testing of the Lux parser, typechecker, and REPL.
## Critical Bugs ## Critical Bugs (All Fixed)
### 1. Record Equality Returns False for Equal Records ### 1. ~~Record Equality Returns False for Equal Records~~ (FIXED)
```lux ```lux
let r1 = { x: 1, y: 2 } let r1 = { x: 1, y: 2 }
let r2 = { x: 1, y: 2 } let r2 = { x: 1, y: 2 }
r1 == r2 // Returns false! Should be true r1 == r2 // Now returns true
``` ```
**Impact**: High - breaks expected semantics for record comparison **Fix**: Added Record case to `Interpreter::values_equal` in `src/interpreter.rs`
**Location**: Likely in interpreter's equality logic for `Value::Record`
### 2. Invalid Escape Sequences Silently Accepted ### 2. ~~Invalid Escape Sequences Silently Accepted~~ (FIXED)
```lux ```lux
"\z\q\w" // Returns "zqw" - backslashes silently dropped "\z" // Now produces error: "Invalid escape sequence: \z"
"\x41" // Returns "x41" - hex escapes not supported but no error
"\u0041" // Returns "u0041" - unicode escapes not supported but no error
``` ```
**Impact**: High - users expect invalid escapes to error **Fix**: Modified lexer in `src/lexer.rs` to error on unknown escape sequences
**Fix**: Parser should reject unknown escape sequences
### 3. Unknown Effects Silently Accepted in Declarations ### 3. ~~Unknown Effects Silently Accepted in Declarations~~ (FIXED)
```lux ```lux
fn test(): Unit with {CompletelyFakeEffect} = () // No error! fn test(): Unit with {CompletelyFakeEffect} = () // Now produces error with suggestions
``` ```
**Impact**: Medium - typos in effect names not caught until call site **Fix**: Added effect validation in `TypeChecker::check_function` in `src/typechecker.rs`
**Fix**: Validate effect names during function declaration checking
### 4. No Forward References for Mutual Recursion ### 4. ~~No Forward References for Mutual Recursion~~ (FIXED/DOCUMENTED)
```lux ```lux
fn isEven(n: Int): Bool = if n == 0 then true else isOdd(n - 1) // Error: isOdd undefined fn isEven(n: Int): Bool = if n == 0 then true else isOdd(n - 1)
fn isOdd(n: Int): Bool = if n == 0 then false else isEven(n - 1) fn isOdd(n: Int): Bool = if n == 0 then false else isEven(n - 1)
// Works in files (two-pass type checking), REPL limitation documented
``` ```
**Impact**: Medium - common pattern not supported **Note**: Works when loaded from files due to two-pass type checking. REPL processes line-by-line, so forward references don't work there.
**Fix**: Two-pass type checking or explicit forward declarations
### 5. Circular Type Definitions Silently Fail ### 5. ~~Circular Type Definitions Silently Fail~~ (FIXED)
```lux ```lux
type A = B type A = B
type B = A type B = A
// No output, no error // Now produces error: "Circular type definition detected: A -> B -> A"
``` ```
**Impact**: Medium - should produce clear error about circular definition **Fix**: Added `check_type_cycles` method with DFS cycle detection in `src/typechecker.rs`. Also fixed parser to require `|` for enum variants, allowing `type A = B` to be parsed as a type alias.
## Parser Issues ## Parser Issues

View File

@@ -2810,6 +2810,11 @@ impl Interpreter {
(Value::Tuple(a), Value::Tuple(b)) => { (Value::Tuple(a), Value::Tuple(b)) => {
a.len() == b.len() && a.iter().zip(b).all(|(x, y)| self.values_equal(x, y)) a.len() == b.len() && a.iter().zip(b).all(|(x, y)| self.values_equal(x, y))
} }
(Value::Record(a), Value::Record(b)) => {
a.len() == b.len() && a.iter().all(|(k, v)| {
b.get(k).map(|bv| self.values_equal(v, bv)).unwrap_or(false)
})
}
( (
Value::Constructor { Value::Constructor {
name: n1, name: n1,
@@ -2824,6 +2829,7 @@ impl Interpreter {
&& f1.len() == f2.len() && f1.len() == f2.len()
&& f1.iter().zip(f2).all(|(x, y)| self.values_equal(x, y)) && f1.iter().zip(f2).all(|(x, y)| self.values_equal(x, y))
} }
(Value::Json(a), Value::Json(b)) => a == b,
_ => false, _ => false,
} }
} }

View File

@@ -484,13 +484,21 @@ impl<'a> Lexer<'a> {
current_literal.push('}'); current_literal.push('}');
} }
_ => { _ => {
let escape_start = self.pos;
let escaped = match self.advance() { let escaped = match self.advance() {
Some('n') => '\n', Some('n') => '\n',
Some('r') => '\r', Some('r') => '\r',
Some('t') => '\t', Some('t') => '\t',
Some('\\') => '\\', Some('\\') => '\\',
Some('"') => '"', Some('"') => '"',
Some(c) => c, Some('0') => '\0',
Some('\'') => '\'',
Some(c) => {
return Err(LexError {
message: format!("Invalid escape sequence: \\{}", c),
span: Span::new(escape_start - 1, self.pos),
});
}
None => { None => {
return Err(LexError { return Err(LexError {
message: "Unterminated string".into(), message: "Unterminated string".into(),

View File

@@ -1813,6 +1813,46 @@ c")"#;
assert_eq!(eval(r#"let x = typeOf("hello")"#).unwrap(), r#""String""#); assert_eq!(eval(r#"let x = typeOf("hello")"#).unwrap(), r#""String""#);
} }
// Bug fix tests
#[test]
fn test_record_equality() {
assert_eq!(eval("let x = { a: 1, b: 2 } == { a: 1, b: 2 }").unwrap(), "true");
assert_eq!(eval("let x = { a: 1 } == { a: 2 }").unwrap(), "false");
assert_eq!(eval("let x = { a: 1, b: 2 } == { a: 1, b: 3 }").unwrap(), "false");
}
#[test]
fn test_invalid_escape_sequence() {
let result = eval(r#"let x = "\z""#);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid escape sequence"));
}
#[test]
fn test_unknown_effect_error() {
let result = eval("fn test(): Unit with {FakeEffect} = ()");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Unknown effect"));
}
#[test]
fn test_circular_type_definitions() {
let result = eval("type A = B\ntype B = A");
assert!(result.is_err(), "Should detect circular type definitions");
let err = result.unwrap_err();
assert!(err.contains("Circular"), "Error should mention 'Circular': {}", err);
}
#[test]
fn test_mutual_recursion() {
let source = r#"
fn isEven(n: Int): Bool = if n == 0 then true else isOdd(n - 1)
fn isOdd(n: Int): Bool = if n == 0 then false else isEven(n - 1)
let result = isEven(10)
"#;
assert_eq!(eval(source).unwrap(), "true");
}
// Pipe with stdlib tests // Pipe with stdlib tests
#[test] #[test]
fn test_pipe_with_list() { fn test_pipe_with_list() {
@@ -2948,7 +2988,6 @@ c")"#;
mod example_tests { mod example_tests {
use super::*; use super::*;
use std::fs; use std::fs;
use std::path::Path;
fn check_file(path: &str) -> Result<(), String> { fn check_file(path: &str) -> Result<(), String> {
let source = fs::read_to_string(path) let source = fs::read_to_string(path)

View File

@@ -478,11 +478,11 @@ impl Parser {
self.advance(); self.advance();
self.skip_newlines(); self.skip_newlines();
if self.check(TokenKind::Pipe) || self.peek_is_variant() { if self.check(TokenKind::Pipe) {
// Enum type // Enum type - requires leading | for variants
(TypeDef::Enum(self.parse_variants()?), Vec::new()) (TypeDef::Enum(self.parse_variants()?), Vec::new())
} else { } else {
// Type alias // Type alias - any type expression
(TypeDef::Alias(self.parse_type()?), Vec::new()) (TypeDef::Alias(self.parse_type()?), Vec::new())
} }
} else { } else {

View File

@@ -532,6 +532,9 @@ impl TypeChecker {
self.collect_declaration(decl); self.collect_declaration(decl);
} }
// Check for circular type definitions
self.check_type_cycles(program);
// Second pass: type check all declarations // Second pass: type check all declarations
for decl in &program.declarations { for decl in &program.declarations {
self.check_declaration(decl); self.check_declaration(decl);
@@ -544,6 +547,130 @@ impl TypeChecker {
} }
} }
/// Check for circular type alias definitions
fn check_type_cycles(&mut self, program: &Program) {
use std::collections::HashSet;
// Build a map of type alias dependencies
let mut alias_deps: HashMap<String, Vec<String>> = HashMap::new();
for decl in &program.declarations {
if let Declaration::Type(type_decl) = decl {
if let ast::TypeDef::Alias(type_expr) = &type_decl.definition {
let deps = self.collect_type_references(type_expr);
alias_deps.insert(type_decl.name.name.clone(), deps);
}
}
}
// Check for cycles using DFS
fn has_cycle(
name: &str,
alias_deps: &HashMap<String, Vec<String>>,
visiting: &mut HashSet<String>,
visited: &mut HashSet<String>,
cycle_path: &mut Vec<String>,
) -> bool {
if visiting.contains(name) {
cycle_path.push(name.to_string());
return true;
}
if visited.contains(name) {
return false;
}
visiting.insert(name.to_string());
cycle_path.push(name.to_string());
if let Some(deps) = alias_deps.get(name) {
for dep in deps {
if alias_deps.contains_key(dep) {
if has_cycle(dep, alias_deps, visiting, visited, cycle_path) {
return true;
}
}
}
}
visiting.remove(name);
cycle_path.pop();
visited.insert(name.to_string());
false
}
let mut visited = HashSet::new();
for type_name in alias_deps.keys() {
let mut visiting = HashSet::new();
let mut cycle_path = Vec::new();
if has_cycle(type_name, &alias_deps, &mut visiting, &mut visited, &mut cycle_path) {
// Find the span for this type declaration
let span = program.declarations.iter().find_map(|d| {
if let Declaration::Type(t) = d {
if t.name.name == *type_name {
return Some(t.span);
}
}
None
}).unwrap_or(Span::default());
self.errors.push(TypeError {
message: format!(
"Circular type definition detected: {}",
cycle_path.join(" -> ")
),
span,
});
}
}
}
/// Collect all type names referenced in a type expression
fn collect_type_references(&self, type_expr: &TypeExpr) -> Vec<String> {
let mut refs = Vec::new();
self.collect_type_refs_helper(type_expr, &mut refs);
refs
}
fn collect_type_refs_helper(&self, type_expr: &TypeExpr, refs: &mut Vec<String>) {
match type_expr {
TypeExpr::Named(ident) => {
// Only include user-defined types, not builtins
match ident.name.as_str() {
"Int" | "Float" | "Bool" | "String" | "Char" | "Unit" | "_" => {}
name => refs.push(name.to_string()),
}
}
TypeExpr::App(constructor, args) => {
self.collect_type_refs_helper(constructor, refs);
for arg in args {
self.collect_type_refs_helper(arg, refs);
}
}
TypeExpr::Function { params, return_type, .. } => {
for p in params {
self.collect_type_refs_helper(p, refs);
}
self.collect_type_refs_helper(return_type, refs);
}
TypeExpr::Tuple(elements) => {
for e in elements {
self.collect_type_refs_helper(e, refs);
}
}
TypeExpr::Record(fields) => {
for f in fields {
self.collect_type_refs_helper(&f.typ, refs);
}
}
TypeExpr::Unit => {}
TypeExpr::Versioned { base, .. } => {
self.collect_type_refs_helper(base, refs);
}
}
}
/// Type check a program with module support /// Type check a program with module support
pub fn check_program_with_modules( pub fn check_program_with_modules(
&mut self, &mut self,
@@ -790,6 +917,28 @@ impl TypeChecker {
} }
fn check_function(&mut self, func: &FunctionDecl) { fn check_function(&mut self, func: &FunctionDecl) {
// Validate that all declared effects exist
let builtin_effects = ["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http", "HttpServer", "Test"];
for effect in &func.effects {
let is_builtin = builtin_effects.contains(&effect.name.as_str());
let is_defined = self.env.lookup_effect(&effect.name).is_some();
if !is_builtin && !is_defined {
// Find similar effect names for suggestion
let mut available: Vec<&str> = builtin_effects.to_vec();
available.extend(self.env.effects.keys().map(|s| s.as_str()));
let suggestions = find_similar_names(&effect.name, available, 2);
let mut message = format!("Unknown effect: {}", effect.name);
if let Some(hint) = format_did_you_mean(&suggestions) {
message.push_str(&format!(". {}", hint));
}
self.errors.push(TypeError {
message,
span: effect.span,
});
}
}
// Set up the environment with parameters // Set up the environment with parameters
let mut local_env = self.env.clone(); let mut local_env = self.env.clone();
for param in &func.params { for param in &func.params {