Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions LANGUAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Quilon is a statically-typed, **symbol-based** language (no control-flow keyword
| `>>` | Export an item from a module | `>> add = (a, b) => a + b` |
| `\|>` | Pipe (first-arg injection) | `x \|> f(a)` ≡ `f(x, a)` |
| `for n <- xs => body` | Loop over a collection | `for n <- [1,2,3] => print(n)` |
| `<-` (infix) | Inclusive range → `[]Num` | `1 <- 4` ≡ `[1,2,3,4]` · `4 <- 1` ≡ `[4,3,2,1]` |
| `?` `\|` `_` | Pattern match | `v ? \| 0 => "zero" \| _ => "other"` |
| `/` | Division **or** sum-type variant separator | `a / b` · `Color = Red / Green` |
| `? :` | Ternary | `x < 0 ? -x : x` |
Expand Down Expand Up @@ -333,6 +334,28 @@ for (val, i) <- xs => print(i) ~ with index
```
The body may be a single expression or a `< >` block. (See `examples/for_loop.ql`.)

### Ranges — infix `lo <- hi`
The infix `<-` operator builds an **inclusive** `[]Num`:
```quilon
1 <- 4 ~ [1, 2, 3, 4]
4 <- 1 ~ [4, 3, 2, 1] (descends when the left end is larger)
5 <- 5 ~ [5] (single point)
```
It is pure **array sugar** — there is no distinct `Range` type; the result *is* a
`[]Num`, so it composes with `.size`, indexing `[i]`, and `for` loops:
```quilon
r = 2 <- 5 ~ [2, 3, 4, 5]
n = r.size ~ 4 (inclusive count = |hi - lo| + 1)
first = r[0] ~ 2
for x <- 1 <- 3 => print(x) ~ a range drives a loop like any array
```
Both ends are full `Num` expressions (they may be dynamic, not just literals); the
direction (ascending vs descending) is decided at runtime. (See `examples/ranges.ql`.)

> Note: the infix range `<-` is distinct from the `for` header's `<-`
> (`for n <- collection => …`). The `for` form is the loop binder; the infix form,
> *between two value expressions*, is the range constructor.

---

## Pattern matching
Expand Down Expand Up @@ -471,6 +494,7 @@ message instead. Any compile error exits with status 1.
| Functions, recursion, blocks, type inference | ✅ |
| Pipe `\|>` (first-arg injection) | ✅ |
| `for n <- collection => body` loops | ✅ |
| Ranges: infix `lo <- hi` → inclusive `[]Num` (descends when `lo > hi`) | ✅ |
| Pattern matching (numbers, wildcard, identifiers, sum-type variants) | ✅ |
| User-defined sum types (`/` separator), exhaustive matching, payload binding | ✅ |
| `Result` as a normal predefined sum type (`Ok`/`NotOk`) | ✅ |
Expand Down
25 changes: 25 additions & 0 deletions examples/ranges.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
~ Ranges: infix `<-` builds an inclusive `[]Num`. It is array sugar — the result
~ IS a `[]Num`, so it has `.size`, indexes with `[i]`, and iterates with `for`.
~ `1 <- 4` -> [1, 2, 3, 4] (inclusive endpoints)
~ `4 <- 1` -> [4, 3, 2, 1] (descends when the left end is larger)
<< core.io

^ = () -> Num => <
asc = 1 <- 4 ~ [1, 2, 3, 4]
desc = 4 <- 1 ~ [4, 3, 2, 1]

count = asc.size ~ 4 (inclusive count = |hi - lo| + 1)

~ Ascending: first endpoint is the small end, last is the large end.
lo = asc[0] ~ 1
hi = asc[3] ~ 4

~ Descending: the order is reversed — desc[0] is the larger end.
top = desc[0] ~ 4
bottom = desc[3] ~ 1

~ A range also drives a `for` loop, since it's just a `[]Num`.
for n <- asc => print(n) ~ prints 1, 2, 3, 4

count + lo + hi + top + bottom ~ 4 + 1 + 4 + 4 + 1 = exit 14
>
12 changes: 12 additions & 0 deletions src/ast/nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,17 @@ pub enum Expr {
body: Box<Expr>,
span: Span,
},

// Inclusive range `lo <- hi`: materialized `[]Num` sugar. `1 <- 4` is
// `[1, 2, 3, 4]`; when `lo > hi` it descends (`4 <- 1` is `[4, 3, 2, 1]`).
// There is no distinct Range type — the result IS a `[]Num`, so it composes
// with array ops / `.size` / indexing. (The infix `<-`; the `for` header's
// `<-` is parsed separately and never produces this node.)
Range {
start: Box<Expr>,
end: Box<Expr>,
span: Span,
},
}

impl Expr {
Expand All @@ -278,6 +289,7 @@ impl Expr {
Expr::Constructor { span, .. } => span,
Expr::SumConstructor { span, .. } => span,
Expr::ForLoop { span, .. } => span,
Expr::Range { span, .. } => span,
}
}

Expand Down
163 changes: 163 additions & 0 deletions src/codegen/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,8 @@ impl<'ctx> CodeGenerator<'ctx> {

Expr::Match { expr, arms, .. } => self.generate_match(expr, arms),

Expr::Range { start, end, .. } => self.generate_range(start, end),

Expr::ForLoop {
collection,
pattern,
Expand Down Expand Up @@ -1667,6 +1669,167 @@ impl<'ctx> CodeGenerator<'ctx> {
.map_err(|e| format!("Failed to load array struct: {:?}", e))
}

/// Materialize an inclusive range `lo <- hi` into a `[]Num` (the `{ptr, size}`
/// array shape, same as `generate_array`). The element count is `|hi - lo| + 1`
/// and the direction (ascending vs descending) is decided at runtime, since the
/// ends can be dynamic: `lo <= hi` counts up (`1 <- 4` → `[1,2,3,4]`), otherwise
/// down (`4 <- 1` → `[4,3,2,1]`). The backing storage is GC-allocated (`__alloc`)
/// so the array may safely escape the current frame.
fn generate_range(&mut self, start: &Expr, end: &Expr) -> Result<BasicValueEnum<'ctx>, String> {
let function = self
.current_function
.ok_or_else(|| "Range must be in a function".to_string())?;

let i64_type = self.context.i64_type();
let f64_type = self.context.f64_type();

// Evaluate both ends (Num = f64) and truncate to i64 endpoints.
let lo_f = self.generate_expr(start)?.into_float_value();
let hi_f = self.generate_expr(end)?.into_float_value();
let lo = self
.builder
.build_float_to_signed_int(lo_f, i64_type, "range_lo")
.map_err(|e| format!("Failed to convert range start: {:?}", e))?;
let hi = self
.builder
.build_float_to_signed_int(hi_f, i64_type, "range_hi")
.map_err(|e| format!("Failed to convert range end: {:?}", e))?;

// Ascending iff lo <= hi; pick step = +1 / -1 and the inclusive span.
let ascending = self
.builder
.build_int_compare(inkwell::IntPredicate::SLE, lo, hi, "range_asc")
.map_err(|e| format!("Failed to compare range ends: {:?}", e))?;
let one = i64_type.const_int(1, false);
let neg_one = i64_type.const_all_ones(); // -1 in two's complement
let step = self
.builder
.build_select(ascending, one, neg_one, "range_step")
.map_err(|e| format!("Failed to select range step: {:?}", e))?
.into_int_value();
// |hi - lo| + 1: compute the signed delta once, then pick it or its
// negation so the span is non-negative in either direction.
let delta = self
.builder
.build_int_sub(hi, lo, "range_delta")
.map_err(|e| format!("Failed to subtract range ends: {:?}", e))?;
let neg_delta = self
.builder
.build_int_neg(delta, "range_neg_delta")
.map_err(|e| format!("Failed to negate range delta: {:?}", e))?;
let span_abs = self
.builder
.build_select(ascending, delta, neg_delta, "range_span")
.map_err(|e| format!("Failed to select range span: {:?}", e))?
.into_int_value();
let count = self
.builder
.build_int_add(span_abs, one, "range_count")
.map_err(|e| format!("Failed to add range count: {:?}", e))?;

// GC-allocate count * sizeof(f64) bytes for the backing data.
let eight = i64_type.const_int(8, false);
let bytes = self
.builder
.build_int_mul(count, eight, "range_bytes")
.map_err(|e| format!("Failed to size range alloc: {:?}", e))?;
let alloc = self.get_intrinsic("__alloc")?;
let alloc_call = self
.builder
.build_call(alloc, &[bytes.into()], "range_data")
.map_err(|e| format!("Failed to allocate range: {:?}", e))?;
let data_ptr = {
use inkwell::values::AnyValue;
alloc_call.as_any_value_enum().into_pointer_value()
};

// Fill loop: for i in 0..count: data[i] = (f64)(lo + i*step).
let counter = self.create_entry_block_alloca("range_i", i64_type.into())?;
self.builder
.build_store(counter, i64_type.const_zero())
.map_err(|e| format!("Failed to init range counter: {:?}", e))?;

let header = self.context.append_basic_block(function, "range_header");
let body = self.context.append_basic_block(function, "range_body");
let exit = self.context.append_basic_block(function, "range_exit");

self.builder
.build_unconditional_branch(header)
.map_err(|e| format!("Failed to branch to range header: {:?}", e))?;

self.builder.position_at_end(header);
let i = self
.builder
.build_load(i64_type, counter, "i")
.map_err(|e| format!("Failed to load range counter: {:?}", e))?
.into_int_value();
let cond = self
.builder
.build_int_compare(inkwell::IntPredicate::SLT, i, count, "range_cond")
.map_err(|e| format!("Failed to build range condition: {:?}", e))?;
self.builder
.build_conditional_branch(cond, body, exit)
.map_err(|e| format!("Failed to build range branch: {:?}", e))?;

self.builder.position_at_end(body);
// value = lo + i*step
let i_step = self
.builder
.build_int_mul(i, step, "range_i_step")
.map_err(|e| format!("Failed to scale range index: {:?}", e))?;
let val_i = self
.builder
.build_int_add(lo, i_step, "range_val_i")
.map_err(|e| format!("Failed to compute range element: {:?}", e))?;
let val_f = self
.builder
.build_signed_int_to_float(val_i, f64_type, "range_val")
.map_err(|e| format!("Failed to convert range element: {:?}", e))?;
let elem_ptr = unsafe {
self.builder
.build_gep(f64_type, data_ptr, &[i], "range_elem")
.map_err(|e| format!("Failed to index range data: {:?}", e))?
};
self.builder
.build_store(elem_ptr, val_f)
.map_err(|e| format!("Failed to store range element: {:?}", e))?;
let next = self
.builder
.build_int_add(i, one, "range_next")
.map_err(|e| format!("Failed to increment range counter: {:?}", e))?;
self.builder
.build_store(counter, next)
.map_err(|e| format!("Failed to store range counter: {:?}", e))?;
self.builder
.build_unconditional_branch(header)
.map_err(|e| format!("Failed to loop range: {:?}", e))?;

// Build the { ptr, size } array struct (the shared array/Text shape).
self.builder.position_at_end(exit);
let array_struct_type = self.ptr_len_struct_type();
let array_struct = self
.builder
.build_alloca(array_struct_type, "range_array")
.map_err(|e| format!("Failed to allocate range struct: {:?}", e))?;
let ptr_field = self
.builder
.build_struct_gep(array_struct_type, array_struct, 0, "range_ptr_field")
.map_err(|e| format!("Failed to get range ptr field: {:?}", e))?;
self.builder
.build_store(ptr_field, data_ptr)
.map_err(|e| format!("Failed to store range ptr: {:?}", e))?;
let size_field = self
.builder
.build_struct_gep(array_struct_type, array_struct, 1, "range_size_field")
.map_err(|e| format!("Failed to get range size field: {:?}", e))?;
self.builder
.build_store(size_field, count)
.map_err(|e| format!("Failed to store range size: {:?}", e))?;
self.builder
.build_load(array_struct_type, array_struct, "range_array")
.map_err(|e| format!("Failed to load range struct: {:?}", e))
}

fn generate_record(
&mut self,
fields: &[(String, Expr)],
Expand Down
58 changes: 56 additions & 2 deletions src/parser/ast_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -871,10 +871,10 @@ impl<'a> Parser<'a> {
}

fn parse_comparison(&mut self) -> Result<Expr, ParseError> {
let mut left = self.parse_pipeline()?;
let mut left = self.parse_range()?;

while let Some(op) = self.match_comparison() {
let right = self.parse_pipeline()?;
let right = self.parse_range()?;
let span = Span::new(left.span().start, right.span().end);
left = Expr::BinOp {
left: Box::new(left),
Expand All @@ -887,6 +887,27 @@ impl<'a> Parser<'a> {
Ok(left)
}

/// Infix range `lo <- hi` → inclusive `[]Num` (see the `Expr::Range` node).
/// Non-associative: consumes at most one `<-`, so `a <- b <- c` is rejected.
/// Only general expression position reaches here; the `for` header consumes its
/// own `<-` in `parse_for_loop`, so `for n <- coll` never parses as a range.
fn parse_range(&mut self) -> Result<Expr, ParseError> {
let left = self.parse_pipeline()?;

if self.check(&TokenKind::LeftArrow) {
self.advance(); // consume `<-`
let right = self.parse_pipeline()?;
let span = Span::new(left.span().start, right.span().end);
return Ok(Expr::Range {
start: Box::new(left),
end: Box::new(right),
span,
});
}

Ok(left)
}

fn parse_pipeline(&mut self) -> Result<Expr, ParseError> {
let mut left = self.parse_additive()?;

Expand Down Expand Up @@ -1948,6 +1969,39 @@ mod tests {
}
}

#[test]
fn test_parse_infix_range() {
// `1 <- 4` in general expression position parses as an Expr::Range,
// NOT a for-loop (no `for` keyword precedes it).
let tokens = Lexer::tokenize("r = 1 <- 4").unwrap();
let program = parse(&tokens).expect("range should parse");
if let Item::VarDecl(v) = &program.items[0] {
assert!(
matches!(v.value, Expr::Range { .. }),
"expected Expr::Range, got {:?}",
v.value
);
} else {
panic!("expected a var decl");
}
}

#[test]
fn test_infix_range_does_not_capture_for_header() {
// CRITICAL coexistence: the `for` header's `<-` must still parse as a
// for-loop, never as an infix range. `for n <- [1,2,3]` is a ForLoop.
let tokens = Lexer::tokenize("test = => for n <- [1, 2, 3] => print(n)").unwrap();
let program = parse(&tokens).expect("for loop should still parse");
if let Item::FunctionDecl(func) = &program.items[0] {
assert!(
matches!(func.body, Expr::ForLoop { .. }),
"for header must parse as ForLoop, not Range"
);
} else {
panic!("expected a function decl");
}
}

#[test]
fn test_parse_nested_for_loops_with_blocks() {
let tokens =
Expand Down
9 changes: 9 additions & 0 deletions src/typechecker/checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1531,6 +1531,15 @@ impl TypeChecker {
}
}

Expr::Range { start, end, span } => {
// `lo <- hi` materializes an inclusive `[]Num`; both ends must be Num.
let start_type = self.infer_expr(start)?;
self.check_type_compatibility(&Type::Num, &start_type, span)?;
let end_type = self.infer_expr(end)?;
self.check_type_compatibility(&Type::Num, &end_type, span)?;
Ok(Type::Array(Box::new(Type::Num)))
}

Expr::ForLoop {
collection,
pattern,
Expand Down
1 change: 1 addition & 0 deletions tests/examples_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const EXPECTED_EXIT: &[(&str, i32)] = &[
("pattern_match.ql", 50),
("arrays.ql", 5),
("for_loop.ql", 0),
("ranges.ql", 14),
("pipeline.ql", 25),
("text.ql", 7),
("io.ql", 0),
Expand Down
Loading