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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/lexer.rs
10
src/lexer.rs
@@ -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(),
|
||||||
|
|||||||
41
src/main.rs
41
src/main.rs
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user