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
|
||||
```
|
||||
|
||||
### 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:**
|
||||
- Run with interpreter: `./target/release/lux file.lux`
|
||||
- Compile to binary: `./target/release/lux compile file.lux`
|
||||
|
||||
@@ -499,6 +499,12 @@ pub enum Expr {
|
||||
field: Ident,
|
||||
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 {
|
||||
params: Vec<Parameter>,
|
||||
@@ -563,6 +569,7 @@ impl Expr {
|
||||
Expr::Call { span, .. } => *span,
|
||||
Expr::EffectOp { span, .. } => *span,
|
||||
Expr::Field { span, .. } => *span,
|
||||
Expr::TupleIndex { span, .. } => *span,
|
||||
Expr::Lambda { span, .. } => *span,
|
||||
Expr::Let { span, .. } => *span,
|
||||
Expr::If { span, .. } => *span,
|
||||
|
||||
@@ -3237,6 +3237,11 @@ impl CBackend {
|
||||
Ok(format!("{}.{}", obj, field.name))
|
||||
}
|
||||
|
||||
Expr::TupleIndex { object, index, .. } => {
|
||||
let obj = self.emit_expr(object)?;
|
||||
Ok(format!("{}.__{}", obj, index))
|
||||
}
|
||||
|
||||
Expr::Match { scrutinee, arms, .. } => {
|
||||
self.emit_match(scrutinee, arms)
|
||||
}
|
||||
@@ -4518,7 +4523,7 @@ impl CBackend {
|
||||
|| self.expr_uses_rc_vars_from_scope(right)
|
||||
}
|
||||
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, .. } => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Expr::Field { object, .. } => {
|
||||
Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => {
|
||||
self.collect_free_vars(object, bound, free);
|
||||
}
|
||||
Expr::Match { scrutinee, arms, .. } => {
|
||||
|
||||
@@ -1268,6 +1268,11 @@ impl JsBackend {
|
||||
Ok(format!("{}.{}", obj, field.name))
|
||||
}
|
||||
|
||||
Expr::TupleIndex { object, index, .. } => {
|
||||
let obj = self.emit_expr(object)?;
|
||||
Ok(format!("{}[{}]", obj, index))
|
||||
}
|
||||
|
||||
Expr::Run {
|
||||
expr, handlers, ..
|
||||
} => {
|
||||
|
||||
@@ -598,6 +598,9 @@ impl Formatter {
|
||||
Expr::Field { object, field, .. } => {
|
||||
format!("{}.{}", self.format_expr(object), field.name)
|
||||
}
|
||||
Expr::TupleIndex { object, index, .. } => {
|
||||
format!("{}.{}", self.format_expr(object), index)
|
||||
}
|
||||
Expr::If { condition, then_branch, else_branch, .. } => {
|
||||
format!(
|
||||
"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, .. } => {
|
||||
let closure = Closure {
|
||||
params: params.iter().map(|p| p.name.name.clone()).collect(),
|
||||
|
||||
@@ -510,7 +510,7 @@ impl Linter {
|
||||
self.collect_refs_expr(&arm.body);
|
||||
}
|
||||
}
|
||||
Expr::Field { object, .. } => {
|
||||
Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => {
|
||||
self.collect_refs_expr(object);
|
||||
}
|
||||
Expr::Record { fields, .. } => {
|
||||
|
||||
@@ -1576,7 +1576,7 @@ fn collect_call_site_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);
|
||||
}
|
||||
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
|
||||
mod exhaustiveness_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1646,6 +1646,20 @@ impl Parser {
|
||||
} else if self.check(TokenKind::Dot) {
|
||||
let start = expr.span();
|
||||
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()?;
|
||||
|
||||
// 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> {
|
||||
let mut args = Vec::new();
|
||||
self.skip_newlines();
|
||||
|
||||
while !self.check(TokenKind::RParen) {
|
||||
args.push(self.parse_expr()?);
|
||||
self.skip_newlines();
|
||||
if !self.check(TokenKind::RParen) {
|
||||
self.expect(TokenKind::Comma)?;
|
||||
self.skip_newlines();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -484,7 +484,7 @@ impl SymbolTable {
|
||||
self.visit_expr(arg, scope_idx);
|
||||
}
|
||||
}
|
||||
Expr::Field { object, .. } => {
|
||||
Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => {
|
||||
self.visit_expr(object, scope_idx);
|
||||
}
|
||||
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),
|
||||
}) || 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::Tuple { 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, .. } => {
|
||||
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, .. } => {
|
||||
has_recursive_calls(func_name, value) || has_recursive_calls(func_name, body)
|
||||
}
|
||||
@@ -1673,6 +1673,42 @@ impl TypeChecker {
|
||||
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 {
|
||||
params,
|
||||
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)
|
||||
// A pure function (empty effects) can be called anywhere
|
||||
// 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!(
|
||||
"Effect mismatch: expected {{{}}}, got {{{}}}",
|
||||
e1, e2
|
||||
|
||||
Reference in New Issue
Block a user