Compare commits
2 Commits
bac63bab2a
...
73b5eee664
| Author | SHA1 | Date | |
|---|---|---|---|
| 73b5eee664 | |||
| 542255780d |
@@ -51,6 +51,9 @@ nix develop --command cargo test # All tests pass (currently 381)
|
|||||||
./target/release/lux lint # Standalone lint pass
|
./target/release/lux lint # Standalone lint pass
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Commit after every piece of work
|
||||||
|
**After completing each logical unit of work, commit immediately.** Do not let changes accumulate uncommitted across multiple features. Each commit should be a single logical change (one feature, one bugfix, etc.). Use `--no-gpg-sign` flag for all commits.
|
||||||
|
|
||||||
**IMPORTANT: Always verify Lux code you write:**
|
**IMPORTANT: Always verify Lux code you write:**
|
||||||
- Run with interpreter: `./target/release/lux file.lux`
|
- Run with interpreter: `./target/release/lux file.lux`
|
||||||
- Compile to binary: `./target/release/lux compile file.lux`
|
- Compile to binary: `./target/release/lux compile file.lux`
|
||||||
|
|||||||
@@ -499,6 +499,12 @@ pub enum Expr {
|
|||||||
field: Ident,
|
field: Ident,
|
||||||
span: Span,
|
span: Span,
|
||||||
},
|
},
|
||||||
|
/// Tuple index access: tuple.0, tuple.1
|
||||||
|
TupleIndex {
|
||||||
|
object: Box<Expr>,
|
||||||
|
index: usize,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
/// Lambda: fn(x, y) => x + y or fn(x: Int): Int => x + 1
|
/// Lambda: fn(x, y) => x + y or fn(x: Int): Int => x + 1
|
||||||
Lambda {
|
Lambda {
|
||||||
params: Vec<Parameter>,
|
params: Vec<Parameter>,
|
||||||
@@ -563,6 +569,7 @@ impl Expr {
|
|||||||
Expr::Call { span, .. } => *span,
|
Expr::Call { span, .. } => *span,
|
||||||
Expr::EffectOp { span, .. } => *span,
|
Expr::EffectOp { span, .. } => *span,
|
||||||
Expr::Field { span, .. } => *span,
|
Expr::Field { span, .. } => *span,
|
||||||
|
Expr::TupleIndex { span, .. } => *span,
|
||||||
Expr::Lambda { span, .. } => *span,
|
Expr::Lambda { span, .. } => *span,
|
||||||
Expr::Let { span, .. } => *span,
|
Expr::Let { span, .. } => *span,
|
||||||
Expr::If { span, .. } => *span,
|
Expr::If { span, .. } => *span,
|
||||||
|
|||||||
@@ -3237,6 +3237,11 @@ impl CBackend {
|
|||||||
Ok(format!("{}.{}", obj, field.name))
|
Ok(format!("{}.{}", obj, field.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Expr::TupleIndex { object, index, .. } => {
|
||||||
|
let obj = self.emit_expr(object)?;
|
||||||
|
Ok(format!("{}.__{}", obj, index))
|
||||||
|
}
|
||||||
|
|
||||||
Expr::Match { scrutinee, arms, .. } => {
|
Expr::Match { scrutinee, arms, .. } => {
|
||||||
self.emit_match(scrutinee, arms)
|
self.emit_match(scrutinee, arms)
|
||||||
}
|
}
|
||||||
@@ -4518,7 +4523,7 @@ impl CBackend {
|
|||||||
|| self.expr_uses_rc_vars_from_scope(right)
|
|| self.expr_uses_rc_vars_from_scope(right)
|
||||||
}
|
}
|
||||||
Expr::UnaryOp { operand, .. } => self.expr_uses_rc_vars_from_scope(operand),
|
Expr::UnaryOp { operand, .. } => self.expr_uses_rc_vars_from_scope(operand),
|
||||||
Expr::Field { object, .. } => self.expr_uses_rc_vars_from_scope(object),
|
Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => self.expr_uses_rc_vars_from_scope(object),
|
||||||
Expr::EffectOp { args, .. } => {
|
Expr::EffectOp { args, .. } => {
|
||||||
args.iter().any(|a| self.expr_uses_rc_vars_from_scope(a))
|
args.iter().any(|a| self.expr_uses_rc_vars_from_scope(a))
|
||||||
}
|
}
|
||||||
@@ -4707,7 +4712,7 @@ impl CBackend {
|
|||||||
self.collect_free_vars(val, bound, free);
|
self.collect_free_vars(val, bound, free);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::Field { object, .. } => {
|
Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => {
|
||||||
self.collect_free_vars(object, bound, free);
|
self.collect_free_vars(object, bound, free);
|
||||||
}
|
}
|
||||||
Expr::Match { scrutinee, arms, .. } => {
|
Expr::Match { scrutinee, arms, .. } => {
|
||||||
|
|||||||
@@ -1268,6 +1268,11 @@ impl JsBackend {
|
|||||||
Ok(format!("{}.{}", obj, field.name))
|
Ok(format!("{}.{}", obj, field.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Expr::TupleIndex { object, index, .. } => {
|
||||||
|
let obj = self.emit_expr(object)?;
|
||||||
|
Ok(format!("{}[{}]", obj, index))
|
||||||
|
}
|
||||||
|
|
||||||
Expr::Run {
|
Expr::Run {
|
||||||
expr, handlers, ..
|
expr, handlers, ..
|
||||||
} => {
|
} => {
|
||||||
|
|||||||
@@ -598,6 +598,9 @@ impl Formatter {
|
|||||||
Expr::Field { object, field, .. } => {
|
Expr::Field { object, field, .. } => {
|
||||||
format!("{}.{}", self.format_expr(object), field.name)
|
format!("{}.{}", self.format_expr(object), field.name)
|
||||||
}
|
}
|
||||||
|
Expr::TupleIndex { object, index, .. } => {
|
||||||
|
format!("{}.{}", self.format_expr(object), index)
|
||||||
|
}
|
||||||
Expr::If { condition, then_branch, else_branch, .. } => {
|
Expr::If { condition, then_branch, else_branch, .. } => {
|
||||||
format!(
|
format!(
|
||||||
"if {} then {} else {}",
|
"if {} then {} else {}",
|
||||||
|
|||||||
@@ -1415,6 +1415,34 @@ impl Interpreter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Expr::TupleIndex {
|
||||||
|
object,
|
||||||
|
index,
|
||||||
|
span,
|
||||||
|
} => {
|
||||||
|
let obj_val = self.eval_expr(object, env)?;
|
||||||
|
match obj_val {
|
||||||
|
Value::Tuple(elements) => {
|
||||||
|
if *index < elements.len() {
|
||||||
|
Ok(EvalResult::Value(elements[*index].clone()))
|
||||||
|
} else {
|
||||||
|
Err(RuntimeError {
|
||||||
|
message: format!(
|
||||||
|
"Tuple index {} out of bounds for tuple with {} elements",
|
||||||
|
index,
|
||||||
|
elements.len()
|
||||||
|
),
|
||||||
|
span: Some(*span),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(RuntimeError {
|
||||||
|
message: format!("Cannot use tuple index on {}", obj_val.type_name()),
|
||||||
|
span: Some(*span),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Expr::Lambda { params, body, .. } => {
|
Expr::Lambda { params, body, .. } => {
|
||||||
let closure = Closure {
|
let closure = Closure {
|
||||||
params: params.iter().map(|p| p.name.name.clone()).collect(),
|
params: params.iter().map(|p| p.name.name.clone()).collect(),
|
||||||
|
|||||||
@@ -510,7 +510,7 @@ impl Linter {
|
|||||||
self.collect_refs_expr(&arm.body);
|
self.collect_refs_expr(&arm.body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::Field { object, .. } => {
|
Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => {
|
||||||
self.collect_refs_expr(object);
|
self.collect_refs_expr(object);
|
||||||
}
|
}
|
||||||
Expr::Record { fields, .. } => {
|
Expr::Record { fields, .. } => {
|
||||||
|
|||||||
@@ -1576,7 +1576,7 @@ fn collect_call_site_hints(
|
|||||||
collect_call_site_hints(source, e, param_names, hints);
|
collect_call_site_hints(source, e, param_names, hints);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::Field { object, .. } => {
|
Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => {
|
||||||
collect_call_site_hints(source, object, param_names, hints);
|
collect_call_site_hints(source, object, param_names, hints);
|
||||||
}
|
}
|
||||||
Expr::Run { expr, handlers, .. } => {
|
Expr::Run { expr, handlers, .. } => {
|
||||||
|
|||||||
65
src/main.rs
65
src/main.rs
@@ -4831,6 +4831,71 @@ c")"#;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ Multi-line Arguments Tests ============
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiline_function_args() {
|
||||||
|
let source = r#"
|
||||||
|
fn add(a: Int, b: Int): Int = a + b
|
||||||
|
let result = add(
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
"#;
|
||||||
|
assert_eq!(eval(source).unwrap(), "3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiline_function_args_with_lambda() {
|
||||||
|
let source = r#"
|
||||||
|
let xs = List.map(
|
||||||
|
[1, 2, 3],
|
||||||
|
fn(x) => x * 2
|
||||||
|
)
|
||||||
|
"#;
|
||||||
|
assert_eq!(eval(source).unwrap(), "[2, 4, 6]");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ Tuple Index Tests ============
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tuple_index_access() {
|
||||||
|
let source = r#"
|
||||||
|
let pair = (42, "hello")
|
||||||
|
let first = pair.0
|
||||||
|
"#;
|
||||||
|
assert_eq!(eval(source).unwrap(), "42");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tuple_index_access_second() {
|
||||||
|
let source = r#"
|
||||||
|
let pair = (42, "hello")
|
||||||
|
let second = pair.1
|
||||||
|
"#;
|
||||||
|
assert_eq!(eval(source).unwrap(), "\"hello\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tuple_index_triple() {
|
||||||
|
let source = r#"
|
||||||
|
let triple = (1, 2, 3)
|
||||||
|
let sum = triple.0 + triple.1 + triple.2
|
||||||
|
"#;
|
||||||
|
assert_eq!(eval(source).unwrap(), "6");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tuple_index_in_function() {
|
||||||
|
let source = r#"
|
||||||
|
fn first(pair: (Int, String)): Int = pair.0
|
||||||
|
fn second(pair: (Int, String)): String = pair.1
|
||||||
|
let p = (42, "hello")
|
||||||
|
let result = first(p)
|
||||||
|
"#;
|
||||||
|
assert_eq!(eval(source).unwrap(), "42");
|
||||||
|
}
|
||||||
|
|
||||||
// Exhaustiveness checking tests
|
// Exhaustiveness checking tests
|
||||||
mod exhaustiveness_tests {
|
mod exhaustiveness_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -1646,6 +1646,20 @@ impl Parser {
|
|||||||
} else if self.check(TokenKind::Dot) {
|
} else if self.check(TokenKind::Dot) {
|
||||||
let start = expr.span();
|
let start = expr.span();
|
||||||
self.advance();
|
self.advance();
|
||||||
|
|
||||||
|
// Check for tuple index access: expr.0, expr.1, etc.
|
||||||
|
if let TokenKind::Int(n) = self.peek_kind() {
|
||||||
|
let index = n as usize;
|
||||||
|
self.advance();
|
||||||
|
let span = start.merge(self.previous_span());
|
||||||
|
expr = Expr::TupleIndex {
|
||||||
|
object: Box::new(expr),
|
||||||
|
index,
|
||||||
|
span,
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let field = self.parse_ident()?;
|
let field = self.parse_ident()?;
|
||||||
|
|
||||||
// Check if this is an effect operation: Effect.operation(args)
|
// Check if this is an effect operation: Effect.operation(args)
|
||||||
@@ -1681,11 +1695,14 @@ impl Parser {
|
|||||||
|
|
||||||
fn parse_args(&mut self) -> Result<Vec<Expr>, ParseError> {
|
fn parse_args(&mut self) -> Result<Vec<Expr>, ParseError> {
|
||||||
let mut args = Vec::new();
|
let mut args = Vec::new();
|
||||||
|
self.skip_newlines();
|
||||||
|
|
||||||
while !self.check(TokenKind::RParen) {
|
while !self.check(TokenKind::RParen) {
|
||||||
args.push(self.parse_expr()?);
|
args.push(self.parse_expr()?);
|
||||||
|
self.skip_newlines();
|
||||||
if !self.check(TokenKind::RParen) {
|
if !self.check(TokenKind::RParen) {
|
||||||
self.expect(TokenKind::Comma)?;
|
self.expect(TokenKind::Comma)?;
|
||||||
|
self.skip_newlines();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -484,7 +484,7 @@ impl SymbolTable {
|
|||||||
self.visit_expr(arg, scope_idx);
|
self.visit_expr(arg, scope_idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::Field { object, .. } => {
|
Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => {
|
||||||
self.visit_expr(object, scope_idx);
|
self.visit_expr(object, scope_idx);
|
||||||
}
|
}
|
||||||
Expr::If { condition, then_branch, else_branch, .. } => {
|
Expr::If { condition, then_branch, else_branch, .. } => {
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ fn references_params(expr: &Expr, params: &[&str]) -> bool {
|
|||||||
Statement::Expr(e) => references_params(e, params),
|
Statement::Expr(e) => references_params(e, params),
|
||||||
}) || references_params(result, params)
|
}) || references_params(result, params)
|
||||||
}
|
}
|
||||||
Expr::Field { object, .. } => references_params(object, params),
|
Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => references_params(object, params),
|
||||||
Expr::Lambda { body, .. } => references_params(body, params),
|
Expr::Lambda { body, .. } => references_params(body, params),
|
||||||
Expr::Tuple { elements, .. } => elements.iter().any(|e| references_params(e, params)),
|
Expr::Tuple { elements, .. } => elements.iter().any(|e| references_params(e, params)),
|
||||||
Expr::List { elements, .. } => elements.iter().any(|e| references_params(e, params)),
|
Expr::List { elements, .. } => elements.iter().any(|e| references_params(e, params)),
|
||||||
@@ -519,7 +519,7 @@ fn has_recursive_calls(func_name: &str, body: &Expr) -> bool {
|
|||||||
Expr::Record { fields, .. } => {
|
Expr::Record { fields, .. } => {
|
||||||
fields.iter().any(|(_, e)| has_recursive_calls(func_name, e))
|
fields.iter().any(|(_, e)| has_recursive_calls(func_name, e))
|
||||||
}
|
}
|
||||||
Expr::Field { object, .. } => has_recursive_calls(func_name, object),
|
Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => has_recursive_calls(func_name, object),
|
||||||
Expr::Let { value, body, .. } => {
|
Expr::Let { value, body, .. } => {
|
||||||
has_recursive_calls(func_name, value) || has_recursive_calls(func_name, body)
|
has_recursive_calls(func_name, value) || has_recursive_calls(func_name, body)
|
||||||
}
|
}
|
||||||
@@ -1673,6 +1673,42 @@ impl TypeChecker {
|
|||||||
span,
|
span,
|
||||||
} => self.infer_field(object, field, *span),
|
} => self.infer_field(object, field, *span),
|
||||||
|
|
||||||
|
Expr::TupleIndex {
|
||||||
|
object,
|
||||||
|
index,
|
||||||
|
span,
|
||||||
|
} => {
|
||||||
|
let object_type = self.infer_expr(object);
|
||||||
|
match &object_type {
|
||||||
|
Type::Tuple(types) => {
|
||||||
|
if *index < types.len() {
|
||||||
|
types[*index].clone()
|
||||||
|
} else {
|
||||||
|
self.errors.push(TypeError {
|
||||||
|
message: format!(
|
||||||
|
"Tuple index {} out of bounds for tuple with {} elements",
|
||||||
|
index,
|
||||||
|
types.len()
|
||||||
|
),
|
||||||
|
span: *span,
|
||||||
|
});
|
||||||
|
Type::Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Type::Var(_) => Type::var(),
|
||||||
|
_ => {
|
||||||
|
self.errors.push(TypeError {
|
||||||
|
message: format!(
|
||||||
|
"Cannot use tuple index on non-tuple type {}",
|
||||||
|
object_type
|
||||||
|
),
|
||||||
|
span: *span,
|
||||||
|
});
|
||||||
|
Type::Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Expr::Lambda {
|
Expr::Lambda {
|
||||||
params,
|
params,
|
||||||
return_type,
|
return_type,
|
||||||
|
|||||||
@@ -2040,7 +2040,9 @@ pub fn unify(t1: &Type, t2: &Type) -> Result<Substitution, String> {
|
|||||||
// Function's required effects (e1) must be a subset of available effects (e2)
|
// Function's required effects (e1) must be a subset of available effects (e2)
|
||||||
// A pure function (empty effects) can be called anywhere
|
// A pure function (empty effects) can be called anywhere
|
||||||
// A function requiring {Logger} can be called in context with {Logger} or {Logger, Console}
|
// A function requiring {Logger} can be called in context with {Logger} or {Logger, Console}
|
||||||
if !e1.is_subset(&e2) {
|
// When expected effects (e2) are empty, it means "no constraint" (e.g., callback parameter)
|
||||||
|
// so we allow any actual effects through
|
||||||
|
if !e2.is_empty() && !e1.is_subset(&e2) {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Effect mismatch: expected {{{}}}, got {{{}}}",
|
"Effect mismatch: expected {{{}}}, got {{{}}}",
|
||||||
e1, e2
|
e1, e2
|
||||||
|
|||||||
Reference in New Issue
Block a user