6 Commits

Author SHA1 Message Date
d26fd975d1 feat: enhance LSP with inlay hints, parameter hints, and improved hover
Add inlay type hints for let bindings, parameter name hints at call sites,
behavioral property documentation in hover, and long signature wrapping.

- Inlay hints: show inferred types for let bindings without annotations
- Parameter hints: show param names at call sites for multi-arg functions
- Hover: wrap long signatures, show behavioral property docs (pure, total, etc.)
- Rich docs: detailed hover for keywords like pure, total, idempotent, run, with
- TypeChecker: expose get_inferred_type() for LSP consumption
- Symbol table: include behavioral properties in function type signatures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 08:06:36 -05:00
1fa599f856 fix: support comma-separated behavioral properties without repeating 'is'
Allows `is pure, commutative` syntax in addition to `is pure is commutative`.
After the initial `is`, comma-separated properties no longer require repeating
the `is` keyword (though it's still accepted for compatibility).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 07:44:18 -05:00
c2404a5ec1 docs: update CLAUDE.md with post-work checklist and CLI aliases table
Adds the post-work checklist (cargo check, cargo test, lux check, lux fmt,
lux lint) and documents all CLI command aliases. Updates test count to 381.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 07:36:09 -05:00
19068ead96 feat: add lux lint command with Lux-specific static analysis
Implements a linter with 21 lint rules across 6 categories (correctness,
suspicious, idiom, performance, style, pedantic). Lux-specific lints include
could-be-pure, could-be-total, unnecessary-effect-decl, and single-arm-match.
Integrates lints into `lux check` for unified type+lint checking. Available
standalone via `lux lint` (alias: `lux l`) with --explain for detailed help.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 07:35:36 -05:00
44ea1eebb0 style: auto-format example files with lux fmt
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 06:52:44 -05:00
8c90d5a8dc feat: CLI UX overhaul with colored output, timing, shorthands, and fuzzy suggestions
Add polished CLI output across all commands: colored help text, green/red
pass/fail indicators (✓/✗), elapsed timing on compile/check/test/fmt,
command shorthands (c/t/f/s/k), fuzzy "did you mean?" on typos, and
smart port-in-use suggestions for serve. Respects NO_COLOR/TERM=dumb.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 06:52:36 -05:00
62 changed files with 2817 additions and 1607 deletions

111
CLAUDE.md Normal file
View 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

View File

@@ -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))

View File

@@ -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)))

View File

@@ -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!")

View File

@@ -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,86 +8,56 @@ 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
// ============================================================================
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 + "\"",
@@ -106,27 +65,27 @@ fn showAttr(attr: Attr<Msg>): String =
OnClick(msg) => match msg {
Increment => "onclick=\"Increment\"",
Decrement => "onclick=\"Decrement\"",
Reset => "onclick=\"Reset\""
}
}
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)
}
}
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)
}
}
Some(rest) => showHtml(c, indent) + showChildren(rest, indent),
},
}
fn showHtml(html: Html<Msg>, indent: Int): String =
match html {
@@ -137,12 +96,8 @@ fn showHtml(html: Html<Msg>, indent: Int): String =
let attrPart = if String.length(attrStr) > 0 then " " + attrStr else ""
let childStr = showChildren(children, indent + 2)
"<" + tag + attrPart + ">" + childStr + "</" + tag + ">"
}
}
// ============================================================================
// Entry point
// ============================================================================
},
}
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 {}

View File

@@ -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)
}
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)
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)
}
Some(n) => "Result: " + toString(n),
}
// Print results
fn printResults(): Unit with {Console} = {
Console.print("Tree sum: " + toString(treeSum))
Console.print("Tree depth: " + toString(treeDepth))

View File

@@ -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))
}

View File

@@ -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 {}

View File

@@ -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 {
} 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")
}

View File

@@ -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))

View File

@@ -1,45 +1,34 @@
// 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))
}
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)
@@ -47,8 +36,8 @@ let doubled = mapOption(Some(21), double)
fn showOption(opt: Option<Int>): String =
match opt {
None => "None",
Some(x) => "Some(" + toString(x) + ")"
}
Some(x) => "Some(" + toString(x) + ")",
}
fn printResults(): Unit with {Console} = {
Console.print("identity(42) = " + toString(id_int))

View File

@@ -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) = {
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))
}

View File

@@ -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 {}

View File

@@ -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)")
}
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)
}
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)
}
},
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)
}
},
Err(e) => Console.print("Request failed: " + e),
}
}
let result = run main() with {}

View File

@@ -1,68 +1,31 @@
// 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 {
if List.length(pathParts) == 0 then true else {
match List.head(pathParts) {
None => true,
Some(pathPart) => {
@@ -74,12 +37,12 @@ fn matchParts(pathParts: List<String>, patternParts: List<String>): Bool = {
let restPath = Option.getOrElse(List.tail(pathParts), [])
let restPattern = Option.getOrElse(List.tail(patternParts), [])
matchParts(restPath, restPattern)
} else false
}
}
}
}
}
} 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"))
@@ -108,9 +65,9 @@ fn getUserHandler(path: String): { status: Int, body: String } = {
Some(id) => {
let body = jsonObj(jsonStr("id", id) + "," + jsonStr("name", "User " + id))
httpOk(body)
},
None => httpNotFound(jsonError("User not found"))
}
},
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 {
} 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} = {

View File

@@ -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")]))
@@ -32,41 +12,28 @@ fn getUserHandler(path: String): { status: Int, body: String } = {
Some(id) => {
let body = jsonObject(jsonJoin([jsonString("id", id), jsonString("name", "User " + id)]))
httpOk(body)
},
None => httpNotFound(jsonErrorMsg("User ID required"))
}
},
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 {
} 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} = {

View File

@@ -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)

View File

@@ -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
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)")
}
// Get bool field
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)")
}
// Get array field
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")
// 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)")
}
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)
}
None => Console.print(" (not an object)"),
}
},
Err(e) => Console.print("Parse error: " + e),
}
Console.print("")
Console.print("=== JSON Null Check ===")
let nullVal = Json.null()

View File

@@ -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))

View File

@@ -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"))
}

View File

@@ -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)))

View File

@@ -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)

View File

@@ -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 + "!"

View File

@@ -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)))

View File

@@ -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))

View File

@@ -1,36 +1,9 @@
// 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)
@@ -38,35 +11,32 @@ fn insertUser(connId: Int, name: String, email: String): Int with {Console, Post
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} => {
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")
}
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 {}

View File

@@ -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 {
} else {
let xs = genIntList(0, 100, 20)
if List.reverse(List.reverse(xs)) == xs then
testReverseInvolutive(n - 1, count)
else {
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 {
} else {
let xs = genIntList(0, 100, 20)
if List.length(List.reverse(xs)) == List.length(xs) then
testReverseLength(n - 1, count)
else {
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 {
} 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 {
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 {
} 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 {
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 {
} else {
let a = genInt(-1000, 1000)
let b = genInt(-1000, 1000)
if a + b == b + a then
testAddCommutative(n - 1, count)
else {
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 {
} 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 {
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 {
} 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 {
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 {
} else {
let x = genInt(-10000, 10000)
if x + 0 == x && 0 + x == x then
testAddIdentity(n - 1, count)
else {
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 {
} 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 {
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 {
} else {
let xs = genIntList(0, 100, 10)
if List.concat(xs, []) == xs && List.concat([], xs) == xs then
testConcatIdentity(n - 1, count)
else {
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!")

View File

@@ -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 {
} 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))
}

View File

@@ -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 ===")

View File

@@ -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)")
}
None => Console.print(" USER: (not set)"),
}
match Process.env("HOME") {
Some(home) => Console.print(" HOME: " + home),
None => Console.print(" HOME: (not set)")
}
None => Console.print(" HOME: (not set)"),
}
match Process.env("SHELL") {
Some(shell) => Console.print(" SHELL: " + shell),
None => Console.print(" SHELL: (not set)")
}
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 {
} else {
Console.print(" Count: " + toString(argCount))
match List.head(args) {
Some(first) => Console.print(" First: " + first),
None => Console.print(" First: (empty)")
}
}
None => Console.print(" First: (empty)"),
}
}
}
let result = run main() with {}

View File

@@ -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)
}

View File

@@ -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) = {
fn info(msg) =
{
Console.print("[INFO] " + msg)
resume(())
}
fn debug(msg) = {
}
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))
}

View File

@@ -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))
}

View File

@@ -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))
}

View File

@@ -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)))
}

View File

@@ -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)
@@ -22,16 +9,16 @@ fn eval(e: Expr): Int =
Num(n) => n,
Add(a, b) => eval(a) + eval(b),
Sub(a, b) => eval(a) - eval(b),
Mul(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) + ")"
}
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)
}

View File

@@ -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)

View File

@@ -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 {
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 {}

View File

@@ -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!")
}

View File

@@ -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 {}

View File

@@ -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 {
if remaining <= 0 then () else if isPrime(current) then {
Console.print(toString(current))
findPrimesHelper(current + 1, remaining - 1)
}
else findPrimesHelper(current + 1, remaining)
} else findPrimesHelper(current + 1, remaining)
fn main(): Unit with {Console} = {
Console.print("First 20 prime numbers:")

View File

@@ -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)))

View File

@@ -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
@@ -17,24 +7,23 @@ fn nextLight(light: TrafficLight): TrafficLight =
match light {
Red => Green,
Green => Yellow,
Yellow => Red
}
Yellow => Red,
}
fn canGo(light: TrafficLight): Bool =
match light {
Green => true,
Yellow => false,
Red => false
}
Red => false,
}
fn lightColor(light: TrafficLight): String =
match light {
Red => "red",
Yellow => "yellow",
Green => "green"
}
Green => "green",
}
// Door state machine
type DoorState =
| Open
| Closed
@@ -52,27 +41,30 @@ fn applyAction(state: DoorState, action: DoorAction): DoorState =
(Open, CloseDoor) => Closed,
(Closed, LockDoor) => Locked,
(Locked, UnlockDoor) => Closed,
_ => state
}
_ => state,
}
fn doorStateName(state: DoorState): String =
match state {
Open => "Open",
Closed => "Closed",
Locked => "Locked"
}
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))

View File

@@ -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")
}

View File

@@ -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")
}

View File

@@ -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))

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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]))

View File

@@ -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)

View File

@@ -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")
}

View File

@@ -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")
}

View File

@@ -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
@@ -27,15 +17,15 @@ fn colorName(c: Color): String =
Red => "red",
Green => "green",
Blue => "blue",
RGB(r, g, b) => "rgb(" + toString(r) + "," + toString(g) + "," + toString(b) + ")"
}
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))

View File

@@ -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")

View File

@@ -1,54 +1,30 @@
// 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
// ============================================================================
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 =
@@ -56,7 +32,7 @@ fn luxUpdate(model: Model, msgName: String): Model =
"Increment" => update(model, Increment),
"Decrement" => update(model, Decrement),
"Reset" => update(model, Reset),
_ => model
}
_ => model,
}
fn luxView(model: Model): String = view(model)

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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, &param_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, &param_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);

File diff suppressed because it is too large Load Diff

View File

@@ -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,10 +902,16 @@ 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) {
// 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);
}
}
Ok(properties)

View File

@@ -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(),

View File

@@ -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>> {