Compare commits
6 Commits
bc60f1c8f1
...
d26fd975d1
| Author | SHA1 | Date | |
|---|---|---|---|
| d26fd975d1 | |||
| 1fa599f856 | |||
| c2404a5ec1 | |||
| 19068ead96 | |||
| 44ea1eebb0 | |||
| 8c90d5a8dc |
111
CLAUDE.md
Normal file
111
CLAUDE.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Lux Project Notes
|
||||
|
||||
## Development Environment
|
||||
|
||||
This is a **Nix environment**. Tools like `cargo`, `rustc`, `clippy`, etc. are not available in the base shell.
|
||||
|
||||
To run Rust/Cargo commands, use one of:
|
||||
```bash
|
||||
nix develop --command cargo test
|
||||
nix develop --command cargo build
|
||||
nix develop --command cargo clippy
|
||||
nix develop --command cargo fmt
|
||||
```
|
||||
|
||||
Or enter the development shell first:
|
||||
```bash
|
||||
nix develop
|
||||
# then run commands normally
|
||||
cargo test
|
||||
```
|
||||
|
||||
The `lux` binary can be run directly if already built:
|
||||
```bash
|
||||
./target/debug/lux test
|
||||
./target/release/lux <file.lux>
|
||||
```
|
||||
|
||||
For additional tools not in the dev shell:
|
||||
```bash
|
||||
nix-shell -p <program>
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
When making changes:
|
||||
1. **Always run tests**: `cargo check && cargo test` - fix all errors and warnings
|
||||
2. **Lint the Lux code**: `./target/release/lux lint` - fix warnings
|
||||
3. **Check Lux code**: `./target/release/lux check` - type check + lint in one pass
|
||||
4. **Format Lux code**: `./target/release/lux fmt` - auto-format all .lux files
|
||||
5. **Write tests**: Add tests to cover new code
|
||||
6. **Document features**: Provide documentation and tutorials for new features/frameworks
|
||||
7. **Fix language limitations**: If you encounter parser/type system limitations, fix them (without regressions on guarantees or speed)
|
||||
8. **Git commits**: Always use `--no-gpg-sign` flag
|
||||
|
||||
### Post-work checklist (run after each major piece of work)
|
||||
```bash
|
||||
nix develop --command cargo check # No Rust errors
|
||||
nix develop --command cargo test # All tests pass (currently 381)
|
||||
./target/release/lux check # Type check + lint all .lux files
|
||||
./target/release/lux fmt # Format all .lux files
|
||||
./target/release/lux lint # Standalone lint pass
|
||||
```
|
||||
|
||||
**IMPORTANT: Always verify Lux code you write:**
|
||||
- Run with interpreter: `./target/release/lux file.lux`
|
||||
- Compile to binary: `./target/release/lux compile file.lux`
|
||||
- Both must work before claiming code is functional
|
||||
- The C backend has limited effect support (Console, File only - no HttpServer, Http, etc.)
|
||||
|
||||
## CLI Commands & Aliases
|
||||
|
||||
| Command | Alias | Description |
|
||||
|---------|-------|-------------|
|
||||
| `lux fmt` | `lux f` | Format .lux files |
|
||||
| `lux test` | `lux t` | Run test suite |
|
||||
| `lux check` | `lux k` | Type check + lint |
|
||||
| `lux lint` | `lux l` | Lint only (with `--explain` for detailed help) |
|
||||
| `lux serve` | `lux s` | Static file server |
|
||||
| `lux compile` | `lux c` | Compile to binary |
|
||||
|
||||
## Code Quality
|
||||
|
||||
- Fix all compiler warnings before committing
|
||||
- Ensure all tests pass (currently 381 tests)
|
||||
- Add new tests when adding features
|
||||
- Keep examples and documentation in sync
|
||||
|
||||
## Lux Language Notes
|
||||
|
||||
### Top-level expressions
|
||||
Bare `run` expressions are not allowed at top-level. You must wrap them in a `let` binding:
|
||||
```lux
|
||||
// WRONG: parse error
|
||||
run main() with {}
|
||||
|
||||
// CORRECT
|
||||
let output = run main() with {}
|
||||
```
|
||||
|
||||
### String methods
|
||||
Lux uses module-qualified function calls, not method syntax on primitives:
|
||||
```lux
|
||||
// WRONG: not valid syntax
|
||||
path.endsWith(".html")
|
||||
|
||||
// CORRECT
|
||||
String.endsWith(path, ".html")
|
||||
```
|
||||
|
||||
### Available String functions
|
||||
Key string functions (all in `String.` namespace):
|
||||
- `String.length(s)` - get length
|
||||
- `String.startsWith(s, prefix)` - check prefix
|
||||
- `String.endsWith(s, suffix)` - check suffix
|
||||
- `String.split(s, delimiter)` - split into list
|
||||
- `String.join(list, delimiter)` - join list
|
||||
- `String.substring(s, start, end)` - extract substring
|
||||
- `String.indexOf(s, needle)` - find position (returns Option)
|
||||
- `String.replace(s, old, new)` - replace occurrences
|
||||
- `String.trim(s)` - trim whitespace
|
||||
- `String.toLower(s)` / `String.toUpper(s)` - case conversion
|
||||
@@ -1,36 +1,19 @@
|
||||
// Demonstrating behavioral properties in Lux
|
||||
// Behavioral properties are compile-time guarantees about function behavior
|
||||
//
|
||||
// Expected output:
|
||||
// add(5, 3) = 8
|
||||
// factorial(5) = 120
|
||||
// multiply(7, 6) = 42
|
||||
// abs(-5) = 5
|
||||
fn add(a: Int, b: Int): Int is pure = a + b
|
||||
|
||||
// A pure function - no side effects, same input always gives same output
|
||||
fn add(a: Int, b: Int): Int is pure =
|
||||
a + b
|
||||
fn factorial(n: Int): Int is deterministic = if n <= 1 then 1 else n * factorial(n - 1)
|
||||
|
||||
// A deterministic function - same input always gives same output
|
||||
fn factorial(n: Int): Int is deterministic =
|
||||
if n <= 1 then 1
|
||||
else n * factorial(n - 1)
|
||||
fn multiply(a: Int, b: Int): Int is commutative = a * b
|
||||
|
||||
// A commutative function - order of arguments doesn't matter
|
||||
fn multiply(a: Int, b: Int): Int is commutative =
|
||||
a * b
|
||||
fn abs(x: Int): Int is idempotent = if x < 0 then 0 - x else x
|
||||
|
||||
// An idempotent function - absolute value
|
||||
fn abs(x: Int): Int is idempotent =
|
||||
if x < 0 then 0 - x else x
|
||||
|
||||
// Test the functions
|
||||
let sumResult = add(5, 3)
|
||||
|
||||
let factResult = factorial(5)
|
||||
|
||||
let productResult = multiply(7, 6)
|
||||
|
||||
let absResult = abs(0 - 5)
|
||||
|
||||
// Print results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("add(5, 3) = " + toString(sumResult))
|
||||
Console.print("factorial(5) = " + toString(factResult))
|
||||
|
||||
@@ -1,82 +1,42 @@
|
||||
// Behavioral Types Demo
|
||||
// Demonstrates compile-time verification of function properties
|
||||
|
||||
// ============================================================
|
||||
// PART 1: Pure Functions
|
||||
// ============================================================
|
||||
|
||||
// Pure functions have no side effects
|
||||
fn add(a: Int, b: Int): Int is pure = a + b
|
||||
|
||||
fn subtract(a: Int, b: Int): Int is pure = a - b
|
||||
|
||||
// ============================================================
|
||||
// PART 2: Commutative Functions
|
||||
// ============================================================
|
||||
|
||||
// Commutative functions: f(a, b) = f(b, a)
|
||||
fn multiply(a: Int, b: Int): Int is commutative = a * b
|
||||
|
||||
fn sum(a: Int, b: Int): Int is commutative = a + b
|
||||
|
||||
// ============================================================
|
||||
// PART 3: Idempotent Functions
|
||||
// ============================================================
|
||||
|
||||
// Idempotent functions: f(f(x)) = f(x)
|
||||
fn abs(x: Int): Int is idempotent =
|
||||
if x < 0 then 0 - x else x
|
||||
fn abs(x: Int): Int is idempotent = if x < 0 then 0 - x else x
|
||||
|
||||
fn identity(x: Int): Int is idempotent = x
|
||||
|
||||
// ============================================================
|
||||
// PART 4: Deterministic Functions
|
||||
// ============================================================
|
||||
fn factorial(n: Int): Int is deterministic = if n <= 1 then 1 else n * factorial(n - 1)
|
||||
|
||||
// Deterministic functions always produce the same output for the same input
|
||||
fn factorial(n: Int): Int is deterministic =
|
||||
if n <= 1 then 1 else n * factorial(n - 1)
|
||||
fn fib(n: Int): Int is deterministic = if n <= 1 then n else fib(n - 1) + fib(n - 2)
|
||||
|
||||
fn fib(n: Int): Int is deterministic =
|
||||
if n <= 1 then n else fib(n - 1) + fib(n - 2)
|
||||
fn sumTo(n: Int): Int is total = if n <= 0 then 0 else n + sumTo(n - 1)
|
||||
|
||||
// ============================================================
|
||||
// PART 5: Total Functions
|
||||
// ============================================================
|
||||
|
||||
// Total functions are defined for all inputs (no infinite loops, no exceptions)
|
||||
fn sumTo(n: Int): Int is total =
|
||||
if n <= 0 then 0 else n + sumTo(n - 1)
|
||||
|
||||
fn power(base: Int, exp: Int): Int is total =
|
||||
if exp <= 0 then 1 else base * power(base, exp - 1)
|
||||
|
||||
// ============================================================
|
||||
// RESULTS
|
||||
// ============================================================
|
||||
fn power(base: Int, exp: Int): Int is total = if exp <= 0 then 1 else base * power(base, exp - 1)
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Behavioral Types Demo ===")
|
||||
Console.print("")
|
||||
|
||||
Console.print("Part 1: Pure functions")
|
||||
Console.print(" add(5, 3) = " + toString(add(5, 3)))
|
||||
Console.print(" subtract(10, 4) = " + toString(subtract(10, 4)))
|
||||
Console.print("")
|
||||
|
||||
Console.print("Part 2: Commutative functions")
|
||||
Console.print(" multiply(7, 6) = " + toString(multiply(7, 6)))
|
||||
Console.print(" sum(10, 20) = " + toString(sum(10, 20)))
|
||||
Console.print("")
|
||||
|
||||
Console.print("Part 3: Idempotent functions")
|
||||
Console.print(" abs(-42) = " + toString(abs(0 - 42)))
|
||||
Console.print(" identity(100) = " + toString(identity(100)))
|
||||
Console.print("")
|
||||
|
||||
Console.print("Part 4: Deterministic functions")
|
||||
Console.print(" factorial(5) = " + toString(factorial(5)))
|
||||
Console.print(" fib(10) = " + toString(fib(10)))
|
||||
Console.print("")
|
||||
|
||||
Console.print("Part 5: Total functions")
|
||||
Console.print(" sumTo(10) = " + toString(sumTo(10)))
|
||||
Console.print(" power(2, 8) = " + toString(power(2, 8)))
|
||||
|
||||
@@ -1,31 +1,7 @@
|
||||
// Demonstrating built-in effects in Lux
|
||||
//
|
||||
// Lux provides several built-in effects:
|
||||
// - Console: print and read from terminal
|
||||
// - Fail: early termination with error
|
||||
// - State: get/put mutable state (requires runtime initialization)
|
||||
// - Reader: read-only environment access (requires runtime initialization)
|
||||
//
|
||||
// This example demonstrates Console and Fail effects.
|
||||
//
|
||||
// Expected output:
|
||||
// Starting computation...
|
||||
// Step 1: validating input
|
||||
// Step 2: processing
|
||||
// Result: 42
|
||||
// Done!
|
||||
fn safeDivide(a: Int, b: Int): Int with {Fail} = if b == 0 then Fail.fail("Division by zero") else a / b
|
||||
|
||||
// A function that can fail
|
||||
fn safeDivide(a: Int, b: Int): Int with {Fail} =
|
||||
if b == 0 then Fail.fail("Division by zero")
|
||||
else a / b
|
||||
fn validatePositive(n: Int): Int with {Fail} = if n < 0 then Fail.fail("Negative number not allowed") else n
|
||||
|
||||
// A function that validates input
|
||||
fn validatePositive(n: Int): Int with {Fail} =
|
||||
if n < 0 then Fail.fail("Negative number not allowed")
|
||||
else n
|
||||
|
||||
// A computation that uses multiple effects
|
||||
fn compute(input: Int): Int with {Console, Fail} = {
|
||||
Console.print("Starting computation...")
|
||||
Console.print("Step 1: validating input")
|
||||
@@ -36,7 +12,6 @@ fn compute(input: Int): Int with {Console, Fail} = {
|
||||
result
|
||||
}
|
||||
|
||||
// Main function
|
||||
fn main(): Unit with {Console} = {
|
||||
let result = run compute(21) with {}
|
||||
Console.print("Done!")
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
// Counter Example - A simple interactive counter using TEA pattern
|
||||
//
|
||||
// This example demonstrates:
|
||||
// - Model-View-Update architecture (TEA)
|
||||
// - Html DSL for describing UI (inline version)
|
||||
// - Message-based state updates
|
||||
|
||||
// ============================================================================
|
||||
// Html Types (subset of stdlib/html)
|
||||
// ============================================================================
|
||||
|
||||
type Html<M> =
|
||||
| Element(String, List<Attr<M>>, List<Html<M>>)
|
||||
| Text(String)
|
||||
@@ -19,130 +8,96 @@ type Attr<M> =
|
||||
| Id(String)
|
||||
| OnClick(M)
|
||||
|
||||
// Html builder helpers
|
||||
fn div<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("div", attrs, children)
|
||||
fn div<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = Element("div", attrs, children)
|
||||
|
||||
fn span<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("span", attrs, children)
|
||||
fn span<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = Element("span", attrs, children)
|
||||
|
||||
fn h1<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("h1", attrs, children)
|
||||
fn h1<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = Element("h1", attrs, children)
|
||||
|
||||
fn button<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("button", attrs, children)
|
||||
fn button<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = Element("button", attrs, children)
|
||||
|
||||
fn text<M>(content: String): Html<M> =
|
||||
Text(content)
|
||||
fn text<M>(content: String): Html<M> = Text(content)
|
||||
|
||||
fn class<M>(name: String): Attr<M> =
|
||||
Class(name)
|
||||
fn class<M>(name: String): Attr<M> = Class(name)
|
||||
|
||||
fn onClick<M>(msg: M): Attr<M> =
|
||||
OnClick(msg)
|
||||
|
||||
// ============================================================================
|
||||
// Model - The application state (using ADT wrapper)
|
||||
// ============================================================================
|
||||
fn onClick<M>(msg: M): Attr<M> = OnClick(msg)
|
||||
|
||||
type Model =
|
||||
| Counter(Int)
|
||||
|
||||
fn getCount(model: Model): Int =
|
||||
match model {
|
||||
Counter(n) => n
|
||||
}
|
||||
Counter(n) => n,
|
||||
}
|
||||
|
||||
fn init(): Model = Counter(0)
|
||||
|
||||
// ============================================================================
|
||||
// Messages - Events that can occur
|
||||
// ============================================================================
|
||||
|
||||
type Msg =
|
||||
| Increment
|
||||
| Decrement
|
||||
| Reset
|
||||
|
||||
// ============================================================================
|
||||
// Update - State transitions
|
||||
// ============================================================================
|
||||
|
||||
fn update(model: Model, msg: Msg): Model =
|
||||
match msg {
|
||||
Increment => Counter(getCount(model) + 1),
|
||||
Decrement => Counter(getCount(model) - 1),
|
||||
Reset => Counter(0)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// View - Render the UI
|
||||
// ============================================================================
|
||||
Increment => Counter(getCount(model) + 1),
|
||||
Decrement => Counter(getCount(model) - 1),
|
||||
Reset => Counter(0),
|
||||
}
|
||||
|
||||
fn viewCounter(count: Int): Html<Msg> = {
|
||||
let countText = text(toString(count))
|
||||
let countSpan = span([class("count")], [countText])
|
||||
let displayDiv = div([class("counter-display")], [countSpan])
|
||||
|
||||
let minusBtn = button([onClick(Decrement), class("btn")], [text("-")])
|
||||
let resetBtn = button([onClick(Reset), class("btn btn-reset")], [text("Reset")])
|
||||
let plusBtn = button([onClick(Increment), class("btn")], [text("+")])
|
||||
let buttonsDiv = div([class("counter-buttons")], [minusBtn, resetBtn, plusBtn])
|
||||
|
||||
let title = h1([], [text("Counter")])
|
||||
div([class("counter-app")], [title, displayDiv, buttonsDiv])
|
||||
}
|
||||
|
||||
fn view(model: Model): Html<Msg> = viewCounter(getCount(model))
|
||||
|
||||
// ============================================================================
|
||||
// Debug: Print Html structure
|
||||
// ============================================================================
|
||||
|
||||
fn showAttr(attr: Attr<Msg>): String =
|
||||
match attr {
|
||||
Class(s) => "class=\"" + s + "\"",
|
||||
Id(s) => "id=\"" + s + "\"",
|
||||
OnClick(msg) => match msg {
|
||||
Increment => "onclick=\"Increment\"",
|
||||
Decrement => "onclick=\"Decrement\"",
|
||||
Reset => "onclick=\"Reset\""
|
||||
}
|
||||
}
|
||||
Class(s) => "class=\"" + s + "\"",
|
||||
Id(s) => "id=\"" + s + "\"",
|
||||
OnClick(msg) => match msg {
|
||||
Increment => "onclick=\"Increment\"",
|
||||
Decrement => "onclick=\"Decrement\"",
|
||||
Reset => "onclick=\"Reset\"",
|
||||
},
|
||||
}
|
||||
|
||||
fn showAttrs(attrs: List<Attr<Msg>>): String =
|
||||
match List.head(attrs) {
|
||||
None => "",
|
||||
Some(a) => match List.tail(attrs) {
|
||||
None => showAttr(a),
|
||||
Some(rest) => showAttr(a) + " " + showAttrs(rest)
|
||||
}
|
||||
}
|
||||
None => "",
|
||||
Some(a) => match List.tail(attrs) {
|
||||
None => showAttr(a),
|
||||
Some(rest) => showAttr(a) + " " + showAttrs(rest),
|
||||
},
|
||||
}
|
||||
|
||||
fn showChildren(children: List<Html<Msg>>, indent: Int): String =
|
||||
match List.head(children) {
|
||||
None => "",
|
||||
Some(c) => match List.tail(children) {
|
||||
None => showHtml(c, indent),
|
||||
Some(rest) => showHtml(c, indent) + showChildren(rest, indent)
|
||||
}
|
||||
}
|
||||
None => "",
|
||||
Some(c) => match List.tail(children) {
|
||||
None => showHtml(c, indent),
|
||||
Some(rest) => showHtml(c, indent) + showChildren(rest, indent),
|
||||
},
|
||||
}
|
||||
|
||||
fn showHtml(html: Html<Msg>, indent: Int): String =
|
||||
match html {
|
||||
Empty => "",
|
||||
Text(s) => s,
|
||||
Element(tag, attrs, children) => {
|
||||
let attrStr = showAttrs(attrs)
|
||||
let attrPart = if String.length(attrStr) > 0 then " " + attrStr else ""
|
||||
let childStr = showChildren(children, indent + 2)
|
||||
"<" + tag + attrPart + ">" + childStr + "</" + tag + ">"
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Entry point
|
||||
// ============================================================================
|
||||
Empty => "",
|
||||
Text(s) => s,
|
||||
Element(tag, attrs, children) => {
|
||||
let attrStr = showAttrs(attrs)
|
||||
let attrPart = if String.length(attrStr) > 0 then " " + attrStr else ""
|
||||
let childStr = showChildren(children, indent + 2)
|
||||
"<" + tag + attrPart + ">" + childStr + "</" + tag + ">"
|
||||
},
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
let model = init()
|
||||
@@ -150,24 +105,19 @@ fn main(): Unit with {Console} = {
|
||||
Console.print("")
|
||||
Console.print("Initial count: " + toString(getCount(model)))
|
||||
Console.print("")
|
||||
|
||||
let m1 = update(model, Increment)
|
||||
Console.print("After Increment: " + toString(getCount(m1)))
|
||||
|
||||
let m2 = update(m1, Increment)
|
||||
Console.print("After Increment: " + toString(getCount(m2)))
|
||||
|
||||
let m3 = update(m2, Increment)
|
||||
Console.print("After Increment: " + toString(getCount(m3)))
|
||||
|
||||
let m4 = update(m3, Decrement)
|
||||
Console.print("After Decrement: " + toString(getCount(m4)))
|
||||
|
||||
let m5 = update(m4, Reset)
|
||||
Console.print("After Reset: " + toString(getCount(m5)))
|
||||
|
||||
Console.print("")
|
||||
Console.print("=== View (HTML Structure) ===")
|
||||
Console.print(showHtml(view(m2), 0))
|
||||
}
|
||||
|
||||
let output = run main() with {}
|
||||
|
||||
@@ -1,57 +1,37 @@
|
||||
// Demonstrating algebraic data types and pattern matching
|
||||
//
|
||||
// Expected output:
|
||||
// Tree sum: 8
|
||||
// Tree depth: 3
|
||||
// Safe divide 10/2: Result: 5
|
||||
// Safe divide 10/0: Division by zero!
|
||||
|
||||
// Define a binary tree
|
||||
type Tree =
|
||||
| Leaf(Int)
|
||||
| Node(Tree, Tree)
|
||||
|
||||
// Sum all values in a tree
|
||||
fn sumTree(tree: Tree): Int =
|
||||
match tree {
|
||||
Leaf(n) => n,
|
||||
Node(left, right) => sumTree(left) + sumTree(right)
|
||||
}
|
||||
Leaf(n) => n,
|
||||
Node(left, right) => sumTree(left) + sumTree(right),
|
||||
}
|
||||
|
||||
// Find the depth of a tree
|
||||
fn depth(tree: Tree): Int =
|
||||
match tree {
|
||||
Leaf(_) => 1,
|
||||
Node(left, right) => {
|
||||
let leftDepth = depth(left)
|
||||
let rightDepth = depth(right)
|
||||
1 + (if leftDepth > rightDepth then leftDepth else rightDepth)
|
||||
}
|
||||
}
|
||||
|
||||
// Example tree:
|
||||
// Node
|
||||
// / \
|
||||
// Node Leaf(5)
|
||||
// / \
|
||||
// Leaf(1) Leaf(2)
|
||||
Leaf(_) => 1,
|
||||
Node(left, right) => {
|
||||
let leftDepth = depth(left)
|
||||
let rightDepth = depth(right)
|
||||
1 + if leftDepth > rightDepth then leftDepth else rightDepth
|
||||
},
|
||||
}
|
||||
|
||||
let myTree = Node(Node(Leaf(1), Leaf(2)), Leaf(5))
|
||||
|
||||
let treeSum = sumTree(myTree)
|
||||
|
||||
let treeDepth = depth(myTree)
|
||||
|
||||
// Option type example
|
||||
fn safeDivide(a: Int, b: Int): Option<Int> =
|
||||
if b == 0 then None
|
||||
else Some(a / b)
|
||||
fn safeDivide(a: Int, b: Int): Option<Int> = if b == 0 then None else Some(a / b)
|
||||
|
||||
fn showResult(result: Option<Int>): String =
|
||||
match result {
|
||||
None => "Division by zero!",
|
||||
Some(n) => "Result: " + toString(n)
|
||||
}
|
||||
None => "Division by zero!",
|
||||
Some(n) => "Result: " + toString(n),
|
||||
}
|
||||
|
||||
// Print results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("Tree sum: " + toString(treeSum))
|
||||
Console.print("Tree depth: " + toString(treeDepth))
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
// Demonstrating algebraic effects in Lux
|
||||
//
|
||||
// Expected output:
|
||||
// [info] Processing data...
|
||||
// [debug] Result computed
|
||||
// Final result: 42
|
||||
|
||||
// Define a custom logging effect
|
||||
effect Logger {
|
||||
fn log(level: String, msg: String): Unit
|
||||
fn getLevel(): String
|
||||
}
|
||||
|
||||
// A function that uses the Logger effect
|
||||
fn processData(data: Int): Int with {Logger} = {
|
||||
Logger.log("info", "Processing data...")
|
||||
let result = data * 2
|
||||
@@ -19,17 +10,15 @@ fn processData(data: Int): Int with {Logger} = {
|
||||
result
|
||||
}
|
||||
|
||||
// A handler that prints logs to console
|
||||
handler consoleLogger: Logger {
|
||||
fn log(level, msg) = Console.print("[" + level + "] " + msg)
|
||||
fn getLevel() = "debug"
|
||||
}
|
||||
|
||||
// Run and print
|
||||
fn main(): Unit with {Console} = {
|
||||
let result = run processData(21) with {
|
||||
Logger = consoleLogger
|
||||
}
|
||||
Logger = consoleLogger,
|
||||
}
|
||||
Console.print("Final result: " + toString(result))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
// Factorial function demonstrating recursion
|
||||
//
|
||||
// Expected output: 10! = 3628800
|
||||
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
|
||||
|
||||
fn factorial(n: Int): Int =
|
||||
if n <= 1 then 1
|
||||
else n * factorial(n - 1)
|
||||
|
||||
// Calculate factorial of 10
|
||||
let result = factorial(10)
|
||||
|
||||
// Print result using Console effect
|
||||
fn showResult(): Unit with {Console} =
|
||||
Console.print("10! = " + toString(result))
|
||||
fn showResult(): Unit with {Console} = Console.print("10! = " + toString(result))
|
||||
|
||||
let output = run showResult() with {}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
// File I/O example - demonstrates the File effect
|
||||
//
|
||||
// This script reads a file, counts lines/words, and writes a report
|
||||
|
||||
fn countLines(content: String): Int = {
|
||||
let lines = String.split(content, "\n")
|
||||
let lines = String.split(content, "
|
||||
")
|
||||
List.length(lines)
|
||||
}
|
||||
|
||||
@@ -14,35 +11,28 @@ fn countWords(content: String): Int = {
|
||||
|
||||
fn analyzeFile(path: String): Unit with {File, Console} = {
|
||||
Console.print("Analyzing file: " + path)
|
||||
|
||||
if File.exists(path) then {
|
||||
let content = File.read(path)
|
||||
let lines = countLines(content)
|
||||
let words = countWords(content)
|
||||
let chars = String.length(content)
|
||||
|
||||
Console.print(" Lines: " + toString(lines))
|
||||
Console.print(" Words: " + toString(words))
|
||||
Console.print(" Chars: " + toString(chars))
|
||||
} else {
|
||||
Console.print(" Error: File not found!")
|
||||
}
|
||||
let content = File.read(path)
|
||||
let lines = countLines(content)
|
||||
let words = countWords(content)
|
||||
let chars = String.length(content)
|
||||
Console.print(" Lines: " + toString(lines))
|
||||
Console.print(" Words: " + toString(words))
|
||||
Console.print(" Chars: " + toString(chars))
|
||||
} else {
|
||||
Console.print(" Error: File not found!")
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {File, Console} = {
|
||||
Console.print("=== Lux File Analyzer ===")
|
||||
Console.print("")
|
||||
|
||||
// Analyze this file itself
|
||||
analyzeFile("examples/file_io.lux")
|
||||
Console.print("")
|
||||
|
||||
// Analyze hello.lux
|
||||
analyzeFile("examples/hello.lux")
|
||||
Console.print("")
|
||||
|
||||
// Write a report
|
||||
let report = "File analysis complete.\nAnalyzed 2 files."
|
||||
let report = "File analysis complete.
|
||||
Analyzed 2 files."
|
||||
File.write("/tmp/lux_report.txt", report)
|
||||
Console.print("Report written to /tmp/lux_report.txt")
|
||||
}
|
||||
|
||||
@@ -1,55 +1,39 @@
|
||||
// Demonstrating functional programming features
|
||||
//
|
||||
// Expected output:
|
||||
// apply(double, 21) = 42
|
||||
// compose(addOne, double)(5) = 11
|
||||
// pipe: 5 |> double |> addOne |> square = 121
|
||||
// curried add5(10) = 15
|
||||
// partial times3(7) = 21
|
||||
// record transform = 30
|
||||
|
||||
// Higher-order functions
|
||||
fn apply(f: fn(Int): Int, x: Int): Int = f(x)
|
||||
|
||||
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int =
|
||||
fn(x: Int): Int => f(g(x))
|
||||
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int = fn(x: Int): Int => f(g(x))
|
||||
|
||||
// Basic functions
|
||||
fn double(x: Int): Int = x * 2
|
||||
|
||||
fn addOne(x: Int): Int = x + 1
|
||||
|
||||
fn square(x: Int): Int = x * x
|
||||
|
||||
// Using apply
|
||||
let result1 = apply(double, 21)
|
||||
|
||||
// Using compose
|
||||
let doubleAndAddOne = compose(addOne, double)
|
||||
|
||||
let result2 = doubleAndAddOne(5)
|
||||
|
||||
// Using the pipe operator
|
||||
let result3 = 5 |> double |> addOne |> square
|
||||
let result3 = square(addOne(double(5)))
|
||||
|
||||
// Currying example
|
||||
fn add(a: Int): fn(Int): Int =
|
||||
fn(b: Int): Int => a + b
|
||||
fn add(a: Int): fn(Int): Int = fn(b: Int): Int => a + b
|
||||
|
||||
let add5 = add(5)
|
||||
|
||||
let result4 = add5(10)
|
||||
|
||||
// Partial application simulation
|
||||
fn multiply(a: Int, b: Int): Int = a * b
|
||||
|
||||
let times3 = fn(x: Int): Int => multiply(3, x)
|
||||
|
||||
let result5 = times3(7)
|
||||
|
||||
// Working with records
|
||||
let transform = fn(record: { x: Int, y: Int }): Int =>
|
||||
record.x + record.y
|
||||
let transform = fn(record: { x: Int, y: Int }): Int => record.x + record.y
|
||||
|
||||
let point = { x: 10, y: 20 }
|
||||
|
||||
let recordSum = transform(point)
|
||||
|
||||
// Print all results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("apply(double, 21) = " + toString(result1))
|
||||
Console.print("compose(addOne, double)(5) = " + toString(result2))
|
||||
|
||||
@@ -1,54 +1,43 @@
|
||||
// Demonstrating generic type parameters in Lux
|
||||
//
|
||||
// Expected output:
|
||||
// identity(42) = 42
|
||||
// identity("hello") = hello
|
||||
// first(MkPair(1, "one")) = 1
|
||||
// second(MkPair(1, "one")) = one
|
||||
// map(Some(21), double) = Some(42)
|
||||
|
||||
// Generic identity function
|
||||
fn identity<T>(x: T): T = x
|
||||
|
||||
// Generic pair type
|
||||
type Pair<A, B> =
|
||||
| MkPair(A, B)
|
||||
|
||||
fn first<A, B>(p: Pair<A, B>): A =
|
||||
match p {
|
||||
MkPair(a, _) => a
|
||||
}
|
||||
MkPair(a, _) => a,
|
||||
}
|
||||
|
||||
fn second<A, B>(p: Pair<A, B>): B =
|
||||
match p {
|
||||
MkPair(_, b) => b
|
||||
}
|
||||
MkPair(_, b) => b,
|
||||
}
|
||||
|
||||
// Generic map function for Option
|
||||
fn mapOption<T, U>(opt: Option<T>, f: fn(T): U): Option<U> =
|
||||
match opt {
|
||||
None => None,
|
||||
Some(x) => Some(f(x))
|
||||
}
|
||||
None => None,
|
||||
Some(x) => Some(f(x)),
|
||||
}
|
||||
|
||||
// Helper function for testing
|
||||
fn double(x: Int): Int = x * 2
|
||||
|
||||
// Test usage
|
||||
let id_int = identity(42)
|
||||
|
||||
let id_str = identity("hello")
|
||||
|
||||
let pair = MkPair(1, "one")
|
||||
|
||||
let fst = first(pair)
|
||||
|
||||
let snd = second(pair)
|
||||
|
||||
let doubled = mapOption(Some(21), double)
|
||||
|
||||
fn showOption(opt: Option<Int>): String =
|
||||
match opt {
|
||||
None => "None",
|
||||
Some(x) => "Some(" + toString(x) + ")"
|
||||
}
|
||||
None => "None",
|
||||
Some(x) => "Some(" + toString(x) + ")",
|
||||
}
|
||||
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("identity(42) = " + toString(id_int))
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
// Demonstrating resumable effect handlers in Lux
|
||||
//
|
||||
// Handlers can use `resume(value)` to return a value to the effect call site
|
||||
// and continue the computation. This enables powerful control flow patterns.
|
||||
//
|
||||
// Expected output:
|
||||
// [INFO] Starting computation
|
||||
// [DEBUG] Intermediate result: 10
|
||||
// [INFO] Computation complete
|
||||
// Final result: 20
|
||||
|
||||
// Define a custom logging effect
|
||||
effect Logger {
|
||||
fn log(level: String, msg: String): Unit
|
||||
fn getLogLevel(): String
|
||||
}
|
||||
|
||||
// A function that uses the Logger effect
|
||||
fn compute(): Int with {Logger} = {
|
||||
Logger.log("INFO", "Starting computation")
|
||||
let x = 10
|
||||
@@ -25,20 +12,19 @@ fn compute(): Int with {Logger} = {
|
||||
result
|
||||
}
|
||||
|
||||
// A handler that prints logs with brackets and resumes with Unit
|
||||
handler prettyLogger: Logger {
|
||||
fn log(level, msg) = {
|
||||
Console.print("[" + level + "] " + msg)
|
||||
resume(())
|
||||
}
|
||||
fn log(level, msg) =
|
||||
{
|
||||
Console.print("[" + level + "] " + msg)
|
||||
resume(())
|
||||
}
|
||||
fn getLogLevel() = resume("DEBUG")
|
||||
}
|
||||
|
||||
// Main function
|
||||
fn main(): Unit with {Console} = {
|
||||
let result = run compute() with {
|
||||
Logger = prettyLogger
|
||||
}
|
||||
Logger = prettyLogger,
|
||||
}
|
||||
Console.print("Final result: " + toString(result))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
// Hello World in Lux
|
||||
// Demonstrates basic effect usage
|
||||
//
|
||||
// Expected output: Hello, World!
|
||||
fn greet(): Unit with {Console} = Console.print("Hello, World!")
|
||||
|
||||
fn greet(): Unit with {Console} =
|
||||
Console.print("Hello, World!")
|
||||
|
||||
// Run the greeting with the Console effect
|
||||
let output = run greet() with {}
|
||||
|
||||
@@ -1,91 +1,72 @@
|
||||
// HTTP example - demonstrates the Http effect
|
||||
//
|
||||
// This script makes HTTP requests and parses JSON responses
|
||||
|
||||
fn main(): Unit with {Console, Http} = {
|
||||
Console.print("=== Lux HTTP Example ===")
|
||||
Console.print("")
|
||||
|
||||
// Make a GET request to a public API
|
||||
Console.print("Fetching data from httpbin.org...")
|
||||
Console.print("")
|
||||
|
||||
match Http.get("https://httpbin.org/get") {
|
||||
Ok(response) => {
|
||||
Console.print("GET request successful!")
|
||||
Console.print(" Status: " + toString(response.status))
|
||||
Console.print(" Body length: " + toString(String.length(response.body)) + " bytes")
|
||||
Console.print("")
|
||||
|
||||
// Parse the JSON response
|
||||
match Json.parse(response.body) {
|
||||
Ok(json) => {
|
||||
Console.print("Parsed JSON response:")
|
||||
match Json.get(json, "origin") {
|
||||
Some(origin) => match Json.asString(origin) {
|
||||
Some(ip) => Console.print(" Your IP: " + ip),
|
||||
None => Console.print(" origin: (not a string)")
|
||||
},
|
||||
None => Console.print(" origin: (not found)")
|
||||
}
|
||||
match Json.get(json, "url") {
|
||||
Some(url) => match Json.asString(url) {
|
||||
Some(u) => Console.print(" URL: " + u),
|
||||
None => Console.print(" url: (not a string)")
|
||||
},
|
||||
None => Console.print(" url: (not found)")
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("JSON parse error: " + e)
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("GET request failed: " + e)
|
||||
}
|
||||
|
||||
Ok(response) => {
|
||||
Console.print("GET request successful!")
|
||||
Console.print(" Status: " + toString(response.status))
|
||||
Console.print(" Body length: " + toString(String.length(response.body)) + " bytes")
|
||||
Console.print("")
|
||||
match Json.parse(response.body) {
|
||||
Ok(json) => {
|
||||
Console.print("Parsed JSON response:")
|
||||
match Json.get(json, "origin") {
|
||||
Some(origin) => match Json.asString(origin) {
|
||||
Some(ip) => Console.print(" Your IP: " + ip),
|
||||
None => Console.print(" origin: (not a string)"),
|
||||
},
|
||||
None => Console.print(" origin: (not found)"),
|
||||
}
|
||||
match Json.get(json, "url") {
|
||||
Some(url) => match Json.asString(url) {
|
||||
Some(u) => Console.print(" URL: " + u),
|
||||
None => Console.print(" url: (not a string)"),
|
||||
},
|
||||
None => Console.print(" url: (not found)"),
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("JSON parse error: " + e),
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("GET request failed: " + e),
|
||||
}
|
||||
Console.print("")
|
||||
Console.print("--- POST Request ---")
|
||||
Console.print("")
|
||||
|
||||
// Make a POST request with JSON body
|
||||
let requestBody = Json.object([("message", Json.string("Hello from Lux!")), ("version", Json.int(1))])
|
||||
Console.print("Sending POST with JSON body...")
|
||||
Console.print(" Body: " + Json.stringify(requestBody))
|
||||
Console.print("")
|
||||
|
||||
match Http.postJson("https://httpbin.org/post", requestBody) {
|
||||
Ok(response) => {
|
||||
Console.print("POST request successful!")
|
||||
Console.print(" Status: " + toString(response.status))
|
||||
|
||||
// Parse and extract what we sent
|
||||
match Json.parse(response.body) {
|
||||
Ok(json) => match Json.get(json, "json") {
|
||||
Some(sentJson) => {
|
||||
Console.print(" Server received:")
|
||||
Console.print(" " + Json.stringify(sentJson))
|
||||
},
|
||||
None => Console.print(" (no json field in response)")
|
||||
},
|
||||
Err(e) => Console.print("JSON parse error: " + e)
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("POST request failed: " + e)
|
||||
}
|
||||
|
||||
Ok(response) => {
|
||||
Console.print("POST request successful!")
|
||||
Console.print(" Status: " + toString(response.status))
|
||||
match Json.parse(response.body) {
|
||||
Ok(json) => match Json.get(json, "json") {
|
||||
Some(sentJson) => {
|
||||
Console.print(" Server received:")
|
||||
Console.print(" " + Json.stringify(sentJson))
|
||||
},
|
||||
None => Console.print(" (no json field in response)"),
|
||||
},
|
||||
Err(e) => Console.print("JSON parse error: " + e),
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("POST request failed: " + e),
|
||||
}
|
||||
Console.print("")
|
||||
Console.print("--- Headers ---")
|
||||
Console.print("")
|
||||
|
||||
// Show response headers
|
||||
match Http.get("https://httpbin.org/headers") {
|
||||
Ok(response) => {
|
||||
Console.print("Response headers (first 5):")
|
||||
let count = 0
|
||||
// Note: Can't easily iterate with effects in callbacks, so just show count
|
||||
Console.print(" Total headers: " + toString(List.length(response.headers)))
|
||||
},
|
||||
Err(e) => Console.print("Request failed: " + e)
|
||||
}
|
||||
Ok(response) => {
|
||||
Console.print("Response headers (first 5):")
|
||||
let count = 0
|
||||
Console.print(" Total headers: " + toString(List.length(response.headers)))
|
||||
},
|
||||
Err(e) => Console.print("Request failed: " + e),
|
||||
}
|
||||
}
|
||||
|
||||
let result = run main() with {}
|
||||
|
||||
@@ -1,85 +1,48 @@
|
||||
// HTTP API Example
|
||||
//
|
||||
// A complete REST API demonstrating:
|
||||
// - Route matching with path parameters
|
||||
// - Response builders
|
||||
// - JSON construction
|
||||
//
|
||||
// Run with: lux examples/http_api.lux
|
||||
// Test with:
|
||||
// curl http://localhost:8080/
|
||||
// curl http://localhost:8080/users
|
||||
// curl http://localhost:8080/users/42
|
||||
fn httpOk(body: String): { status: Int, body: String } = { status: 200, body: body }
|
||||
|
||||
// ============================================================
|
||||
// Response Helpers
|
||||
// ============================================================
|
||||
fn httpCreated(body: String): { status: Int, body: String } = { status: 201, body: body }
|
||||
|
||||
fn httpOk(body: String): { status: Int, body: String } =
|
||||
{ status: 200, body: body }
|
||||
fn httpNotFound(body: String): { status: Int, body: String } = { status: 404, body: body }
|
||||
|
||||
fn httpCreated(body: String): { status: Int, body: String } =
|
||||
{ status: 201, body: body }
|
||||
fn httpBadRequest(body: String): { status: Int, body: String } = { status: 400, body: body }
|
||||
|
||||
fn httpNotFound(body: String): { status: Int, body: String } =
|
||||
{ status: 404, body: body }
|
||||
fn jsonEscape(s: String): String = String.replace(String.replace(s, "\\", "\\\\"), "\"", "\\\"")
|
||||
|
||||
fn httpBadRequest(body: String): { status: Int, body: String } =
|
||||
{ status: 400, body: body }
|
||||
fn jsonStr(key: String, value: String): String = "\"" + jsonEscape(key) + "\":\"" + jsonEscape(value) + "\""
|
||||
|
||||
// ============================================================
|
||||
// JSON Helpers
|
||||
// ============================================================
|
||||
fn jsonNum(key: String, value: Int): String = "\"" + jsonEscape(key) + "\":" + toString(value)
|
||||
|
||||
fn jsonEscape(s: String): String =
|
||||
String.replace(String.replace(s, "\\", "\\\\"), "\"", "\\\"")
|
||||
fn jsonObj(content: String): String = toString(" + content + ")
|
||||
|
||||
fn jsonStr(key: String, value: String): String =
|
||||
"\"" + jsonEscape(key) + "\":\"" + jsonEscape(value) + "\""
|
||||
fn jsonArr(content: String): String = "[" + content + "]"
|
||||
|
||||
fn jsonNum(key: String, value: Int): String =
|
||||
"\"" + jsonEscape(key) + "\":" + toString(value)
|
||||
|
||||
fn jsonObj(content: String): String =
|
||||
"{" + content + "}"
|
||||
|
||||
fn jsonArr(content: String): String =
|
||||
"[" + content + "]"
|
||||
|
||||
fn jsonError(message: String): String =
|
||||
jsonObj(jsonStr("error", message))
|
||||
|
||||
// ============================================================
|
||||
// Path Matching
|
||||
// ============================================================
|
||||
fn jsonError(message: String): String = jsonObj(jsonStr("error", message))
|
||||
|
||||
fn pathMatches(path: String, pattern: String): Bool = {
|
||||
let pathParts = String.split(path, "/")
|
||||
let patternParts = String.split(pattern, "/")
|
||||
if List.length(pathParts) != List.length(patternParts) then false
|
||||
else matchParts(pathParts, patternParts)
|
||||
if List.length(pathParts) != List.length(patternParts) then false else matchParts(pathParts, patternParts)
|
||||
}
|
||||
|
||||
fn matchParts(pathParts: List<String>, patternParts: List<String>): Bool = {
|
||||
if List.length(pathParts) == 0 then true
|
||||
else {
|
||||
match List.head(pathParts) {
|
||||
None => true,
|
||||
Some(pathPart) => {
|
||||
match List.head(patternParts) {
|
||||
None => true,
|
||||
Some(patternPart) => {
|
||||
let isMatch = if String.startsWith(patternPart, ":") then true else pathPart == patternPart
|
||||
if isMatch then {
|
||||
let restPath = Option.getOrElse(List.tail(pathParts), [])
|
||||
let restPattern = Option.getOrElse(List.tail(patternParts), [])
|
||||
matchParts(restPath, restPattern)
|
||||
} else false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if List.length(pathParts) == 0 then true else {
|
||||
match List.head(pathParts) {
|
||||
None => true,
|
||||
Some(pathPart) => {
|
||||
match List.head(patternParts) {
|
||||
None => true,
|
||||
Some(patternPart) => {
|
||||
let isMatch = if String.startsWith(patternPart, ":") then true else pathPart == patternPart
|
||||
if isMatch then {
|
||||
let restPath = Option.getOrElse(List.tail(pathParts), [])
|
||||
let restPattern = Option.getOrElse(List.tail(patternParts), [])
|
||||
matchParts(restPath, restPattern)
|
||||
} else false
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn getPathSegment(path: String, index: Int): Option<String> = {
|
||||
@@ -87,15 +50,9 @@ fn getPathSegment(path: String, index: Int): Option<String> = {
|
||||
List.get(parts, index + 1)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Handlers
|
||||
// ============================================================
|
||||
fn indexHandler(): { status: Int, body: String } = httpOk(jsonObj(jsonStr("message", "Welcome to Lux HTTP API")))
|
||||
|
||||
fn indexHandler(): { status: Int, body: String } =
|
||||
httpOk(jsonObj(jsonStr("message", "Welcome to Lux HTTP API")))
|
||||
|
||||
fn healthHandler(): { status: Int, body: String } =
|
||||
httpOk(jsonObj(jsonStr("status", "healthy")))
|
||||
fn healthHandler(): { status: Int, body: String } = httpOk(jsonObj(jsonStr("status", "healthy")))
|
||||
|
||||
fn listUsersHandler(): { status: Int, body: String } = {
|
||||
let user1 = jsonObj(jsonNum("id", 1) + "," + jsonStr("name", "Alice"))
|
||||
@@ -105,12 +62,12 @@ fn listUsersHandler(): { status: Int, body: String } = {
|
||||
|
||||
fn getUserHandler(path: String): { status: Int, body: String } = {
|
||||
match getPathSegment(path, 1) {
|
||||
Some(id) => {
|
||||
let body = jsonObj(jsonStr("id", id) + "," + jsonStr("name", "User " + id))
|
||||
httpOk(body)
|
||||
},
|
||||
None => httpNotFound(jsonError("User not found"))
|
||||
}
|
||||
Some(id) => {
|
||||
let body = jsonObj(jsonStr("id", id) + "," + jsonStr("name", "User " + id))
|
||||
httpOk(body)
|
||||
},
|
||||
None => httpNotFound(jsonError("User not found")),
|
||||
}
|
||||
}
|
||||
|
||||
fn createUserHandler(body: String): { status: Int, body: String } = {
|
||||
@@ -118,34 +75,21 @@ fn createUserHandler(body: String): { status: Int, body: String } = {
|
||||
httpCreated(newUser)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Router
|
||||
// ============================================================
|
||||
|
||||
fn router(method: String, path: String, body: String): { status: Int, body: String } = {
|
||||
if method == "GET" && path == "/" then indexHandler()
|
||||
else if method == "GET" && path == "/health" then healthHandler()
|
||||
else if method == "GET" && path == "/users" then listUsersHandler()
|
||||
else if method == "GET" && pathMatches(path, "/users/:id") then getUserHandler(path)
|
||||
else if method == "POST" && path == "/users" then createUserHandler(body)
|
||||
else httpNotFound(jsonError("Not found: " + path))
|
||||
if method == "GET" && path == "/" then indexHandler() else if method == "GET" && path == "/health" then healthHandler() else if method == "GET" && path == "/users" then listUsersHandler() else if method == "GET" && pathMatches(path, "/users/:id") then getUserHandler(path) else if method == "POST" && path == "/users" then createUserHandler(body) else httpNotFound(jsonError("Not found: " + path))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Server
|
||||
// ============================================================
|
||||
|
||||
fn serveLoop(remaining: Int): Unit with {Console, HttpServer} = {
|
||||
if remaining <= 0 then {
|
||||
Console.print("Max requests reached, stopping server.")
|
||||
HttpServer.stop()
|
||||
} else {
|
||||
let req = HttpServer.accept()
|
||||
Console.print(req.method + " " + req.path)
|
||||
let resp = router(req.method, req.path, req.body)
|
||||
HttpServer.respond(resp.status, resp.body)
|
||||
serveLoop(remaining - 1)
|
||||
}
|
||||
Console.print("Max requests reached, stopping server.")
|
||||
HttpServer.stop()
|
||||
} else {
|
||||
let req = HttpServer.accept()
|
||||
Console.print(req.method + " " + req.path)
|
||||
let resp = router(req.method, req.path, req.body)
|
||||
HttpServer.respond(resp.status, resp.body)
|
||||
serveLoop(remaining - 1)
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console, HttpServer} = {
|
||||
|
||||
@@ -1,24 +1,4 @@
|
||||
// HTTP Router Example
|
||||
//
|
||||
// Demonstrates the HTTP helper library with:
|
||||
// - Path pattern matching
|
||||
// - Response builders
|
||||
// - JSON helpers
|
||||
//
|
||||
// Run with: lux examples/http_router.lux
|
||||
// Test with:
|
||||
// curl http://localhost:8080/
|
||||
// curl http://localhost:8080/users
|
||||
// curl http://localhost:8080/users/42
|
||||
|
||||
import stdlib/http
|
||||
|
||||
// ============================================================
|
||||
// Route Handlers
|
||||
// ============================================================
|
||||
|
||||
fn indexHandler(): { status: Int, body: String } =
|
||||
httpOk("Welcome to Lux HTTP Framework!")
|
||||
fn indexHandler(): { status: Int, body: String } = httpOk("Welcome to Lux HTTP Framework!")
|
||||
|
||||
fn listUsersHandler(): { status: Int, body: String } = {
|
||||
let user1 = jsonObject(jsonJoin([jsonNumber("id", 1), jsonString("name", "Alice")]))
|
||||
@@ -29,44 +9,31 @@ fn listUsersHandler(): { status: Int, body: String } = {
|
||||
|
||||
fn getUserHandler(path: String): { status: Int, body: String } = {
|
||||
match getPathSegment(path, 1) {
|
||||
Some(id) => {
|
||||
let body = jsonObject(jsonJoin([jsonString("id", id), jsonString("name", "User " + id)]))
|
||||
httpOk(body)
|
||||
},
|
||||
None => httpNotFound(jsonErrorMsg("User ID required"))
|
||||
}
|
||||
Some(id) => {
|
||||
let body = jsonObject(jsonJoin([jsonString("id", id), jsonString("name", "User " + id)]))
|
||||
httpOk(body)
|
||||
},
|
||||
None => httpNotFound(jsonErrorMsg("User ID required")),
|
||||
}
|
||||
}
|
||||
|
||||
fn healthHandler(): { status: Int, body: String } =
|
||||
httpOk(jsonObject(jsonString("status", "healthy")))
|
||||
|
||||
// ============================================================
|
||||
// Router
|
||||
// ============================================================
|
||||
fn healthHandler(): { status: Int, body: String } = httpOk(jsonObject(jsonString("status", "healthy")))
|
||||
|
||||
fn router(method: String, path: String, body: String): { status: Int, body: String } = {
|
||||
if method == "GET" && path == "/" then indexHandler()
|
||||
else if method == "GET" && path == "/health" then healthHandler()
|
||||
else if method == "GET" && path == "/users" then listUsersHandler()
|
||||
else if method == "GET" && pathMatches(path, "/users/:id") then getUserHandler(path)
|
||||
else httpNotFound(jsonErrorMsg("Not found: " + path))
|
||||
if method == "GET" && path == "/" then indexHandler() else if method == "GET" && path == "/health" then healthHandler() else if method == "GET" && path == "/users" then listUsersHandler() else if method == "GET" && pathMatches(path, "/users/:id") then getUserHandler(path) else httpNotFound(jsonErrorMsg("Not found: " + path))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Server
|
||||
// ============================================================
|
||||
|
||||
fn serveLoop(remaining: Int): Unit with {Console, HttpServer} = {
|
||||
if remaining <= 0 then {
|
||||
Console.print("Max requests reached, stopping server.")
|
||||
HttpServer.stop()
|
||||
} else {
|
||||
let req = HttpServer.accept()
|
||||
Console.print(req.method + " " + req.path)
|
||||
let resp = router(req.method, req.path, req.body)
|
||||
HttpServer.respond(resp.status, resp.body)
|
||||
serveLoop(remaining - 1)
|
||||
}
|
||||
Console.print("Max requests reached, stopping server.")
|
||||
HttpServer.stop()
|
||||
} else {
|
||||
let req = HttpServer.accept()
|
||||
Console.print(req.method + " " + req.path)
|
||||
let resp = router(req.method, req.path, req.body)
|
||||
HttpServer.respond(resp.status, resp.body)
|
||||
serveLoop(remaining - 1)
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console, HttpServer} = {
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
// Test file for JIT compilation
|
||||
// This uses only features the JIT supports: integers, arithmetic, conditionals, functions
|
||||
fn fib(n: Int): Int = if n <= 1 then n else fib(n - 1) + fib(n - 2)
|
||||
|
||||
fn fib(n: Int): Int =
|
||||
if n <= 1 then n
|
||||
else fib(n - 1) + fib(n - 2)
|
||||
|
||||
fn factorial(n: Int): Int =
|
||||
if n <= 1 then 1
|
||||
else n * factorial(n - 1)
|
||||
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
let fibResult = fib(30)
|
||||
|
||||
@@ -1,107 +1,79 @@
|
||||
// JSON example - demonstrates JSON parsing and manipulation
|
||||
//
|
||||
// This script parses JSON, extracts values, and builds new JSON structures
|
||||
|
||||
fn main(): Unit with {Console, File} = {
|
||||
Console.print("=== Lux JSON Example ===")
|
||||
Console.print("")
|
||||
|
||||
// First, build some JSON programmatically
|
||||
Console.print("=== Building JSON ===")
|
||||
Console.print("")
|
||||
|
||||
let name = Json.string("Alice")
|
||||
let age = Json.int(30)
|
||||
let active = Json.bool(true)
|
||||
let scores = Json.array([Json.int(95), Json.int(87), Json.int(92)])
|
||||
|
||||
let person = Json.object([("name", name), ("age", age), ("active", active), ("scores", scores)])
|
||||
|
||||
Console.print("Built JSON:")
|
||||
let pretty = Json.prettyPrint(person)
|
||||
Console.print(pretty)
|
||||
Console.print("")
|
||||
|
||||
// Stringify to a compact string
|
||||
let jsonStr = Json.stringify(person)
|
||||
Console.print("Compact: " + jsonStr)
|
||||
Console.print("")
|
||||
|
||||
// Write to file and read back to test parsing
|
||||
File.write("/tmp/test.json", jsonStr)
|
||||
Console.print("Written to /tmp/test.json")
|
||||
Console.print("")
|
||||
|
||||
// Read and parse from file
|
||||
Console.print("=== Parsing JSON ===")
|
||||
Console.print("")
|
||||
let content = File.read("/tmp/test.json")
|
||||
Console.print("Read from file: " + content)
|
||||
Console.print("")
|
||||
|
||||
match Json.parse(content) {
|
||||
Ok(json) => {
|
||||
Console.print("Parse succeeded!")
|
||||
Console.print("")
|
||||
|
||||
// Get string field
|
||||
Console.print("Extracting fields:")
|
||||
match Json.get(json, "name") {
|
||||
Some(nameJson) => match Json.asString(nameJson) {
|
||||
Some(n) => Console.print(" name: " + n),
|
||||
None => Console.print(" name: (not a string)")
|
||||
},
|
||||
None => Console.print(" name: (not found)")
|
||||
}
|
||||
|
||||
// Get int field
|
||||
match Json.get(json, "age") {
|
||||
Some(ageJson) => match Json.asInt(ageJson) {
|
||||
Some(a) => Console.print(" age: " + toString(a)),
|
||||
None => Console.print(" age: (not an int)")
|
||||
},
|
||||
None => Console.print(" age: (not found)")
|
||||
}
|
||||
|
||||
// Get bool field
|
||||
match Json.get(json, "active") {
|
||||
Some(activeJson) => match Json.asBool(activeJson) {
|
||||
Some(a) => Console.print(" active: " + toString(a)),
|
||||
None => Console.print(" active: (not a bool)")
|
||||
},
|
||||
None => Console.print(" active: (not found)")
|
||||
}
|
||||
|
||||
// Get array field
|
||||
match Json.get(json, "scores") {
|
||||
Some(scoresJson) => match Json.asArray(scoresJson) {
|
||||
Some(arr) => {
|
||||
Console.print(" scores: " + toString(List.length(arr)) + " items")
|
||||
// Get first score
|
||||
match Json.getIndex(scoresJson, 0) {
|
||||
Some(firstJson) => match Json.asInt(firstJson) {
|
||||
Some(first) => Console.print(" first score: " + toString(first)),
|
||||
None => Console.print(" first score: (not an int)")
|
||||
},
|
||||
None => Console.print(" (no first element)")
|
||||
}
|
||||
},
|
||||
None => Console.print(" scores: (not an array)")
|
||||
},
|
||||
None => Console.print(" scores: (not found)")
|
||||
}
|
||||
Console.print("")
|
||||
|
||||
// Get the keys
|
||||
Console.print("Object keys:")
|
||||
match Json.keys(json) {
|
||||
Some(ks) => Console.print(" " + String.join(ks, ", ")),
|
||||
None => Console.print(" (not an object)")
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("Parse error: " + e)
|
||||
}
|
||||
|
||||
Ok(json) => {
|
||||
Console.print("Parse succeeded!")
|
||||
Console.print("")
|
||||
Console.print("Extracting fields:")
|
||||
match Json.get(json, "name") {
|
||||
Some(nameJson) => match Json.asString(nameJson) {
|
||||
Some(n) => Console.print(" name: " + n),
|
||||
None => Console.print(" name: (not a string)"),
|
||||
},
|
||||
None => Console.print(" name: (not found)"),
|
||||
}
|
||||
match Json.get(json, "age") {
|
||||
Some(ageJson) => match Json.asInt(ageJson) {
|
||||
Some(a) => Console.print(" age: " + toString(a)),
|
||||
None => Console.print(" age: (not an int)"),
|
||||
},
|
||||
None => Console.print(" age: (not found)"),
|
||||
}
|
||||
match Json.get(json, "active") {
|
||||
Some(activeJson) => match Json.asBool(activeJson) {
|
||||
Some(a) => Console.print(" active: " + toString(a)),
|
||||
None => Console.print(" active: (not a bool)"),
|
||||
},
|
||||
None => Console.print(" active: (not found)"),
|
||||
}
|
||||
match Json.get(json, "scores") {
|
||||
Some(scoresJson) => match Json.asArray(scoresJson) {
|
||||
Some(arr) => {
|
||||
Console.print(" scores: " + toString(List.length(arr)) + " items")
|
||||
match Json.getIndex(scoresJson, 0) {
|
||||
Some(firstJson) => match Json.asInt(firstJson) {
|
||||
Some(first) => Console.print(" first score: " + toString(first)),
|
||||
None => Console.print(" first score: (not an int)"),
|
||||
},
|
||||
None => Console.print(" (no first element)"),
|
||||
}
|
||||
},
|
||||
None => Console.print(" scores: (not an array)"),
|
||||
},
|
||||
None => Console.print(" scores: (not found)"),
|
||||
}
|
||||
Console.print("")
|
||||
Console.print("Object keys:")
|
||||
match Json.keys(json) {
|
||||
Some(ks) => Console.print(" " + String.join(ks, ", ")),
|
||||
None => Console.print(" (not an object)"),
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("Parse error: " + e),
|
||||
}
|
||||
Console.print("")
|
||||
Console.print("=== JSON Null Check ===")
|
||||
let nullVal = Json.null()
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
// Main program that imports modules
|
||||
import examples/modules/math_utils
|
||||
import examples/modules/string_utils
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Testing Module Imports ===")
|
||||
|
||||
// Use math_utils
|
||||
Console.print("square(5) = " + toString(math_utils.square(5)))
|
||||
Console.print("cube(3) = " + toString(math_utils.cube(3)))
|
||||
Console.print("factorial(6) = " + toString(math_utils.factorial(6)))
|
||||
Console.print("sumRange(1, 10) = " + toString(math_utils.sumRange(1, 10)))
|
||||
|
||||
// Use string_utils
|
||||
Console.print(string_utils.greet("World"))
|
||||
Console.print(string_utils.exclaim("Modules work"))
|
||||
Console.print("repeat(\"ab\", 3) = " + string_utils.repeat("ab", 3))
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
// Test selective imports
|
||||
import examples/modules/math_utils.{square, factorial}
|
||||
import examples/modules/string_utils as str
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Selective & Aliased Imports ===")
|
||||
|
||||
// Direct imports (no module prefix)
|
||||
Console.print("square(7) = " + toString(square(7)))
|
||||
Console.print("factorial(5) = " + toString(factorial(5)))
|
||||
|
||||
// Aliased import
|
||||
Console.print(str.greet("Lux"))
|
||||
Console.print(str.exclaim("Aliased imports work"))
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
// Test wildcard imports
|
||||
import examples/modules/math_utils.*
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Wildcard Imports ===")
|
||||
|
||||
// All functions available directly
|
||||
Console.print("square(4) = " + toString(square(4)))
|
||||
Console.print("cube(4) = " + toString(cube(4)))
|
||||
Console.print("factorial(4) = " + toString(factorial(4)))
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
// Math utilities module
|
||||
// Exports: square, cube, factorial
|
||||
fn square(n: Int): Int = n * n
|
||||
|
||||
pub fn square(n: Int): Int = n * n
|
||||
fn cube(n: Int): Int = n * n * n
|
||||
|
||||
pub fn cube(n: Int): Int = n * n * n
|
||||
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
|
||||
|
||||
pub fn factorial(n: Int): Int =
|
||||
if n <= 1 then 1
|
||||
else n * factorial(n - 1)
|
||||
|
||||
pub fn sumRange(start: Int, end: Int): Int =
|
||||
if start > end then 0
|
||||
else start + sumRange(start + 1, end)
|
||||
fn sumRange(start: Int, end: Int): Int = if start > end then 0 else start + sumRange(start + 1, end)
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
// String utilities module
|
||||
// Exports: repeat, exclaim
|
||||
fn repeat(s: String, n: Int): String = if n <= 0 then "" else s + repeat(s, n - 1)
|
||||
|
||||
pub fn repeat(s: String, n: Int): String =
|
||||
if n <= 0 then ""
|
||||
else s + repeat(s, n - 1)
|
||||
fn exclaim(s: String): String = s + "!"
|
||||
|
||||
pub fn exclaim(s: String): String = s + "!"
|
||||
|
||||
pub fn greet(name: String): String =
|
||||
"Hello, " + name + "!"
|
||||
fn greet(name: String): String = "Hello, " + name + "!"
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
// Example using the standard library
|
||||
import std/prelude.*
|
||||
import std/option as opt
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Using Standard Library ===")
|
||||
|
||||
// Prelude functions
|
||||
Console.print("identity(42) = " + toString(identity(42)))
|
||||
Console.print("not(true) = " + toString(not(true)))
|
||||
Console.print("and(true, false) = " + toString(and(true, false)))
|
||||
Console.print("or(true, false) = " + toString(or(true, false)))
|
||||
|
||||
// Option utilities
|
||||
let x = opt.some(10)
|
||||
let y = opt.none()
|
||||
Console.print("isSome(Some(10)) = " + toString(opt.isSome(x)))
|
||||
|
||||
@@ -1,47 +1,31 @@
|
||||
// Demonstrating the pipe operator and functional data processing
|
||||
//
|
||||
// Expected output:
|
||||
// 5 |> double |> addTen |> square = 400
|
||||
// Pipeline result2 = 42
|
||||
// process(1) = 144
|
||||
// process(2) = 196
|
||||
// process(3) = 256
|
||||
// clamped = 0
|
||||
// composed = 121
|
||||
|
||||
// Basic transformations
|
||||
fn double(x: Int): Int = x * 2
|
||||
|
||||
fn addTen(x: Int): Int = x + 10
|
||||
|
||||
fn square(x: Int): Int = x * x
|
||||
|
||||
fn negate(x: Int): Int = -x
|
||||
|
||||
// Using the pipe operator for data transformation
|
||||
let result1 = 5 |> double |> addTen |> square
|
||||
let result1 = square(addTen(double(5)))
|
||||
|
||||
// Chaining multiple operations
|
||||
let result2 = 3 |> double |> addTen |> double |> addTen
|
||||
let result2 = addTen(double(addTen(double(3))))
|
||||
|
||||
// More complex pipelines
|
||||
fn process(n: Int): Int =
|
||||
n |> double |> addTen |> square
|
||||
fn process(n: Int): Int = square(addTen(double(n)))
|
||||
|
||||
// Multiple values through same pipeline
|
||||
let a = process(1)
|
||||
|
||||
let b = process(2)
|
||||
|
||||
let c = process(3)
|
||||
|
||||
// Conditional in pipeline
|
||||
fn clampPositive(x: Int): Int =
|
||||
if x < 0 then 0 else x
|
||||
fn clampPositive(x: Int): Int = if x < 0 then 0 else x
|
||||
|
||||
let clamped = -5 |> double |> clampPositive
|
||||
let clamped = clampPositive(double(-5))
|
||||
|
||||
// Function composition using pipe
|
||||
fn increment(x: Int): Int = x + 1
|
||||
|
||||
let composed = 5 |> double |> increment |> square
|
||||
let composed = square(increment(double(5)))
|
||||
|
||||
// Print results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("5 |> double |> addTen |> square = " + toString(result1))
|
||||
Console.print("Pipeline result2 = " + toString(result2))
|
||||
|
||||
@@ -1,72 +1,42 @@
|
||||
// PostgreSQL Database Example
|
||||
//
|
||||
// Demonstrates the Postgres effect for database operations.
|
||||
//
|
||||
// Prerequisites:
|
||||
// - PostgreSQL server running locally
|
||||
// - Database 'testdb' created
|
||||
// - User 'testuser' with password 'testpass'
|
||||
//
|
||||
// To set up:
|
||||
// createdb testdb
|
||||
// psql testdb -c "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT, email TEXT);"
|
||||
//
|
||||
// Run with: lux examples/postgres_demo.lux
|
||||
fn jsonStr(key: String, value: String): String = "\"" + key + "\":\"" + value + "\""
|
||||
|
||||
// ============================================================
|
||||
// Helper Functions
|
||||
// ============================================================
|
||||
fn jsonNum(key: String, value: Int): String = "\"" + key + "\":" + toString(value)
|
||||
|
||||
fn jsonStr(key: String, value: String): String =
|
||||
"\"" + key + "\":\"" + value + "\""
|
||||
fn jsonObj(content: String): String = toString(" + content + ")
|
||||
|
||||
fn jsonNum(key: String, value: Int): String =
|
||||
"\"" + key + "\":" + toString(value)
|
||||
|
||||
fn jsonObj(content: String): String =
|
||||
"{" + content + "}"
|
||||
|
||||
// ============================================================
|
||||
// Database Operations
|
||||
// ============================================================
|
||||
|
||||
// Insert a user
|
||||
fn insertUser(connId: Int, name: String, email: String): Int with {Console, Postgres} = {
|
||||
let sql = "INSERT INTO users (name, email) VALUES ('" + name + "', '" + email + "') RETURNING id"
|
||||
Console.print("Inserting user: " + name)
|
||||
match Postgres.queryOne(connId, sql) {
|
||||
Some(row) => {
|
||||
Console.print(" Inserted with ID: " + toString(row.id))
|
||||
row.id
|
||||
},
|
||||
None => {
|
||||
Console.print(" Insert failed")
|
||||
-1
|
||||
}
|
||||
}
|
||||
Some(row) => {
|
||||
Console.print(" Inserted with ID: " + toString(row.id))
|
||||
row.id
|
||||
},
|
||||
None => {
|
||||
Console.print(" Insert failed")
|
||||
-1
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Get all users
|
||||
fn getUsers(connId: Int): Unit with {Console, Postgres} = {
|
||||
Console.print("Fetching all users...")
|
||||
let rows = Postgres.query(connId, "SELECT id, name, email FROM users ORDER BY id")
|
||||
Console.print(" Found " + toString(List.length(rows)) + " users:")
|
||||
List.forEach(rows, fn(row: { id: Int, name: String, email: String }): Unit with {Console} => {
|
||||
Console.print(" - " + toString(row.id) + ": " + row.name + " <" + row.email + ">")
|
||||
})
|
||||
List.forEach(rows, fn(row: { id: Int, name: String, email: String }): Unit => {
|
||||
Console.print(" - " + toString(row.id) + ": " + row.name + " <" + row.email + ">")
|
||||
})
|
||||
}
|
||||
|
||||
// Get user by ID
|
||||
fn getUserById(connId: Int, id: Int): Unit with {Console, Postgres} = {
|
||||
let sql = "SELECT id, name, email FROM users WHERE id = " + toString(id)
|
||||
Console.print("Looking up user " + toString(id) + "...")
|
||||
match Postgres.queryOne(connId, sql) {
|
||||
Some(row) => Console.print(" Found: " + row.name + " <" + row.email + ">"),
|
||||
None => Console.print(" User not found")
|
||||
}
|
||||
Some(row) => Console.print(" Found: " + row.name + " <" + row.email + ">"),
|
||||
None => Console.print(" User not found"),
|
||||
}
|
||||
}
|
||||
|
||||
// Update user email
|
||||
fn updateUserEmail(connId: Int, id: Int, newEmail: String): Unit with {Console, Postgres} = {
|
||||
let sql = "UPDATE users SET email = '" + newEmail + "' WHERE id = " + toString(id)
|
||||
Console.print("Updating user " + toString(id) + " email to " + newEmail)
|
||||
@@ -74,7 +44,6 @@ fn updateUserEmail(connId: Int, id: Int, newEmail: String): Unit with {Console,
|
||||
Console.print(" Rows affected: " + toString(affected))
|
||||
}
|
||||
|
||||
// Delete user
|
||||
fn deleteUser(connId: Int, id: Int): Unit with {Console, Postgres} = {
|
||||
let sql = "DELETE FROM users WHERE id = " + toString(id)
|
||||
Console.print("Deleting user " + toString(id))
|
||||
@@ -82,104 +51,63 @@ fn deleteUser(connId: Int, id: Int): Unit with {Console, Postgres} = {
|
||||
Console.print(" Rows affected: " + toString(affected))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Transaction Example
|
||||
// ============================================================
|
||||
|
||||
fn transactionDemo(connId: Int): Unit with {Console, Postgres} = {
|
||||
Console.print("")
|
||||
Console.print("=== Transaction Demo ===")
|
||||
|
||||
// Start transaction
|
||||
Console.print("Beginning transaction...")
|
||||
Postgres.beginTx(connId)
|
||||
|
||||
// Make some changes
|
||||
insertUser(connId, "TxUser1", "tx1@example.com")
|
||||
insertUser(connId, "TxUser2", "tx2@example.com")
|
||||
|
||||
// Show users before commit
|
||||
Console.print("Users before commit:")
|
||||
getUsers(connId)
|
||||
|
||||
// Commit the transaction
|
||||
Console.print("Committing transaction...")
|
||||
Postgres.commit(connId)
|
||||
|
||||
Console.print("Transaction committed!")
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Main
|
||||
// ============================================================
|
||||
|
||||
fn main(): Unit with {Console, Postgres} = {
|
||||
Console.print("========================================")
|
||||
Console.print(" PostgreSQL Demo")
|
||||
Console.print("========================================")
|
||||
Console.print("")
|
||||
|
||||
// Connect to database
|
||||
Console.print("Connecting to PostgreSQL...")
|
||||
let connStr = "host=localhost user=testuser password=testpass dbname=testdb"
|
||||
let connId = Postgres.connect(connStr)
|
||||
Console.print("Connected! Connection ID: " + toString(connId))
|
||||
Console.print("")
|
||||
|
||||
// Create table if not exists
|
||||
Console.print("Creating users table...")
|
||||
Postgres.execute(connId, "CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL)")
|
||||
Console.print("")
|
||||
|
||||
// Clear table for demo
|
||||
Console.print("Clearing existing data...")
|
||||
Postgres.execute(connId, "DELETE FROM users")
|
||||
Console.print("")
|
||||
|
||||
// Insert some users
|
||||
Console.print("=== Inserting Users ===")
|
||||
let id1 = insertUser(connId, "Alice", "alice@example.com")
|
||||
let id2 = insertUser(connId, "Bob", "bob@example.com")
|
||||
let id3 = insertUser(connId, "Charlie", "charlie@example.com")
|
||||
Console.print("")
|
||||
|
||||
// Query all users
|
||||
Console.print("=== All Users ===")
|
||||
getUsers(connId)
|
||||
Console.print("")
|
||||
|
||||
// Query single user
|
||||
Console.print("=== Single User Lookup ===")
|
||||
getUserById(connId, id2)
|
||||
Console.print("")
|
||||
|
||||
// Update user
|
||||
Console.print("=== Update User ===")
|
||||
updateUserEmail(connId, id2, "bob.new@example.com")
|
||||
getUserById(connId, id2)
|
||||
Console.print("")
|
||||
|
||||
// Delete user
|
||||
Console.print("=== Delete User ===")
|
||||
deleteUser(connId, id3)
|
||||
getUsers(connId)
|
||||
Console.print("")
|
||||
|
||||
// Transaction demo
|
||||
transactionDemo(connId)
|
||||
Console.print("")
|
||||
|
||||
// Final state
|
||||
Console.print("=== Final State ===")
|
||||
getUsers(connId)
|
||||
Console.print("")
|
||||
|
||||
// Close connection
|
||||
Console.print("Closing connection...")
|
||||
Postgres.close(connId)
|
||||
Console.print("Done!")
|
||||
}
|
||||
|
||||
// Note: This will fail if PostgreSQL is not running
|
||||
// To test the syntax only, you can comment out the last line
|
||||
let output = run main() with {}
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
// Property-Based Testing Example
|
||||
//
|
||||
// This example demonstrates property-based testing in Lux,
|
||||
// where we verify properties hold for randomly generated inputs.
|
||||
//
|
||||
// Run with: lux examples/property_testing.lux
|
||||
|
||||
// ============================================================
|
||||
// Generator Functions (using Random effect)
|
||||
// ============================================================
|
||||
|
||||
let CHARS = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
fn genInt(min: Int, max: Int): Int with {Random} =
|
||||
Random.int(min, max)
|
||||
fn genInt(min: Int, max: Int): Int with {Random} = Random.int(min, max)
|
||||
|
||||
fn genIntList(min: Int, max: Int, maxLen: Int): List<Int> with {Random} = {
|
||||
let len = Random.int(0, maxLen)
|
||||
@@ -20,10 +8,7 @@ fn genIntList(min: Int, max: Int, maxLen: Int): List<Int> with {Random} = {
|
||||
}
|
||||
|
||||
fn genIntListHelper(min: Int, max: Int, len: Int): List<Int> with {Random} = {
|
||||
if len <= 0 then
|
||||
[]
|
||||
else
|
||||
List.concat([Random.int(min, max)], genIntListHelper(min, max, len - 1))
|
||||
if len <= 0 then [] else List.concat([Random.int(min, max)], genIntListHelper(min, max, len - 1))
|
||||
}
|
||||
|
||||
fn genChar(): String with {Random} = {
|
||||
@@ -37,195 +22,147 @@ fn genString(maxLen: Int): String with {Random} = {
|
||||
}
|
||||
|
||||
fn genStringHelper(len: Int): String with {Random} = {
|
||||
if len <= 0 then
|
||||
""
|
||||
else
|
||||
genChar() + genStringHelper(len - 1)
|
||||
if len <= 0 then "" else genChar() + genStringHelper(len - 1)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Test Runner State
|
||||
// ============================================================
|
||||
|
||||
fn printResult(name: String, passed: Bool, count: Int): Unit with {Console} = {
|
||||
if passed then
|
||||
Console.print(" PASS " + name + " (" + toString(count) + " tests)")
|
||||
else
|
||||
Console.print(" FAIL " + name)
|
||||
if passed then Console.print(" PASS " + name + " (" + toString(count) + " tests)") else Console.print(" FAIL " + name)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Property Tests
|
||||
// ============================================================
|
||||
|
||||
// Test: List reverse is involutive
|
||||
fn testReverseInvolutive(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("reverse(reverse(xs)) == xs", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.reverse(List.reverse(xs)) == xs then
|
||||
testReverseInvolutive(n - 1, count)
|
||||
else {
|
||||
printResult("reverse(reverse(xs)) == xs", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
printResult("reverse(reverse(xs)) == xs", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.reverse(List.reverse(xs)) == xs then testReverseInvolutive(n - 1, count) else {
|
||||
printResult("reverse(reverse(xs)) == xs", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: List reverse preserves length
|
||||
fn testReverseLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("length(reverse(xs)) == length(xs)", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.length(List.reverse(xs)) == List.length(xs) then
|
||||
testReverseLength(n - 1, count)
|
||||
else {
|
||||
printResult("length(reverse(xs)) == length(xs)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
printResult("length(reverse(xs)) == length(xs)", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.length(List.reverse(xs)) == List.length(xs) then testReverseLength(n - 1, count) else {
|
||||
printResult("length(reverse(xs)) == length(xs)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: List map preserves length
|
||||
fn testMapLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("length(map(xs, f)) == length(xs)", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.length(List.map(xs, fn(x) => x * 2)) == List.length(xs) then
|
||||
testMapLength(n - 1, count)
|
||||
else {
|
||||
printResult("length(map(xs, f)) == length(xs)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
printResult("length(map(xs, f)) == length(xs)", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.length(List.map(xs, fn(x: _) => x * 2)) == List.length(xs) then testMapLength(n - 1, count) else {
|
||||
printResult("length(map(xs, f)) == length(xs)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: List concat length is sum
|
||||
fn testConcatLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("length(xs ++ ys) == length(xs) + length(ys)", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 50, 10)
|
||||
let ys = genIntList(0, 50, 10)
|
||||
if List.length(List.concat(xs, ys)) == List.length(xs) + List.length(ys) then
|
||||
testConcatLength(n - 1, count)
|
||||
else {
|
||||
printResult("length(xs ++ ys) == length(xs) + length(ys)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
printResult("length(xs ++ ys) == length(xs) + length(ys)", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 50, 10)
|
||||
let ys = genIntList(0, 50, 10)
|
||||
if List.length(List.concat(xs, ys)) == List.length(xs) + List.length(ys) then testConcatLength(n - 1, count) else {
|
||||
printResult("length(xs ++ ys) == length(xs) + length(ys)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Addition is commutative
|
||||
fn testAddCommutative(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("a + b == b + a", true, count)
|
||||
true
|
||||
} else {
|
||||
let a = genInt(-1000, 1000)
|
||||
let b = genInt(-1000, 1000)
|
||||
if a + b == b + a then
|
||||
testAddCommutative(n - 1, count)
|
||||
else {
|
||||
printResult("a + b == b + a", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
printResult("a + b == b + a", true, count)
|
||||
true
|
||||
} else {
|
||||
let a = genInt(-1000, 1000)
|
||||
let b = genInt(-1000, 1000)
|
||||
if a + b == b + a then testAddCommutative(n - 1, count) else {
|
||||
printResult("a + b == b + a", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Multiplication is associative
|
||||
fn testMulAssociative(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("(a * b) * c == a * (b * c)", true, count)
|
||||
true
|
||||
} else {
|
||||
let a = genInt(-100, 100)
|
||||
let b = genInt(-100, 100)
|
||||
let c = genInt(-100, 100)
|
||||
if (a * b) * c == a * (b * c) then
|
||||
testMulAssociative(n - 1, count)
|
||||
else {
|
||||
printResult("(a * b) * c == a * (b * c)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
printResult("(a * b) * c == a * (b * c)", true, count)
|
||||
true
|
||||
} else {
|
||||
let a = genInt(-100, 100)
|
||||
let b = genInt(-100, 100)
|
||||
let c = genInt(-100, 100)
|
||||
if a * b * c == a * b * c then testMulAssociative(n - 1, count) else {
|
||||
printResult("(a * b) * c == a * (b * c)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: String concat length is sum
|
||||
fn testStringConcatLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("length(s1 + s2) == length(s1) + length(s2)", true, count)
|
||||
true
|
||||
} else {
|
||||
let s1 = genString(10)
|
||||
let s2 = genString(10)
|
||||
if String.length(s1 + s2) == String.length(s1) + String.length(s2) then
|
||||
testStringConcatLength(n - 1, count)
|
||||
else {
|
||||
printResult("length(s1 + s2) == length(s1) + length(s2)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
printResult("length(s1 + s2) == length(s1) + length(s2)", true, count)
|
||||
true
|
||||
} else {
|
||||
let s1 = genString(10)
|
||||
let s2 = genString(10)
|
||||
if String.length(s1 + s2) == String.length(s1) + String.length(s2) then testStringConcatLength(n - 1, count) else {
|
||||
printResult("length(s1 + s2) == length(s1) + length(s2)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Zero is identity for addition
|
||||
fn testAddIdentity(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("x + 0 == x && 0 + x == x", true, count)
|
||||
true
|
||||
} else {
|
||||
let x = genInt(-10000, 10000)
|
||||
if x + 0 == x && 0 + x == x then
|
||||
testAddIdentity(n - 1, count)
|
||||
else {
|
||||
printResult("x + 0 == x && 0 + x == x", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
printResult("x + 0 == x && 0 + x == x", true, count)
|
||||
true
|
||||
} else {
|
||||
let x = genInt(-10000, 10000)
|
||||
if x + 0 == x && 0 + x == x then testAddIdentity(n - 1, count) else {
|
||||
printResult("x + 0 == x && 0 + x == x", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Filter reduces or maintains length
|
||||
fn testFilterLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("length(filter(xs, p)) <= length(xs)", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.length(List.filter(xs, fn(x) => x > 50)) <= List.length(xs) then
|
||||
testFilterLength(n - 1, count)
|
||||
else {
|
||||
printResult("length(filter(xs, p)) <= length(xs)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
printResult("length(filter(xs, p)) <= length(xs)", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.length(List.filter(xs, fn(x: _) => x > 50)) <= List.length(xs) then testFilterLength(n - 1, count) else {
|
||||
printResult("length(filter(xs, p)) <= length(xs)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Empty list is identity for concat
|
||||
fn testConcatIdentity(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("concat(xs, []) == xs && concat([], xs) == xs", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 10)
|
||||
if List.concat(xs, []) == xs && List.concat([], xs) == xs then
|
||||
testConcatIdentity(n - 1, count)
|
||||
else {
|
||||
printResult("concat(xs, []) == xs && concat([], xs) == xs", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
printResult("concat(xs, []) == xs && concat([], xs) == xs", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 10)
|
||||
if List.concat(xs, []) == xs && List.concat([], xs) == xs then testConcatIdentity(n - 1, count) else {
|
||||
printResult("concat(xs, []) == xs && concat([], xs) == xs", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Main
|
||||
// ============================================================
|
||||
|
||||
fn main(): Unit with {Console, Random} = {
|
||||
Console.print("========================================")
|
||||
@@ -234,7 +171,6 @@ fn main(): Unit with {Console, Random} = {
|
||||
Console.print("")
|
||||
Console.print("Running 100 iterations per property...")
|
||||
Console.print("")
|
||||
|
||||
testReverseInvolutive(100, 100)
|
||||
testReverseLength(100, 100)
|
||||
testMapLength(100, 100)
|
||||
@@ -245,7 +181,6 @@ fn main(): Unit with {Console, Random} = {
|
||||
testAddIdentity(100, 100)
|
||||
testFilterLength(100, 100)
|
||||
testConcatIdentity(100, 100)
|
||||
|
||||
Console.print("")
|
||||
Console.print("========================================")
|
||||
Console.print(" All property tests completed!")
|
||||
|
||||
@@ -1,39 +1,22 @@
|
||||
// Demonstrating Random and Time effects in Lux
|
||||
//
|
||||
// Expected output (values will vary):
|
||||
// Rolling dice...
|
||||
// Die 1: <random 1-6>
|
||||
// Die 2: <random 1-6>
|
||||
// Die 3: <random 1-6>
|
||||
// Coin flip: <true/false>
|
||||
// Random float: <0.0-1.0>
|
||||
// Current time: <timestamp>
|
||||
|
||||
// Roll a single die (1-6)
|
||||
fn rollDie(): Int with {Random} = Random.int(1, 6)
|
||||
|
||||
// Roll multiple dice and print results
|
||||
fn rollDice(count: Int): Unit with {Random, Console} = {
|
||||
if count > 0 then {
|
||||
let value = rollDie()
|
||||
Console.print("Die " + toString(4 - count) + ": " + toString(value))
|
||||
rollDice(count - 1)
|
||||
} else {
|
||||
()
|
||||
}
|
||||
let value = rollDie()
|
||||
Console.print("Die " + toString(4 - count) + ": " + toString(value))
|
||||
rollDice(count - 1)
|
||||
} else {
|
||||
()
|
||||
}
|
||||
}
|
||||
|
||||
// Main function demonstrating random effects
|
||||
fn main(): Unit with {Random, Console, Time} = {
|
||||
Console.print("Rolling dice...")
|
||||
rollDice(3)
|
||||
|
||||
let coin = Random.bool()
|
||||
Console.print("Coin flip: " + toString(coin))
|
||||
|
||||
let f = Random.float()
|
||||
Console.print("Random float: " + toString(f))
|
||||
|
||||
let now = Time.now()
|
||||
Console.print("Current time: " + toString(now))
|
||||
}
|
||||
|
||||
@@ -1,67 +1,41 @@
|
||||
// Schema Evolution Demo
|
||||
// Demonstrates version tracking and automatic migrations
|
||||
|
||||
// ============================================================
|
||||
// PART 1: Type-Declared Migrations
|
||||
// ============================================================
|
||||
|
||||
// Define a versioned type with a migration from v1 to v2
|
||||
type User @v2 {
|
||||
type User = {
|
||||
name: String,
|
||||
email: String,
|
||||
|
||||
// Migration from v1: add default email
|
||||
from @v1 = { name: old.name, email: "unknown@example.com" }
|
||||
}
|
||||
|
||||
// Create a v1 user
|
||||
let v1_user = Schema.versioned("User", 1, { name: "Alice" })
|
||||
let v1_version = Schema.getVersion(v1_user) // 1
|
||||
|
||||
// Migrate to v2 - uses the declared migration automatically
|
||||
let v1_version = Schema.getVersion(v1_user)
|
||||
|
||||
let v2_user = Schema.migrate(v1_user, 2)
|
||||
let v2_version = Schema.getVersion(v2_user) // 2
|
||||
|
||||
// ============================================================
|
||||
// PART 2: Runtime Schema Operations (separate type)
|
||||
// ============================================================
|
||||
let v2_version = Schema.getVersion(v2_user)
|
||||
|
||||
// Create versioned values for a different type (no migration)
|
||||
let config1 = Schema.versioned("Config", 1, "debug")
|
||||
|
||||
let config2 = Schema.versioned("Config", 2, "release")
|
||||
|
||||
// Check versions
|
||||
let c1 = Schema.getVersion(config1) // 1
|
||||
let c2 = Schema.getVersion(config2) // 2
|
||||
let c1 = Schema.getVersion(config1)
|
||||
|
||||
let c2 = Schema.getVersion(config2)
|
||||
|
||||
// Migrate config (auto-migration since no explicit migration defined)
|
||||
let upgradedConfig = Schema.migrate(config1, 2)
|
||||
let upgradedConfigVersion = Schema.getVersion(upgradedConfig) // 2
|
||||
|
||||
// ============================================================
|
||||
// PART 2: Practical Example - API Versioning
|
||||
// ============================================================
|
||||
let upgradedConfigVersion = Schema.getVersion(upgradedConfig)
|
||||
|
||||
// Simulate different API response versions
|
||||
fn createResponseV1(data: String): { version: Int, payload: String } =
|
||||
{ version: 1, payload: data }
|
||||
fn createResponseV1(data: String): { version: Int, payload: String } = { version: 1, payload: data }
|
||||
|
||||
fn createResponseV2(data: String, timestamp: Int): { version: Int, payload: String, meta: { ts: Int } } =
|
||||
{ version: 2, payload: data, meta: { ts: timestamp } }
|
||||
fn createResponseV2(data: String, timestamp: Int): { version: Int, payload: String, meta: { ts: Int } } = { version: 2, payload: data, meta: { ts: timestamp } }
|
||||
|
||||
// Version-aware processing
|
||||
fn getPayload(response: { version: Int, payload: String }): String =
|
||||
response.payload
|
||||
fn getPayload(response: { version: Int, payload: String }): String = response.payload
|
||||
|
||||
let resp1 = createResponseV1("Hello")
|
||||
|
||||
let resp2 = createResponseV2("World", 1234567890)
|
||||
|
||||
let payload1 = getPayload(resp1)
|
||||
let payload2 = resp2.payload
|
||||
|
||||
// ============================================================
|
||||
// RESULTS
|
||||
// ============================================================
|
||||
let payload2 = resp2.payload
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Schema Evolution Demo ===")
|
||||
|
||||
@@ -1,58 +1,43 @@
|
||||
// Shell/Process example - demonstrates the Process effect
|
||||
//
|
||||
// This script runs shell commands and uses environment variables
|
||||
|
||||
fn main(): Unit with {Process, Console} = {
|
||||
Console.print("=== Lux Shell Example ===")
|
||||
Console.print("")
|
||||
|
||||
// Get current working directory
|
||||
let cwd = Process.cwd()
|
||||
Console.print("Current directory: " + cwd)
|
||||
Console.print("")
|
||||
|
||||
// Get environment variables
|
||||
Console.print("Environment variables:")
|
||||
match Process.env("USER") {
|
||||
Some(user) => Console.print(" USER: " + user),
|
||||
None => Console.print(" USER: (not set)")
|
||||
}
|
||||
Some(user) => Console.print(" USER: " + user),
|
||||
None => Console.print(" USER: (not set)"),
|
||||
}
|
||||
match Process.env("HOME") {
|
||||
Some(home) => Console.print(" HOME: " + home),
|
||||
None => Console.print(" HOME: (not set)")
|
||||
}
|
||||
Some(home) => Console.print(" HOME: " + home),
|
||||
None => Console.print(" HOME: (not set)"),
|
||||
}
|
||||
match Process.env("SHELL") {
|
||||
Some(shell) => Console.print(" SHELL: " + shell),
|
||||
None => Console.print(" SHELL: (not set)")
|
||||
}
|
||||
Some(shell) => Console.print(" SHELL: " + shell),
|
||||
None => Console.print(" SHELL: (not set)"),
|
||||
}
|
||||
Console.print("")
|
||||
|
||||
// Run shell commands
|
||||
Console.print("Running shell commands:")
|
||||
|
||||
let date = Process.exec("date")
|
||||
Console.print(" date: " + String.trim(date))
|
||||
|
||||
let kernel = Process.exec("uname -r")
|
||||
Console.print(" kernel: " + String.trim(kernel))
|
||||
|
||||
let files = Process.exec("ls examples/*.lux | wc -l")
|
||||
Console.print(" .lux files in examples/: " + String.trim(files))
|
||||
Console.print("")
|
||||
|
||||
// Command line arguments
|
||||
Console.print("Command line arguments:")
|
||||
let args = Process.args()
|
||||
let argCount = List.length(args)
|
||||
if argCount == 0 then {
|
||||
Console.print(" (no arguments)")
|
||||
} else {
|
||||
Console.print(" Count: " + toString(argCount))
|
||||
match List.head(args) {
|
||||
Some(first) => Console.print(" First: " + first),
|
||||
None => Console.print(" First: (empty)")
|
||||
}
|
||||
}
|
||||
Console.print(" (no arguments)")
|
||||
} else {
|
||||
Console.print(" Count: " + toString(argCount))
|
||||
match List.head(args) {
|
||||
Some(first) => Console.print(" First: " + first),
|
||||
None => Console.print(" First: (empty)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = run main() with {}
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
// The "Ask" Pattern - Resumable Effects
|
||||
//
|
||||
// Unlike exceptions which unwind the stack, effect handlers can
|
||||
// RESUME with a value. This enables "ask the environment" patterns.
|
||||
//
|
||||
// Expected output:
|
||||
// Need config: api_url
|
||||
// Got: https://api.example.com
|
||||
// Need config: timeout
|
||||
// Got: 30
|
||||
// Configured with url=https://api.example.com, timeout=30
|
||||
|
||||
effect Config {
|
||||
fn get(key: String): String
|
||||
}
|
||||
@@ -25,14 +13,13 @@ fn configure(): String with {Config, Console} = {
|
||||
}
|
||||
|
||||
handler envConfig: Config {
|
||||
fn get(key) =
|
||||
if key == "api_url" then resume("https://api.example.com")
|
||||
else if key == "timeout" then resume("30")
|
||||
else resume("unknown")
|
||||
fn get(key) = if key == "api_url" then resume("https://api.example.com") else if key == "timeout" then resume("30") else resume("unknown")
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
let result = run configure() with { Config = envConfig }
|
||||
let result = run configure() with {
|
||||
Config = envConfig,
|
||||
}
|
||||
Console.print(result)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
// Custom Logging with Effects
|
||||
//
|
||||
// This demonstrates how effects let you abstract side effects.
|
||||
// The same code can be run with different logging implementations.
|
||||
//
|
||||
// Expected output:
|
||||
// [INFO] Starting computation
|
||||
// [DEBUG] x = 10
|
||||
// [INFO] Processing
|
||||
// [DEBUG] result = 20
|
||||
// Final: 20
|
||||
|
||||
effect Log {
|
||||
fn info(msg: String): Unit
|
||||
fn debug(msg: String): Unit
|
||||
@@ -26,18 +14,22 @@ fn computation(): Int with {Log} = {
|
||||
}
|
||||
|
||||
handler consoleLogger: Log {
|
||||
fn info(msg) = {
|
||||
Console.print("[INFO] " + msg)
|
||||
resume(())
|
||||
}
|
||||
fn debug(msg) = {
|
||||
Console.print("[DEBUG] " + msg)
|
||||
resume(())
|
||||
}
|
||||
fn info(msg) =
|
||||
{
|
||||
Console.print("[INFO] " + msg)
|
||||
resume(())
|
||||
}
|
||||
fn debug(msg) =
|
||||
{
|
||||
Console.print("[DEBUG] " + msg)
|
||||
resume(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
let result = run computation() with { Log = consoleLogger }
|
||||
let result = run computation() with {
|
||||
Log = consoleLogger,
|
||||
}
|
||||
Console.print("Final: " + toString(result))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,18 @@
|
||||
// Early Return with Fail Effect
|
||||
//
|
||||
// The Fail effect provides clean early termination.
|
||||
// Functions declare their failure modes in the type signature.
|
||||
//
|
||||
// Expected output:
|
||||
// Parsing "42"...
|
||||
// Result: 42
|
||||
// Parsing "100"...
|
||||
// Result: 100
|
||||
// Dividing 100 by 4...
|
||||
// Result: 25
|
||||
|
||||
fn parsePositive(s: String): Int with {Fail, Console} = {
|
||||
Console.print("Parsing \"" + s + "\"...")
|
||||
if s == "42" then 42
|
||||
else if s == "100" then 100
|
||||
else Fail.fail("Invalid number: " + s)
|
||||
if s == "42" then 42 else if s == "100" then 100 else Fail.fail("Invalid number: " + s)
|
||||
}
|
||||
|
||||
fn safeDivide(a: Int, b: Int): Int with {Fail, Console} = {
|
||||
Console.print("Dividing " + toString(a) + " by " + toString(b) + "...")
|
||||
if b == 0 then Fail.fail("Division by zero")
|
||||
else a / b
|
||||
if b == 0 then Fail.fail("Division by zero") else a / b
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
// These succeed
|
||||
let n1 = run parsePositive("42") with {}
|
||||
Console.print("Result: " + toString(n1))
|
||||
|
||||
let n2 = run parsePositive("100") with {}
|
||||
Console.print("Result: " + toString(n2))
|
||||
|
||||
let n3 = run safeDivide(100, 4) with {}
|
||||
Console.print("Result: " + toString(n3))
|
||||
}
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
// Effect Composition - Combine multiple effects cleanly
|
||||
//
|
||||
// Unlike monad transformers (which have ordering issues),
|
||||
// effects can be freely combined without boilerplate.
|
||||
// Each handler handles its own effect, ignoring others.
|
||||
//
|
||||
// Expected output:
|
||||
// [LOG] Starting computation
|
||||
// Generated: 7
|
||||
// [LOG] Processing value
|
||||
// [LOG] Done
|
||||
// Result: 14
|
||||
|
||||
effect Log {
|
||||
fn log(msg: String): Unit
|
||||
}
|
||||
@@ -30,8 +17,8 @@ handler consoleLog: Log {
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
let result = run computation() with {
|
||||
Log = consoleLog
|
||||
}
|
||||
Log = consoleLog,
|
||||
}
|
||||
Console.print("Generated: " + toString(result / 2))
|
||||
Console.print("Result: " + toString(result))
|
||||
}
|
||||
|
||||
@@ -1,38 +1,19 @@
|
||||
// Higher-Order Functions and Closures
|
||||
//
|
||||
// Functions are first-class values in Lux.
|
||||
// Closures capture their environment.
|
||||
//
|
||||
// Expected output:
|
||||
// Square of 5: 25
|
||||
// Cube of 3: 27
|
||||
// Add 10 to 5: 15
|
||||
// Add 10 to 20: 30
|
||||
// Composed: 15625 (cube(square(5)) = cube(25) = 15625)
|
||||
|
||||
fn apply(f: fn(Int): Int, x: Int): Int = f(x)
|
||||
|
||||
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int =
|
||||
fn(x: Int): Int => f(g(x))
|
||||
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int = fn(x: Int): Int => f(g(x))
|
||||
|
||||
fn square(n: Int): Int = n * n
|
||||
|
||||
fn cube(n: Int): Int = n * n * n
|
||||
|
||||
fn makeAdder(n: Int): fn(Int): Int =
|
||||
fn(x: Int): Int => x + n
|
||||
fn makeAdder(n: Int): fn(Int): Int = fn(x: Int): Int => x + n
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
// Apply functions
|
||||
Console.print("Square of 5: " + toString(apply(square, 5)))
|
||||
Console.print("Cube of 3: " + toString(apply(cube, 3)))
|
||||
|
||||
// Closures
|
||||
let add10 = makeAdder(10)
|
||||
Console.print("Add 10 to 5: " + toString(add10(5)))
|
||||
Console.print("Add 10 to 20: " + toString(add10(20)))
|
||||
|
||||
// Function composition
|
||||
let squareThenCube = compose(cube, square)
|
||||
Console.print("Composed: " + toString(squareThenCube(5)))
|
||||
}
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
// Algebraic Data Types and Pattern Matching
|
||||
//
|
||||
// Lux has powerful ADTs with exhaustive pattern matching.
|
||||
// The type system ensures all cases are handled.
|
||||
//
|
||||
// Expected output:
|
||||
// Evaluating: (2 + 3)
|
||||
// Result: 5
|
||||
// Evaluating: ((1 + 2) * (3 + 4))
|
||||
// Result: 21
|
||||
// Evaluating: (10 - (2 * 3))
|
||||
// Result: 4
|
||||
|
||||
type Expr =
|
||||
| Num(Int)
|
||||
| Add(Expr, Expr)
|
||||
@@ -19,19 +6,19 @@ type Expr =
|
||||
|
||||
fn eval(e: Expr): Int =
|
||||
match e {
|
||||
Num(n) => n,
|
||||
Add(a, b) => eval(a) + eval(b),
|
||||
Sub(a, b) => eval(a) - eval(b),
|
||||
Mul(a, b) => eval(a) * eval(b)
|
||||
}
|
||||
Num(n) => n,
|
||||
Add(a, b) => eval(a) + eval(b),
|
||||
Sub(a, b) => eval(a) - eval(b),
|
||||
Mul(a, b) => eval(a) * eval(b),
|
||||
}
|
||||
|
||||
fn showExpr(e: Expr): String =
|
||||
match e {
|
||||
Num(n) => toString(n),
|
||||
Add(a, b) => "(" + showExpr(a) + " + " + showExpr(b) + ")",
|
||||
Sub(a, b) => "(" + showExpr(a) + " - " + showExpr(b) + ")",
|
||||
Mul(a, b) => "(" + showExpr(a) + " * " + showExpr(b) + ")"
|
||||
}
|
||||
Num(n) => toString(n),
|
||||
Add(a, b) => "(" + showExpr(a) + " + " + showExpr(b) + ")",
|
||||
Sub(a, b) => "(" + showExpr(a) + " - " + showExpr(b) + ")",
|
||||
Mul(a, b) => "(" + showExpr(a) + " * " + showExpr(b) + ")",
|
||||
}
|
||||
|
||||
fn evalAndPrint(e: Expr): Unit with {Console} = {
|
||||
Console.print("Evaluating: " + showExpr(e))
|
||||
@@ -39,15 +26,10 @@ fn evalAndPrint(e: Expr): Unit with {Console} = {
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
// (2 + 3)
|
||||
let e1 = Add(Num(2), Num(3))
|
||||
evalAndPrint(e1)
|
||||
|
||||
// ((1 + 2) * (3 + 4))
|
||||
let e2 = Mul(Add(Num(1), Num(2)), Add(Num(3), Num(4)))
|
||||
evalAndPrint(e2)
|
||||
|
||||
// (10 - (2 * 3))
|
||||
let e3 = Sub(Num(10), Mul(Num(2), Num(3)))
|
||||
evalAndPrint(e3)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
// Factorial - compute n!
|
||||
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
|
||||
|
||||
// Recursive version
|
||||
fn factorial(n: Int): Int =
|
||||
if n <= 1 then 1
|
||||
else n * factorial(n - 1)
|
||||
|
||||
// Tail-recursive version (optimized)
|
||||
fn factorialTail(n: Int, acc: Int): Int =
|
||||
if n <= 1 then acc
|
||||
else factorialTail(n - 1, n * acc)
|
||||
fn factorialTail(n: Int, acc: Int): Int = if n <= 1 then acc else factorialTail(n - 1, n * acc)
|
||||
|
||||
fn factorial2(n: Int): Int = factorialTail(n, 1)
|
||||
|
||||
|
||||
@@ -1,22 +1,11 @@
|
||||
// FizzBuzz - print numbers 1-100, but:
|
||||
// - multiples of 3: print "Fizz"
|
||||
// - multiples of 5: print "Buzz"
|
||||
// - multiples of both: print "FizzBuzz"
|
||||
|
||||
fn fizzbuzz(n: Int): String =
|
||||
if n % 15 == 0 then "FizzBuzz"
|
||||
else if n % 3 == 0 then "Fizz"
|
||||
else if n % 5 == 0 then "Buzz"
|
||||
else toString(n)
|
||||
fn fizzbuzz(n: Int): String = if n % 15 == 0 then "FizzBuzz" else if n % 3 == 0 then "Fizz" else if n % 5 == 0 then "Buzz" else toString(n)
|
||||
|
||||
fn printFizzbuzz(i: Int, max: Int): Unit with {Console} =
|
||||
if i > max then ()
|
||||
else {
|
||||
Console.print(fizzbuzz(i))
|
||||
printFizzbuzz(i + 1, max)
|
||||
}
|
||||
if i > max then () else {
|
||||
Console.print(fizzbuzz(i))
|
||||
printFizzbuzz(i + 1, max)
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} =
|
||||
printFizzbuzz(1, 100)
|
||||
fn main(): Unit with {Console} = printFizzbuzz(1, 100)
|
||||
|
||||
let output = run main() with {}
|
||||
|
||||
@@ -1,42 +1,17 @@
|
||||
// Number guessing game - demonstrates Random and Console effects
|
||||
//
|
||||
// Expected output:
|
||||
// Welcome to the Guessing Game!
|
||||
// Target number: 42
|
||||
// Simulating guesses...
|
||||
// Guess 50: Too high!
|
||||
// Guess 25: Too low!
|
||||
// Guess 37: Too low!
|
||||
// Guess 43: Too high!
|
||||
// Guess 40: Too low!
|
||||
// Guess 41: Too low!
|
||||
// Guess 42: Correct!
|
||||
// Found in 7 attempts!
|
||||
fn checkGuess(guess: Int, secret: Int): String = if guess == secret then "Correct" else if guess < secret then "Too low" else "Too high"
|
||||
|
||||
// Game logic - check a guess against the secret
|
||||
fn checkGuess(guess: Int, secret: Int): String =
|
||||
if guess == secret then "Correct"
|
||||
else if guess < secret then "Too low"
|
||||
else "Too high"
|
||||
|
||||
// Binary search simulation to find the number
|
||||
fn binarySearch(low: Int, high: Int, secret: Int, attempts: Int): Int with {Console} = {
|
||||
let mid = (low + high) / 2
|
||||
let mid = low + high / 2
|
||||
let result = checkGuess(mid, secret)
|
||||
Console.print("Guess " + toString(mid) + ": " + result + "!")
|
||||
|
||||
if result == "Correct" then attempts
|
||||
else if result == "Too low" then binarySearch(mid + 1, high, secret, attempts + 1)
|
||||
else binarySearch(low, mid - 1, secret, attempts + 1)
|
||||
if result == "Correct" then attempts else if result == "Too low" then binarySearch(mid + 1, high, secret, attempts + 1) else binarySearch(low, mid - 1, secret, attempts + 1)
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("Welcome to the Guessing Game!")
|
||||
// Use a fixed "secret" for reproducible output
|
||||
let secret = 42
|
||||
Console.print("Target number: " + toString(secret))
|
||||
Console.print("Simulating guesses...")
|
||||
|
||||
let attempts = binarySearch(1, 100, secret, 1)
|
||||
Console.print("Found in " + toString(attempts) + " attempts!")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// The classic first program
|
||||
// Expected output: Hello, World!
|
||||
|
||||
fn main(): Unit with {Console} =
|
||||
Console.print("Hello, World!")
|
||||
fn main(): Unit with {Console} = Console.print("Hello, World!")
|
||||
|
||||
let output = run main() with {}
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
// Prime number utilities
|
||||
fn isPrime(n: Int): Bool = if n < 2 then false else isPrimeHelper(n, 2)
|
||||
|
||||
fn isPrime(n: Int): Bool =
|
||||
if n < 2 then false
|
||||
else isPrimeHelper(n, 2)
|
||||
fn isPrimeHelper(n: Int, i: Int): Bool = if i * i > n then true else if n % i == 0 then false else isPrimeHelper(n, i + 1)
|
||||
|
||||
fn isPrimeHelper(n: Int, i: Int): Bool =
|
||||
if i * i > n then true
|
||||
else if n % i == 0 then false
|
||||
else isPrimeHelper(n, i + 1)
|
||||
|
||||
// Find first n primes
|
||||
fn findPrimes(count: Int): Unit with {Console} =
|
||||
findPrimesHelper(2, count)
|
||||
fn findPrimes(count: Int): Unit with {Console} = findPrimesHelper(2, count)
|
||||
|
||||
fn findPrimesHelper(current: Int, remaining: Int): Unit with {Console} =
|
||||
if remaining <= 0 then ()
|
||||
else if isPrime(current) then {
|
||||
Console.print(toString(current))
|
||||
findPrimesHelper(current + 1, remaining - 1)
|
||||
}
|
||||
else findPrimesHelper(current + 1, remaining)
|
||||
if remaining <= 0 then () else if isPrime(current) then {
|
||||
Console.print(toString(current))
|
||||
findPrimesHelper(current + 1, remaining - 1)
|
||||
} else findPrimesHelper(current + 1, remaining)
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("First 20 prime numbers:")
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// Standard Library Demo
|
||||
// Demonstrates the built-in modules: List, String, Option, Math
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== List Operations ===")
|
||||
let nums = [1, 2, 3, 4, 5]
|
||||
@@ -11,7 +8,6 @@ fn main(): Unit with {Console} = {
|
||||
Console.print("Length: " + toString(List.length(nums)))
|
||||
Console.print("Reversed: " + toString(List.reverse(nums)))
|
||||
Console.print("Range 1-5: " + toString(List.range(1, 6)))
|
||||
|
||||
Console.print("")
|
||||
Console.print("=== String Operations ===")
|
||||
let text = " Hello, World! "
|
||||
@@ -22,7 +18,6 @@ fn main(): Unit with {Console} = {
|
||||
Console.print("Contains 'World': " + toString(String.contains(text, "World")))
|
||||
Console.print("Split by comma: " + toString(String.split("a,b,c", ",")))
|
||||
Console.print("Join with dash: " + String.join(["x", "y", "z"], "-"))
|
||||
|
||||
Console.print("")
|
||||
Console.print("=== Option Operations ===")
|
||||
let some_val = Some(42)
|
||||
@@ -31,7 +26,6 @@ fn main(): Unit with {Console} = {
|
||||
Console.print("None mapped: " + toString(Option.map(none_val, fn(x: Int): Int => x * 2)))
|
||||
Console.print("Some(42) getOrElse(0): " + toString(Option.getOrElse(some_val, 0)))
|
||||
Console.print("None getOrElse(0): " + toString(Option.getOrElse(none_val, 0)))
|
||||
|
||||
Console.print("")
|
||||
Console.print("=== Math Operations ===")
|
||||
Console.print("abs(-5): " + toString(Math.abs(-5)))
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
// State machine example using algebraic data types
|
||||
// Demonstrates pattern matching for state transitions
|
||||
//
|
||||
// Expected output:
|
||||
// Initial light: red
|
||||
// After transition: green
|
||||
// After two transitions: yellow
|
||||
// Door: Closed -> Open -> Closed -> Locked
|
||||
|
||||
// Traffic light state machine
|
||||
type TrafficLight =
|
||||
| Red
|
||||
| Yellow
|
||||
@@ -15,26 +5,25 @@ type TrafficLight =
|
||||
|
||||
fn nextLight(light: TrafficLight): TrafficLight =
|
||||
match light {
|
||||
Red => Green,
|
||||
Green => Yellow,
|
||||
Yellow => Red
|
||||
}
|
||||
Red => Green,
|
||||
Green => Yellow,
|
||||
Yellow => Red,
|
||||
}
|
||||
|
||||
fn canGo(light: TrafficLight): Bool =
|
||||
match light {
|
||||
Green => true,
|
||||
Yellow => false,
|
||||
Red => false
|
||||
}
|
||||
Green => true,
|
||||
Yellow => false,
|
||||
Red => false,
|
||||
}
|
||||
|
||||
fn lightColor(light: TrafficLight): String =
|
||||
match light {
|
||||
Red => "red",
|
||||
Yellow => "yellow",
|
||||
Green => "green"
|
||||
}
|
||||
Red => "red",
|
||||
Yellow => "yellow",
|
||||
Green => "green",
|
||||
}
|
||||
|
||||
// Door state machine
|
||||
type DoorState =
|
||||
| Open
|
||||
| Closed
|
||||
@@ -48,31 +37,34 @@ type DoorAction =
|
||||
|
||||
fn applyAction(state: DoorState, action: DoorAction): DoorState =
|
||||
match (state, action) {
|
||||
(Closed, OpenDoor) => Open,
|
||||
(Open, CloseDoor) => Closed,
|
||||
(Closed, LockDoor) => Locked,
|
||||
(Locked, UnlockDoor) => Closed,
|
||||
_ => state
|
||||
}
|
||||
(Closed, OpenDoor) => Open,
|
||||
(Open, CloseDoor) => Closed,
|
||||
(Closed, LockDoor) => Locked,
|
||||
(Locked, UnlockDoor) => Closed,
|
||||
_ => state,
|
||||
}
|
||||
|
||||
fn doorStateName(state: DoorState): String =
|
||||
match state {
|
||||
Open => "Open",
|
||||
Closed => "Closed",
|
||||
Locked => "Locked"
|
||||
}
|
||||
Open => "Open",
|
||||
Closed => "Closed",
|
||||
Locked => "Locked",
|
||||
}
|
||||
|
||||
// Test the state machines
|
||||
let light1 = Red
|
||||
|
||||
let light2 = nextLight(light1)
|
||||
|
||||
let light3 = nextLight(light2)
|
||||
|
||||
let door1 = Closed
|
||||
|
||||
let door2 = applyAction(door1, OpenDoor)
|
||||
|
||||
let door3 = applyAction(door2, CloseDoor)
|
||||
|
||||
let door4 = applyAction(door3, LockDoor)
|
||||
|
||||
// Print results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("Initial light: " + lightColor(light1))
|
||||
Console.print("After transition: " + lightColor(light2))
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
// Stress test for RC system with large lists
|
||||
// Tests FBIP optimization with single-owner chains
|
||||
|
||||
fn processChain(n: Int): Int = {
|
||||
// Single owner chain - FBIP should reuse lists
|
||||
let nums = List.range(1, n)
|
||||
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
||||
let filtered = List.filter(doubled, fn(x: Int): Bool => x > n)
|
||||
@@ -12,13 +8,10 @@ fn processChain(n: Int): Int = {
|
||||
|
||||
fn main(): Unit = {
|
||||
Console.print("=== RC Stress Test ===")
|
||||
|
||||
// Run multiple iterations of list operations
|
||||
let result1 = processChain(100)
|
||||
let result2 = processChain(200)
|
||||
let result3 = processChain(500)
|
||||
let result4 = processChain(1000)
|
||||
|
||||
Console.print("Completed 4 chains")
|
||||
Console.print("Sizes: 100, 200, 500, 1000")
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
// Stress test for RC system WITH shared references
|
||||
// Forces rc>1 path by keeping aliases
|
||||
|
||||
fn processWithAlias(n: Int): Int = {
|
||||
let nums = List.range(1, n)
|
||||
let alias = nums // This increments rc, forcing copy path
|
||||
let _len = List.length(alias) // Use the alias
|
||||
|
||||
// Now nums has rc>1, so map must allocate new
|
||||
let alias = nums
|
||||
let _len = List.length(alias)
|
||||
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
||||
let filtered = List.filter(doubled, fn(x: Int): Bool => x > n)
|
||||
let reversed = List.reverse(filtered)
|
||||
@@ -15,12 +10,9 @@ fn processWithAlias(n: Int): Int = {
|
||||
|
||||
fn main(): Unit = {
|
||||
Console.print("=== RC Stress Test (Shared Refs) ===")
|
||||
|
||||
// Run multiple iterations with shared references
|
||||
let result1 = processWithAlias(100)
|
||||
let result2 = processWithAlias(200)
|
||||
let result3 = processWithAlias(500)
|
||||
let result4 = processWithAlias(1000)
|
||||
|
||||
Console.print("Completed 4 chains with shared refs")
|
||||
}
|
||||
|
||||
@@ -1,45 +1,25 @@
|
||||
// Demonstrating tail call optimization (TCO) in Lux
|
||||
// TCO allows recursive functions to run in constant stack space
|
||||
//
|
||||
// Expected output:
|
||||
// factorial(20) = 2432902008176640000
|
||||
// fib(30) = 832040
|
||||
// sumTo(1000) = 500500
|
||||
// countdown(10000) completed
|
||||
|
||||
// Factorial with accumulator - tail recursive
|
||||
fn factorialTCO(n: Int, acc: Int): Int =
|
||||
if n <= 1 then acc
|
||||
else factorialTCO(n - 1, n * acc)
|
||||
fn factorialTCO(n: Int, acc: Int): Int = if n <= 1 then acc else factorialTCO(n - 1, n * acc)
|
||||
|
||||
fn factorial(n: Int): Int = factorialTCO(n, 1)
|
||||
|
||||
// Fibonacci with accumulator - tail recursive
|
||||
fn fibTCO(n: Int, a: Int, b: Int): Int =
|
||||
if n <= 0 then a
|
||||
else fibTCO(n - 1, b, a + b)
|
||||
fn fibTCO(n: Int, a: Int, b: Int): Int = if n <= 0 then a else fibTCO(n - 1, b, a + b)
|
||||
|
||||
fn fib(n: Int): Int = fibTCO(n, 0, 1)
|
||||
|
||||
// Count down - simple tail recursion
|
||||
fn countdown(n: Int): Int =
|
||||
if n <= 0 then 0
|
||||
else countdown(n - 1)
|
||||
fn countdown(n: Int): Int = if n <= 0 then 0 else countdown(n - 1)
|
||||
|
||||
// Sum with accumulator - tail recursive
|
||||
fn sumToTCO(n: Int, acc: Int): Int =
|
||||
if n <= 0 then acc
|
||||
else sumToTCO(n - 1, acc + n)
|
||||
fn sumToTCO(n: Int, acc: Int): Int = if n <= 0 then acc else sumToTCO(n - 1, acc + n)
|
||||
|
||||
fn sumTo(n: Int): Int = sumToTCO(n, 0)
|
||||
|
||||
// Test the functions
|
||||
let fact20 = factorial(20)
|
||||
|
||||
let fib30 = fib(30)
|
||||
|
||||
let sum1000 = sumTo(1000)
|
||||
|
||||
let countResult = countdown(10000)
|
||||
|
||||
// Print results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("factorial(20) = " + toString(fact20))
|
||||
Console.print("fib(30) = " + toString(fib30))
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
// This test shows FBIP optimization by comparing allocation counts
|
||||
// With FBIP (rc=1): lists are reused in-place
|
||||
// Without FBIP (rc>1): new lists are allocated
|
||||
|
||||
fn main(): Unit = {
|
||||
Console.print("=== FBIP Allocation Test ===")
|
||||
|
||||
// Case 1: Single owner (FBIP active) - should reuse list
|
||||
let a = List.range(1, 100)
|
||||
let b = List.map(a, fn(x: Int): Int => x * 2)
|
||||
let c = List.filter(b, fn(x: Int): Bool => x > 50)
|
||||
let d = List.reverse(c)
|
||||
Console.print("Single owner chain done")
|
||||
|
||||
// The allocation count will show FBIP is working
|
||||
// if allocations are low relative to operations performed
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
fn main(): Unit = {
|
||||
// Test FBIP without string operations
|
||||
let nums = [1, 2, 3, 4, 5]
|
||||
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
||||
let filtered = List.filter(doubled, fn(x: Int): Bool => x > 4)
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// List Operations Test Suite
|
||||
// Run with: lux test examples/test_lists.lux
|
||||
|
||||
fn test_list_length(): Unit with {Test} = {
|
||||
Test.assertEqual(0, List.length([]))
|
||||
Test.assertEqual(1, List.length([1]))
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// Math Test Suite
|
||||
// Run with: lux test examples/test_math.lux
|
||||
|
||||
fn test_addition(): Unit with {Test} = {
|
||||
Test.assertEqual(4, 2 + 2)
|
||||
Test.assertEqual(0, 0 + 0)
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
// Test demonstrating ownership transfer with aliases
|
||||
// The ownership transfer optimization ensures FBIP still works
|
||||
// even when variables are aliased, because ownership is transferred
|
||||
// rather than reference count being incremented.
|
||||
|
||||
fn main(): Unit = {
|
||||
Console.print("=== Ownership Transfer Test ===")
|
||||
|
||||
let a = List.range(1, 100)
|
||||
// Ownership transfers from 'a' to 'alias', keeping rc=1
|
||||
let alias = a
|
||||
let len1 = List.length(alias)
|
||||
|
||||
// Since ownership transferred, 'a' still has rc=1
|
||||
// FBIP can still optimize map/filter/reverse
|
||||
let b = List.map(a, fn(x: Int): Int => x * 2)
|
||||
let c = List.filter(b, fn(x: Int): Bool => x > 50)
|
||||
let d = List.reverse(c)
|
||||
|
||||
Console.print("Ownership transfer chain done")
|
||||
}
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
fn main(): Unit = {
|
||||
Console.print("=== Allocation Comparison ===")
|
||||
|
||||
// FBIP path (rc=1): list is reused
|
||||
Console.print("Test 1: FBIP path")
|
||||
let a1 = List.range(1, 50)
|
||||
let b1 = List.map(a1, fn(x: Int): Int => x * 2)
|
||||
let c1 = List.reverse(b1)
|
||||
Console.print("FBIP done")
|
||||
|
||||
// To show non-FBIP, we need concat which doesn't have FBIP
|
||||
Console.print("Test 2: Non-FBIP path (concat)")
|
||||
let x = List.range(1, 25)
|
||||
let y = List.range(26, 50)
|
||||
let z = List.concat(x, y) // concat always allocates new
|
||||
let z = List.concat(x, y)
|
||||
Console.print("Concat done")
|
||||
}
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
// Demonstrating type classes (traits) in Lux
|
||||
//
|
||||
// Expected output:
|
||||
// RGB color: rgb(255,128,0)
|
||||
// Red color: red
|
||||
// Green color: green
|
||||
|
||||
// Define a simple Printable trait
|
||||
trait Printable {
|
||||
fn format(value: Int): String
|
||||
}
|
||||
|
||||
// Implement Printable
|
||||
impl Printable for Int {
|
||||
fn format(value: Int): String = "Number: " + toString(value)
|
||||
}
|
||||
|
||||
// A Color type with pattern matching
|
||||
type Color =
|
||||
| Red
|
||||
| Green
|
||||
@@ -24,18 +14,18 @@ type Color =
|
||||
|
||||
fn colorName(c: Color): String =
|
||||
match c {
|
||||
Red => "red",
|
||||
Green => "green",
|
||||
Blue => "blue",
|
||||
RGB(r, g, b) => "rgb(" + toString(r) + "," + toString(g) + "," + toString(b) + ")"
|
||||
}
|
||||
Red => "red",
|
||||
Green => "green",
|
||||
Blue => "blue",
|
||||
RGB(r, g, b) => "rgb(" + toString(r) + "," + toString(g) + "," + toString(b) + ")",
|
||||
}
|
||||
|
||||
// Test
|
||||
let myColor = RGB(255, 128, 0)
|
||||
|
||||
let redColor = Red
|
||||
|
||||
let greenColor = Green
|
||||
|
||||
// Print results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("RGB color: " + colorName(myColor))
|
||||
Console.print("Red color: " + colorName(redColor))
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
// Demonstrating Schema Evolution in Lux
|
||||
//
|
||||
// Lux provides versioned types to help manage data evolution over time.
|
||||
// The Schema module provides functions for creating and migrating versioned values.
|
||||
//
|
||||
// Expected output:
|
||||
// Created user v1: Alice (age unknown)
|
||||
// User version: 1
|
||||
// Migrated to v2: Alice (age unknown)
|
||||
// User version after migration: 2
|
||||
|
||||
// Create a versioned User value at v1
|
||||
fn createUserV1(name: String): Unit with {Console} = {
|
||||
let user = Schema.versioned("User", 1, { name: name })
|
||||
let version = Schema.getVersion(user)
|
||||
@@ -17,7 +5,6 @@ fn createUserV1(name: String): Unit with {Console} = {
|
||||
Console.print("User version: " + toString(version))
|
||||
}
|
||||
|
||||
// Migrate a user to v2
|
||||
fn migrateUserToV2(name: String): Unit with {Console} = {
|
||||
let userV1 = Schema.versioned("User", 1, { name: name })
|
||||
let userV2 = Schema.migrate(userV1, 2)
|
||||
@@ -26,7 +13,6 @@ fn migrateUserToV2(name: String): Unit with {Console} = {
|
||||
Console.print("User version after migration: " + toString(newVersion))
|
||||
}
|
||||
|
||||
// Main
|
||||
fn main(): Unit with {Console} = {
|
||||
createUserV1("Alice")
|
||||
migrateUserToV2("Alice")
|
||||
|
||||
@@ -1,62 +1,38 @@
|
||||
// Simple Counter for Browser
|
||||
// Compile with: lux compile examples/web/counter.lux --target js -o examples/web/counter.js
|
||||
type Model =
|
||||
| Counter(Int)
|
||||
|
||||
// ============================================================================
|
||||
// Model
|
||||
// ============================================================================
|
||||
|
||||
type Model = | Counter(Int)
|
||||
|
||||
fn getCount(m: Model): Int = match m { Counter(n) => n }
|
||||
fn getCount(m: Model): Int =
|
||||
match m {
|
||||
Counter(n) => n,
|
||||
}
|
||||
|
||||
fn init(): Model = Counter(0)
|
||||
|
||||
// ============================================================================
|
||||
// Messages
|
||||
// ============================================================================
|
||||
|
||||
type Msg = | Increment | Decrement | Reset
|
||||
|
||||
// ============================================================================
|
||||
// Update
|
||||
// ============================================================================
|
||||
type Msg =
|
||||
| Increment
|
||||
| Decrement
|
||||
| Reset
|
||||
|
||||
fn update(model: Model, msg: Msg): Model =
|
||||
match msg {
|
||||
Increment => Counter(getCount(model) + 1),
|
||||
Decrement => Counter(getCount(model) - 1),
|
||||
Reset => Counter(0)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// View - Returns HTML string for simplicity
|
||||
// ============================================================================
|
||||
Increment => Counter(getCount(model) + 1),
|
||||
Decrement => Counter(getCount(model) - 1),
|
||||
Reset => Counter(0),
|
||||
}
|
||||
|
||||
fn view(model: Model): String = {
|
||||
let count = getCount(model)
|
||||
"<div class=\"counter\">" +
|
||||
"<h1>Lux Counter</h1>" +
|
||||
"<div class=\"display\">" + toString(count) + "</div>" +
|
||||
"<div class=\"buttons\">" +
|
||||
"<button onclick=\"dispatch('Decrement')\">-</button>" +
|
||||
"<button onclick=\"dispatch('Reset')\">Reset</button>" +
|
||||
"<button onclick=\"dispatch('Increment')\">+</button>" +
|
||||
"</div>" +
|
||||
"</div>"
|
||||
"<div class=\"counter\">" + "<h1>Lux Counter</h1>" + "<div class=\"display\">" + toString(count) + "</div>" + "<div class=\"buttons\">" + "<button onclick=\"dispatch('Decrement')\">-</button>" + "<button onclick=\"dispatch('Reset')\">Reset</button>" + "<button onclick=\"dispatch('Increment')\">+</button>" + "</div>" + "</div>"
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Export for browser runtime
|
||||
// ============================================================================
|
||||
|
||||
fn luxInit(): Model = init()
|
||||
|
||||
fn luxUpdate(model: Model, msgName: String): Model =
|
||||
match msgName {
|
||||
"Increment" => update(model, Increment),
|
||||
"Decrement" => update(model, Decrement),
|
||||
"Reset" => update(model, Reset),
|
||||
_ => model
|
||||
}
|
||||
"Increment" => update(model, Increment),
|
||||
"Decrement" => update(model, Decrement),
|
||||
"Reset" => update(model, Reset),
|
||||
_ => model,
|
||||
}
|
||||
|
||||
fn luxView(model: Model): String = view(model)
|
||||
|
||||
@@ -224,10 +224,31 @@ pub mod colors {
|
||||
pub const BOLD: &str = "\x1b[1m";
|
||||
pub const DIM: &str = "\x1b[2m";
|
||||
pub const RED: &str = "\x1b[31m";
|
||||
pub const GREEN: &str = "\x1b[32m";
|
||||
pub const YELLOW: &str = "\x1b[33m";
|
||||
pub const BLUE: &str = "\x1b[34m";
|
||||
pub const MAGENTA: &str = "\x1b[35m";
|
||||
pub const CYAN: &str = "\x1b[36m";
|
||||
pub const WHITE: &str = "\x1b[37m";
|
||||
pub const GRAY: &str = "\x1b[90m";
|
||||
}
|
||||
|
||||
/// Apply color to text, respecting NO_COLOR / TERM=dumb
|
||||
pub fn c(color: &str, text: &str) -> String {
|
||||
if supports_color() {
|
||||
format!("{}{}{}", color, text, colors::RESET)
|
||||
} else {
|
||||
text.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply bold + color to text
|
||||
pub fn bc(color: &str, text: &str) -> String {
|
||||
if supports_color() {
|
||||
format!("{}{}{}{}", colors::BOLD, color, text, colors::RESET)
|
||||
} else {
|
||||
text.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Severity level for diagnostics
|
||||
|
||||
1143
src/linter.rs
Normal file
1143
src/linter.rs
Normal file
File diff suppressed because it is too large
Load Diff
377
src/lsp.rs
377
src/lsp.rs
@@ -19,7 +19,7 @@ use crate::formatter::{format as format_source, FormatConfig};
|
||||
use lsp_server::{Connection, ExtractError, Message, Request, RequestId, Response};
|
||||
use lsp_types::{
|
||||
notification::{DidChangeTextDocument, DidOpenTextDocument, Notification},
|
||||
request::{Completion, GotoDefinition, HoverRequest, References, DocumentSymbolRequest, Rename, SignatureHelpRequest, Formatting},
|
||||
request::{Completion, GotoDefinition, HoverRequest, References, DocumentSymbolRequest, Rename, SignatureHelpRequest, Formatting, InlayHintRequest},
|
||||
CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse,
|
||||
Diagnostic, DiagnosticSeverity, DidChangeTextDocumentParams, DidOpenTextDocumentParams,
|
||||
GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams,
|
||||
@@ -28,7 +28,8 @@ use lsp_types::{
|
||||
TextDocumentSyncKind, Url, ReferenceParams, Location, DocumentSymbolParams,
|
||||
DocumentSymbolResponse, SymbolInformation, RenameParams, WorkspaceEdit, TextEdit,
|
||||
SignatureHelpParams, SignatureHelp, SignatureInformation, ParameterInformation,
|
||||
SignatureHelpOptions, DocumentFormattingParams, TextDocumentIdentifier,
|
||||
SignatureHelpOptions, DocumentFormattingParams,
|
||||
InlayHint, InlayHintKind, InlayHintLabel, InlayHintParams,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
@@ -88,6 +89,7 @@ impl LspServer {
|
||||
work_done_progress_options: Default::default(),
|
||||
}),
|
||||
document_formatting_provider: Some(lsp_types::OneOf::Left(true)),
|
||||
inlay_hint_provider: Some(lsp_types::OneOf::Left(true)),
|
||||
..Default::default()
|
||||
})?;
|
||||
|
||||
@@ -191,7 +193,7 @@ impl LspServer {
|
||||
Err(req) => req,
|
||||
};
|
||||
|
||||
let _req = match cast_request::<Formatting>(req) {
|
||||
let req = match cast_request::<Formatting>(req) {
|
||||
Ok((id, params)) => {
|
||||
let result = self.handle_formatting(params);
|
||||
let resp = Response::new_ok(id, result);
|
||||
@@ -201,6 +203,16 @@ impl LspServer {
|
||||
Err(req) => req,
|
||||
};
|
||||
|
||||
let _req = match cast_request::<InlayHintRequest>(req) {
|
||||
Ok((id, params)) => {
|
||||
let result = self.handle_inlay_hints(params);
|
||||
let resp = Response::new_ok(id, result);
|
||||
self.connection.sender.send(Message::Response(resp))?;
|
||||
return Ok(());
|
||||
}
|
||||
Err(req) => req,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -328,10 +340,16 @@ impl LspServer {
|
||||
.map(|d| format!("\n\n{}", d))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Format signature: wrap long signatures onto multiple lines
|
||||
let formatted_sig = format_signature_for_hover(signature);
|
||||
|
||||
// Add behavioral property documentation if present
|
||||
let property_docs = extract_property_docs(signature);
|
||||
|
||||
return Some(Hover {
|
||||
contents: HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: format!("```lux\n{}\n```\n\n*{}*{}", signature, kind_str, doc_str),
|
||||
value: format!("```lux\n{}\n```\n\n*{}*{}{}", formatted_sig, kind_str, property_docs, doc_str),
|
||||
}),
|
||||
range: None,
|
||||
});
|
||||
@@ -343,19 +361,20 @@ impl LspServer {
|
||||
// Extract the word at the cursor position
|
||||
let word = self.get_word_at_position(source, position)?;
|
||||
|
||||
// Look up documentation for known symbols
|
||||
let info = self.get_symbol_info(&word);
|
||||
// Look up rich documentation for known symbols
|
||||
let info = self.get_rich_symbol_info(&word)
|
||||
.or_else(|| self.get_symbol_info(&word).map(|(s, d)| (s.to_string(), d.to_string())));
|
||||
|
||||
if let Some((signature, doc)) = info {
|
||||
let formatted_sig = format_signature_for_hover(&signature);
|
||||
Some(Hover {
|
||||
contents: HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: format!("```lux\n{}\n```\n\n{}", signature, doc),
|
||||
value: format!("```lux\n{}\n```\n\n{}", formatted_sig, doc),
|
||||
}),
|
||||
range: None,
|
||||
})
|
||||
} else {
|
||||
// Return generic info for unknown symbols
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -439,6 +458,84 @@ impl LspServer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rich documentation for behavioral properties and keywords
|
||||
fn get_rich_symbol_info(&self, word: &str) -> Option<(String, String)> {
|
||||
match word {
|
||||
"pure" => Some((
|
||||
"is pure".to_string(),
|
||||
"**Behavioral Property: Pure**\n\n\
|
||||
A pure function has no side effects and always produces the same output for the same inputs. \
|
||||
The compiler can safely memoize calls, reorder them, or eliminate duplicates.\n\n\
|
||||
```lux\nfn add(a: Int, b: Int): Int is pure = a + b\n```\n\n\
|
||||
**Guarantees:**\n\
|
||||
- No effect operations (Console, File, Http, etc.)\n\
|
||||
- Referential transparency: `f(x)` can be replaced with its result\n\
|
||||
- Enables memoization and common subexpression elimination".to_string(),
|
||||
)),
|
||||
"total" => Some((
|
||||
"is total".to_string(),
|
||||
"**Behavioral Property: Total**\n\n\
|
||||
A total function always terminates and never throws exceptions. \
|
||||
The compiler verifies termination through structural recursion analysis.\n\n\
|
||||
```lux\nfn factorial(n: Int): Int is total =\n if n <= 0 then 1\n else n * factorial(n - 1)\n```\n\n\
|
||||
**Guarantees:**\n\
|
||||
- Always produces a result (no infinite loops)\n\
|
||||
- Cannot use the `Fail` effect\n\
|
||||
- Recursive calls must be structurally decreasing".to_string(),
|
||||
)),
|
||||
"idempotent" => Some((
|
||||
"is idempotent".to_string(),
|
||||
"**Behavioral Property: Idempotent**\n\n\
|
||||
An idempotent function satisfies `f(f(x)) == f(x)` for all inputs. \
|
||||
Applying it multiple times has the same effect as applying it once.\n\n\
|
||||
```lux\nfn abs(x: Int): Int is idempotent =\n if x < 0 then 0 - x else x\n\n\
|
||||
fn clamp(x: Int): Int is idempotent =\n if x < 0 then 0\n else if x > 100 then 100\n else x\n```\n\n\
|
||||
**Guarantees:**\n\
|
||||
- `f(f(x)) == f(x)` for all valid inputs\n\
|
||||
- Safe to retry without changing outcome\n\
|
||||
- Compiler can deduplicate consecutive calls".to_string(),
|
||||
)),
|
||||
"deterministic" => Some((
|
||||
"is deterministic".to_string(),
|
||||
"**Behavioral Property: Deterministic**\n\n\
|
||||
A deterministic function always produces the same output for the same inputs, \
|
||||
with no dependence on randomness, time, or external state.\n\n\
|
||||
```lux\nfn multiply(a: Int, b: Int): Int is deterministic = a * b\n```\n\n\
|
||||
**Guarantees:**\n\
|
||||
- Cannot use `Random` or `Time` effects\n\
|
||||
- Same inputs always produce same outputs\n\
|
||||
- Results can be cached across runs".to_string(),
|
||||
)),
|
||||
"commutative" => Some((
|
||||
"is commutative".to_string(),
|
||||
"**Behavioral Property: Commutative**\n\n\
|
||||
A commutative function satisfies `f(a, b) == f(b, a)`. \
|
||||
The order of arguments doesn't affect the result.\n\n\
|
||||
```lux\nfn add(a: Int, b: Int): Int is commutative = a + b\nfn max(a: Int, b: Int): Int is commutative =\n if a > b then a else b\n```\n\n\
|
||||
**Guarantees:**\n\
|
||||
- Must have exactly 2 parameters\n\
|
||||
- `f(a, b) == f(b, a)` for all inputs\n\
|
||||
- Compiler can normalize argument order for optimization".to_string(),
|
||||
)),
|
||||
"run" => Some((
|
||||
"run expr with { handlers }".to_string(),
|
||||
"**Effect Handler**\n\n\
|
||||
Execute an effectful expression with explicit effect handlers. \
|
||||
Must be bound to a variable at top level.\n\n\
|
||||
```lux\nlet result = run myFunction() with {\n Console = { /* handler */ }\n}\n```\n\n\
|
||||
Handlers intercept effect operations and provide implementations.".to_string(),
|
||||
)),
|
||||
"with" => Some((
|
||||
"with {Effect1, Effect2}".to_string(),
|
||||
"**Effect Declaration / Handler Block**\n\n\
|
||||
Declares which effects a function may perform, or provides handlers in a `run` expression.\n\n\
|
||||
```lux\n// In function signature:\nfn greet(name: String): Unit with {Console} =\n Console.print(\"Hello, \" + name)\n\n\
|
||||
// In run expression:\nlet _ = run greet(\"world\") with {}\n```".to_string(),
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_completion(&self, params: CompletionParams) -> Option<CompletionResponse> {
|
||||
let uri = params.text_document_position.text_document.uri;
|
||||
let position = params.text_document_position.position;
|
||||
@@ -1022,6 +1119,90 @@ impl LspServer {
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_inlay_hints(&self, params: InlayHintParams) -> Option<Vec<InlayHint>> {
|
||||
let uri = params.text_document.uri;
|
||||
let doc = self.documents.get(&uri)?;
|
||||
let source = &doc.text;
|
||||
|
||||
// Parse the document to get AST
|
||||
let program = Parser::parse_source(source).ok()?;
|
||||
|
||||
// Type-check to get inferred types
|
||||
let mut checker = TypeChecker::new();
|
||||
let _ = checker.check_program(&program);
|
||||
|
||||
let mut hints = Vec::new();
|
||||
|
||||
// Collect parameter names for known functions (from symbol table)
|
||||
let param_names = self.collect_function_params(&program);
|
||||
|
||||
for decl in &program.declarations {
|
||||
match decl {
|
||||
crate::ast::Declaration::Let(l) => {
|
||||
// Show inferred type for let bindings without explicit type annotations
|
||||
if l.typ.is_none() {
|
||||
if let Some(inferred_type) = checker.get_inferred_type(&l.name.name) {
|
||||
let type_str = format!(": {}", inferred_type);
|
||||
let pos = offset_to_position(source, l.name.span.end);
|
||||
hints.push(InlayHint {
|
||||
position: pos,
|
||||
label: InlayHintLabel::String(type_str),
|
||||
kind: Some(InlayHintKind::TYPE),
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: Some(false),
|
||||
padding_right: Some(true),
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Walk into the value expression for call-site parameter hints
|
||||
collect_call_site_hints(source, &l.value, ¶m_names, &mut hints);
|
||||
}
|
||||
crate::ast::Declaration::Function(f) => {
|
||||
// Walk into the function body for call-site parameter hints
|
||||
collect_call_site_hints(source, &f.body, ¶m_names, &mut hints);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if hints.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(hints)
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect parameter names for all functions defined in the program
|
||||
fn collect_function_params(&self, program: &crate::ast::Program) -> HashMap<String, Vec<String>> {
|
||||
let mut params = HashMap::new();
|
||||
for decl in &program.declarations {
|
||||
if let crate::ast::Declaration::Function(f) = decl {
|
||||
let names: Vec<String> = f.params.iter()
|
||||
.map(|p| p.name.name.clone())
|
||||
.collect();
|
||||
params.insert(f.name.name.clone(), names);
|
||||
}
|
||||
}
|
||||
// Add builtin function parameter names
|
||||
params.insert("map".into(), vec!["list".into(), "f".into()]);
|
||||
params.insert("filter".into(), vec!["list".into(), "predicate".into()]);
|
||||
params.insert("fold".into(), vec!["list".into(), "init".into(), "f".into()]);
|
||||
params.insert("concat".into(), vec!["a".into(), "b".into()]);
|
||||
params.insert("range".into(), vec!["start".into(), "end".into()]);
|
||||
params.insert("get".into(), vec!["list".into(), "index".into()]);
|
||||
params.insert("take".into(), vec!["list".into(), "n".into()]);
|
||||
params.insert("drop".into(), vec!["list".into(), "n".into()]);
|
||||
params.insert("split".into(), vec!["s".into(), "delimiter".into()]);
|
||||
params.insert("join".into(), vec!["list".into(), "delimiter".into()]);
|
||||
params.insert("replace".into(), vec!["s".into(), "old".into(), "new".into()]);
|
||||
params.insert("substring".into(), vec!["s".into(), "start".into(), "end".into()]);
|
||||
params.insert("contains".into(), vec!["s".into(), "substr".into()]);
|
||||
params.insert("getOrElse".into(), vec!["opt".into(), "default".into()]);
|
||||
params
|
||||
}
|
||||
|
||||
fn handle_formatting(&self, params: DocumentFormattingParams) -> Option<Vec<TextEdit>> {
|
||||
let uri = params.text_document.uri;
|
||||
let doc = self.documents.get(&uri)?;
|
||||
@@ -1061,6 +1242,186 @@ impl LspServer {
|
||||
}
|
||||
|
||||
/// Convert byte offsets to LSP Position
|
||||
/// Format a function signature for hover display, wrapping long lines
|
||||
fn format_signature_for_hover(sig: &str) -> String {
|
||||
// If it fits in ~60 chars, keep it on one line
|
||||
if sig.len() <= 60 {
|
||||
return sig.to_string();
|
||||
}
|
||||
|
||||
// Try to break at parameter list for function signatures
|
||||
if let Some(paren_start) = sig.find('(') {
|
||||
if let Some(paren_end) = sig.rfind(')') {
|
||||
let prefix = &sig[..paren_start + 1];
|
||||
let params = &sig[paren_start + 1..paren_end];
|
||||
let suffix = &sig[paren_end..];
|
||||
|
||||
// Split parameters and format each on its own line
|
||||
let param_parts: Vec<&str> = params.split(", ").collect();
|
||||
if param_parts.len() > 1 {
|
||||
let indent = " ";
|
||||
let formatted_params = param_parts.join(&format!(",\n{}", indent));
|
||||
return format!("{}\n{}{}\n{}", prefix, indent, formatted_params, suffix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sig.to_string()
|
||||
}
|
||||
|
||||
/// Extract behavioral property documentation from a signature string
|
||||
fn extract_property_docs(sig: &str) -> String {
|
||||
let properties = [
|
||||
("is pure", "**pure** — no side effects, same output for same inputs"),
|
||||
("is total", "**total** — always terminates, no exceptions"),
|
||||
("is idempotent", "**idempotent** — `f(f(x)) == f(x)`"),
|
||||
("is deterministic", "**deterministic** — no randomness or time dependence"),
|
||||
("is commutative", "**commutative** — `f(a, b) == f(b, a)`"),
|
||||
];
|
||||
|
||||
let mut found = Vec::new();
|
||||
for (keyword, description) in &properties {
|
||||
if sig.contains(keyword) {
|
||||
found.push(*description);
|
||||
}
|
||||
}
|
||||
|
||||
if found.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("\n\n{}", found.join(" \n"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively collect parameter name hints at call sites
|
||||
fn collect_call_site_hints(
|
||||
source: &str,
|
||||
expr: &crate::ast::Expr,
|
||||
param_names: &HashMap<String, Vec<String>>,
|
||||
hints: &mut Vec<InlayHint>,
|
||||
) {
|
||||
use crate::ast::Expr;
|
||||
match expr {
|
||||
Expr::Call { func, args, .. } => {
|
||||
// Get the function name for parameter lookup
|
||||
let func_name = match func.as_ref() {
|
||||
Expr::Var(ident) => Some(ident.name.clone()),
|
||||
// Module.method calls like List.map
|
||||
Expr::Field { object, field, .. } => {
|
||||
if let Expr::Var(_) = object.as_ref() {
|
||||
Some(field.name.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(name) = func_name {
|
||||
if let Some(names) = param_names.get(&name) {
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
if let Some(param_name) = names.get(i) {
|
||||
// Skip hint if the argument is already a variable with the same name
|
||||
if let Expr::Var(ident) = arg {
|
||||
if &ident.name == param_name {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Skip hints for single-arg functions (obvious)
|
||||
if args.len() <= 1 {
|
||||
continue;
|
||||
}
|
||||
let pos = offset_to_position(source, arg.span().start);
|
||||
hints.push(InlayHint {
|
||||
position: pos,
|
||||
label: InlayHintLabel::String(format!("{}:", param_name)),
|
||||
kind: Some(InlayHintKind::PARAMETER),
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: Some(false),
|
||||
padding_right: Some(true),
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse into function expression and arguments
|
||||
collect_call_site_hints(source, func, param_names, hints);
|
||||
for arg in args {
|
||||
collect_call_site_hints(source, arg, param_names, hints);
|
||||
}
|
||||
}
|
||||
Expr::BinaryOp { left, right, .. } => {
|
||||
collect_call_site_hints(source, left, param_names, hints);
|
||||
collect_call_site_hints(source, right, param_names, hints);
|
||||
}
|
||||
Expr::UnaryOp { operand, .. } => {
|
||||
collect_call_site_hints(source, operand, param_names, hints);
|
||||
}
|
||||
Expr::If { condition, then_branch, else_branch, .. } => {
|
||||
collect_call_site_hints(source, condition, param_names, hints);
|
||||
collect_call_site_hints(source, then_branch, param_names, hints);
|
||||
collect_call_site_hints(source, else_branch, param_names, hints);
|
||||
}
|
||||
Expr::Let { value, body, .. } => {
|
||||
collect_call_site_hints(source, value, param_names, hints);
|
||||
collect_call_site_hints(source, body, param_names, hints);
|
||||
}
|
||||
Expr::Block { statements, result, .. } => {
|
||||
for stmt in statements {
|
||||
match stmt {
|
||||
crate::ast::Statement::Expr(e) => {
|
||||
collect_call_site_hints(source, e, param_names, hints);
|
||||
}
|
||||
crate::ast::Statement::Let { value, .. } => {
|
||||
collect_call_site_hints(source, value, param_names, hints);
|
||||
}
|
||||
}
|
||||
}
|
||||
collect_call_site_hints(source, result, param_names, hints);
|
||||
}
|
||||
Expr::Match { scrutinee, arms, .. } => {
|
||||
collect_call_site_hints(source, scrutinee, param_names, hints);
|
||||
for arm in arms {
|
||||
collect_call_site_hints(source, &arm.body, param_names, hints);
|
||||
}
|
||||
}
|
||||
Expr::Lambda { body, .. } => {
|
||||
collect_call_site_hints(source, body, param_names, hints);
|
||||
}
|
||||
Expr::Tuple { elements, .. } | Expr::List { elements, .. } => {
|
||||
for e in elements {
|
||||
collect_call_site_hints(source, e, param_names, hints);
|
||||
}
|
||||
}
|
||||
Expr::Record { fields, .. } => {
|
||||
for (_, e) in fields {
|
||||
collect_call_site_hints(source, e, param_names, hints);
|
||||
}
|
||||
}
|
||||
Expr::Field { object, .. } => {
|
||||
collect_call_site_hints(source, object, param_names, hints);
|
||||
}
|
||||
Expr::Run { expr, handlers, .. } => {
|
||||
collect_call_site_hints(source, expr, param_names, hints);
|
||||
for (_, handler_expr) in handlers {
|
||||
collect_call_site_hints(source, handler_expr, param_names, hints);
|
||||
}
|
||||
}
|
||||
Expr::Resume { value, .. } => {
|
||||
collect_call_site_hints(source, value, param_names, hints);
|
||||
}
|
||||
Expr::EffectOp { args, .. } => {
|
||||
for arg in args {
|
||||
collect_call_site_hints(source, arg, param_names, hints);
|
||||
}
|
||||
}
|
||||
Expr::Literal { .. } | Expr::Var(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn span_to_range(source: &str, start: usize, end: usize) -> Range {
|
||||
let start_pos = offset_to_position(source, start);
|
||||
let end_pos = offset_to_position(source, end);
|
||||
|
||||
667
src/main.rs
667
src/main.rs
File diff suppressed because it is too large
Load Diff
@@ -879,7 +879,8 @@ impl Parser {
|
||||
Ok(effects)
|
||||
}
|
||||
|
||||
/// Parse behavioral properties: is pure, is total, is idempotent, etc.
|
||||
/// Parse behavioral properties: is pure, total, idempotent, etc.
|
||||
/// Supports: `is pure`, `is pure is total`, `is pure, total`, `is pure, is total`
|
||||
fn parse_behavioral_properties(&mut self) -> Result<Vec<BehavioralProperty>, ParseError> {
|
||||
let mut properties = Vec::new();
|
||||
|
||||
@@ -901,9 +902,15 @@ impl Parser {
|
||||
let property = self.parse_single_property()?;
|
||||
properties.push(property);
|
||||
|
||||
// Optional comma for multiple properties: is pure, is total
|
||||
if self.check(TokenKind::Comma) {
|
||||
self.advance();
|
||||
// After first property, allow comma-separated list without repeating 'is'
|
||||
while self.check(TokenKind::Comma) {
|
||||
self.advance(); // consume comma
|
||||
// Allow optional 'is' after comma: `is pure, is total` or `is pure, total`
|
||||
if self.check(TokenKind::Is) {
|
||||
self.advance();
|
||||
}
|
||||
let property = self.parse_single_property()?;
|
||||
properties.push(property);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -263,7 +263,21 @@ impl SymbolTable {
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "))
|
||||
};
|
||||
let type_sig = format!("fn {}({}): {}{}", f.name.name, param_types.join(", "), return_type, effects);
|
||||
let properties = if f.properties.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" is {}", f.properties.iter()
|
||||
.map(|p| match p {
|
||||
crate::ast::BehavioralProperty::Pure => "pure",
|
||||
crate::ast::BehavioralProperty::Total => "total",
|
||||
crate::ast::BehavioralProperty::Idempotent => "idempotent",
|
||||
crate::ast::BehavioralProperty::Deterministic => "deterministic",
|
||||
crate::ast::BehavioralProperty::Commutative => "commutative",
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "))
|
||||
};
|
||||
let type_sig = format!("fn {}({}): {}{}{}", f.name.name, param_types.join(", "), return_type, properties, effects);
|
||||
|
||||
let symbol = self.new_symbol(
|
||||
f.name.name.clone(),
|
||||
|
||||
@@ -759,6 +759,17 @@ impl TypeChecker {
|
||||
self.env.bindings.get(name)
|
||||
}
|
||||
|
||||
/// Get the inferred type of a binding as a display string (for LSP inlay hints)
|
||||
pub fn get_inferred_type(&self, name: &str) -> Option<String> {
|
||||
let scheme = self.env.bindings.get(name)?;
|
||||
let type_str = scheme.typ.to_string();
|
||||
// Skip unhelpful types
|
||||
if type_str == "<error>" || type_str.contains('?') {
|
||||
return None;
|
||||
}
|
||||
Some(type_str)
|
||||
}
|
||||
|
||||
/// Get auto-generated migrations from type checking
|
||||
/// Returns: type_name -> from_version -> migration_body
|
||||
pub fn get_auto_migrations(&self) -> &HashMap<String, HashMap<u32, Expr>> {
|
||||
|
||||
Reference in New Issue
Block a user