Compare commits
32 Commits
552e7a4972
...
v0.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ee3050704 | |||
| 80b1276f9f | |||
| bd843d2219 | |||
| d76aa17b38 | |||
| c23d9c7078 | |||
| fffacd2467 | |||
| 2ae2c132e5 | |||
| 4909ff9fff | |||
| 8e788c8a9f | |||
| dbdd3cca57 | |||
| 3ac022c04a | |||
| 6bedd37ac7 | |||
| 2909bf14b6 | |||
| d8871acf7e | |||
| 73b5eee664 | |||
| 542255780d | |||
| bac63bab2a | |||
| db82ca1a1c | |||
| 98605d2b70 | |||
| e3b6f4322a | |||
| d26fd975d1 | |||
| 1fa599f856 | |||
| c2404a5ec1 | |||
| 19068ead96 | |||
| 44ea1eebb0 | |||
| 8c90d5a8dc | |||
| bc60f1c8f1 | |||
| 52e3876b81 | |||
| 7e76acab18 | |||
| 5a853702d1 | |||
| fe985c96f5 | |||
| 4b553031fd |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,6 +1,14 @@
|
|||||||
/target
|
/target
|
||||||
/result
|
/result
|
||||||
|
|
||||||
|
# Claude Code project instructions
|
||||||
|
CLAUDE.md
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
_site/
|
||||||
|
docs/*.html
|
||||||
|
docs/*.css
|
||||||
|
|
||||||
# Test binaries
|
# Test binaries
|
||||||
hello
|
hello
|
||||||
test_rc
|
test_rc
|
||||||
|
|||||||
168
CLAUDE.md
Normal file
168
CLAUDE.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# 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 committable change)
|
||||||
|
|
||||||
|
**MANDATORY: Run the full validation script after every committable change:**
|
||||||
|
```bash
|
||||||
|
./scripts/validate.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This script runs ALL of the following checks and will fail if any regress:
|
||||||
|
1. `cargo check` — no Rust compilation errors
|
||||||
|
2. `cargo test` — all Rust tests pass (currently 387)
|
||||||
|
3. `cargo build --release` — release binary builds
|
||||||
|
4. `lux test` on every package (path, frontmatter, xml, rss, markdown) — all 286 package tests pass
|
||||||
|
5. `lux check` on every package — type checking + lint passes
|
||||||
|
|
||||||
|
If `validate.sh` is not available or you need to run manually:
|
||||||
|
```bash
|
||||||
|
nix develop --command cargo check # No Rust errors
|
||||||
|
nix develop --command cargo test # All Rust tests pass
|
||||||
|
nix develop --command cargo build --release # Build release binary
|
||||||
|
cd ../packages/path && ../../lang/target/release/lux test # Package tests
|
||||||
|
cd ../packages/frontmatter && ../../lang/target/release/lux test
|
||||||
|
cd ../packages/xml && ../../lang/target/release/lux test
|
||||||
|
cd ../packages/rss && ../../lang/target/release/lux test
|
||||||
|
cd ../packages/markdown && ../../lang/target/release/lux test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Do NOT commit if any check fails.** Fix the issue first.
|
||||||
|
|
||||||
|
### Commit after every piece of work
|
||||||
|
**After completing each logical unit of work, commit immediately.** Do not let changes accumulate uncommitted across multiple features. Each commit should be a single logical change (one feature, one bugfix, etc.). Use `--no-gpg-sign` flag for all commits.
|
||||||
|
|
||||||
|
**IMPORTANT: Always verify Lux code you write:**
|
||||||
|
- Run with interpreter: `./target/release/lux file.lux`
|
||||||
|
- Compile to binary: `./target/release/lux compile file.lux`
|
||||||
|
- 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 |
|
||||||
|
|
||||||
|
## Documenting Lux Language Errors
|
||||||
|
|
||||||
|
When working on any major task that involves writing Lux code, **document every language error, limitation, or surprising behavior** you encounter. This log is optimized for LLM consumption so future sessions can avoid repeating mistakes.
|
||||||
|
|
||||||
|
**File:** Maintain an `ISSUES.md` in the relevant project directory (e.g., `~/src/blu-site/ISSUES.md`).
|
||||||
|
|
||||||
|
**Format for each entry:**
|
||||||
|
```markdown
|
||||||
|
## Issue N: <Short descriptive title>
|
||||||
|
|
||||||
|
**Category**: Parser limitation | Type checker gap | Missing feature | Runtime error | Documentation gap
|
||||||
|
**Severity**: High | Medium | Low
|
||||||
|
**Status**: Open | **Fixed** (commit hash or version)
|
||||||
|
|
||||||
|
<1-2 sentence description of the problem>
|
||||||
|
|
||||||
|
**Reproduction:**
|
||||||
|
```lux
|
||||||
|
// Minimal code that triggers the issue
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error message:** `<exact error text>`
|
||||||
|
|
||||||
|
**Workaround:** <how to accomplish the goal despite the limitation>
|
||||||
|
|
||||||
|
**Fix:** <if fixed, what was changed and where>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
- Add new issues as you encounter them during any task
|
||||||
|
- When a previously documented issue gets fixed, update its status to **Fixed** and note the commit/version
|
||||||
|
- Remove entries that are no longer relevant (e.g., the feature was redesigned entirely)
|
||||||
|
- Keep the summary table at the bottom of ISSUES.md in sync with the entries
|
||||||
|
- Do NOT duplicate issues already documented -- check existing entries first
|
||||||
|
|
||||||
|
## Code Quality
|
||||||
|
|
||||||
|
- Fix all compiler warnings before committing
|
||||||
|
- Ensure all tests pass (currently 387 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
|
||||||
214
Cargo.lock
generated
214
Cargo.lock
generated
@@ -135,16 +135,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "core-foundation"
|
|
||||||
version = "0.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
|
||||||
dependencies = [
|
|
||||||
"core-foundation-sys",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
@@ -235,7 +225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -297,21 +287,6 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "foreign-types"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
|
||||||
dependencies = [
|
|
||||||
"foreign-types-shared",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "foreign-types-shared"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
@@ -552,16 +527,17 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-tls"
|
name = "hyper-rustls"
|
||||||
version = "0.5.0"
|
version = "0.24.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"futures-util",
|
||||||
|
"http",
|
||||||
"hyper",
|
"hyper",
|
||||||
"native-tls",
|
"rustls",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-rustls",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -843,23 +819,6 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "native-tls"
|
|
||||||
version = "0.2.16"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9d5d26952a508f321b4d3d2e80e78fc2603eaefcdf0c30783867f19586518bdc"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"log",
|
|
||||||
"openssl",
|
|
||||||
"openssl-probe",
|
|
||||||
"openssl-sys",
|
|
||||||
"schannel",
|
|
||||||
"security-framework",
|
|
||||||
"security-framework-sys",
|
|
||||||
"tempfile",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nibble_vec"
|
name = "nibble_vec"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -905,50 +864,6 @@ version = "1.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "openssl"
|
|
||||||
version = "0.10.75"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.10.0",
|
|
||||||
"cfg-if",
|
|
||||||
"foreign-types",
|
|
||||||
"libc",
|
|
||||||
"once_cell",
|
|
||||||
"openssl-macros",
|
|
||||||
"openssl-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "openssl-macros"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "openssl-probe"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "openssl-sys"
|
|
||||||
version = "0.9.111"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"libc",
|
|
||||||
"pkg-config",
|
|
||||||
"vcpkg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -1203,15 +1118,15 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-tls",
|
"hyper-rustls",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
"mime",
|
||||||
"native-tls",
|
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"rustls",
|
||||||
"rustls-pemfile",
|
"rustls-pemfile",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -1219,15 +1134,30 @@ dependencies = [
|
|||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"system-configuration",
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-rustls",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
|
"webpki-roots",
|
||||||
"winreg",
|
"winreg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.17.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"libc",
|
||||||
|
"untrusted",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rusqlite"
|
name = "rusqlite"
|
||||||
version = "0.31.0"
|
version = "0.31.0"
|
||||||
@@ -1252,7 +1182,19 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.21.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"ring",
|
||||||
|
"rustls-webpki",
|
||||||
|
"sct",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1264,6 +1206,16 @@ dependencies = [
|
|||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.101.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
@@ -1298,15 +1250,6 @@ version = "1.0.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "schannel"
|
|
||||||
version = "0.1.28"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
|
|
||||||
dependencies = [
|
|
||||||
"windows-sys 0.61.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -1314,26 +1257,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "sct"
|
||||||
version = "3.6.0"
|
version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38"
|
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"ring",
|
||||||
"core-foundation 0.10.1",
|
"untrusted",
|
||||||
"core-foundation-sys",
|
|
||||||
"libc",
|
|
||||||
"security-framework-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "security-framework-sys"
|
|
||||||
version = "2.16.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a"
|
|
||||||
dependencies = [
|
|
||||||
"core-foundation-sys",
|
|
||||||
"libc",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1521,7 +1451,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
|
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"core-foundation 0.9.4",
|
"core-foundation",
|
||||||
"system-configuration-sys",
|
"system-configuration-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1545,7 +1475,7 @@ dependencies = [
|
|||||||
"getrandom 0.4.1",
|
"getrandom 0.4.1",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1619,16 +1549,6 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tokio-native-tls"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
|
||||||
dependencies = [
|
|
||||||
"native-tls",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-postgres"
|
name = "tokio-postgres"
|
||||||
version = "0.7.16"
|
version = "0.7.16"
|
||||||
@@ -1655,6 +1575,16 @@ dependencies = [
|
|||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-rustls"
|
||||||
|
version = "0.24.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
|
||||||
|
dependencies = [
|
||||||
|
"rustls",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.18"
|
version = "0.7.18"
|
||||||
@@ -1750,6 +1680,12 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
@@ -1941,6 +1877,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "0.25.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "whoami"
|
name = "whoami"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lux"
|
name = "lux"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "A functional programming language with first-class effects, schema evolution, and behavioral types"
|
description = "A functional programming language with first-class effects, schema evolution, and behavioral types"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -13,7 +13,7 @@ lsp-types = "0.94"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
||||||
tiny_http = "0.12"
|
tiny_http = "0.12"
|
||||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||||
postgres = "0.19"
|
postgres = "0.19"
|
||||||
|
|||||||
367
PACKAGES.md
Normal file
367
PACKAGES.md
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
# Lux Package Ecosystem Plan
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
### Stdlib (built-in)
|
||||||
|
| Module | Coverage |
|
||||||
|
|--------|----------|
|
||||||
|
| String | Comprehensive (split, join, trim, indexOf, replace, etc.) |
|
||||||
|
| List | Good (map, filter, fold, head, tail, concat, range, find, any, all, take, drop) |
|
||||||
|
| Option | Basic (map, flatMap, getOrElse, isSome, isNone) |
|
||||||
|
| Result | Basic (map, flatMap, getOrElse, isOk, isErr) |
|
||||||
|
| Math | Basic (abs, min, max, sqrt, pow, floor, ceil, round) |
|
||||||
|
| Json | Comprehensive (parse, stringify, get, typed extractors, constructors) |
|
||||||
|
| File | Good (read, write, append, exists, delete, readDir, isDir, mkdir) |
|
||||||
|
| Console | Good (print, read, readLine, readInt) |
|
||||||
|
| Process | Good (exec, execStatus, env, args, exit, cwd) |
|
||||||
|
| Http | Basic (get, post, put, delete, setHeader) |
|
||||||
|
| HttpServer | Basic (listen, accept, respond) |
|
||||||
|
| Time | Minimal (now, sleep) |
|
||||||
|
| Random | Basic (int, float, bool) |
|
||||||
|
| Sql | Good (SQLite: open, query, execute, transactions) |
|
||||||
|
| Postgres | Good (connect, query, execute, transactions) |
|
||||||
|
| Schema | Niche (versioned data migration) |
|
||||||
|
| Test | Good (assert, assertEqual, assertTrue) |
|
||||||
|
| Concurrent | Experimental (spawn, await, yield, cancel) |
|
||||||
|
| Channel | Experimental (create, send, receive) |
|
||||||
|
|
||||||
|
### Registry (pkgs.lux) - 3 packages
|
||||||
|
| Package | Version | Notes |
|
||||||
|
|---------|---------|-------|
|
||||||
|
| json | 1.0.0 | Wraps stdlib Json with convenience functions (getPath, getString, etc.) |
|
||||||
|
| http-client | 0.1.0 | Wraps stdlib Http with JSON helpers, URL encoding |
|
||||||
|
| testing | 0.1.0 | Wraps stdlib Test with describe/it structure |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gap Analysis
|
||||||
|
|
||||||
|
### What's Missing vs Other Languages
|
||||||
|
|
||||||
|
Compared to ecosystems like Rust/cargo, Go, Python, Elm, Gleam:
|
||||||
|
|
||||||
|
| Category | Gap | Impact | Notes |
|
||||||
|
|----------|-----|--------|-------|
|
||||||
|
| **Collections** | No HashMap, Set, Queue, Stack | Critical | List-of-pairs with O(n) lookup is the only option |
|
||||||
|
| **Sorting** | No List.sort or List.sortBy | High | Must implement insertion sort manually |
|
||||||
|
| **Date/Time** | Only `Time.now()` (epoch ms), no parsing/formatting | High | blu-site does string-based date formatting manually |
|
||||||
|
| **Markdown** | No markdown parser | High | blu-site has 300+ lines of hand-rolled markdown |
|
||||||
|
| **XML/RSS** | No XML generation | High | Can't generate RSS feeds or sitemaps |
|
||||||
|
| **Regex** | No pattern matching on strings | High | Character-by-character scanning required |
|
||||||
|
| **Path** | No file path utilities | Medium | basename/dirname manually reimplemented |
|
||||||
|
| **YAML/TOML** | No config file parsing (beyond JSON) | Medium | Frontmatter parsing is manual |
|
||||||
|
| **Template** | No string templating | Medium | HTML built via raw string concatenation |
|
||||||
|
| **URL** | No URL parsing/encoding | Medium | http-client has basic urlEncode but no parser |
|
||||||
|
| **Crypto** | No hashing (SHA256, etc.) | Medium | Can't do checksums, content hashing |
|
||||||
|
| **Base64** | No encoding/decoding | Low | Needed for data URIs, some auth |
|
||||||
|
| **CSV** | No CSV parsing | Low | Common data format |
|
||||||
|
| **UUID** | No UUID generation | Low | Useful for IDs |
|
||||||
|
| **Logging** | No structured logging | Low | Just Console.print |
|
||||||
|
| **CLI** | No argument parsing library | Low | Manual arg handling |
|
||||||
|
|
||||||
|
### What Should Be Stdlib vs Package
|
||||||
|
|
||||||
|
**Should be stdlib additions** (too fundamental to be packages):
|
||||||
|
- HashMap / Map type (requires runtime support)
|
||||||
|
- List.sort / List.sortBy (fundamental operation)
|
||||||
|
- Better Time module (date parsing, formatting)
|
||||||
|
- Regex (needs runtime/C support for performance)
|
||||||
|
- Path module (cross-platform file path handling)
|
||||||
|
|
||||||
|
**Should be packages** (application-level, opinionated, composable):
|
||||||
|
- markdown
|
||||||
|
- xml
|
||||||
|
- rss/atom
|
||||||
|
- frontmatter
|
||||||
|
- template
|
||||||
|
- csv
|
||||||
|
- crypto
|
||||||
|
- ssg (static site generator framework)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priority Package Plans
|
||||||
|
|
||||||
|
Ordered by what unblocks blu-site fixes first, then general ecosystem value.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Package 1: `markdown` (Priority: HIGHEST)
|
||||||
|
|
||||||
|
**Why:** The 300-line markdown parser in blu-site's main.lux is general-purpose code that belongs in a reusable package. It's also the most complex part of blu-site and has known bugs (e.g., `### ` inside list items renders literally).
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
```
|
||||||
|
markdown/
|
||||||
|
lux.toml
|
||||||
|
lib.lux # Public API: parse, parseInline
|
||||||
|
src/
|
||||||
|
inline.lux # Inline parsing (bold, italic, links, images, code)
|
||||||
|
block.lux # Block parsing (headings, lists, code blocks, blockquotes, hr)
|
||||||
|
types.lux # AST types (optional - could emit HTML directly)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Public API:**
|
||||||
|
```lux
|
||||||
|
// Convert markdown string to HTML string
|
||||||
|
pub fn toHtml(markdown: String): String
|
||||||
|
|
||||||
|
// Convert inline markdown only (no blocks)
|
||||||
|
pub fn inlineToHtml(text: String): String
|
||||||
|
|
||||||
|
// Escape HTML entities
|
||||||
|
pub fn escapeHtml(s: String): String
|
||||||
|
```
|
||||||
|
|
||||||
|
**Improvements over current blu-site code:**
|
||||||
|
- Fix heading-inside-list-item rendering (`- ### Title` should work)
|
||||||
|
- Support nested lists (currently flat only)
|
||||||
|
- Support reference-style links `[text][ref]`
|
||||||
|
- Handle edge cases (empty lines in code blocks, nested blockquotes)
|
||||||
|
- Proper HTML entity escaping in more contexts
|
||||||
|
|
||||||
|
**Depends on:** Nothing (pure string processing)
|
||||||
|
|
||||||
|
**Estimated size:** ~400-500 lines of Lux
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Package 2: `xml` (Priority: HIGH)
|
||||||
|
|
||||||
|
**Why:** Needed for RSS/Atom feed generation, sitemap.xml, and robots.txt generation. General-purpose XML builder that doesn't try to parse XML (which would need regex), just emits it.
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
```
|
||||||
|
xml/
|
||||||
|
lux.toml
|
||||||
|
lib.lux # Public API: element, document, serialize
|
||||||
|
```
|
||||||
|
|
||||||
|
**Public API:**
|
||||||
|
```lux
|
||||||
|
type XmlNode =
|
||||||
|
| Element(String, List<XmlAttr>, List<XmlNode>)
|
||||||
|
| Text(String)
|
||||||
|
| CData(String)
|
||||||
|
| Comment(String)
|
||||||
|
| Declaration(String, String) // version, encoding
|
||||||
|
|
||||||
|
type XmlAttr =
|
||||||
|
| Attr(String, String)
|
||||||
|
|
||||||
|
// Build an XML element
|
||||||
|
pub fn element(tag: String, attrs: List<XmlAttr>, children: List<XmlNode>): XmlNode
|
||||||
|
|
||||||
|
// Build a text node (auto-escapes)
|
||||||
|
pub fn text(content: String): XmlNode
|
||||||
|
|
||||||
|
// Build a CDATA section
|
||||||
|
pub fn cdata(content: String): XmlNode
|
||||||
|
|
||||||
|
// Serialize XML tree to string
|
||||||
|
pub fn serialize(node: XmlNode): String
|
||||||
|
|
||||||
|
// Serialize with XML declaration header
|
||||||
|
pub fn document(version: String, encoding: String, root: XmlNode): String
|
||||||
|
|
||||||
|
// Convenience: self-closing element
|
||||||
|
pub fn selfClosing(tag: String, attrs: List<XmlAttr>): XmlNode
|
||||||
|
```
|
||||||
|
|
||||||
|
**Depends on:** Nothing
|
||||||
|
|
||||||
|
**Estimated size:** ~150-200 lines
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Package 3: `rss` (Priority: HIGH)
|
||||||
|
|
||||||
|
**Why:** Directly needed for blu-site's #6 priority fix (add RSS feed). Builds on `xml` package.
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
```
|
||||||
|
rss/
|
||||||
|
lux.toml # depends on xml
|
||||||
|
lib.lux # Public API: feed, item, toXml, toAtom
|
||||||
|
```
|
||||||
|
|
||||||
|
**Public API:**
|
||||||
|
```lux
|
||||||
|
type FeedInfo =
|
||||||
|
| FeedInfo(String, String, String, String, String)
|
||||||
|
// title, link, description, language, lastBuildDate
|
||||||
|
|
||||||
|
type FeedItem =
|
||||||
|
| FeedItem(String, String, String, String, String, String)
|
||||||
|
// title, link, description, pubDate, guid, categories (comma-separated)
|
||||||
|
|
||||||
|
// Generate RSS 2.0 XML string
|
||||||
|
pub fn toRss(info: FeedInfo, items: List<FeedItem>): String
|
||||||
|
|
||||||
|
// Generate Atom 1.0 XML string
|
||||||
|
pub fn toAtom(info: FeedInfo, items: List<FeedItem>): String
|
||||||
|
```
|
||||||
|
|
||||||
|
**Depends on:** `xml`
|
||||||
|
|
||||||
|
**Estimated size:** ~100-150 lines
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Package 4: `frontmatter` (Priority: HIGH)
|
||||||
|
|
||||||
|
**Why:** blu-site has ~50 lines of fragile frontmatter parsing. This is a common need for any content-driven Lux project. The current parser uses `String.indexOf(line, ": ")` which breaks on values containing `: `.
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
```
|
||||||
|
frontmatter/
|
||||||
|
lux.toml
|
||||||
|
lib.lux # Public API: parse
|
||||||
|
```
|
||||||
|
|
||||||
|
**Public API:**
|
||||||
|
```lux
|
||||||
|
type FrontmatterResult =
|
||||||
|
| FrontmatterResult(List<(String, String)>, String)
|
||||||
|
// key-value pairs, remaining body
|
||||||
|
|
||||||
|
// Parse frontmatter from a string (--- delimited YAML-like header)
|
||||||
|
pub fn parse(content: String): FrontmatterResult
|
||||||
|
|
||||||
|
// Get a value by key from parsed frontmatter
|
||||||
|
pub fn get(pairs: List<(String, String)>, key: String): Option<String>
|
||||||
|
|
||||||
|
// Get a value or default
|
||||||
|
pub fn getOrDefault(pairs: List<(String, String)>, key: String, default: String): String
|
||||||
|
|
||||||
|
// Parse a space-separated tag string into a list
|
||||||
|
pub fn parseTags(tagString: String): List<String>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Improvements over current blu-site code:**
|
||||||
|
- Handle values with `: ` in them (only split on first `: `)
|
||||||
|
- Handle multi-line values (indented continuation)
|
||||||
|
- Handle quoted values with embedded newlines
|
||||||
|
- Strip quotes from values consistently
|
||||||
|
|
||||||
|
**Depends on:** Nothing
|
||||||
|
|
||||||
|
**Estimated size:** ~100-150 lines
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Package 5: `path` (Priority: MEDIUM)
|
||||||
|
|
||||||
|
**Why:** blu-site manually implements `basename` and `dirname`. Any file-processing Lux program needs these. Tiny but universally useful.
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
```
|
||||||
|
path/
|
||||||
|
lux.toml
|
||||||
|
lib.lux
|
||||||
|
```
|
||||||
|
|
||||||
|
**Public API:**
|
||||||
|
```lux
|
||||||
|
// Get filename from path: "/foo/bar.txt" -> "bar.txt"
|
||||||
|
pub fn basename(p: String): String
|
||||||
|
|
||||||
|
// Get directory from path: "/foo/bar.txt" -> "/foo"
|
||||||
|
pub fn dirname(p: String): String
|
||||||
|
|
||||||
|
// Get file extension: "file.txt" -> "txt", "file" -> ""
|
||||||
|
pub fn extension(p: String): String
|
||||||
|
|
||||||
|
// Remove file extension: "file.txt" -> "file"
|
||||||
|
pub fn stem(p: String): String
|
||||||
|
|
||||||
|
// Join path segments: join("foo", "bar") -> "foo/bar"
|
||||||
|
pub fn join(a: String, b: String): String
|
||||||
|
|
||||||
|
// Normalize path: "foo//bar/../baz" -> "foo/baz"
|
||||||
|
pub fn normalize(p: String): String
|
||||||
|
|
||||||
|
// Check if path is absolute
|
||||||
|
pub fn isAbsolute(p: String): Bool
|
||||||
|
```
|
||||||
|
|
||||||
|
**Depends on:** Nothing
|
||||||
|
|
||||||
|
**Estimated size:** ~80-120 lines
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Package 6: `sitemap` (Priority: MEDIUM)
|
||||||
|
|
||||||
|
**Why:** Directly needed for blu-site's #9 priority fix. Simple package that generates sitemap.xml.
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
```
|
||||||
|
sitemap/
|
||||||
|
lux.toml # depends on xml
|
||||||
|
lib.lux
|
||||||
|
```
|
||||||
|
|
||||||
|
**Public API:**
|
||||||
|
```lux
|
||||||
|
type SitemapEntry =
|
||||||
|
| SitemapEntry(String, String, String, String)
|
||||||
|
// url, lastmod (ISO date), changefreq, priority
|
||||||
|
|
||||||
|
// Generate sitemap.xml string
|
||||||
|
pub fn generate(entries: List<SitemapEntry>): String
|
||||||
|
|
||||||
|
// Generate a simple robots.txt pointing to the sitemap
|
||||||
|
pub fn robotsTxt(sitemapUrl: String): String
|
||||||
|
```
|
||||||
|
|
||||||
|
**Depends on:** `xml`
|
||||||
|
|
||||||
|
**Estimated size:** ~50-70 lines
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Package 7: `ssg` (Priority: LOW - future)
|
||||||
|
|
||||||
|
**Why:** Once markdown, frontmatter, rss, sitemap, and path packages exist, the remaining logic in blu-site's main.lux is generic SSG framework code: read content dirs, parse posts, sort by date, generate section indexes, generate tag pages, copy static assets. This could be extracted into a framework package that other Lux users could use to build their own static sites.
|
||||||
|
|
||||||
|
**This should wait** until the foundation packages above are stable and battle-tested through blu-site usage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Non-Package Stdlib Improvements Needed
|
||||||
|
|
||||||
|
These gaps are too fundamental to be packages and should be added to the Lux language itself:
|
||||||
|
|
||||||
|
### HashMap (Critical)
|
||||||
|
Every package above that needs key-value lookups (frontmatter, xml attributes, etc.) is working around the lack of HashMap with `List<(String, String)>`. This is O(n) per lookup and makes code verbose. A stdlib `Map` module would transform the ecosystem.
|
||||||
|
|
||||||
|
### List.sort / List.sortBy (High)
|
||||||
|
blu-site implements insertion sort manually. Every content-driven app needs sorting. This should be a stdlib function.
|
||||||
|
|
||||||
|
### Time.format / Time.parse (High)
|
||||||
|
blu-site manually parses "2025-01-15" by substring extraction and maps month numbers to names. A proper date/time library (even just ISO 8601 parsing and basic formatting) would help every package above.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 1 (unblock blu-site fixes):
|
||||||
|
1. markdown - extract from blu-site, fix bugs, publish
|
||||||
|
2. frontmatter - extract from blu-site, improve robustness
|
||||||
|
3. path - tiny, universally useful
|
||||||
|
4. xml - needed by rss and sitemap
|
||||||
|
|
||||||
|
Phase 2 (complete blu-site features):
|
||||||
|
5. rss - depends on xml
|
||||||
|
6. sitemap - depends on xml
|
||||||
|
|
||||||
|
Phase 3 (ecosystem growth):
|
||||||
|
7. template - string templating (mustache-like)
|
||||||
|
8. csv - data processing
|
||||||
|
9. cli - argument parsing
|
||||||
|
10. ssg - framework extraction from blu-site
|
||||||
|
```
|
||||||
|
|
||||||
|
Each package should be developed in its own directory under `~/src/`, published to the git.qrty.ink registry, and tested by integrating it into blu-site.
|
||||||
20
README.md
20
README.md
@@ -2,15 +2,22 @@
|
|||||||
|
|
||||||
A functional programming language with first-class effects, schema evolution, and behavioral types.
|
A functional programming language with first-class effects, schema evolution, and behavioral types.
|
||||||
|
|
||||||
## Vision
|
## Philosophy
|
||||||
|
|
||||||
Most programming languages treat three critical concerns as afterthoughts:
|
**Make the important things visible.**
|
||||||
|
|
||||||
1. **Effects** — What can this code do? (Hidden, untraceable, untestable)
|
Most languages hide what matters most: what code can do (effects), how data changes over time (schema evolution), and what guarantees functions provide (behavioral properties). Lux makes all three first-class, compiler-checked language features.
|
||||||
2. **Data Evolution** — Types change, data persists. (Manual migrations, runtime failures)
|
|
||||||
3. **Behavioral Properties** — Is this idempotent? Does it terminate? (Comments and hope)
|
|
||||||
|
|
||||||
Lux makes these first-class language features. The compiler knows what your code does, how your data evolves, and what properties your functions guarantee.
|
| Principle | What it means |
|
||||||
|
|-----------|--------------|
|
||||||
|
| **Explicit over implicit** | Effects in types — see what code does |
|
||||||
|
| **Composition over configuration** | No DI frameworks — effects compose naturally |
|
||||||
|
| **Safety without ceremony** | Type inference + explicit signatures where they matter |
|
||||||
|
| **Practical over academic** | Familiar syntax, ML semantics, no monads |
|
||||||
|
| **One right way** | Opinionated formatter, integrated tooling, built-in test framework |
|
||||||
|
| **Tools are the language** | `lux fmt/lint/check/test/compile` — one binary, not seven tools |
|
||||||
|
|
||||||
|
See [docs/PHILOSOPHY.md](./docs/PHILOSOPHY.md) for the full philosophy with language comparisons and design rationale.
|
||||||
|
|
||||||
## Core Principles
|
## Core Principles
|
||||||
|
|
||||||
@@ -144,6 +151,7 @@ fn main(): Unit with {Console} =
|
|||||||
- String, List, Option, Result, Math, JSON modules
|
- String, List, Option, Result, Math, JSON modules
|
||||||
- Console, File, Http, Random, Time, Process effects
|
- Console, File, Http, Random, Time, Process effects
|
||||||
- SQL effect (SQLite with transactions)
|
- SQL effect (SQLite with transactions)
|
||||||
|
- PostgreSQL effect (connection pooling ready)
|
||||||
- DOM effect (40+ browser operations)
|
- DOM effect (40+ browser operations)
|
||||||
|
|
||||||
See:
|
See:
|
||||||
|
|||||||
38
build.rs
Normal file
38
build.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Capture the absolute C compiler path at build time so the binary is self-contained.
|
||||||
|
// This is critical for Nix builds where cc/gcc live in /nix/store paths.
|
||||||
|
let cc_path = std::env::var("CC").ok()
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.and_then(|s| resolve_absolute(&s))
|
||||||
|
.or_else(|| find_in_path("cc"))
|
||||||
|
.or_else(|| find_in_path("gcc"))
|
||||||
|
.or_else(|| find_in_path("clang"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
println!("cargo:rustc-env=LUX_CC_PATH={}", cc_path);
|
||||||
|
println!("cargo:rerun-if-env-changed=CC");
|
||||||
|
println!("cargo:rerun-if-env-changed=PATH");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a command name to its absolute path by searching PATH.
|
||||||
|
fn find_in_path(cmd: &str) -> Option<String> {
|
||||||
|
let path_var = std::env::var("PATH").ok()?;
|
||||||
|
for dir in path_var.split(':') {
|
||||||
|
let candidate = PathBuf::from(dir).join(cmd);
|
||||||
|
if candidate.is_file() {
|
||||||
|
return Some(candidate.to_string_lossy().into_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If the path is already absolute and exists, return it. Otherwise search PATH.
|
||||||
|
fn resolve_absolute(cmd: &str) -> Option<String> {
|
||||||
|
let p = PathBuf::from(cmd);
|
||||||
|
if p.is_absolute() && p.is_file() {
|
||||||
|
return Some(cmd.to_string());
|
||||||
|
}
|
||||||
|
find_in_path(cmd)
|
||||||
|
}
|
||||||
400
docs/COMPILER_OPTIMIZATIONS.md
Normal file
400
docs/COMPILER_OPTIMIZATIONS.md
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
# Compiler Optimizations from Behavioral Types
|
||||||
|
|
||||||
|
This document describes optimization opportunities enabled by Lux's behavioral type system. When functions are annotated with properties like `is pure`, `is total`, `is idempotent`, `is deterministic`, or `is commutative`, the compiler gains knowledge that enables aggressive optimizations.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
| Property | Key Optimizations |
|
||||||
|
|----------|-------------------|
|
||||||
|
| `is pure` | Memoization, CSE, dead code elimination, auto-parallelization |
|
||||||
|
| `is total` | No exception handling, aggressive inlining, loop unrolling |
|
||||||
|
| `is deterministic` | Result caching, test reproducibility, parallel execution |
|
||||||
|
| `is idempotent` | Duplicate call elimination, retry optimization |
|
||||||
|
| `is commutative` | Argument reordering, parallel reduction, algebraic simplification |
|
||||||
|
|
||||||
|
## Pure Function Optimizations
|
||||||
|
|
||||||
|
When a function is marked `is pure`:
|
||||||
|
|
||||||
|
### 1. Memoization (Automatic Caching)
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn fib(n: Int): Int is pure =
|
||||||
|
if n <= 1 then n else fib(n - 1) + fib(n - 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization**: The compiler can automatically memoize results. Since `fib` is pure, `fib(10)` will always return the same value, so we can cache it.
|
||||||
|
|
||||||
|
**Implementation approach**:
|
||||||
|
- Maintain a hash map of argument → result mappings
|
||||||
|
- Before computing, check if result exists
|
||||||
|
- Store results after computation
|
||||||
|
- Use LRU eviction for memory management
|
||||||
|
|
||||||
|
**Impact**: Reduces exponential recursive calls to linear time.
|
||||||
|
|
||||||
|
### 2. Common Subexpression Elimination (CSE)
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn compute(x: Int): Int is pure =
|
||||||
|
expensive(x) + expensive(x) // Same call twice
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization**: The compiler recognizes both calls are identical and computes `expensive(x)` only once.
|
||||||
|
|
||||||
|
**Transformed to**:
|
||||||
|
```lux
|
||||||
|
fn compute(x: Int): Int is pure =
|
||||||
|
let temp = expensive(x)
|
||||||
|
temp + temp
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Eliminates redundant computation.
|
||||||
|
|
||||||
|
### 3. Dead Code Elimination
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn example(): Int is pure = {
|
||||||
|
let unused = expensiveComputation() // Result not used
|
||||||
|
42
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization**: Since `expensiveComputation` is pure (no side effects), and its result is unused, the entire call can be eliminated.
|
||||||
|
|
||||||
|
**Impact**: Removes unnecessary work.
|
||||||
|
|
||||||
|
### 4. Auto-Parallelization
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn processAll(items: List<Item>): List<Result> is pure =
|
||||||
|
List.map(items, processItem) // processItem is pure
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization**: Since `processItem` is pure, each invocation is independent. The compiler can automatically parallelize the map operation.
|
||||||
|
|
||||||
|
**Implementation approach**:
|
||||||
|
- Detect pure functions in map/filter/fold operations
|
||||||
|
- Split work across available cores
|
||||||
|
- Merge results (order-preserving for map)
|
||||||
|
|
||||||
|
**Impact**: Linear speedup with core count for CPU-bound operations.
|
||||||
|
|
||||||
|
### 5. Speculative Execution
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn decide(cond: Bool, a: Int, b: Int): Int is pure =
|
||||||
|
if cond then computeA(a) else computeB(b)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization**: Both branches can be computed in parallel before the condition is known, since neither has side effects.
|
||||||
|
|
||||||
|
**Impact**: Reduced latency when condition evaluation is slow.
|
||||||
|
|
||||||
|
## Total Function Optimizations
|
||||||
|
|
||||||
|
When a function is marked `is total`:
|
||||||
|
|
||||||
|
### 1. Exception Handling Elimination
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn safeCompute(x: Int): Int is total =
|
||||||
|
complexCalculation(x)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization**: No try/catch blocks needed around calls to `safeCompute`. The compiler knows it will never throw or fail.
|
||||||
|
|
||||||
|
**Generated code difference**:
|
||||||
|
```c
|
||||||
|
// Without is total - needs error checking
|
||||||
|
Result result = safeCompute(x);
|
||||||
|
if (result.is_error) { handle_error(); }
|
||||||
|
|
||||||
|
// With is total - direct call
|
||||||
|
int result = safeCompute(x);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Reduced code size, better branch prediction.
|
||||||
|
|
||||||
|
### 2. Aggressive Inlining
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn square(x: Int): Int is total = x * x
|
||||||
|
|
||||||
|
fn sumOfSquares(a: Int, b: Int): Int is total =
|
||||||
|
square(a) + square(b)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization**: Total functions are safe to inline aggressively because:
|
||||||
|
- They won't change control flow unexpectedly
|
||||||
|
- They won't introduce exception handling complexity
|
||||||
|
- Their termination is guaranteed
|
||||||
|
|
||||||
|
**Impact**: Eliminates function call overhead, enables further optimizations.
|
||||||
|
|
||||||
|
### 3. Loop Unrolling
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn sumList(xs: List<Int>): Int is total =
|
||||||
|
List.fold(xs, 0, fn(acc: Int, x: Int): Int is total => acc + x)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization**: When the list size is known at compile time and the fold function is total, the loop can be fully unrolled.
|
||||||
|
|
||||||
|
**Impact**: Eliminates loop overhead, enables vectorization.
|
||||||
|
|
||||||
|
### 4. Termination Assumptions
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn processRecursive(data: Tree): Result is total =
|
||||||
|
match data {
|
||||||
|
Leaf(v) => Result.single(v),
|
||||||
|
Node(left, right) => {
|
||||||
|
let l = processRecursive(left)
|
||||||
|
let r = processRecursive(right)
|
||||||
|
Result.merge(l, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization**: The compiler can assume this recursion terminates, allowing optimizations like:
|
||||||
|
- Converting recursion to iteration
|
||||||
|
- Allocating fixed stack space
|
||||||
|
- Tail call optimization
|
||||||
|
|
||||||
|
**Impact**: Stack safety, predictable memory usage.
|
||||||
|
|
||||||
|
## Deterministic Function Optimizations
|
||||||
|
|
||||||
|
When a function is marked `is deterministic`:
|
||||||
|
|
||||||
|
### 1. Compile-Time Evaluation
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn hashConstant(s: String): Int is deterministic = computeHash(s)
|
||||||
|
|
||||||
|
let key = hashConstant("api_key") // Constant input
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization**: Since the input is a compile-time constant and the function is deterministic, the result can be computed at compile time.
|
||||||
|
|
||||||
|
**Transformed to**:
|
||||||
|
```lux
|
||||||
|
let key = 7823491 // Pre-computed
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Zero runtime cost for constant computations.
|
||||||
|
|
||||||
|
### 2. Result Caching Across Runs
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn parseConfig(path: String): Config is deterministic with {File} =
|
||||||
|
Json.parse(File.read(path))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization**: Results can be cached persistently. If the file hasn't changed, the cached result is valid.
|
||||||
|
|
||||||
|
**Implementation approach**:
|
||||||
|
- Hash inputs (including file contents)
|
||||||
|
- Store results in persistent cache
|
||||||
|
- Validate cache on next run
|
||||||
|
|
||||||
|
**Impact**: Faster startup times, reduced I/O.
|
||||||
|
|
||||||
|
### 3. Reproducible Parallel Execution
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn renderImages(images: List<Image>): List<Bitmap> is deterministic =
|
||||||
|
List.map(images, render)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization**: Deterministic parallel execution guarantees same results regardless of scheduling order. This enables:
|
||||||
|
- Work stealing without synchronization concerns
|
||||||
|
- Speculative execution without rollback complexity
|
||||||
|
- Distributed computation across machines
|
||||||
|
|
||||||
|
**Impact**: Easier parallelization, simpler distributed systems.
|
||||||
|
|
||||||
|
## Idempotent Function Optimizations
|
||||||
|
|
||||||
|
When a function is marked `is idempotent`:
|
||||||
|
|
||||||
|
### 1. Duplicate Call Elimination
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn setFlag(config: Config, flag: Bool): Config is idempotent =
|
||||||
|
{ ...config, enabled: flag }
|
||||||
|
|
||||||
|
fn configure(c: Config): Config is idempotent =
|
||||||
|
c |> setFlag(true) |> setFlag(true) |> setFlag(true)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization**: Multiple consecutive calls with the same arguments can be collapsed to one.
|
||||||
|
|
||||||
|
**Transformed to**:
|
||||||
|
```lux
|
||||||
|
fn configure(c: Config): Config is idempotent =
|
||||||
|
setFlag(c, true)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Eliminates redundant operations.
|
||||||
|
|
||||||
|
### 2. Retry Optimization
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn sendRequest(data: Request): Response is idempotent with {Http} =
|
||||||
|
Http.put("/api/resource", data)
|
||||||
|
|
||||||
|
fn reliableSend(data: Request): Response with {Http} =
|
||||||
|
retry(3, fn(): Response => sendRequest(data))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization**: The retry mechanism knows the operation is safe to retry without side effects accumulating.
|
||||||
|
|
||||||
|
**Implementation approach**:
|
||||||
|
- No need for transaction logs
|
||||||
|
- No need for "already processed" checks
|
||||||
|
- Simple retry loop
|
||||||
|
|
||||||
|
**Impact**: Simpler error recovery, reduced complexity.
|
||||||
|
|
||||||
|
### 3. Convergent Computation
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn normalize(value: Float): Float is idempotent =
|
||||||
|
clamp(round(value, 2), 0.0, 1.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization**: In iterative algorithms, the compiler can detect when a value has converged (applying the function no longer changes it).
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Can terminate early when values stop changing
|
||||||
|
fn iterateUntilStable(values: List<Float>): List<Float> =
|
||||||
|
let normalized = List.map(values, normalize)
|
||||||
|
if normalized == values then values
|
||||||
|
else iterateUntilStable(normalized)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Early termination of iterative algorithms.
|
||||||
|
|
||||||
|
## Commutative Function Optimizations
|
||||||
|
|
||||||
|
When a function is marked `is commutative`:
|
||||||
|
|
||||||
|
### 1. Argument Reordering
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn multiply(a: Int, b: Int): Int is commutative = a * b
|
||||||
|
|
||||||
|
// In a computation
|
||||||
|
multiply(expensiveA(), cheapB())
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization**: Evaluate the cheaper argument first to enable short-circuit optimizations or better register allocation.
|
||||||
|
|
||||||
|
**Impact**: Improved instruction scheduling.
|
||||||
|
|
||||||
|
### 2. Parallel Reduction
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn add(a: Int, b: Int): Int is commutative = a + b
|
||||||
|
|
||||||
|
fn sum(xs: List<Int>): Int =
|
||||||
|
List.fold(xs, 0, add)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization**: Since `add` is commutative (and associative), the fold can be parallelized:
|
||||||
|
|
||||||
|
```
|
||||||
|
[1, 2, 3, 4, 5, 6, 7, 8]
|
||||||
|
↓ parallel reduce
|
||||||
|
[(1+2), (3+4), (5+6), (7+8)]
|
||||||
|
↓ parallel reduce
|
||||||
|
[(3+7), (11+15)]
|
||||||
|
↓ parallel reduce
|
||||||
|
[36]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: O(log n) parallel reduction instead of O(n) sequential.
|
||||||
|
|
||||||
|
### 3. Algebraic Simplification
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn add(a: Int, b: Int): Int is commutative = a + b
|
||||||
|
|
||||||
|
// Expression: add(x, add(y, z))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization**: Commutative operations can be reordered for simplification:
|
||||||
|
- `add(x, 0)` → `x`
|
||||||
|
- `add(add(x, 1), add(y, 1))` → `add(add(x, y), 2)`
|
||||||
|
|
||||||
|
**Impact**: Constant folding, strength reduction.
|
||||||
|
|
||||||
|
## Combined Property Optimizations
|
||||||
|
|
||||||
|
Properties can be combined for even more powerful optimizations:
|
||||||
|
|
||||||
|
### Pure + Deterministic + Total
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn computeKey(data: String): Int
|
||||||
|
is pure
|
||||||
|
is deterministic
|
||||||
|
is total = {
|
||||||
|
// Hash computation
|
||||||
|
List.fold(String.chars(data), 0, fn(acc: Int, c: Char): Int =>
|
||||||
|
acc * 31 + Char.code(c))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Enabled optimizations**:
|
||||||
|
- Compile-time evaluation for constants
|
||||||
|
- Automatic memoization at runtime
|
||||||
|
- Parallel execution in batch operations
|
||||||
|
- No exception handling needed
|
||||||
|
- Safe to inline anywhere
|
||||||
|
|
||||||
|
### Idempotent + Commutative
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn setUnionItem<T>(set: Set<T>, item: T): Set<T>
|
||||||
|
is idempotent
|
||||||
|
is commutative = {
|
||||||
|
Set.add(set, item)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Enabled optimizations**:
|
||||||
|
- Parallel set building (order doesn't matter)
|
||||||
|
- Duplicate insertions are free (idempotent)
|
||||||
|
- Reorder insertions for cache locality
|
||||||
|
|
||||||
|
## Implementation Status
|
||||||
|
|
||||||
|
| Optimization | Status |
|
||||||
|
|--------------|--------|
|
||||||
|
| Pure: CSE | Planned |
|
||||||
|
| Pure: Dead code elimination | Partial (basic) |
|
||||||
|
| Pure: Auto-parallelization | Planned |
|
||||||
|
| Total: Exception elimination | Planned |
|
||||||
|
| Total: Aggressive inlining | Partial |
|
||||||
|
| Deterministic: Compile-time eval | Planned |
|
||||||
|
| Idempotent: Duplicate elimination | Planned |
|
||||||
|
| Commutative: Parallel reduction | Planned |
|
||||||
|
|
||||||
|
## Adding New Optimizations
|
||||||
|
|
||||||
|
When implementing new optimizations based on behavioral types:
|
||||||
|
|
||||||
|
1. **Verify the property is correct**: The optimization is only valid if the property holds
|
||||||
|
2. **Consider combinations**: Multiple properties together enable more optimizations
|
||||||
|
3. **Measure impact**: Profile before and after to ensure benefit
|
||||||
|
4. **Handle `assume`**: Functions using `assume` bypass verification but still enable optimizations (risk is on the programmer)
|
||||||
|
|
||||||
|
## Future Work
|
||||||
|
|
||||||
|
1. **Inter-procedural analysis**: Track properties across function boundaries
|
||||||
|
2. **Automatic property inference**: Derive properties when not explicitly stated
|
||||||
|
3. **Profile-guided optimization**: Use runtime data to decide when to apply optimizations
|
||||||
|
4. **LLVM integration**: Pass behavioral hints to LLVM for backend optimizations
|
||||||
1048
docs/COMPREHENSIVE_ROADMAP.md
Normal file
1048
docs/COMPREHENSIVE_ROADMAP.md
Normal file
File diff suppressed because it is too large
Load Diff
449
docs/PHILOSOPHY.md
Normal file
449
docs/PHILOSOPHY.md
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
# The Lux Philosophy
|
||||||
|
|
||||||
|
## In One Sentence
|
||||||
|
|
||||||
|
**Make the important things visible.**
|
||||||
|
|
||||||
|
## The Three Pillars
|
||||||
|
|
||||||
|
Most programming languages hide the things that matter most in production:
|
||||||
|
|
||||||
|
1. **What can this code do?** — Side effects are invisible in function signatures
|
||||||
|
2. **How does data change over time?** — Schema evolution is a deployment problem, not a language one
|
||||||
|
3. **What guarantees does this code provide?** — Properties like idempotency live in comments and hope
|
||||||
|
|
||||||
|
Lux makes all three first-class, compiler-checked language features.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
### 1. Explicit Over Implicit
|
||||||
|
|
||||||
|
Every function signature tells you what it does:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn processOrder(order: Order): Receipt with {Database, Email, Logger}
|
||||||
|
```
|
||||||
|
|
||||||
|
You don't need to read the body, trace call chains, or check documentation. The signature *is* the documentation. Code review becomes: "should this function really send emails?"
|
||||||
|
|
||||||
|
**What this means in practice:**
|
||||||
|
- Effects are declared in types, not hidden behind interfaces
|
||||||
|
- No dependency injection frameworks — just swap handlers
|
||||||
|
- No mocking libraries — test with different effect implementations
|
||||||
|
- No "spooky action at a distance" — if a function can fail, its type says so
|
||||||
|
|
||||||
|
**How this compares:**
|
||||||
|
| Language | Side effects | Lux equivalent |
|
||||||
|
|----------|-------------|----------------|
|
||||||
|
| JavaScript | Anything, anywhere, silently | `with {Console, Http, File}` |
|
||||||
|
| Python | Implicit, discovered by reading code | Effect declarations in signature |
|
||||||
|
| Java | Checked exceptions (partial), DI frameworks | Effects + handlers |
|
||||||
|
| Go | Return error values (partial) | `with {Fail}` or `Result` |
|
||||||
|
| Rust | `unsafe` blocks, `Result`/`Option` | Effects for I/O, Result for values |
|
||||||
|
| Haskell | Monad transformers (explicit but heavy) | Effects (explicit and lightweight) |
|
||||||
|
| Koka | Algebraic effects (similar) | Same family, more familiar syntax |
|
||||||
|
|
||||||
|
### 2. Composition Over Configuration
|
||||||
|
|
||||||
|
Things combine naturally without glue code:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Multiple effects compose by listing them
|
||||||
|
fn sync(id: UserId): User with {Database, Http, Logger} = ...
|
||||||
|
|
||||||
|
// Handlers compose by providing them
|
||||||
|
run sync(id) with {
|
||||||
|
Database = postgres(conn),
|
||||||
|
Http = realHttp,
|
||||||
|
Logger = consoleLogger
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
No monad transformers. No middleware stacks. No factory factories. Effects are sets; they union naturally.
|
||||||
|
|
||||||
|
**What this means in practice:**
|
||||||
|
- Functions compose with `|>` (pipes)
|
||||||
|
- Effects compose by set union
|
||||||
|
- Types compose via generics and ADTs
|
||||||
|
- Tests compose by handler substitution
|
||||||
|
|
||||||
|
### 3. Safety Without Ceremony
|
||||||
|
|
||||||
|
The type system catches errors at compile time, but doesn't make you fight it:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Type inference keeps code clean
|
||||||
|
let x = 42 // Int, inferred
|
||||||
|
let names = ["Alice", "Bob"] // List<String>, inferred
|
||||||
|
|
||||||
|
// But function signatures are always explicit
|
||||||
|
fn greet(name: String): String = "Hello, {name}"
|
||||||
|
```
|
||||||
|
|
||||||
|
**The balance:**
|
||||||
|
- Function signatures: always annotated (documentation + API contract)
|
||||||
|
- Local bindings: inferred (reduces noise in implementation)
|
||||||
|
- Effects: declared or inferred (explicit at boundaries, lightweight inside)
|
||||||
|
- Behavioral properties: opt-in (`is pure`, `is total` — add when valuable)
|
||||||
|
|
||||||
|
### 4. Practical Over Academic
|
||||||
|
|
||||||
|
Lux borrows from the best of programming language research, but wraps it in familiar syntax:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// This is algebraic effects. But it reads like normal code.
|
||||||
|
fn main(): Unit with {Console} = {
|
||||||
|
Console.print("What's your name?")
|
||||||
|
let name = Console.readLine()
|
||||||
|
Console.print("Hello, {name}!")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Compare with Haskell's equivalent:
|
||||||
|
```haskell
|
||||||
|
main :: IO ()
|
||||||
|
main = do
|
||||||
|
putStrLn "What's your name?"
|
||||||
|
name <- getLine
|
||||||
|
putStrLn ("Hello, " ++ name ++ "!")
|
||||||
|
```
|
||||||
|
|
||||||
|
Both are explicit about effects. Lux chooses syntax that reads like imperative code while maintaining the same guarantees.
|
||||||
|
|
||||||
|
**What this means in practice:**
|
||||||
|
- ML-family semantics, C-family appearance
|
||||||
|
- No monads to learn (effects replace them)
|
||||||
|
- No category theory prerequisites
|
||||||
|
- The learning curve is: functions → types → effects (days, not months)
|
||||||
|
|
||||||
|
### 5. One Right Way
|
||||||
|
|
||||||
|
Like Go and Python, Lux favors having one obvious way to do things:
|
||||||
|
|
||||||
|
- **One formatter** (`lux fmt`) — opinionated, not configurable, ends all style debates
|
||||||
|
- **One test framework** (built-in `Test` effect) — no framework shopping
|
||||||
|
- **One way to handle effects** — declare, handle, compose
|
||||||
|
- **One package manager** (`lux pkg`) — integrated, not bolted on
|
||||||
|
|
||||||
|
This is a deliberate rejection of the JavaScript/Ruby approach where every project assembles its own stack from dozens of competing libraries.
|
||||||
|
|
||||||
|
### 6. Tools Are Part of the Language
|
||||||
|
|
||||||
|
The compiler, linter, formatter, LSP, package manager, and test runner are one thing, not seven:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lux fmt # Format
|
||||||
|
lux lint # Lint (with --explain for education)
|
||||||
|
lux check # Type check + lint
|
||||||
|
lux test # Run tests
|
||||||
|
lux compile # Build a binary
|
||||||
|
lux serve # Serve files
|
||||||
|
lux --lsp # Editor integration
|
||||||
|
```
|
||||||
|
|
||||||
|
This follows Go's philosophy: a language is its toolchain. The formatter knows the AST. The linter knows the type system. The LSP knows the effects. They're not afterthoughts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Decisions and Their Reasons
|
||||||
|
|
||||||
|
### Why algebraic effects instead of monads?
|
||||||
|
|
||||||
|
Monads are powerful but have poor ergonomics for composition. Combining `IO`, `State`, and `Error` in Haskell requires monad transformers — a notoriously difficult concept. Effects compose naturally:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Just list the effects you need. No transformers.
|
||||||
|
fn app(): Unit with {Console, File, Http, Time} = ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why not just `async/await`?
|
||||||
|
|
||||||
|
`async/await` solves one effect (concurrency). Effects solve all of them: I/O, state, randomness, failure, concurrency, logging, databases. One mechanism, universally applicable.
|
||||||
|
|
||||||
|
### Why require function type annotations?
|
||||||
|
|
||||||
|
Three reasons:
|
||||||
|
1. **Documentation**: Every function signature is self-documenting
|
||||||
|
2. **Error messages**: Inference failures produce confusing errors; annotations localize them
|
||||||
|
3. **API stability**: Changing a function body shouldn't silently change its type
|
||||||
|
|
||||||
|
### Why an opinionated formatter?
|
||||||
|
|
||||||
|
Style debates waste engineering time. `gofmt` proved that an opinionated, non-configurable formatter eliminates an entire category of bikeshedding. `lux fmt` does the same.
|
||||||
|
|
||||||
|
### Why immutable by default?
|
||||||
|
|
||||||
|
Mutable state is the root of most concurrency bugs and many logic bugs. Immutability makes code easier to reason about. When you need state, the `State` effect makes it explicit and trackable.
|
||||||
|
|
||||||
|
### Why behavioral types?
|
||||||
|
|
||||||
|
Properties like "this function is idempotent" or "this function always terminates" are critical for correctness but typically live in comments. Making them part of the type system means:
|
||||||
|
- The compiler can verify them (or generate property tests)
|
||||||
|
- Callers can require them (`where F is idempotent`)
|
||||||
|
- They serve as machine-readable documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comparison with Popular Languages
|
||||||
|
|
||||||
|
### JavaScript / TypeScript (SO #1 / #6 by usage)
|
||||||
|
|
||||||
|
| Aspect | JavaScript/TypeScript | Lux |
|
||||||
|
|--------|----------------------|-----|
|
||||||
|
| **Type system** | Optional/gradual (TS) | Required, Hindley-Milner |
|
||||||
|
| **Side effects** | Anywhere, implicit | Declared in types |
|
||||||
|
| **Testing** | Mock libraries (Jest, etc.) | Swap effect handlers |
|
||||||
|
| **Formatting** | Prettier (configurable) | `lux fmt` (opinionated) |
|
||||||
|
| **Package management** | npm (massive ecosystem) | `lux pkg` (small ecosystem) |
|
||||||
|
| **Paradigm** | Multi-paradigm | Functional-first |
|
||||||
|
| **Null safety** | Optional chaining (partial) | `Option<T>`, no null |
|
||||||
|
| **Error handling** | try/catch (unchecked) | `Result<T, E>` + `Fail` effect |
|
||||||
|
| **Shared** | Familiar syntax, first-class functions, closures, string interpolation |
|
||||||
|
|
||||||
|
**What Lux learns from JS/TS:** Familiar syntax matters. String interpolation, arrow functions, and readable code lower the barrier to entry.
|
||||||
|
|
||||||
|
**What Lux rejects:** Implicit `any`, unchecked exceptions, the "pick your own adventure" toolchain.
|
||||||
|
|
||||||
|
### Python (SO #4 by usage, #1 most desired)
|
||||||
|
|
||||||
|
| Aspect | Python | Lux |
|
||||||
|
|--------|--------|-----|
|
||||||
|
| **Type system** | Optional (type hints) | Required, static |
|
||||||
|
| **Side effects** | Implicit | Explicit |
|
||||||
|
| **Performance** | Slow (interpreted) | Faster (compiled to C) |
|
||||||
|
| **Syntax** | Whitespace-significant | Braces/keywords |
|
||||||
|
| **Immutability** | Mutable by default | Immutable by default |
|
||||||
|
| **Tooling** | Fragmented (black, ruff, mypy, pytest...) | Unified (`lux` binary) |
|
||||||
|
| **Shared** | Clean syntax philosophy, "one way to do it", readability focus |
|
||||||
|
|
||||||
|
**What Lux learns from Python:** Readability counts. The Zen of Python's emphasis on one obvious way to do things resonates with Lux's design.
|
||||||
|
|
||||||
|
**What Lux rejects:** Dynamic typing, mutable-by-default, fragmented tooling.
|
||||||
|
|
||||||
|
### Rust (SO #1 most admired)
|
||||||
|
|
||||||
|
| Aspect | Rust | Lux |
|
||||||
|
|--------|------|-----|
|
||||||
|
| **Memory** | Ownership/borrowing (manual) | Reference counting (automatic) |
|
||||||
|
| **Type system** | Traits, generics, lifetimes | ADTs, effects, generics |
|
||||||
|
| **Side effects** | Implicit (except `unsafe`) | Explicit (effect system) |
|
||||||
|
| **Error handling** | `Result<T, E>` + `?` | `Result<T, E>` + `Fail` effect |
|
||||||
|
| **Performance** | Zero-cost, systems-level | Good, not systems-level |
|
||||||
|
| **Learning curve** | Steep (ownership) | Moderate (effects) |
|
||||||
|
| **Pattern matching** | Excellent, exhaustive | Excellent, exhaustive |
|
||||||
|
| **Shared** | ADTs, pattern matching, `Option`/`Result`, no null, immutable by default, strong type system |
|
||||||
|
|
||||||
|
**What Lux learns from Rust:** ADTs with exhaustive matching, `Option`/`Result` instead of null/exceptions, excellent error messages, integrated tooling (cargo model).
|
||||||
|
|
||||||
|
**What Lux rejects:** Ownership complexity (Lux uses GC/RC instead), lifetimes, `unsafe`.
|
||||||
|
|
||||||
|
### Go (SO #13 by usage, #11 most admired)
|
||||||
|
|
||||||
|
| Aspect | Go | Lux |
|
||||||
|
|--------|-----|-----|
|
||||||
|
| **Type system** | Structural, simple | HM inference, ADTs |
|
||||||
|
| **Side effects** | Implicit | Explicit |
|
||||||
|
| **Error handling** | Multiple returns (`val, err`) | `Result<T, E>` + effects |
|
||||||
|
| **Formatting** | `gofmt` (opinionated) | `lux fmt` (opinionated) |
|
||||||
|
| **Tooling** | All-in-one (`go` binary) | All-in-one (`lux` binary) |
|
||||||
|
| **Concurrency** | Goroutines + channels | `Concurrent` + `Channel` effects |
|
||||||
|
| **Generics** | Added late, limited | First-class from day one |
|
||||||
|
| **Shared** | Opinionated formatter, unified tooling, practical philosophy |
|
||||||
|
|
||||||
|
**What Lux learns from Go:** Unified toolchain, opinionated formatting, simplicity as a feature, fast compilation.
|
||||||
|
|
||||||
|
**What Lux rejects:** Verbose error handling (`if err != nil`), no ADTs, no generics (historically), nil.
|
||||||
|
|
||||||
|
### Java / C# (SO #7 / #8 by usage)
|
||||||
|
|
||||||
|
| Aspect | Java/C# | Lux |
|
||||||
|
|--------|---------|-----|
|
||||||
|
| **Paradigm** | OOP-first | FP-first |
|
||||||
|
| **Effects** | DI frameworks (Spring, etc.) | Language-level effects |
|
||||||
|
| **Testing** | Mock frameworks (Mockito, etc.) | Handler swapping |
|
||||||
|
| **Null safety** | Nullable (Java), nullable ref types (C#) | `Option<T>` |
|
||||||
|
| **Boilerplate** | High (getters, setters, factories) | Low (records, inference) |
|
||||||
|
| **Shared** | Static typing, generics, pattern matching (recent), established ecosystems |
|
||||||
|
|
||||||
|
**What Lux learns from Java/C#:** Enterprise needs (database effects, HTTP, serialization) matter. Testability is a first-class concern.
|
||||||
|
|
||||||
|
**What Lux rejects:** OOP ceremony, DI frameworks, null, boilerplate.
|
||||||
|
|
||||||
|
### Haskell / OCaml / Elm (FP family)
|
||||||
|
|
||||||
|
| Aspect | Haskell | Elm | Lux |
|
||||||
|
|--------|---------|-----|-----|
|
||||||
|
| **Effects** | Monads + transformers | Cmd/Sub (Elm Architecture) | Algebraic effects |
|
||||||
|
| **Learning curve** | Steep | Moderate | Moderate |
|
||||||
|
| **Error messages** | Improving | Excellent | Good (aspiring to Elm-quality) |
|
||||||
|
| **Practical focus** | Academic-leaning | Web-focused | General-purpose |
|
||||||
|
| **Syntax** | Unique | Unique | Familiar (C-family feel) |
|
||||||
|
| **Shared** | Immutability, ADTs, pattern matching, type inference, no null |
|
||||||
|
|
||||||
|
**What Lux learns from Haskell:** Effects must be explicit. Types must be powerful. Purity matters.
|
||||||
|
|
||||||
|
**What Lux learns from Elm:** Error messages should teach. Tooling should be integrated. Simplicity beats power.
|
||||||
|
|
||||||
|
**What Lux rejects (from Haskell):** Monad transformers, academic syntax, steep learning curve.
|
||||||
|
|
||||||
|
### Gleam / Elixir (SO #2 / #3 most admired, 2025)
|
||||||
|
|
||||||
|
| Aspect | Gleam | Elixir | Lux |
|
||||||
|
|--------|-------|--------|-----|
|
||||||
|
| **Type system** | Static, HM | Dynamic | Static, HM |
|
||||||
|
| **Effects** | No special tracking | Implicit | First-class |
|
||||||
|
| **Concurrency** | BEAM (built-in) | BEAM (built-in) | Effect-based |
|
||||||
|
| **Error handling** | `Result` | Pattern matching on tuples | `Result` + `Fail` effect |
|
||||||
|
| **Shared** | Friendly errors, pipe operator, functional style, immutability |
|
||||||
|
|
||||||
|
**What Lux learns from Gleam:** Friendly developer experience, clear error messages, and pragmatic FP resonate with developers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tooling Philosophy Audit
|
||||||
|
|
||||||
|
### Does the linter follow the philosophy?
|
||||||
|
|
||||||
|
**Yes, strongly.** The linter embodies "make the important things visible":
|
||||||
|
|
||||||
|
- `could-be-pure`: Nudges users toward declaring purity — making guarantees visible
|
||||||
|
- `could-be-total`: Same for termination
|
||||||
|
- `unnecessary-effect-decl`: Keeps effect signatures honest — don't claim effects you don't use
|
||||||
|
- `unused-variable/import/function`: Keeps code focused — everything visible should be meaningful
|
||||||
|
- `single-arm-match` / `manual-map-option`: Teaches idiomatic patterns
|
||||||
|
|
||||||
|
The category system (correctness > suspicious > idiom > style > pedantic) reflects the philosophy of being practical, not academic: real bugs are errors, style preferences are opt-in.
|
||||||
|
|
||||||
|
### Does the formatter follow the philosophy?
|
||||||
|
|
||||||
|
**Yes, with one gap.** The formatter is opinionated and non-configurable, matching the "one right way" principle. It enforces consistent style across all Lux code.
|
||||||
|
|
||||||
|
**Gap:** `max_width` and `trailing_commas` are declared in `FormatConfig` but never used. This is harmless but inconsistent — either remove the unused config or implement line wrapping.
|
||||||
|
|
||||||
|
### Does the type checker follow the philosophy?
|
||||||
|
|
||||||
|
**Yes.** The type checker embodies every core principle:
|
||||||
|
- Effects are tracked and verified in function types
|
||||||
|
- Behavioral properties are checked where possible
|
||||||
|
- Error messages include context and suggestions
|
||||||
|
- Type inference reduces ceremony while maintaining safety
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Could Be Improved
|
||||||
|
|
||||||
|
### High-value additions (improve experience significantly, low verbosity cost)
|
||||||
|
|
||||||
|
1. **Pipe-friendly standard library**
|
||||||
|
- Currently: `List.map(myList, fn(x: Int): Int => x * 2)`
|
||||||
|
- Better: Allow `myList |> List.map(fn(x: Int): Int => x * 2)`
|
||||||
|
- Many languages (Elixir, F#, Gleam) make the pipe operator the primary composition tool. If the first argument of stdlib functions is always the data, pipes become natural. This is a **library convention**, not a language change.
|
||||||
|
- **LLM impact:** Pipe chains are easier for LLMs to generate and read — linear data flow with no nesting.
|
||||||
|
- **Human impact:** Reduces cognitive load. Reading left-to-right matches how humans think about data transformation.
|
||||||
|
|
||||||
|
2. **Exhaustive `match` warnings for non-enum types**
|
||||||
|
- The linter warns about `wildcard-on-small-enum`, but could also warn when a match on `Option` or `Result` uses a wildcard instead of handling both cases explicitly.
|
||||||
|
- **Both audiences:** Prevents subtle bugs where new variants are silently caught by `_`.
|
||||||
|
|
||||||
|
3. **Error message improvements toward Elm quality**
|
||||||
|
- Current errors show the right information but could be more conversational and suggest fixes more consistently.
|
||||||
|
- Example improvement: When a function is called with wrong argument count, show the expected signature and highlight which argument is wrong.
|
||||||
|
- **LLM impact:** Structured error messages with clear "expected X, got Y" patterns are easier for LLMs to parse and fix.
|
||||||
|
- **Human impact:** Friendly errors reduce frustration, especially for beginners.
|
||||||
|
|
||||||
|
4. **`let ... else` for fallible destructuring**
|
||||||
|
- Rust's `let ... else` pattern handles the "unwrap or bail" case elegantly:
|
||||||
|
```lux
|
||||||
|
let Some(value) = maybeValue else return defaultValue
|
||||||
|
```
|
||||||
|
- Currently requires a full `match` expression for this common pattern.
|
||||||
|
- **Both audiences:** Reduces boilerplate for the most common Option/Result handling pattern.
|
||||||
|
|
||||||
|
5. **Trait/typeclass system for overloading**
|
||||||
|
- Currently `toString`, `==`, and similar operations are built-in. A trait system would let users define their own:
|
||||||
|
```lux
|
||||||
|
trait Show<T> { fn show(value: T): String }
|
||||||
|
impl Show<User> { fn show(u: User): String = "User({u.name})" }
|
||||||
|
```
|
||||||
|
- **Note:** This exists partially. Expanding it would enable more generic programming without losing explicitness.
|
||||||
|
- **LLM impact:** Traits provide clear, greppable contracts. LLMs can generate trait impls from examples.
|
||||||
|
|
||||||
|
### Medium-value additions (good improvements, some verbosity cost)
|
||||||
|
|
||||||
|
6. **Named arguments or builder pattern for records**
|
||||||
|
- When functions take many parameters, the linter already warns at 5+. Named arguments or record-punning would help:
|
||||||
|
```lux
|
||||||
|
fn createUser({ name, email, age }: UserConfig): User = ...
|
||||||
|
createUser({ name: "Alice", email: "alice@ex.com", age: 30 })
|
||||||
|
```
|
||||||
|
- **Trade-off:** Adds syntax, but the linter already pushes users toward records for many params.
|
||||||
|
|
||||||
|
7. **Async/concurrent effect sugar**
|
||||||
|
- The `Concurrent` effect exists but could benefit from syntactic sugar:
|
||||||
|
```lux
|
||||||
|
let (a, b) = concurrent {
|
||||||
|
fetch("/api/users"),
|
||||||
|
fetch("/api/posts")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Trade-off:** Adds syntax, but concurrent code is important enough to warrant it.
|
||||||
|
|
||||||
|
8. **Module-level documentation with `///` doc comments**
|
||||||
|
- The `missing-doc-comment` lint exists, but the doc generation system could be enhanced with richer doc comments that include examples, parameter descriptions, and effect documentation.
|
||||||
|
- **LLM impact:** Structured documentation is the single highest-value feature for LLM code understanding.
|
||||||
|
|
||||||
|
### Lower-value or risky additions (consider carefully)
|
||||||
|
|
||||||
|
9. **Type inference for function return types**
|
||||||
|
- Would reduce ceremony: `fn double(x: Int) = x * 2` instead of `fn double(x: Int): Int = x * 2`
|
||||||
|
- **Risk:** Violates the "function signatures are documentation" principle. A body change could silently change the API. Current approach is the right trade-off.
|
||||||
|
|
||||||
|
10. **Operator overloading**
|
||||||
|
- Tempting for numeric types, but quickly leads to the C++ problem where `+` could mean anything.
|
||||||
|
- **Risk:** Violates "make the important things visible" — you can't tell what `a + b` does.
|
||||||
|
- **Better:** Keep operators for built-in numeric types. Use named functions for everything else.
|
||||||
|
|
||||||
|
11. **Macros**
|
||||||
|
- Powerful but drastically complicate tooling, error messages, and readability.
|
||||||
|
- **Risk:** Rust's macro system is powerful but produces some of the worst error messages in the language.
|
||||||
|
- **Better:** Solve specific problems with language features (effects, generics) rather than a general metaprogramming escape hatch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The LLM Perspective
|
||||||
|
|
||||||
|
Lux has several properties that make it unusually well-suited for LLM-assisted programming:
|
||||||
|
|
||||||
|
1. **Effect signatures are machine-readable contracts.** An LLM reading `fn f(): T with {Database, Logger}` knows exactly what capabilities to provide when generating handler code.
|
||||||
|
|
||||||
|
2. **Behavioral properties are verifiable assertions.** `is pure`, `is idempotent` give LLMs clear constraints to check their own output against.
|
||||||
|
|
||||||
|
3. **The opinionated formatter eliminates style ambiguity.** LLMs don't need to guess indentation, brace style, or naming conventions — `lux fmt` handles it.
|
||||||
|
|
||||||
|
4. **Exhaustive pattern matching forces completeness.** LLMs that generate `match` expressions are reminded by the compiler when they miss cases.
|
||||||
|
|
||||||
|
5. **Small, consistent standard library.** `List.map`, `String.split`, `Option.map` — uniform `Module.function` convention is easy to learn from few examples.
|
||||||
|
|
||||||
|
6. **Effect-based testing needs no framework knowledge.** An LLM doesn't need to know Jest, pytest, or JUnit — just swap handlers.
|
||||||
|
|
||||||
|
**What would help LLMs more:**
|
||||||
|
- Structured error output (JSON mode) for programmatic error fixing
|
||||||
|
- Example-rich documentation that LLMs can learn patterns from
|
||||||
|
- A canonical set of "Lux patterns" (like Go's proverbs) that encode best practices in memorable form
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Lux's philosophy can be compressed to five words: **Make the important things visible.**
|
||||||
|
|
||||||
|
This manifests as:
|
||||||
|
- **Effects in types** — see what code does
|
||||||
|
- **Properties in types** — see what code guarantees
|
||||||
|
- **Versions in types** — see how data evolves
|
||||||
|
- **One tool for everything** — see how to build
|
||||||
|
- **One format for all** — see consistent style
|
||||||
|
|
||||||
|
The language is in the sweet spot between Haskell's rigor and Python's practicality, with Go's tooling philosophy and Elm's developer experience aspirations. It doesn't try to be everything — it tries to make the things that matter most in real software visible, composable, and verifiable.
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
| SQL effect (query, execute) | P1 | 2 weeks | ✅ Complete |
|
| SQL effect (query, execute) | P1 | 2 weeks | ✅ Complete |
|
||||||
| Transaction effect | P2 | 1 week | ✅ Complete |
|
| Transaction effect | P2 | 1 week | ✅ Complete |
|
||||||
| Connection pooling | P2 | 1 week | ❌ Missing |
|
| Connection pooling | P2 | 1 week | ❌ Missing |
|
||||||
|
| PostgreSQL support | P1 | 2 weeks | ✅ Complete |
|
||||||
|
|
||||||
### Phase 1.3: Web Server Framework
|
### Phase 1.3: Web Server Framework
|
||||||
|
|
||||||
@@ -207,8 +208,11 @@
|
|||||||
|------|----------|--------|--------|
|
|------|----------|--------|--------|
|
||||||
| Package manager (lux pkg) | P1 | 3 weeks | ✅ Complete |
|
| Package manager (lux pkg) | P1 | 3 weeks | ✅ Complete |
|
||||||
| Module loader integration | P1 | 1 week | ✅ Complete |
|
| Module loader integration | P1 | 1 week | ✅ Complete |
|
||||||
| Package registry | P2 | 2 weeks | ✅ Complete (server + CLI commands) |
|
| Package registry server | P2 | 2 weeks | ✅ Complete |
|
||||||
| Dependency resolution | P2 | 2 weeks | ❌ Missing |
|
| Registry CLI (search, publish) | P2 | 1 week | ✅ Complete |
|
||||||
|
| Lock file generation | P1 | 1 week | ✅ Complete |
|
||||||
|
| Version constraint parsing | P1 | 1 week | ✅ Complete |
|
||||||
|
| Transitive dependency resolution | P2 | 2 weeks | ⚠️ Basic (direct deps only) |
|
||||||
|
|
||||||
**Package Manager Features:**
|
**Package Manager Features:**
|
||||||
- `lux pkg init` - Initialize project with lux.toml
|
- `lux pkg init` - Initialize project with lux.toml
|
||||||
@@ -300,6 +304,8 @@
|
|||||||
- ✅ Random effect (int, float, range, bool)
|
- ✅ Random effect (int, float, range, bool)
|
||||||
- ✅ Time effect (now, sleep)
|
- ✅ Time effect (now, sleep)
|
||||||
- ✅ Test effect (assert, assertEqual, assertTrue, assertFalse)
|
- ✅ Test effect (assert, assertEqual, assertTrue, assertFalse)
|
||||||
|
- ✅ SQL effect (SQLite with transactions)
|
||||||
|
- ✅ Postgres effect (PostgreSQL connections)
|
||||||
|
|
||||||
**Module System:**
|
**Module System:**
|
||||||
- ✅ Imports, exports, aliases
|
- ✅ Imports, exports, aliases
|
||||||
@@ -319,7 +325,7 @@
|
|||||||
- ✅ C backend (functions, closures, pattern matching, lists)
|
- ✅ C backend (functions, closures, pattern matching, lists)
|
||||||
- ✅ JS backend (full language support, browser & Node.js)
|
- ✅ JS backend (full language support, browser & Node.js)
|
||||||
- ✅ REPL with history
|
- ✅ REPL with history
|
||||||
- ✅ Basic LSP server
|
- ✅ LSP server (diagnostics, hover, completions, go-to-definition, references, symbols)
|
||||||
- ✅ Formatter
|
- ✅ Formatter
|
||||||
- ✅ Watch mode
|
- ✅ Watch mode
|
||||||
- ✅ Debugger (basic)
|
- ✅ Debugger (basic)
|
||||||
|
|||||||
330
docs/SQL_DESIGN_ANALYSIS.md
Normal file
330
docs/SQL_DESIGN_ANALYSIS.md
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
# SQL in Lux: Built-in Effect vs Package
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
This document analyzes whether SQL database access should be a built-in language feature (as it currently is) or a separate package. After comparing approaches across 12+ languages, the recommendation is:
|
||||||
|
|
||||||
|
**Keep SQL as a built-in effect, but refactor the implementation to be more modular.**
|
||||||
|
|
||||||
|
## Current Implementation
|
||||||
|
|
||||||
|
Lux currently implements SQL as a built-in effect:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn main(): Unit with {Console, Sql} = {
|
||||||
|
let db = Sql.openMemory()
|
||||||
|
Sql.execute(db, "CREATE TABLE users (...)")
|
||||||
|
let users = Sql.query(db, "SELECT * FROM users")
|
||||||
|
Sql.close(db)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The implementation uses rusqlite (SQLite) compiled directly into the Lux binary.
|
||||||
|
|
||||||
|
## How Other Languages Handle Database Access
|
||||||
|
|
||||||
|
### Languages with Built-in Database Support
|
||||||
|
|
||||||
|
| Language | Approach | Notes |
|
||||||
|
|----------|----------|-------|
|
||||||
|
| **Python** | `sqlite3` in stdlib | Most languages have SQLite in stdlib |
|
||||||
|
| **Ruby** | `sqlite3` gem + AR are common | ActiveRecord is de facto standard |
|
||||||
|
| **Go** | `database/sql` interface in stdlib | Drivers are packages |
|
||||||
|
| **Elixir** | Ecto as separate package | But universally used |
|
||||||
|
| **PHP** | PDO in core | Multiple backends |
|
||||||
|
|
||||||
|
### Languages with Package-Only Database Support
|
||||||
|
|
||||||
|
| Language | Approach | Notes |
|
||||||
|
|----------|----------|-------|
|
||||||
|
| **Rust** | rusqlite, diesel, sqlx packages | No stdlib database |
|
||||||
|
| **Node.js** | pg, mysql2, better-sqlite3 | Packages only |
|
||||||
|
| **Haskell** | postgresql-simple, persistent | Packages only |
|
||||||
|
| **OCaml** | caqti, postgresql-ocaml | Packages only |
|
||||||
|
|
||||||
|
### Analysis of Each Approach
|
||||||
|
|
||||||
|
#### Go's Model: Interface in Stdlib + Driver Packages
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
_ "github.com/lib/pq" // PostgreSQL driver
|
||||||
|
)
|
||||||
|
|
||||||
|
db, _ := sql.Open("postgres", "...")
|
||||||
|
rows, _ := db.Query("SELECT * FROM users")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Standard interface for all databases
|
||||||
|
- Type-safe at compile time
|
||||||
|
- Drivers are swappable
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- Requires understanding interfaces
|
||||||
|
- Need external packages for actual database
|
||||||
|
|
||||||
|
#### Python's Model: SQLite in Stdlib
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect('example.db')
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('SELECT * FROM users')
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Zero dependencies for getting started
|
||||||
|
- Great for learning/prototyping
|
||||||
|
- Always available
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- Other databases need packages
|
||||||
|
- stdlib vs package API differences
|
||||||
|
|
||||||
|
#### Rust's Model: Everything is Packages
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use rusqlite::{Connection, Result};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let conn = Connection::open("test.db")?;
|
||||||
|
conn.execute("CREATE TABLE users (...)", [])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Minimal core language
|
||||||
|
- Best-in-class implementations
|
||||||
|
- Clear ownership
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- Cargo.toml management
|
||||||
|
- Version conflicts possible
|
||||||
|
- Learning curve for package ecosystem
|
||||||
|
|
||||||
|
#### Elixir's Model: Strong Package Ecosystem
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Ecto is technically a package but universally used
|
||||||
|
Repo.all(from u in User, where: u.age > 18)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Best API emerges naturally
|
||||||
|
- Core team can focus on language
|
||||||
|
- Community ownership
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- Package can become outdated
|
||||||
|
- Multiple competing solutions
|
||||||
|
|
||||||
|
## Arguments For Built-in SQL
|
||||||
|
|
||||||
|
### 1. Effect System Integration
|
||||||
|
|
||||||
|
The most compelling argument: **SQL fits naturally into Lux's effect system.**
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// The effect signature documents database access
|
||||||
|
fn fetchUser(id: Int): User with {Sql} = { ... }
|
||||||
|
|
||||||
|
// Handlers enable testing without mocks
|
||||||
|
handler testDatabase(): Sql { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
This is harder to achieve with packages - they'd need to integrate deeply with the effect system.
|
||||||
|
|
||||||
|
### 2. Zero-Dependency Getting Started
|
||||||
|
|
||||||
|
New users can immediately:
|
||||||
|
- Follow tutorials that use databases
|
||||||
|
- Build real applications
|
||||||
|
- Learn effects with practical examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lux run database_example.lux
|
||||||
|
# Just works - no package installation
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Guaranteed API Stability
|
||||||
|
|
||||||
|
Built-in effects have stable, documented APIs. Package APIs can change between versions.
|
||||||
|
|
||||||
|
### 4. Teaching Functional Effects
|
||||||
|
|
||||||
|
SQL is an excellent teaching example for effects:
|
||||||
|
- Clear side effects (I/O to database)
|
||||||
|
- Handler swapping for testing
|
||||||
|
- Transaction scoping
|
||||||
|
|
||||||
|
### 5. Practical Utility
|
||||||
|
|
||||||
|
90%+ of real applications need database access. Making it trivial benefits most users.
|
||||||
|
|
||||||
|
## Arguments For SQL as Package
|
||||||
|
|
||||||
|
### 1. Smaller Binary Size
|
||||||
|
|
||||||
|
rusqlite adds significant binary size (~2-3MB). Package-based approach lets users opt-in.
|
||||||
|
|
||||||
|
### 2. Database Backend Choice
|
||||||
|
|
||||||
|
Currently locked to SQLite. A package ecosystem could offer:
|
||||||
|
- `lux-sqlite`
|
||||||
|
- `lux-postgres`
|
||||||
|
- `lux-mysql`
|
||||||
|
- `lux-mongodb`
|
||||||
|
|
||||||
|
### 3. Faster Core Language Evolution
|
||||||
|
|
||||||
|
Core team focuses on language; community builds integrations.
|
||||||
|
|
||||||
|
### 4. Better Specialization
|
||||||
|
|
||||||
|
Dedicated package maintainers might build better database tooling than core team.
|
||||||
|
|
||||||
|
### 5. Multiple Competing Implementations
|
||||||
|
|
||||||
|
Competition drives quality. The best SQL package wins adoption.
|
||||||
|
|
||||||
|
## Comparison Matrix
|
||||||
|
|
||||||
|
| Factor | Built-in | Package |
|
||||||
|
|--------|----------|---------|
|
||||||
|
| Effect integration | Excellent | Needs design work |
|
||||||
|
| Learning curve | Low | Medium |
|
||||||
|
| Binary size | Larger | User controls |
|
||||||
|
| Database options | Limited | Unlimited |
|
||||||
|
| API stability | Guaranteed | Version-dependent |
|
||||||
|
| Getting started | Instant | Requires install |
|
||||||
|
| Testing story | Built-in handlers | Package-specific |
|
||||||
|
| Maintenance burden | Core team | Community |
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
### Keep SQL as Built-in Effect, With Changes
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
|
||||||
|
1. **Effect system is Lux's differentiator** - SQL showcases it perfectly
|
||||||
|
2. **Practicality matters** - 90% of apps need databases
|
||||||
|
3. **Teaching value** - SQL is ideal for learning effects
|
||||||
|
4. **Handler testing** - Built-in integration enables powerful testing
|
||||||
|
|
||||||
|
### Proposed Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Core Lux
|
||||||
|
├── Sql effect (interface only)
|
||||||
|
│ ├── open/close
|
||||||
|
│ ├── execute/query
|
||||||
|
│ └── transaction operations
|
||||||
|
│
|
||||||
|
└── Default SQLite handler (built-in)
|
||||||
|
└── Uses rusqlite
|
||||||
|
|
||||||
|
Future packages (optional)
|
||||||
|
├── lux-postgres -- PostgreSQL handler
|
||||||
|
├── lux-mysql -- MySQL handler
|
||||||
|
└── lux-redis -- Redis (key-value, not Sql)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Specific Changes to Consider
|
||||||
|
|
||||||
|
1. **Make SQLite compilation optional**
|
||||||
|
```toml
|
||||||
|
# Cargo.toml
|
||||||
|
[features]
|
||||||
|
default = ["sqlite"]
|
||||||
|
sqlite = ["rusqlite"]
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Define stable Sql effect interface**
|
||||||
|
```lux
|
||||||
|
effect Sql {
|
||||||
|
fn open(path: String): SqlConn
|
||||||
|
fn close(conn: SqlConn): Unit
|
||||||
|
fn execute(conn: SqlConn, sql: String): Int
|
||||||
|
fn query(conn: SqlConn, sql: String): List<SqlRow>
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Allow package handlers to implement Sql**
|
||||||
|
```lux
|
||||||
|
// In lux-postgres package
|
||||||
|
handler postgresHandler(connStr: String): Sql { ... }
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
run myApp() with {
|
||||||
|
Sql -> postgresHandler("postgres://...")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Add connection pooling to core**
|
||||||
|
Important for production, should be standard.
|
||||||
|
|
||||||
|
## Comparison to Similar Decisions
|
||||||
|
|
||||||
|
### Console Effect
|
||||||
|
|
||||||
|
Console is built-in. Nobody questions this because:
|
||||||
|
- Universally needed
|
||||||
|
- Simple interface
|
||||||
|
- Hard to get wrong
|
||||||
|
|
||||||
|
SQL is similar but more complex.
|
||||||
|
|
||||||
|
### HTTP Effect
|
||||||
|
|
||||||
|
HTTP client is built-in in Lux. This was the right call because:
|
||||||
|
- Most apps need HTTP
|
||||||
|
- Complex to implement well
|
||||||
|
- Effect system integration important
|
||||||
|
|
||||||
|
SQL follows same reasoning.
|
||||||
|
|
||||||
|
### File Effect
|
||||||
|
|
||||||
|
File I/O is built-in. Same rationale applies.
|
||||||
|
|
||||||
|
## What Other Effect-System Languages Do
|
||||||
|
|
||||||
|
| Language | Database | Built-in? |
|
||||||
|
|----------|----------|-----------|
|
||||||
|
| **Koka** | No database support | N/A |
|
||||||
|
| **Eff** | No database support | N/A |
|
||||||
|
| **Frank** | No database support | N/A |
|
||||||
|
| **Unison** | Abilities + packages | Both |
|
||||||
|
|
||||||
|
Lux is pioneering practical effects. Built-in SQL makes sense.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
SQL should remain a built-in effect in Lux because:
|
||||||
|
|
||||||
|
1. It demonstrates the power of effects for real-world use
|
||||||
|
2. It enables the handler-based testing story
|
||||||
|
3. It removes friction for most applications
|
||||||
|
4. It serves as a teaching example for effects
|
||||||
|
|
||||||
|
However, the implementation should evolve to:
|
||||||
|
- Support multiple database backends via handlers
|
||||||
|
- Make SQLite optional for minimal binaries
|
||||||
|
- Provide connection pooling
|
||||||
|
- Add parameterized query support
|
||||||
|
|
||||||
|
This hybrid approach gives users the best of both worlds: immediate productivity with built-in SQLite, and flexibility through package-provided handlers for other databases.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Work
|
||||||
|
|
||||||
|
1. **Parameterized queries** - Critical for SQL injection prevention
|
||||||
|
2. **Connection pooling** - Required for production servers
|
||||||
|
3. **PostgreSQL handler** - Most requested database
|
||||||
|
4. **Migration support** - Schema evolution tooling
|
||||||
|
5. **Type-safe queries** - Compile-time SQL checking (ambitious)
|
||||||
1322
docs/WEBSITE_PLAN.md
1322
docs/WEBSITE_PLAN.md
File diff suppressed because it is too large
Load Diff
@@ -53,6 +53,10 @@ Lux provides several built-in effects:
|
|||||||
| `Process` | `exec`, `env`, `args`, `cwd`, `exit` | System processes |
|
| `Process` | `exec`, `env`, `args`, `cwd`, `exit` | System processes |
|
||||||
| `Http` | `get`, `post`, `put`, `delete` | HTTP client |
|
| `Http` | `get`, `post`, `put`, `delete` | HTTP client |
|
||||||
| `HttpServer` | `listen`, `accept`, `respond`, `stop` | HTTP server |
|
| `HttpServer` | `listen`, `accept`, `respond`, `stop` | HTTP server |
|
||||||
|
| `Sql` | `open`, `openMemory`, `close`, `execute`, `query`, `queryOne`, `beginTx`, `commit`, `rollback` | SQLite database |
|
||||||
|
| `Postgres` | `connect`, `close`, `execute`, `query`, `queryOne` | PostgreSQL database |
|
||||||
|
| `Concurrent` | `spawn`, `await`, `yield`, `sleep`, `cancel`, `isRunning`, `taskCount` | Concurrent tasks |
|
||||||
|
| `Channel` | `create`, `send`, `receive`, `tryReceive`, `close` | Inter-task communication |
|
||||||
| `Test` | `assert`, `assertEqual`, `assertTrue`, `assertFalse` | Testing |
|
| `Test` | `assert`, `assertEqual`, `assertTrue`, `assertFalse` | Testing |
|
||||||
|
|
||||||
Example usage:
|
Example usage:
|
||||||
|
|||||||
@@ -320,6 +320,114 @@ fn example(): Int with {Fail} = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Sql (SQLite)
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn example(): Unit with {Sql, Console} = {
|
||||||
|
let conn = Sql.open("mydb.sqlite") // Open database file
|
||||||
|
// Or: let conn = Sql.openMemory() // In-memory database
|
||||||
|
|
||||||
|
// Execute statements (returns row count)
|
||||||
|
Sql.execute(conn, "CREATE TABLE users (id INTEGER, name TEXT)")
|
||||||
|
Sql.execute(conn, "INSERT INTO users VALUES (1, 'Alice')")
|
||||||
|
|
||||||
|
// Query returns list of rows
|
||||||
|
let rows = Sql.query(conn, "SELECT * FROM users")
|
||||||
|
|
||||||
|
// Query for single row
|
||||||
|
let user = Sql.queryOne(conn, "SELECT * FROM users WHERE id = 1")
|
||||||
|
|
||||||
|
// Transactions
|
||||||
|
Sql.beginTx(conn)
|
||||||
|
Sql.execute(conn, "UPDATE users SET name = 'Bob' WHERE id = 1")
|
||||||
|
Sql.commit(conn) // Or: Sql.rollback(conn)
|
||||||
|
|
||||||
|
Sql.close(conn)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Postgres (PostgreSQL)
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn example(): Unit with {Postgres, Console} = {
|
||||||
|
let conn = Postgres.connect("postgres://user:pass@localhost/mydb")
|
||||||
|
|
||||||
|
// Execute statements
|
||||||
|
Postgres.execute(conn, "INSERT INTO users (name) VALUES ('Alice')")
|
||||||
|
|
||||||
|
// Query returns list of rows
|
||||||
|
let rows = Postgres.query(conn, "SELECT * FROM users")
|
||||||
|
|
||||||
|
// Query for single row
|
||||||
|
let user = Postgres.queryOne(conn, "SELECT * FROM users WHERE id = 1")
|
||||||
|
|
||||||
|
Postgres.close(conn)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Concurrent (Parallel Tasks)
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn example(): Unit with {Concurrent, Console} = {
|
||||||
|
// Spawn concurrent tasks
|
||||||
|
let task1 = Concurrent.spawn(fn(): Int => expensiveComputation(1))
|
||||||
|
let task2 = Concurrent.spawn(fn(): Int => expensiveComputation(2))
|
||||||
|
|
||||||
|
// Do other work while tasks run
|
||||||
|
Console.print("Tasks spawned, doing other work...")
|
||||||
|
|
||||||
|
// Wait for tasks to complete
|
||||||
|
let result1 = Concurrent.await(task1)
|
||||||
|
let result2 = Concurrent.await(task2)
|
||||||
|
|
||||||
|
Console.print("Results: " + toString(result1) + ", " + toString(result2))
|
||||||
|
|
||||||
|
// Check task status
|
||||||
|
if Concurrent.isRunning(task1) then
|
||||||
|
Concurrent.cancel(task1)
|
||||||
|
|
||||||
|
// Non-blocking sleep
|
||||||
|
Concurrent.sleep(100) // 100ms
|
||||||
|
|
||||||
|
// Yield to allow other tasks to run
|
||||||
|
Concurrent.yield()
|
||||||
|
|
||||||
|
// Get active task count
|
||||||
|
let count = Concurrent.taskCount()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Channel (Inter-Task Communication)
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn example(): Unit with {Concurrent, Channel, Console} = {
|
||||||
|
// Create a channel for communication
|
||||||
|
let ch = Channel.create()
|
||||||
|
|
||||||
|
// Spawn producer task
|
||||||
|
let producer = Concurrent.spawn(fn(): Unit => {
|
||||||
|
Channel.send(ch, 1)
|
||||||
|
Channel.send(ch, 2)
|
||||||
|
Channel.send(ch, 3)
|
||||||
|
Channel.close(ch)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Consumer receives values
|
||||||
|
match Channel.receive(ch) {
|
||||||
|
Some(value) => Console.print("Received: " + toString(value)),
|
||||||
|
None => Console.print("Channel closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-blocking receive
|
||||||
|
match Channel.tryReceive(ch) {
|
||||||
|
Some(value) => Console.print("Got: " + toString(value)),
|
||||||
|
None => Console.print("No value available")
|
||||||
|
}
|
||||||
|
|
||||||
|
Concurrent.await(producer)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Test
|
### Test
|
||||||
|
|
||||||
Native testing framework:
|
Native testing framework:
|
||||||
@@ -360,6 +468,10 @@ fn main(): Unit with {Console} = {
|
|||||||
| Random | int, float, bool |
|
| Random | int, float, bool |
|
||||||
| State | get, put |
|
| State | get, put |
|
||||||
| Fail | fail |
|
| Fail | fail |
|
||||||
|
| Sql | open, openMemory, close, execute, query, queryOne, beginTx, commit, rollback |
|
||||||
|
| Postgres | connect, close, execute, query, queryOne |
|
||||||
|
| Concurrent | spawn, await, yield, sleep, cancel, isRunning, taskCount |
|
||||||
|
| Channel | create, send, receive, tryReceive, close |
|
||||||
| Test | assert, assertEqual, assertTrue, assertFalse |
|
| Test | assert, assertEqual, assertTrue, assertFalse |
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|||||||
449
docs/guide/12-behavioral-types.md
Normal file
449
docs/guide/12-behavioral-types.md
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
# Chapter 12: Behavioral Types
|
||||||
|
|
||||||
|
Lux's behavioral types let you make **compile-time guarantees** about function behavior. Unlike comments or documentation, these are actually verified by the compiler.
|
||||||
|
|
||||||
|
## Why Behavioral Types Matter
|
||||||
|
|
||||||
|
Consider these real-world scenarios:
|
||||||
|
|
||||||
|
1. **Payment processing**: You retry a failed charge. If the function isn't idempotent, you might charge the customer twice.
|
||||||
|
|
||||||
|
2. **Caching**: You cache a computation. If the function isn't deterministic, you'll serve stale/wrong results.
|
||||||
|
|
||||||
|
3. **Parallelization**: You run tasks in parallel. If they aren't pure, you'll have race conditions.
|
||||||
|
|
||||||
|
4. **Infinite loops**: A function never returns. If it was supposed to be total, you have a bug.
|
||||||
|
|
||||||
|
**Behavioral types catch these bugs at compile time.**
|
||||||
|
|
||||||
|
## The Five Properties
|
||||||
|
|
||||||
|
### 1. Pure (`is pure`)
|
||||||
|
|
||||||
|
A pure function has **no side effects**. It only depends on its inputs.
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// GOOD: No effects, just computation
|
||||||
|
fn add(a: Int, b: Int): Int is pure = a + b
|
||||||
|
|
||||||
|
fn double(x: Int): Int is pure = x * 2
|
||||||
|
|
||||||
|
fn greet(name: String): String is pure = "Hello, " + name
|
||||||
|
|
||||||
|
// ERROR: Pure function cannot have effects
|
||||||
|
fn impure(x: Int): Int is pure with {Console} =
|
||||||
|
Console.print("x = " + toString(x)) // Compiler error!
|
||||||
|
x
|
||||||
|
```
|
||||||
|
|
||||||
|
**What the compiler checks:**
|
||||||
|
- Function must have an empty effect set
|
||||||
|
- No calls to effectful operations
|
||||||
|
|
||||||
|
**When to use `is pure`:**
|
||||||
|
- Mathematical functions
|
||||||
|
- Data transformations
|
||||||
|
- Any function that should be cacheable
|
||||||
|
|
||||||
|
**Compiler optimizations enabled:**
|
||||||
|
- Memoization (cache results)
|
||||||
|
- Common subexpression elimination
|
||||||
|
- Parallel execution
|
||||||
|
- Dead code elimination (if result unused)
|
||||||
|
|
||||||
|
### 2. Total (`is total`)
|
||||||
|
|
||||||
|
A total function **always terminates** and **never fails**. It produces a value for every valid input.
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// GOOD: Always terminates (structural recursion)
|
||||||
|
fn factorial(n: Int): Int is total =
|
||||||
|
if n <= 1 then 1 else n * factorial(n - 1)
|
||||||
|
|
||||||
|
// GOOD: Non-recursive is always total
|
||||||
|
fn max(a: Int, b: Int): Int is total =
|
||||||
|
if a > b then a else b
|
||||||
|
|
||||||
|
// GOOD: List operations that terminate
|
||||||
|
fn length<T>(list: List<T>): Int is total =
|
||||||
|
match list {
|
||||||
|
[] => 0,
|
||||||
|
[_, ...rest] => 1 + length(rest) // Structurally decreasing
|
||||||
|
}
|
||||||
|
|
||||||
|
// ERROR: Uses Fail effect
|
||||||
|
fn divide(a: Int, b: Int): Int is total with {Fail} =
|
||||||
|
if b == 0 then Fail.fail("division by zero") // Compiler error!
|
||||||
|
else a / b
|
||||||
|
|
||||||
|
// ERROR: May not terminate (not structurally decreasing)
|
||||||
|
fn collatz(n: Int): Int is total =
|
||||||
|
if n == 1 then 1
|
||||||
|
else if n % 2 == 0 then collatz(n / 2)
|
||||||
|
else collatz(3 * n + 1) // Not structurally smaller!
|
||||||
|
```
|
||||||
|
|
||||||
|
**What the compiler checks:**
|
||||||
|
- No `Fail` effect used
|
||||||
|
- Recursive calls must have at least one structurally decreasing argument
|
||||||
|
|
||||||
|
**When to use `is total`:**
|
||||||
|
- Core business logic that must never crash
|
||||||
|
- Mathematical functions
|
||||||
|
- Data structure operations
|
||||||
|
|
||||||
|
**Compiler optimizations enabled:**
|
||||||
|
- No exception handling overhead
|
||||||
|
- Aggressive inlining
|
||||||
|
- Removal of termination checks
|
||||||
|
|
||||||
|
### 3. Deterministic (`is deterministic`)
|
||||||
|
|
||||||
|
A deterministic function produces the **same output for the same input**, every time.
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// GOOD: Same input = same output
|
||||||
|
fn hash(s: String): Int is deterministic =
|
||||||
|
List.fold(String.chars(s), 0, fn(acc: Int, c: String): Int => acc * 31 + charCode(c))
|
||||||
|
|
||||||
|
fn formatDate(year: Int, month: Int, day: Int): String is deterministic =
|
||||||
|
toString(year) + "-" + padZero(month) + "-" + padZero(day)
|
||||||
|
|
||||||
|
// ERROR: Random is non-deterministic
|
||||||
|
fn generateId(): String is deterministic with {Random} =
|
||||||
|
"id-" + toString(Random.int(0, 1000000)) // Compiler error!
|
||||||
|
|
||||||
|
// ERROR: Time is non-deterministic
|
||||||
|
fn timestamp(): Int is deterministic with {Time} =
|
||||||
|
Time.now() // Compiler error!
|
||||||
|
```
|
||||||
|
|
||||||
|
**What the compiler checks:**
|
||||||
|
- No `Random` effect
|
||||||
|
- No `Time` effect
|
||||||
|
|
||||||
|
**When to use `is deterministic`:**
|
||||||
|
- Hashing functions
|
||||||
|
- Serialization/formatting
|
||||||
|
- Test helpers
|
||||||
|
|
||||||
|
**Compiler optimizations enabled:**
|
||||||
|
- Result caching
|
||||||
|
- Parallel execution with consistent results
|
||||||
|
- Test reproducibility
|
||||||
|
|
||||||
|
### 4. Idempotent (`is idempotent`)
|
||||||
|
|
||||||
|
An idempotent function satisfies: `f(f(x)) == f(x)`. Applying it multiple times has the same effect as applying it once.
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// GOOD: Pattern 1 - Constants
|
||||||
|
fn alwaysZero(x: Int): Int is idempotent = 0
|
||||||
|
|
||||||
|
// GOOD: Pattern 2 - Identity
|
||||||
|
fn identity<T>(x: T): T is idempotent = x
|
||||||
|
|
||||||
|
// GOOD: Pattern 3 - Projection
|
||||||
|
fn getName(person: Person): String is idempotent = person.name
|
||||||
|
|
||||||
|
// GOOD: Pattern 4 - Clamping
|
||||||
|
fn clampPositive(x: Int): Int is idempotent =
|
||||||
|
if x < 0 then 0 else x
|
||||||
|
|
||||||
|
// GOOD: Pattern 5 - Absolute value
|
||||||
|
fn abs(x: Int): Int is idempotent =
|
||||||
|
if x < 0 then 0 - x else x
|
||||||
|
|
||||||
|
// ERROR: Not idempotent (increment changes value each time)
|
||||||
|
fn increment(x: Int): Int is idempotent = x + 1 // f(f(1)) = 3, not 2
|
||||||
|
|
||||||
|
// If you're certain a function is idempotent but the compiler can't verify:
|
||||||
|
fn normalize(s: String): String assume is idempotent =
|
||||||
|
String.toLower(String.trim(s))
|
||||||
|
```
|
||||||
|
|
||||||
|
**What the compiler checks:**
|
||||||
|
- Pattern recognition: constants, identity, projections, clamping, abs
|
||||||
|
|
||||||
|
**When to use `is idempotent`:**
|
||||||
|
- Setting configuration
|
||||||
|
- Database upserts
|
||||||
|
- API PUT/DELETE operations (REST semantics)
|
||||||
|
- Retry-safe operations
|
||||||
|
|
||||||
|
**Real-world example - safe retries:**
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Payment processing with safe retries
|
||||||
|
fn chargeCard(amount: Int, cardId: String): Receipt
|
||||||
|
is idempotent
|
||||||
|
with {Payment, Logger} = {
|
||||||
|
Logger.log("Charging card " + cardId)
|
||||||
|
Payment.charge(amount, cardId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safe to retry because chargeCard is idempotent
|
||||||
|
fn processWithRetry(amount: Int, cardId: String): Receipt with {Payment, Logger, Fail} = {
|
||||||
|
let result = retry(3, fn(): Receipt => chargeCard(amount, cardId))
|
||||||
|
match result {
|
||||||
|
Ok(receipt) => receipt,
|
||||||
|
Err(e) => Fail.fail("Payment failed after 3 attempts: " + e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Commutative (`is commutative`)
|
||||||
|
|
||||||
|
A commutative function satisfies: `f(a, b) == f(b, a)`. The order of arguments doesn't matter.
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// GOOD: Addition is commutative
|
||||||
|
fn add(a: Int, b: Int): Int is commutative = a + b
|
||||||
|
|
||||||
|
// GOOD: Multiplication is commutative
|
||||||
|
fn multiply(a: Int, b: Int): Int is commutative = a * b
|
||||||
|
|
||||||
|
// GOOD: Min/max are commutative
|
||||||
|
fn minimum(a: Int, b: Int): Int is commutative =
|
||||||
|
if a < b then a else b
|
||||||
|
|
||||||
|
// ERROR: Subtraction is not commutative (3 - 2 != 2 - 3)
|
||||||
|
fn subtract(a: Int, b: Int): Int is commutative = a - b // Compiler error!
|
||||||
|
|
||||||
|
// ERROR: Wrong number of parameters
|
||||||
|
fn triple(a: Int, b: Int, c: Int): Int is commutative = a + b + c // Must have exactly 2
|
||||||
|
```
|
||||||
|
|
||||||
|
**What the compiler checks:**
|
||||||
|
- Must have exactly 2 parameters
|
||||||
|
- Body must be a commutative operation (+, *, min, max, ==, !=, &&, ||)
|
||||||
|
|
||||||
|
**When to use `is commutative`:**
|
||||||
|
- Mathematical operations
|
||||||
|
- Set operations (union, intersection)
|
||||||
|
- Merging/combining functions
|
||||||
|
|
||||||
|
**Compiler optimizations enabled:**
|
||||||
|
- Argument reordering for efficiency
|
||||||
|
- Parallel reduction
|
||||||
|
- Algebraic simplifications
|
||||||
|
|
||||||
|
## Combining Properties
|
||||||
|
|
||||||
|
Properties can be combined for stronger guarantees:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Pure + deterministic + total = perfect for caching
|
||||||
|
fn computeHash(data: String): Int
|
||||||
|
is pure
|
||||||
|
is deterministic
|
||||||
|
is total = {
|
||||||
|
List.fold(String.chars(data), 0, fn(acc: Int, c: String): Int =>
|
||||||
|
acc * 31 + charCode(c)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pure + idempotent = safe transformation
|
||||||
|
fn normalizeEmail(email: String): String
|
||||||
|
is pure
|
||||||
|
is idempotent = {
|
||||||
|
String.toLower(String.trim(email))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commutative + pure = parallel reduction friendly
|
||||||
|
fn merge(a: Record, b: Record): Record
|
||||||
|
is pure
|
||||||
|
is commutative = {
|
||||||
|
{ ...a, ...b } // Last wins, but both contribute
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Property Constraints in Where Clauses
|
||||||
|
|
||||||
|
You can require function arguments to have certain properties:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Higher-order function that requires a pure function
|
||||||
|
fn map<T, U>(list: List<T>, f: fn(T): U is pure): List<U> is pure =
|
||||||
|
match list {
|
||||||
|
[] => [],
|
||||||
|
[x, ...rest] => [f(x), ...map(rest, f)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only accepts idempotent functions - safe to retry
|
||||||
|
fn retry<T>(times: Int, action: fn(): T is idempotent): Result<T, String> = {
|
||||||
|
if times <= 0 then Err("No attempts left")
|
||||||
|
else {
|
||||||
|
match tryCall(action) {
|
||||||
|
Ok(result) => Ok(result),
|
||||||
|
Err(e) => retry(times - 1, action) // Safe because action is idempotent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only accepts deterministic functions - safe to cache
|
||||||
|
fn memoize<K, V>(f: fn(K): V is deterministic): fn(K): V = {
|
||||||
|
let cache = HashMap.new()
|
||||||
|
fn(key: K): V => {
|
||||||
|
match cache.get(key) {
|
||||||
|
Some(v) => v,
|
||||||
|
None => {
|
||||||
|
let v = f(key)
|
||||||
|
cache.set(key, v)
|
||||||
|
v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage:
|
||||||
|
let cachedHash = memoize(computeHash) // OK: computeHash is deterministic
|
||||||
|
let badCache = memoize(generateRandom) // ERROR: generateRandom is not deterministic
|
||||||
|
```
|
||||||
|
|
||||||
|
## The `assume` Escape Hatch
|
||||||
|
|
||||||
|
Sometimes you know a function has a property but the compiler can't verify it. Use `assume`:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Compiler can't verify this is idempotent, but we know it is
|
||||||
|
fn setUserStatus(userId: String, status: String): Unit
|
||||||
|
assume is idempotent
|
||||||
|
with {Database} = {
|
||||||
|
Database.execute("UPDATE users SET status = ? WHERE id = ?", [status, userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use assume sparingly - it bypasses compiler checks!
|
||||||
|
// If you're wrong, you may have subtle bugs.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Warning**: `assume` tells the compiler to trust you. If you're wrong, the optimization or guarantee may be invalid.
|
||||||
|
|
||||||
|
## Compiler Optimizations
|
||||||
|
|
||||||
|
When the compiler knows behavioral properties, it can optimize aggressively:
|
||||||
|
|
||||||
|
| Property | Optimizations |
|
||||||
|
|----------|---------------|
|
||||||
|
| `is pure` | Memoization, CSE, dead code elimination, parallelization |
|
||||||
|
| `is total` | No exception handling, aggressive inlining |
|
||||||
|
| `is deterministic` | Result caching, parallel execution |
|
||||||
|
| `is idempotent` | Retry optimization, duplicate call elimination |
|
||||||
|
| `is commutative` | Argument reordering, parallel reduction |
|
||||||
|
|
||||||
|
### Example: Automatic Memoization
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn expensiveComputation(n: Int): Int
|
||||||
|
is pure
|
||||||
|
is deterministic
|
||||||
|
is total = {
|
||||||
|
// Complex calculation...
|
||||||
|
fib(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The compiler may automatically cache results because:
|
||||||
|
// - pure: no side effects, so caching is safe
|
||||||
|
// - deterministic: same input = same output
|
||||||
|
// - total: will always return a value
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Safe Parallelization
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn processItems(items: List<Item>): List<Result>
|
||||||
|
is pure = {
|
||||||
|
List.map(items, processItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If processItem is pure, the compiler can parallelize this automatically
|
||||||
|
```
|
||||||
|
|
||||||
|
## Practical Examples
|
||||||
|
|
||||||
|
### Example 1: Financial Calculations
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Interest calculation - pure, deterministic, total
|
||||||
|
fn calculateInterest(principal: Int, rate: Float, years: Int): Float
|
||||||
|
is pure
|
||||||
|
is deterministic
|
||||||
|
is total = {
|
||||||
|
let r = rate / 100.0
|
||||||
|
Float.fromInt(principal) * Math.pow(1.0 + r, Float.fromInt(years))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction validation - pure, total
|
||||||
|
fn validateTransaction(tx: Transaction): Result<Transaction, String>
|
||||||
|
is pure
|
||||||
|
is total = {
|
||||||
|
if tx.amount <= 0 then Err("Amount must be positive")
|
||||||
|
else if tx.from == tx.to then Err("Cannot transfer to self")
|
||||||
|
else Ok(tx)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Data Processing Pipeline
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Each step is pure and deterministic
|
||||||
|
fn cleanData(raw: String): String is pure is deterministic =
|
||||||
|
raw |> String.trim |> String.toLower
|
||||||
|
|
||||||
|
fn parseRecord(line: String): Result<Record, String> is pure is deterministic =
|
||||||
|
match String.split(line, ",") {
|
||||||
|
[name, age, email] => Ok({ name, age: parseInt(age), email }),
|
||||||
|
_ => Err("Invalid format")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validateRecord(record: Record): Bool is pure is deterministic is total =
|
||||||
|
String.length(record.name) > 0 && record.age > 0
|
||||||
|
|
||||||
|
// Pipeline can be parallelized because all functions are pure + deterministic
|
||||||
|
fn processFile(contents: String): List<Record> is pure is deterministic = {
|
||||||
|
contents
|
||||||
|
|> String.lines
|
||||||
|
|> List.map(cleanData)
|
||||||
|
|> List.map(parseRecord)
|
||||||
|
|> List.filterMap(fn(r: Result<Record, String>): Option<Record> =>
|
||||||
|
match r { Ok(v) => Some(v), Err(_) => None })
|
||||||
|
|> List.filter(validateRecord)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Idempotent API Handlers
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// PUT /users/:id - idempotent by REST semantics
|
||||||
|
fn handlePutUser(id: String, data: UserData): Response
|
||||||
|
is idempotent
|
||||||
|
with {Database, Logger} = {
|
||||||
|
Logger.log("PUT /users/" + id)
|
||||||
|
Database.upsert("users", id, data)
|
||||||
|
Response.ok({ id, ...data })
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /users/:id - idempotent by REST semantics
|
||||||
|
fn handleDeleteUser(id: String): Response
|
||||||
|
is idempotent
|
||||||
|
with {Database, Logger} = {
|
||||||
|
Logger.log("DELETE /users/" + id)
|
||||||
|
Database.delete("users", id) // Safe to call multiple times
|
||||||
|
Response.noContent()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Property | Meaning | Compiler Checks | Use Case |
|
||||||
|
|----------|---------|-----------------|----------|
|
||||||
|
| `is pure` | No effects | Empty effect set | Caching, parallelization |
|
||||||
|
| `is total` | Always terminates | No Fail, structural recursion | Core logic |
|
||||||
|
| `is deterministic` | Same in = same out | No Random/Time | Caching, testing |
|
||||||
|
| `is idempotent` | f(f(x)) = f(x) | Pattern recognition | Retries, APIs |
|
||||||
|
| `is commutative` | f(a,b) = f(b,a) | 2 params, commutative op | Math, merging |
|
||||||
|
|
||||||
|
## What's Next?
|
||||||
|
|
||||||
|
- [Chapter 13: Schema Evolution](./13-schema-evolution.md) - Version your data types
|
||||||
|
- [Tutorials](../tutorials/README.md) - Practical projects
|
||||||
573
docs/guide/13-schema-evolution.md
Normal file
573
docs/guide/13-schema-evolution.md
Normal file
@@ -0,0 +1,573 @@
|
|||||||
|
# Chapter 13: Schema Evolution
|
||||||
|
|
||||||
|
Data structures change over time. Fields get added, removed, or renamed. Types get split or merged. Without careful handling, these changes break systems—old data can't be read, services fail, migrations corrupt data.
|
||||||
|
|
||||||
|
Lux's **schema evolution** system makes these changes safe and automatic.
|
||||||
|
|
||||||
|
## The Problem
|
||||||
|
|
||||||
|
Consider a real scenario:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Version 1: Simple user
|
||||||
|
type User {
|
||||||
|
name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
// Later, you need email addresses
|
||||||
|
type User {
|
||||||
|
name: String,
|
||||||
|
email: String // Breaking change! Old data doesn't have this.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In most languages, this breaks everything. Existing users in your database don't have email addresses. Deserializing old data fails. Services crash.
|
||||||
|
|
||||||
|
Lux solves this with **versioned types** and **automatic migrations**.
|
||||||
|
|
||||||
|
## Versioned Types
|
||||||
|
|
||||||
|
Add a version annotation to any type:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Version 1: Original definition
|
||||||
|
type User @v1 {
|
||||||
|
name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version 2: Added email field
|
||||||
|
type User @v2 {
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
|
||||||
|
// How to migrate from v1
|
||||||
|
from @v1 = { name: old.name, email: "unknown@example.com" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version 3: Split name into first/last
|
||||||
|
type User @v3 {
|
||||||
|
firstName: String,
|
||||||
|
lastName: String,
|
||||||
|
email: String,
|
||||||
|
|
||||||
|
// How to migrate from v2
|
||||||
|
from @v2 = {
|
||||||
|
firstName: String.split(old.name, " ") |> List.head |> Option.getOrElse(""),
|
||||||
|
lastName: String.split(old.name, " ") |> List.tail |> List.head |> Option.getOrElse(""),
|
||||||
|
email: old.email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `@latest` alias always refers to the most recent version:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
type User @latest {
|
||||||
|
firstName: String,
|
||||||
|
lastName: String,
|
||||||
|
email: String,
|
||||||
|
|
||||||
|
from @v2 = { ... }
|
||||||
|
}
|
||||||
|
|
||||||
|
// These are equivalent:
|
||||||
|
fn createUser(first: String, last: String, email: String): User@latest = ...
|
||||||
|
fn createUser(first: String, last: String, email: String): User@v3 = ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Syntax
|
||||||
|
|
||||||
|
### Basic Migration
|
||||||
|
|
||||||
|
```lux
|
||||||
|
type Config @v2 {
|
||||||
|
theme: String,
|
||||||
|
fontSize: Int,
|
||||||
|
|
||||||
|
// 'old' refers to the v1 value
|
||||||
|
from @v1 = {
|
||||||
|
theme: old.theme,
|
||||||
|
fontSize: 14 // New field with default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Computed Fields
|
||||||
|
|
||||||
|
```lux
|
||||||
|
type Order @v2 {
|
||||||
|
items: List<Item>,
|
||||||
|
total: Int,
|
||||||
|
itemCount: Int, // New computed field
|
||||||
|
|
||||||
|
from @v1 = {
|
||||||
|
items: old.items,
|
||||||
|
total: old.total,
|
||||||
|
itemCount: List.length(old.items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Removing Fields
|
||||||
|
|
||||||
|
When removing fields, simply don't include them in the new version:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
type Settings @v1 {
|
||||||
|
theme: String,
|
||||||
|
legacyMode: Bool, // To be removed
|
||||||
|
volume: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Settings @v2 {
|
||||||
|
theme: String,
|
||||||
|
volume: Int,
|
||||||
|
|
||||||
|
// legacyMode is dropped - just don't migrate it
|
||||||
|
from @v1 = {
|
||||||
|
theme: old.theme,
|
||||||
|
volume: old.volume
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Renaming Fields
|
||||||
|
|
||||||
|
```lux
|
||||||
|
type Product @v1 {
|
||||||
|
name: String,
|
||||||
|
cost: Int // Old field name
|
||||||
|
}
|
||||||
|
|
||||||
|
type Product @v2 {
|
||||||
|
name: String,
|
||||||
|
price: Int, // Renamed from 'cost'
|
||||||
|
|
||||||
|
from @v1 = {
|
||||||
|
name: old.name,
|
||||||
|
price: old.cost // Map old field to new name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complex Transformations
|
||||||
|
|
||||||
|
```lux
|
||||||
|
type Address @v1 {
|
||||||
|
fullAddress: String // "123 Main St, New York, NY 10001"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Address @v2 {
|
||||||
|
street: String,
|
||||||
|
city: String,
|
||||||
|
state: String,
|
||||||
|
zip: String,
|
||||||
|
|
||||||
|
from @v1 = {
|
||||||
|
let parts = String.split(old.fullAddress, ", ")
|
||||||
|
{
|
||||||
|
street: List.get(parts, 0) |> Option.getOrElse(""),
|
||||||
|
city: List.get(parts, 1) |> Option.getOrElse(""),
|
||||||
|
state: List.get(parts, 2)
|
||||||
|
|> Option.map(fn(s: String): String => String.split(s, " ") |> List.head |> Option.getOrElse(""))
|
||||||
|
|> Option.getOrElse(""),
|
||||||
|
zip: List.get(parts, 2)
|
||||||
|
|> Option.map(fn(s: String): String => String.split(s, " ") |> List.last |> Option.getOrElse(""))
|
||||||
|
|> Option.getOrElse("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Working with Versioned Values
|
||||||
|
|
||||||
|
The `Schema` module provides runtime operations for versioned values:
|
||||||
|
|
||||||
|
### Creating Versioned Values
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Create a value tagged with a specific version
|
||||||
|
let userV1 = Schema.versioned("User", 1, { name: "Alice" })
|
||||||
|
let userV2 = Schema.versioned("User", 2, { name: "Alice", email: "alice@example.com" })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checking Versions
|
||||||
|
|
||||||
|
```lux
|
||||||
|
let user = Schema.versioned("User", 1, { name: "Alice" })
|
||||||
|
let version = Schema.getVersion(user) // Returns 1
|
||||||
|
|
||||||
|
// Version-aware logic
|
||||||
|
if version < 2 then
|
||||||
|
Console.print("Legacy user format")
|
||||||
|
else
|
||||||
|
Console.print("Modern user format")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migrating Values
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Migrate to a specific version
|
||||||
|
let userV1 = Schema.versioned("User", 1, { name: "Alice" })
|
||||||
|
let userV2 = Schema.migrate(userV1, 2) // Uses declared migration
|
||||||
|
|
||||||
|
let version = Schema.getVersion(userV2) // Now 2
|
||||||
|
|
||||||
|
// Chain migrations (v1 -> v2 -> v3)
|
||||||
|
let userV3 = Schema.migrate(userV1, 3) // Applies v1->v2, then v2->v3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auto-Generated Migrations
|
||||||
|
|
||||||
|
For simple changes, Lux can **automatically generate** migrations:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
type Profile @v1 {
|
||||||
|
name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adding a field with a default? Migration is auto-generated
|
||||||
|
type Profile @v2 {
|
||||||
|
name: String,
|
||||||
|
bio: String = "" // Default value provided
|
||||||
|
}
|
||||||
|
|
||||||
|
// The compiler generates this for you:
|
||||||
|
// from @v1 = { name: old.name, bio: "" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Auto-migration works for:
|
||||||
|
- Adding fields with default values
|
||||||
|
- Keeping existing fields unchanged
|
||||||
|
|
||||||
|
You must write explicit migrations for:
|
||||||
|
- Field renaming
|
||||||
|
- Field removal (to confirm intent)
|
||||||
|
- Type changes
|
||||||
|
- Computed/derived fields
|
||||||
|
|
||||||
|
## Practical Examples
|
||||||
|
|
||||||
|
### Example 1: API Response Versioning
|
||||||
|
|
||||||
|
```lux
|
||||||
|
type ApiResponse @v1 {
|
||||||
|
status: String,
|
||||||
|
data: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApiResponse @v2 {
|
||||||
|
status: String,
|
||||||
|
data: String,
|
||||||
|
meta: { timestamp: Int, version: String },
|
||||||
|
|
||||||
|
from @v1 = {
|
||||||
|
status: old.status,
|
||||||
|
data: old.data,
|
||||||
|
meta: { timestamp: 0, version: "legacy" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version-aware API client
|
||||||
|
fn handleResponse(raw: ApiResponse@v1): ApiResponse@v2 = {
|
||||||
|
Schema.migrate(Schema.versioned("ApiResponse", 1, raw), 2)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Database Record Evolution
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Original schema
|
||||||
|
type Customer @v1 {
|
||||||
|
name: String,
|
||||||
|
address: String
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split address into components
|
||||||
|
type Customer @v2 {
|
||||||
|
name: String,
|
||||||
|
street: String,
|
||||||
|
city: String,
|
||||||
|
country: String,
|
||||||
|
|
||||||
|
from @v1 = {
|
||||||
|
let parts = String.split(old.address, ", ")
|
||||||
|
{
|
||||||
|
name: old.name,
|
||||||
|
street: List.get(parts, 0) |> Option.getOrElse(old.address),
|
||||||
|
city: List.get(parts, 1) |> Option.getOrElse("Unknown"),
|
||||||
|
country: List.get(parts, 2) |> Option.getOrElse("Unknown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and migrate on read
|
||||||
|
fn loadCustomer(id: String): Customer@v2 with {Database} = {
|
||||||
|
let record = Database.query("SELECT * FROM customers WHERE id = ?", [id])
|
||||||
|
let version = record.schema_version // Stored version
|
||||||
|
|
||||||
|
if version == 1 then
|
||||||
|
let v1 = Schema.versioned("Customer", 1, {
|
||||||
|
name: record.name,
|
||||||
|
address: record.address
|
||||||
|
})
|
||||||
|
Schema.migrate(v1, 2)
|
||||||
|
else
|
||||||
|
{ name: record.name, street: record.street, city: record.city, country: record.country }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Configuration Files
|
||||||
|
|
||||||
|
```lux
|
||||||
|
type AppConfig @v1 {
|
||||||
|
debug: Bool,
|
||||||
|
port: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppConfig @v2 {
|
||||||
|
debug: Bool,
|
||||||
|
port: Int,
|
||||||
|
logLevel: String, // New in v2
|
||||||
|
|
||||||
|
from @v1 = {
|
||||||
|
debug: old.debug,
|
||||||
|
port: old.port,
|
||||||
|
logLevel: if old.debug then "debug" else "info"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppConfig @v3 {
|
||||||
|
environment: String, // Replaces debug flag
|
||||||
|
port: Int,
|
||||||
|
logLevel: String,
|
||||||
|
|
||||||
|
from @v2 = {
|
||||||
|
environment: if old.debug then "development" else "production",
|
||||||
|
port: old.port,
|
||||||
|
logLevel: old.logLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load config with automatic migration
|
||||||
|
fn loadConfig(path: String): AppConfig@v3 with {File} = {
|
||||||
|
let json = File.read(path)
|
||||||
|
let parsed = Json.parse(json)
|
||||||
|
let version = Json.getInt(parsed, "version") |> Option.getOrElse(1)
|
||||||
|
|
||||||
|
match version {
|
||||||
|
1 => {
|
||||||
|
let v1 = Schema.versioned("AppConfig", 1, {
|
||||||
|
debug: Json.getBool(parsed, "debug") |> Option.getOrElse(false),
|
||||||
|
port: Json.getInt(parsed, "port") |> Option.getOrElse(8080)
|
||||||
|
})
|
||||||
|
Schema.migrate(v1, 3)
|
||||||
|
},
|
||||||
|
2 => {
|
||||||
|
let v2 = Schema.versioned("AppConfig", 2, {
|
||||||
|
debug: Json.getBool(parsed, "debug") |> Option.getOrElse(false),
|
||||||
|
port: Json.getInt(parsed, "port") |> Option.getOrElse(8080),
|
||||||
|
logLevel: Json.getString(parsed, "logLevel") |> Option.getOrElse("info")
|
||||||
|
})
|
||||||
|
Schema.migrate(v2, 3)
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
// Already v3
|
||||||
|
{
|
||||||
|
environment: Json.getString(parsed, "environment") |> Option.getOrElse("production"),
|
||||||
|
port: Json.getInt(parsed, "port") |> Option.getOrElse(8080),
|
||||||
|
logLevel: Json.getString(parsed, "logLevel") |> Option.getOrElse("info")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 4: Event Sourcing
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Event types evolve over time
|
||||||
|
type UserCreated @v1 {
|
||||||
|
userId: String,
|
||||||
|
name: String,
|
||||||
|
timestamp: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserCreated @v2 {
|
||||||
|
userId: String,
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
createdAt: Int, // Renamed from timestamp
|
||||||
|
|
||||||
|
from @v1 = {
|
||||||
|
userId: old.userId,
|
||||||
|
name: old.name,
|
||||||
|
email: "", // Not captured in v1
|
||||||
|
createdAt: old.timestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process events regardless of version
|
||||||
|
fn processEvent(event: UserCreated@v1 | UserCreated@v2): Unit with {Console} = {
|
||||||
|
let normalized = Schema.migrate(event, 2) // Always work with v2
|
||||||
|
Console.print("User created: " + normalized.name + " at " + toString(normalized.createdAt))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compile-Time Safety
|
||||||
|
|
||||||
|
The compiler catches schema evolution errors:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
type User @v2 {
|
||||||
|
name: String,
|
||||||
|
email: String
|
||||||
|
|
||||||
|
// ERROR: Migration references non-existent field
|
||||||
|
from @v1 = { name: old.username, email: old.email }
|
||||||
|
// ^^^^^^^^ 'username' does not exist in User@v1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```lux
|
||||||
|
type User @v2 {
|
||||||
|
name: String,
|
||||||
|
email: String
|
||||||
|
|
||||||
|
// ERROR: Migration missing required field
|
||||||
|
from @v1 = { name: old.name }
|
||||||
|
// ^ Missing 'email' field
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```lux
|
||||||
|
type User @v2 {
|
||||||
|
name: String,
|
||||||
|
age: Int
|
||||||
|
|
||||||
|
// ERROR: Type mismatch in migration
|
||||||
|
from @v1 = { name: old.name, age: old.birthYear }
|
||||||
|
// ^^^^^^^^^^^^^ Expected Int, found String
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compatibility Checking
|
||||||
|
|
||||||
|
Lux tracks compatibility between versions:
|
||||||
|
|
||||||
|
| Change Type | Backward Compatible | Forward Compatible |
|
||||||
|
|-------------|--------------------|--------------------|
|
||||||
|
| Add optional field (with default) | Yes | Yes |
|
||||||
|
| Add required field | No | Yes (with migration) |
|
||||||
|
| Remove field | Yes (with migration) | No |
|
||||||
|
| Rename field | No | No (need migration) |
|
||||||
|
| Change field type | No | No (need migration) |
|
||||||
|
|
||||||
|
The compiler warns about breaking changes:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
type User @v1 {
|
||||||
|
name: String,
|
||||||
|
email: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type User @v2 {
|
||||||
|
name: String
|
||||||
|
// Warning: Removing 'email' is a breaking change
|
||||||
|
// Existing v2 consumers expect this field
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Always Version Production Types
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Good: Versioned from the start
|
||||||
|
type Order @v1 {
|
||||||
|
id: String,
|
||||||
|
items: List<Item>,
|
||||||
|
total: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bad: Unversioned type is hard to evolve
|
||||||
|
type Order {
|
||||||
|
id: String,
|
||||||
|
items: List<Item>,
|
||||||
|
total: Int
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Keep Migrations Simple
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Good: Simple, direct mapping
|
||||||
|
from @v1 = {
|
||||||
|
name: old.name,
|
||||||
|
email: old.email |> Option.getOrElse("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid: Complex logic in migrations
|
||||||
|
from @v1 = {
|
||||||
|
name: old.name,
|
||||||
|
email: {
|
||||||
|
// Don't put complex business logic here
|
||||||
|
let domain = inferDomainFromName(old.name)
|
||||||
|
let local = String.toLower(String.replace(old.name, " ", "."))
|
||||||
|
local + "@" + domain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test Migrations
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn testUserMigration(): Unit with {Test} = {
|
||||||
|
let v1User = Schema.versioned("User", 1, { name: "Alice" })
|
||||||
|
let v2User = Schema.migrate(v1User, 2)
|
||||||
|
|
||||||
|
Test.assertEqual(v2User.name, "Alice")
|
||||||
|
Test.assertEqual(v2User.email, "unknown@example.com")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Document Breaking Changes
|
||||||
|
|
||||||
|
```lux
|
||||||
|
type User @v3 {
|
||||||
|
// BREAKING: 'name' split into firstName/lastName
|
||||||
|
// Migration: name.split(" ")[0] -> firstName, name.split(" ")[1] -> lastName
|
||||||
|
firstName: String,
|
||||||
|
lastName: String,
|
||||||
|
email: String,
|
||||||
|
|
||||||
|
from @v2 = { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schema Module Reference
|
||||||
|
|
||||||
|
| Function | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `Schema.versioned(typeName, version, value)` | Create a versioned value |
|
||||||
|
| `Schema.getVersion(value)` | Get the version of a value |
|
||||||
|
| `Schema.migrate(value, targetVersion)` | Migrate to a target version |
|
||||||
|
| `Schema.isCompatible(v1, v2)` | Check if versions are compatible |
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Schema evolution in Lux provides:
|
||||||
|
|
||||||
|
- **Versioned types** with `@v1`, `@v2`, `@latest` annotations
|
||||||
|
- **Explicit migrations** with `from @vN = { ... }` syntax
|
||||||
|
- **Automatic migrations** for simple field additions with defaults
|
||||||
|
- **Runtime operations** via the `Schema` module
|
||||||
|
- **Compile-time safety** catching migration errors early
|
||||||
|
- **Migration chaining** for multi-step upgrades
|
||||||
|
|
||||||
|
This system ensures your data can evolve safely over time, without breaking existing code or losing information.
|
||||||
|
|
||||||
|
## What's Next?
|
||||||
|
|
||||||
|
- [Tutorials](../tutorials/README.md) - Build real projects
|
||||||
|
- [Standard Library Reference](../stdlib/README.md) - Complete API docs
|
||||||
@@ -1,36 +1,19 @@
|
|||||||
// Demonstrating behavioral properties in Lux
|
fn add(a: Int, b: Int): Int is pure = a + b
|
||||||
// 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
|
|
||||||
|
|
||||||
// A pure function - no side effects, same input always gives same output
|
fn factorial(n: Int): Int is deterministic = if n <= 1 then 1 else n * factorial(n - 1)
|
||||||
fn add(a: Int, b: Int): Int is pure =
|
|
||||||
a + b
|
|
||||||
|
|
||||||
// A deterministic function - same input always gives same output
|
fn multiply(a: Int, b: Int): Int is commutative = a * b
|
||||||
fn factorial(n: Int): Int is deterministic =
|
|
||||||
if n <= 1 then 1
|
|
||||||
else n * factorial(n - 1)
|
|
||||||
|
|
||||||
// A commutative function - order of arguments doesn't matter
|
fn abs(x: Int): Int is idempotent = if x < 0 then 0 - x else x
|
||||||
fn multiply(a: Int, b: Int): Int is commutative =
|
|
||||||
a * b
|
|
||||||
|
|
||||||
// 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 sumResult = add(5, 3)
|
||||||
|
|
||||||
let factResult = factorial(5)
|
let factResult = factorial(5)
|
||||||
|
|
||||||
let productResult = multiply(7, 6)
|
let productResult = multiply(7, 6)
|
||||||
|
|
||||||
let absResult = abs(0 - 5)
|
let absResult = abs(0 - 5)
|
||||||
|
|
||||||
// Print results
|
|
||||||
fn printResults(): Unit with {Console} = {
|
fn printResults(): Unit with {Console} = {
|
||||||
Console.print("add(5, 3) = " + toString(sumResult))
|
Console.print("add(5, 3) = " + toString(sumResult))
|
||||||
Console.print("factorial(5) = " + toString(factResult))
|
Console.print("factorial(5) = " + toString(factResult))
|
||||||
|
|||||||
@@ -1,82 +1,42 @@
|
|||||||
// Behavioral Types Demo
|
|
||||||
// Demonstrates compile-time verification of function properties
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// PART 1: Pure Functions
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Pure functions have no side effects
|
|
||||||
fn add(a: Int, b: Int): Int is pure = a + b
|
fn add(a: Int, b: Int): Int is pure = a + b
|
||||||
|
|
||||||
fn subtract(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 multiply(a: Int, b: Int): Int is commutative = a * b
|
||||||
|
|
||||||
fn sum(a: Int, b: Int): Int is commutative = a + b
|
fn sum(a: Int, b: Int): Int is commutative = a + b
|
||||||
|
|
||||||
// ============================================================
|
fn abs(x: Int): Int is idempotent = if x < 0 then 0 - x else x
|
||||||
// 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 identity(x: Int): Int is idempotent = x
|
fn identity(x: Int): Int is idempotent = x
|
||||||
|
|
||||||
// ============================================================
|
fn factorial(n: Int): Int is deterministic = if n <= 1 then 1 else n * factorial(n - 1)
|
||||||
// PART 4: Deterministic Functions
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Deterministic functions always produce the same output for the same input
|
fn fib(n: Int): Int is deterministic = if n <= 1 then n else fib(n - 1) + fib(n - 2)
|
||||||
fn factorial(n: Int): Int is deterministic =
|
|
||||||
if n <= 1 then 1 else n * factorial(n - 1)
|
|
||||||
|
|
||||||
fn fib(n: Int): Int is deterministic =
|
fn sumTo(n: Int): Int is total = if n <= 0 then 0 else n + sumTo(n - 1)
|
||||||
if n <= 1 then n else fib(n - 1) + fib(n - 2)
|
|
||||||
|
|
||||||
// ============================================================
|
fn power(base: Int, exp: Int): Int is total = if exp <= 0 then 1 else base * power(base, exp - 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 main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
Console.print("=== Behavioral Types Demo ===")
|
Console.print("=== Behavioral Types Demo ===")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
Console.print("Part 1: Pure functions")
|
Console.print("Part 1: Pure functions")
|
||||||
Console.print(" add(5, 3) = " + toString(add(5, 3)))
|
Console.print(" add(5, 3) = " + toString(add(5, 3)))
|
||||||
Console.print(" subtract(10, 4) = " + toString(subtract(10, 4)))
|
Console.print(" subtract(10, 4) = " + toString(subtract(10, 4)))
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
Console.print("Part 2: Commutative functions")
|
Console.print("Part 2: Commutative functions")
|
||||||
Console.print(" multiply(7, 6) = " + toString(multiply(7, 6)))
|
Console.print(" multiply(7, 6) = " + toString(multiply(7, 6)))
|
||||||
Console.print(" sum(10, 20) = " + toString(sum(10, 20)))
|
Console.print(" sum(10, 20) = " + toString(sum(10, 20)))
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
Console.print("Part 3: Idempotent functions")
|
Console.print("Part 3: Idempotent functions")
|
||||||
Console.print(" abs(-42) = " + toString(abs(0 - 42)))
|
Console.print(" abs(-42) = " + toString(abs(0 - 42)))
|
||||||
Console.print(" identity(100) = " + toString(identity(100)))
|
Console.print(" identity(100) = " + toString(identity(100)))
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
Console.print("Part 4: Deterministic functions")
|
Console.print("Part 4: Deterministic functions")
|
||||||
Console.print(" factorial(5) = " + toString(factorial(5)))
|
Console.print(" factorial(5) = " + toString(factorial(5)))
|
||||||
Console.print(" fib(10) = " + toString(fib(10)))
|
Console.print(" fib(10) = " + toString(fib(10)))
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
Console.print("Part 5: Total functions")
|
Console.print("Part 5: Total functions")
|
||||||
Console.print(" sumTo(10) = " + toString(sumTo(10)))
|
Console.print(" sumTo(10) = " + toString(sumTo(10)))
|
||||||
Console.print(" power(2, 8) = " + toString(power(2, 8)))
|
Console.print(" power(2, 8) = " + toString(power(2, 8)))
|
||||||
|
|||||||
@@ -1,31 +1,7 @@
|
|||||||
// Demonstrating built-in effects in Lux
|
fn safeDivide(a: Int, b: Int): Int with {Fail} = if b == 0 then Fail.fail("Division by zero") else a / b
|
||||||
//
|
|
||||||
// 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!
|
|
||||||
|
|
||||||
// A function that can fail
|
fn validatePositive(n: Int): Int with {Fail} = if n < 0 then Fail.fail("Negative number not allowed") else n
|
||||||
fn safeDivide(a: Int, b: Int): Int with {Fail} =
|
|
||||||
if b == 0 then Fail.fail("Division by zero")
|
|
||||||
else a / b
|
|
||||||
|
|
||||||
// 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} = {
|
fn compute(input: Int): Int with {Console, Fail} = {
|
||||||
Console.print("Starting computation...")
|
Console.print("Starting computation...")
|
||||||
Console.print("Step 1: validating input")
|
Console.print("Step 1: validating input")
|
||||||
@@ -36,7 +12,6 @@ fn compute(input: Int): Int with {Console, Fail} = {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main function
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
let result = run compute(21) with {}
|
let result = run compute(21) with {}
|
||||||
Console.print("Done!")
|
Console.print("Done!")
|
||||||
|
|||||||
@@ -1,14 +1,3 @@
|
|||||||
// Counter Example - A simple interactive counter using TEA pattern
|
|
||||||
//
|
|
||||||
// This example demonstrates:
|
|
||||||
// - Model-View-Update architecture (TEA)
|
|
||||||
// - Html DSL for describing UI (inline version)
|
|
||||||
// - Message-based state updates
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Html Types (subset of stdlib/html)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
type Html<M> =
|
type Html<M> =
|
||||||
| Element(String, List<Attr<M>>, List<Html<M>>)
|
| Element(String, List<Attr<M>>, List<Html<M>>)
|
||||||
| Text(String)
|
| Text(String)
|
||||||
@@ -19,130 +8,96 @@ type Attr<M> =
|
|||||||
| Id(String)
|
| Id(String)
|
||||||
| OnClick(M)
|
| 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> =
|
fn span<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = Element("span", attrs, children)
|
||||||
Element("span", attrs, children)
|
|
||||||
|
|
||||||
fn h1<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
fn h1<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = Element("h1", attrs, children)
|
||||||
Element("h1", attrs, children)
|
|
||||||
|
|
||||||
fn button<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
fn button<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = Element("button", attrs, children)
|
||||||
Element("button", attrs, children)
|
|
||||||
|
|
||||||
fn text<M>(content: String): Html<M> =
|
fn text<M>(content: String): Html<M> = Text(content)
|
||||||
Text(content)
|
|
||||||
|
|
||||||
fn class<M>(name: String): Attr<M> =
|
fn class<M>(name: String): Attr<M> = Class(name)
|
||||||
Class(name)
|
|
||||||
|
|
||||||
fn onClick<M>(msg: M): Attr<M> =
|
fn onClick<M>(msg: M): Attr<M> = OnClick(msg)
|
||||||
OnClick(msg)
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Model - The application state (using ADT wrapper)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
type Model =
|
type Model =
|
||||||
| Counter(Int)
|
| Counter(Int)
|
||||||
|
|
||||||
fn getCount(model: Model): Int =
|
fn getCount(model: Model): Int =
|
||||||
match model {
|
match model {
|
||||||
Counter(n) => n
|
Counter(n) => n,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init(): Model = Counter(0)
|
fn init(): Model = Counter(0)
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Messages - Events that can occur
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
type Msg =
|
type Msg =
|
||||||
| Increment
|
| Increment
|
||||||
| Decrement
|
| Decrement
|
||||||
| Reset
|
| Reset
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Update - State transitions
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
fn update(model: Model, msg: Msg): Model =
|
fn update(model: Model, msg: Msg): Model =
|
||||||
match msg {
|
match msg {
|
||||||
Increment => Counter(getCount(model) + 1),
|
Increment => Counter(getCount(model) + 1),
|
||||||
Decrement => Counter(getCount(model) - 1),
|
Decrement => Counter(getCount(model) - 1),
|
||||||
Reset => Counter(0)
|
Reset => Counter(0),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// View - Render the UI
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
fn viewCounter(count: Int): Html<Msg> = {
|
fn viewCounter(count: Int): Html<Msg> = {
|
||||||
let countText = text(toString(count))
|
let countText = text(toString(count))
|
||||||
let countSpan = span([class("count")], [countText])
|
let countSpan = span([class("count")], [countText])
|
||||||
let displayDiv = div([class("counter-display")], [countSpan])
|
let displayDiv = div([class("counter-display")], [countSpan])
|
||||||
|
|
||||||
let minusBtn = button([onClick(Decrement), class("btn")], [text("-")])
|
let minusBtn = button([onClick(Decrement), class("btn")], [text("-")])
|
||||||
let resetBtn = button([onClick(Reset), class("btn btn-reset")], [text("Reset")])
|
let resetBtn = button([onClick(Reset), class("btn btn-reset")], [text("Reset")])
|
||||||
let plusBtn = button([onClick(Increment), class("btn")], [text("+")])
|
let plusBtn = button([onClick(Increment), class("btn")], [text("+")])
|
||||||
let buttonsDiv = div([class("counter-buttons")], [minusBtn, resetBtn, plusBtn])
|
let buttonsDiv = div([class("counter-buttons")], [minusBtn, resetBtn, plusBtn])
|
||||||
|
|
||||||
let title = h1([], [text("Counter")])
|
let title = h1([], [text("Counter")])
|
||||||
div([class("counter-app")], [title, displayDiv, buttonsDiv])
|
div([class("counter-app")], [title, displayDiv, buttonsDiv])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(model: Model): Html<Msg> = viewCounter(getCount(model))
|
fn view(model: Model): Html<Msg> = viewCounter(getCount(model))
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Debug: Print Html structure
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
fn showAttr(attr: Attr<Msg>): String =
|
fn showAttr(attr: Attr<Msg>): String =
|
||||||
match attr {
|
match attr {
|
||||||
Class(s) => "class=\"" + s + "\"",
|
Class(s) => "class=\"" + s + "\"",
|
||||||
Id(s) => "id=\"" + s + "\"",
|
Id(s) => "id=\"" + s + "\"",
|
||||||
OnClick(msg) => match msg {
|
OnClick(msg) => match msg {
|
||||||
Increment => "onclick=\"Increment\"",
|
Increment => "onclick=\"Increment\"",
|
||||||
Decrement => "onclick=\"Decrement\"",
|
Decrement => "onclick=\"Decrement\"",
|
||||||
Reset => "onclick=\"Reset\""
|
Reset => "onclick=\"Reset\"",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn showAttrs(attrs: List<Attr<Msg>>): String =
|
fn showAttrs(attrs: List<Attr<Msg>>): String =
|
||||||
match List.head(attrs) {
|
match List.head(attrs) {
|
||||||
None => "",
|
None => "",
|
||||||
Some(a) => match List.tail(attrs) {
|
Some(a) => match List.tail(attrs) {
|
||||||
None => showAttr(a),
|
None => showAttr(a),
|
||||||
Some(rest) => showAttr(a) + " " + showAttrs(rest)
|
Some(rest) => showAttr(a) + " " + showAttrs(rest),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn showChildren(children: List<Html<Msg>>, indent: Int): String =
|
fn showChildren(children: List<Html<Msg>>, indent: Int): String =
|
||||||
match List.head(children) {
|
match List.head(children) {
|
||||||
None => "",
|
None => "",
|
||||||
Some(c) => match List.tail(children) {
|
Some(c) => match List.tail(children) {
|
||||||
None => showHtml(c, indent),
|
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 =
|
fn showHtml(html: Html<Msg>, indent: Int): String =
|
||||||
match html {
|
match html {
|
||||||
Empty => "",
|
Empty => "",
|
||||||
Text(s) => s,
|
Text(s) => s,
|
||||||
Element(tag, attrs, children) => {
|
Element(tag, attrs, children) => {
|
||||||
let attrStr = showAttrs(attrs)
|
let attrStr = showAttrs(attrs)
|
||||||
let attrPart = if String.length(attrStr) > 0 then " " + attrStr else ""
|
let attrPart = if String.length(attrStr) > 0 then " " + attrStr else ""
|
||||||
let childStr = showChildren(children, indent + 2)
|
let childStr = showChildren(children, indent + 2)
|
||||||
"<" + tag + attrPart + ">" + childStr + "</" + tag + ">"
|
"<" + tag + attrPart + ">" + childStr + "</" + tag + ">"
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Entry point
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
let model = init()
|
let model = init()
|
||||||
@@ -150,24 +105,19 @@ fn main(): Unit with {Console} = {
|
|||||||
Console.print("")
|
Console.print("")
|
||||||
Console.print("Initial count: " + toString(getCount(model)))
|
Console.print("Initial count: " + toString(getCount(model)))
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
let m1 = update(model, Increment)
|
let m1 = update(model, Increment)
|
||||||
Console.print("After Increment: " + toString(getCount(m1)))
|
Console.print("After Increment: " + toString(getCount(m1)))
|
||||||
|
|
||||||
let m2 = update(m1, Increment)
|
let m2 = update(m1, Increment)
|
||||||
Console.print("After Increment: " + toString(getCount(m2)))
|
Console.print("After Increment: " + toString(getCount(m2)))
|
||||||
|
|
||||||
let m3 = update(m2, Increment)
|
let m3 = update(m2, Increment)
|
||||||
Console.print("After Increment: " + toString(getCount(m3)))
|
Console.print("After Increment: " + toString(getCount(m3)))
|
||||||
|
|
||||||
let m4 = update(m3, Decrement)
|
let m4 = update(m3, Decrement)
|
||||||
Console.print("After Decrement: " + toString(getCount(m4)))
|
Console.print("After Decrement: " + toString(getCount(m4)))
|
||||||
|
|
||||||
let m5 = update(m4, Reset)
|
let m5 = update(m4, Reset)
|
||||||
Console.print("After Reset: " + toString(getCount(m5)))
|
Console.print("After Reset: " + toString(getCount(m5)))
|
||||||
|
|
||||||
Console.print("")
|
Console.print("")
|
||||||
Console.print("=== View (HTML Structure) ===")
|
Console.print("=== View (HTML Structure) ===")
|
||||||
Console.print(showHtml(view(m2), 0))
|
Console.print(showHtml(view(m2), 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = run main() with {}
|
let output = run main() with {}
|
||||||
|
|||||||
@@ -1,57 +1,37 @@
|
|||||||
// Demonstrating algebraic data types and pattern matching
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// Tree sum: 8
|
|
||||||
// Tree depth: 3
|
|
||||||
// Safe divide 10/2: Result: 5
|
|
||||||
// Safe divide 10/0: Division by zero!
|
|
||||||
|
|
||||||
// Define a binary tree
|
|
||||||
type Tree =
|
type Tree =
|
||||||
| Leaf(Int)
|
| Leaf(Int)
|
||||||
| Node(Tree, Tree)
|
| Node(Tree, Tree)
|
||||||
|
|
||||||
// Sum all values in a tree
|
|
||||||
fn sumTree(tree: Tree): Int =
|
fn sumTree(tree: Tree): Int =
|
||||||
match tree {
|
match tree {
|
||||||
Leaf(n) => n,
|
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 =
|
fn depth(tree: Tree): Int =
|
||||||
match tree {
|
match tree {
|
||||||
Leaf(_) => 1,
|
Leaf(_) => 1,
|
||||||
Node(left, right) => {
|
Node(left, right) => {
|
||||||
let leftDepth = depth(left)
|
let leftDepth = depth(left)
|
||||||
let rightDepth = depth(right)
|
let rightDepth = depth(right)
|
||||||
1 + (if leftDepth > rightDepth then leftDepth else rightDepth)
|
1 + if leftDepth > rightDepth then leftDepth else rightDepth
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example tree:
|
|
||||||
// Node
|
|
||||||
// / \
|
|
||||||
// Node Leaf(5)
|
|
||||||
// / \
|
|
||||||
// Leaf(1) Leaf(2)
|
|
||||||
|
|
||||||
let myTree = Node(Node(Leaf(1), Leaf(2)), Leaf(5))
|
let myTree = Node(Node(Leaf(1), Leaf(2)), Leaf(5))
|
||||||
|
|
||||||
let treeSum = sumTree(myTree)
|
let treeSum = sumTree(myTree)
|
||||||
|
|
||||||
let treeDepth = depth(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 =
|
fn showResult(result: Option<Int>): String =
|
||||||
match result {
|
match result {
|
||||||
None => "Division by zero!",
|
None => "Division by zero!",
|
||||||
Some(n) => "Result: " + toString(n)
|
Some(n) => "Result: " + toString(n),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print results
|
|
||||||
fn printResults(): Unit with {Console} = {
|
fn printResults(): Unit with {Console} = {
|
||||||
Console.print("Tree sum: " + toString(treeSum))
|
Console.print("Tree sum: " + toString(treeSum))
|
||||||
Console.print("Tree depth: " + toString(treeDepth))
|
Console.print("Tree depth: " + toString(treeDepth))
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
// Demonstrating algebraic effects in Lux
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// [info] Processing data...
|
|
||||||
// [debug] Result computed
|
|
||||||
// Final result: 42
|
|
||||||
|
|
||||||
// Define a custom logging effect
|
|
||||||
effect Logger {
|
effect Logger {
|
||||||
fn log(level: String, msg: String): Unit
|
fn log(level: String, msg: String): Unit
|
||||||
fn getLevel(): String
|
fn getLevel(): String
|
||||||
}
|
}
|
||||||
|
|
||||||
// A function that uses the Logger effect
|
|
||||||
fn processData(data: Int): Int with {Logger} = {
|
fn processData(data: Int): Int with {Logger} = {
|
||||||
Logger.log("info", "Processing data...")
|
Logger.log("info", "Processing data...")
|
||||||
let result = data * 2
|
let result = data * 2
|
||||||
@@ -19,17 +10,15 @@ fn processData(data: Int): Int with {Logger} = {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
// A handler that prints logs to console
|
|
||||||
handler consoleLogger: Logger {
|
handler consoleLogger: Logger {
|
||||||
fn log(level, msg) = Console.print("[" + level + "] " + msg)
|
fn log(level, msg) = Console.print("[" + level + "] " + msg)
|
||||||
fn getLevel() = "debug"
|
fn getLevel() = "debug"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run and print
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
let result = run processData(21) with {
|
let result = run processData(21) with {
|
||||||
Logger = consoleLogger
|
Logger = consoleLogger,
|
||||||
}
|
}
|
||||||
Console.print("Final result: " + toString(result))
|
Console.print("Final result: " + toString(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
// Factorial function demonstrating recursion
|
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
|
||||||
//
|
|
||||||
// Expected output: 10! = 3628800
|
|
||||||
|
|
||||||
fn factorial(n: Int): Int =
|
|
||||||
if n <= 1 then 1
|
|
||||||
else n * factorial(n - 1)
|
|
||||||
|
|
||||||
// Calculate factorial of 10
|
|
||||||
let result = factorial(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 {}
|
let output = run showResult() with {}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
// File I/O example - demonstrates the File effect
|
|
||||||
//
|
|
||||||
// This script reads a file, counts lines/words, and writes a report
|
|
||||||
|
|
||||||
fn countLines(content: String): Int = {
|
fn countLines(content: String): Int = {
|
||||||
let lines = String.split(content, "\n")
|
let lines = String.split(content, "
|
||||||
|
")
|
||||||
List.length(lines)
|
List.length(lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,35 +11,28 @@ fn countWords(content: String): Int = {
|
|||||||
|
|
||||||
fn analyzeFile(path: String): Unit with {File, Console} = {
|
fn analyzeFile(path: String): Unit with {File, Console} = {
|
||||||
Console.print("Analyzing file: " + path)
|
Console.print("Analyzing file: " + path)
|
||||||
|
|
||||||
if File.exists(path) then {
|
if File.exists(path) then {
|
||||||
let content = File.read(path)
|
let content = File.read(path)
|
||||||
let lines = countLines(content)
|
let lines = countLines(content)
|
||||||
let words = countWords(content)
|
let words = countWords(content)
|
||||||
let chars = String.length(content)
|
let chars = String.length(content)
|
||||||
|
Console.print(" Lines: " + toString(lines))
|
||||||
Console.print(" Lines: " + toString(lines))
|
Console.print(" Words: " + toString(words))
|
||||||
Console.print(" Words: " + toString(words))
|
Console.print(" Chars: " + toString(chars))
|
||||||
Console.print(" Chars: " + toString(chars))
|
} else {
|
||||||
} else {
|
Console.print(" Error: File not found!")
|
||||||
Console.print(" Error: File not found!")
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main(): Unit with {File, Console} = {
|
fn main(): Unit with {File, Console} = {
|
||||||
Console.print("=== Lux File Analyzer ===")
|
Console.print("=== Lux File Analyzer ===")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Analyze this file itself
|
|
||||||
analyzeFile("examples/file_io.lux")
|
analyzeFile("examples/file_io.lux")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Analyze hello.lux
|
|
||||||
analyzeFile("examples/hello.lux")
|
analyzeFile("examples/hello.lux")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
let report = "File analysis complete.
|
||||||
// Write a report
|
Analyzed 2 files."
|
||||||
let report = "File analysis complete.\nAnalyzed 2 files."
|
|
||||||
File.write("/tmp/lux_report.txt", report)
|
File.write("/tmp/lux_report.txt", report)
|
||||||
Console.print("Report written to /tmp/lux_report.txt")
|
Console.print("Report written to /tmp/lux_report.txt")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,39 @@
|
|||||||
// Demonstrating functional programming features
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// apply(double, 21) = 42
|
|
||||||
// compose(addOne, double)(5) = 11
|
|
||||||
// pipe: 5 |> double |> addOne |> square = 121
|
|
||||||
// curried add5(10) = 15
|
|
||||||
// partial times3(7) = 21
|
|
||||||
// record transform = 30
|
|
||||||
|
|
||||||
// Higher-order functions
|
|
||||||
fn apply(f: fn(Int): Int, x: Int): Int = f(x)
|
fn apply(f: fn(Int): Int, x: Int): Int = f(x)
|
||||||
|
|
||||||
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int =
|
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int = fn(x: Int): Int => f(g(x))
|
||||||
fn(x: Int): Int => f(g(x))
|
|
||||||
|
|
||||||
// Basic functions
|
|
||||||
fn double(x: Int): Int = x * 2
|
fn double(x: Int): Int = x * 2
|
||||||
|
|
||||||
fn addOne(x: Int): Int = x + 1
|
fn addOne(x: Int): Int = x + 1
|
||||||
|
|
||||||
fn square(x: Int): Int = x * x
|
fn square(x: Int): Int = x * x
|
||||||
|
|
||||||
// Using apply
|
|
||||||
let result1 = apply(double, 21)
|
let result1 = apply(double, 21)
|
||||||
|
|
||||||
// Using compose
|
|
||||||
let doubleAndAddOne = compose(addOne, double)
|
let doubleAndAddOne = compose(addOne, double)
|
||||||
|
|
||||||
let result2 = doubleAndAddOne(5)
|
let result2 = doubleAndAddOne(5)
|
||||||
|
|
||||||
// Using the pipe operator
|
let result3 = square(addOne(double(5)))
|
||||||
let result3 = 5 |> double |> addOne |> square
|
|
||||||
|
|
||||||
// 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 add5 = add(5)
|
||||||
|
|
||||||
let result4 = add5(10)
|
let result4 = add5(10)
|
||||||
|
|
||||||
// Partial application simulation
|
|
||||||
fn multiply(a: Int, b: Int): Int = a * b
|
fn multiply(a: Int, b: Int): Int = a * b
|
||||||
|
|
||||||
let times3 = fn(x: Int): Int => multiply(3, x)
|
let times3 = fn(x: Int): Int => multiply(3, x)
|
||||||
|
|
||||||
let result5 = times3(7)
|
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 point = { x: 10, y: 20 }
|
||||||
|
|
||||||
let recordSum = transform(point)
|
let recordSum = transform(point)
|
||||||
|
|
||||||
// Print all results
|
|
||||||
fn printResults(): Unit with {Console} = {
|
fn printResults(): Unit with {Console} = {
|
||||||
Console.print("apply(double, 21) = " + toString(result1))
|
Console.print("apply(double, 21) = " + toString(result1))
|
||||||
Console.print("compose(addOne, double)(5) = " + toString(result2))
|
Console.print("compose(addOne, double)(5) = " + toString(result2))
|
||||||
|
|||||||
@@ -1,54 +1,43 @@
|
|||||||
// Demonstrating generic type parameters in Lux
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// identity(42) = 42
|
|
||||||
// identity("hello") = hello
|
|
||||||
// first(MkPair(1, "one")) = 1
|
|
||||||
// second(MkPair(1, "one")) = one
|
|
||||||
// map(Some(21), double) = Some(42)
|
|
||||||
|
|
||||||
// Generic identity function
|
|
||||||
fn identity<T>(x: T): T = x
|
fn identity<T>(x: T): T = x
|
||||||
|
|
||||||
// Generic pair type
|
|
||||||
type Pair<A, B> =
|
type Pair<A, B> =
|
||||||
| MkPair(A, B)
|
| MkPair(A, B)
|
||||||
|
|
||||||
fn first<A, B>(p: Pair<A, B>): A =
|
fn first<A, B>(p: Pair<A, B>): A =
|
||||||
match p {
|
match p {
|
||||||
MkPair(a, _) => a
|
MkPair(a, _) => a,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn second<A, B>(p: Pair<A, B>): B =
|
fn second<A, B>(p: Pair<A, B>): B =
|
||||||
match p {
|
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> =
|
fn mapOption<T, U>(opt: Option<T>, f: fn(T): U): Option<U> =
|
||||||
match opt {
|
match opt {
|
||||||
None => None,
|
None => None,
|
||||||
Some(x) => Some(f(x))
|
Some(x) => Some(f(x)),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function for testing
|
|
||||||
fn double(x: Int): Int = x * 2
|
fn double(x: Int): Int = x * 2
|
||||||
|
|
||||||
// Test usage
|
|
||||||
let id_int = identity(42)
|
let id_int = identity(42)
|
||||||
|
|
||||||
let id_str = identity("hello")
|
let id_str = identity("hello")
|
||||||
|
|
||||||
let pair = MkPair(1, "one")
|
let pair = MkPair(1, "one")
|
||||||
|
|
||||||
let fst = first(pair)
|
let fst = first(pair)
|
||||||
|
|
||||||
let snd = second(pair)
|
let snd = second(pair)
|
||||||
|
|
||||||
let doubled = mapOption(Some(21), double)
|
let doubled = mapOption(Some(21), double)
|
||||||
|
|
||||||
fn showOption(opt: Option<Int>): String =
|
fn showOption(opt: Option<Int>): String =
|
||||||
match opt {
|
match opt {
|
||||||
None => "None",
|
None => "None",
|
||||||
Some(x) => "Some(" + toString(x) + ")"
|
Some(x) => "Some(" + toString(x) + ")",
|
||||||
}
|
}
|
||||||
|
|
||||||
fn printResults(): Unit with {Console} = {
|
fn printResults(): Unit with {Console} = {
|
||||||
Console.print("identity(42) = " + toString(id_int))
|
Console.print("identity(42) = " + toString(id_int))
|
||||||
|
|||||||
@@ -1,21 +1,8 @@
|
|||||||
// Demonstrating resumable effect handlers in Lux
|
|
||||||
//
|
|
||||||
// Handlers can use `resume(value)` to return a value to the effect call site
|
|
||||||
// and continue the computation. This enables powerful control flow patterns.
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// [INFO] Starting computation
|
|
||||||
// [DEBUG] Intermediate result: 10
|
|
||||||
// [INFO] Computation complete
|
|
||||||
// Final result: 20
|
|
||||||
|
|
||||||
// Define a custom logging effect
|
|
||||||
effect Logger {
|
effect Logger {
|
||||||
fn log(level: String, msg: String): Unit
|
fn log(level: String, msg: String): Unit
|
||||||
fn getLogLevel(): String
|
fn getLogLevel(): String
|
||||||
}
|
}
|
||||||
|
|
||||||
// A function that uses the Logger effect
|
|
||||||
fn compute(): Int with {Logger} = {
|
fn compute(): Int with {Logger} = {
|
||||||
Logger.log("INFO", "Starting computation")
|
Logger.log("INFO", "Starting computation")
|
||||||
let x = 10
|
let x = 10
|
||||||
@@ -25,20 +12,19 @@ fn compute(): Int with {Logger} = {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
// A handler that prints logs with brackets and resumes with Unit
|
|
||||||
handler prettyLogger: Logger {
|
handler prettyLogger: Logger {
|
||||||
fn log(level, msg) = {
|
fn log(level, msg) =
|
||||||
Console.print("[" + level + "] " + msg)
|
{
|
||||||
resume(())
|
Console.print("[" + level + "] " + msg)
|
||||||
}
|
resume(())
|
||||||
|
}
|
||||||
fn getLogLevel() = resume("DEBUG")
|
fn getLogLevel() = resume("DEBUG")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main function
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
let result = run compute() with {
|
let result = run compute() with {
|
||||||
Logger = prettyLogger
|
Logger = prettyLogger,
|
||||||
}
|
}
|
||||||
Console.print("Final result: " + toString(result))
|
Console.print("Final result: " + toString(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
// Hello World in Lux
|
fn greet(): Unit with {Console} = Console.print("Hello, World!")
|
||||||
// Demonstrates basic effect usage
|
|
||||||
//
|
|
||||||
// Expected output: Hello, World!
|
|
||||||
|
|
||||||
fn greet(): Unit with {Console} =
|
|
||||||
Console.print("Hello, World!")
|
|
||||||
|
|
||||||
// Run the greeting with the Console effect
|
|
||||||
let output = run greet() with {}
|
let output = run greet() with {}
|
||||||
|
|||||||
@@ -1,91 +1,72 @@
|
|||||||
// HTTP example - demonstrates the Http effect
|
|
||||||
//
|
|
||||||
// This script makes HTTP requests and parses JSON responses
|
|
||||||
|
|
||||||
fn main(): Unit with {Console, Http} = {
|
fn main(): Unit with {Console, Http} = {
|
||||||
Console.print("=== Lux HTTP Example ===")
|
Console.print("=== Lux HTTP Example ===")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Make a GET request to a public API
|
|
||||||
Console.print("Fetching data from httpbin.org...")
|
Console.print("Fetching data from httpbin.org...")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
match Http.get("https://httpbin.org/get") {
|
match Http.get("https://httpbin.org/get") {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
Console.print("GET request successful!")
|
Console.print("GET request successful!")
|
||||||
Console.print(" Status: " + toString(response.status))
|
Console.print(" Status: " + toString(response.status))
|
||||||
Console.print(" Body length: " + toString(String.length(response.body)) + " bytes")
|
Console.print(" Body length: " + toString(String.length(response.body)) + " bytes")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
match Json.parse(response.body) {
|
||||||
// Parse the JSON response
|
Ok(json) => {
|
||||||
match Json.parse(response.body) {
|
Console.print("Parsed JSON response:")
|
||||||
Ok(json) => {
|
match Json.get(json, "origin") {
|
||||||
Console.print("Parsed JSON response:")
|
Some(origin) => match Json.asString(origin) {
|
||||||
match Json.get(json, "origin") {
|
Some(ip) => Console.print(" Your IP: " + ip),
|
||||||
Some(origin) => match Json.asString(origin) {
|
None => Console.print(" origin: (not a string)"),
|
||||||
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 found)")
|
match Json.get(json, "url") {
|
||||||
}
|
Some(url) => match Json.asString(url) {
|
||||||
match Json.get(json, "url") {
|
Some(u) => Console.print(" URL: " + u),
|
||||||
Some(url) => match Json.asString(url) {
|
None => Console.print(" url: (not a string)"),
|
||||||
Some(u) => Console.print(" URL: " + u),
|
},
|
||||||
None => Console.print(" url: (not a string)")
|
None => Console.print(" url: (not found)"),
|
||||||
},
|
}
|
||||||
None => Console.print(" url: (not found)")
|
},
|
||||||
}
|
Err(e) => Console.print("JSON parse error: " + e),
|
||||||
},
|
}
|
||||||
Err(e) => Console.print("JSON parse error: " + e)
|
},
|
||||||
}
|
Err(e) => Console.print("GET request failed: " + e),
|
||||||
},
|
}
|
||||||
Err(e) => Console.print("GET request failed: " + e)
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.print("")
|
Console.print("")
|
||||||
Console.print("--- POST Request ---")
|
Console.print("--- POST Request ---")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Make a POST request with JSON body
|
|
||||||
let requestBody = Json.object([("message", Json.string("Hello from Lux!")), ("version", Json.int(1))])
|
let requestBody = Json.object([("message", Json.string("Hello from Lux!")), ("version", Json.int(1))])
|
||||||
Console.print("Sending POST with JSON body...")
|
Console.print("Sending POST with JSON body...")
|
||||||
Console.print(" Body: " + Json.stringify(requestBody))
|
Console.print(" Body: " + Json.stringify(requestBody))
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
match Http.postJson("https://httpbin.org/post", requestBody) {
|
match Http.postJson("https://httpbin.org/post", requestBody) {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
Console.print("POST request successful!")
|
Console.print("POST request successful!")
|
||||||
Console.print(" Status: " + toString(response.status))
|
Console.print(" Status: " + toString(response.status))
|
||||||
|
match Json.parse(response.body) {
|
||||||
// Parse and extract what we sent
|
Ok(json) => match Json.get(json, "json") {
|
||||||
match Json.parse(response.body) {
|
Some(sentJson) => {
|
||||||
Ok(json) => match Json.get(json, "json") {
|
Console.print(" Server received:")
|
||||||
Some(sentJson) => {
|
Console.print(" " + Json.stringify(sentJson))
|
||||||
Console.print(" Server received:")
|
},
|
||||||
Console.print(" " + Json.stringify(sentJson))
|
None => Console.print(" (no json field in response)"),
|
||||||
},
|
},
|
||||||
None => Console.print(" (no json field in response)")
|
Err(e) => Console.print("JSON parse error: " + e),
|
||||||
},
|
}
|
||||||
Err(e) => Console.print("JSON parse error: " + e)
|
},
|
||||||
}
|
Err(e) => Console.print("POST request failed: " + e),
|
||||||
},
|
}
|
||||||
Err(e) => Console.print("POST request failed: " + e)
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.print("")
|
Console.print("")
|
||||||
Console.print("--- Headers ---")
|
Console.print("--- Headers ---")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Show response headers
|
|
||||||
match Http.get("https://httpbin.org/headers") {
|
match Http.get("https://httpbin.org/headers") {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
Console.print("Response headers (first 5):")
|
Console.print("Response headers (first 5):")
|
||||||
let count = 0
|
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)))
|
||||||
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 {}
|
let result = run main() with {}
|
||||||
|
|||||||
@@ -1,85 +1,48 @@
|
|||||||
// HTTP API Example
|
fn httpOk(body: String): { status: Int, body: String } = { status: 200, body: body }
|
||||||
//
|
|
||||||
// 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 httpCreated(body: String): { status: Int, body: String } = { status: 201, body: body }
|
||||||
// Response Helpers
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
fn httpOk(body: String): { status: Int, body: String } =
|
fn httpNotFound(body: String): { status: Int, body: String } = { status: 404, body: body }
|
||||||
{ status: 200, body: body }
|
|
||||||
|
|
||||||
fn httpCreated(body: String): { status: Int, body: String } =
|
fn httpBadRequest(body: String): { status: Int, body: String } = { status: 400, body: body }
|
||||||
{ status: 201, body: body }
|
|
||||||
|
|
||||||
fn httpNotFound(body: String): { status: Int, body: String } =
|
fn jsonEscape(s: String): String = String.replace(String.replace(s, "\\", "\\\\"), "\"", "\\\"")
|
||||||
{ status: 404, body: body }
|
|
||||||
|
|
||||||
fn httpBadRequest(body: String): { status: Int, body: String } =
|
fn jsonStr(key: String, value: String): String = "\"" + jsonEscape(key) + "\":\"" + jsonEscape(value) + "\""
|
||||||
{ status: 400, body: body }
|
|
||||||
|
|
||||||
// ============================================================
|
fn jsonNum(key: String, value: Int): String = "\"" + jsonEscape(key) + "\":" + toString(value)
|
||||||
// JSON Helpers
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
fn jsonEscape(s: String): String =
|
fn jsonObj(content: String): String = toString(" + content + ")
|
||||||
String.replace(String.replace(s, "\\", "\\\\"), "\"", "\\\"")
|
|
||||||
|
|
||||||
fn jsonStr(key: String, value: String): String =
|
fn jsonArr(content: String): String = "[" + content + "]"
|
||||||
"\"" + jsonEscape(key) + "\":\"" + jsonEscape(value) + "\""
|
|
||||||
|
|
||||||
fn jsonNum(key: String, value: Int): String =
|
fn jsonError(message: String): String = jsonObj(jsonStr("error", message))
|
||||||
"\"" + 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 pathMatches(path: String, pattern: String): Bool = {
|
fn pathMatches(path: String, pattern: String): Bool = {
|
||||||
let pathParts = String.split(path, "/")
|
let pathParts = String.split(path, "/")
|
||||||
let patternParts = String.split(pattern, "/")
|
let patternParts = String.split(pattern, "/")
|
||||||
if List.length(pathParts) != List.length(patternParts) then false
|
if List.length(pathParts) != List.length(patternParts) then false else matchParts(pathParts, patternParts)
|
||||||
else matchParts(pathParts, patternParts)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn matchParts(pathParts: List<String>, patternParts: List<String>): Bool = {
|
fn matchParts(pathParts: List<String>, patternParts: List<String>): Bool = {
|
||||||
if List.length(pathParts) == 0 then true
|
if List.length(pathParts) == 0 then true else {
|
||||||
else {
|
match List.head(pathParts) {
|
||||||
match List.head(pathParts) {
|
None => true,
|
||||||
None => true,
|
Some(pathPart) => {
|
||||||
Some(pathPart) => {
|
match List.head(patternParts) {
|
||||||
match List.head(patternParts) {
|
None => true,
|
||||||
None => true,
|
Some(patternPart) => {
|
||||||
Some(patternPart) => {
|
let isMatch = if String.startsWith(patternPart, ":") then true else pathPart == patternPart
|
||||||
let isMatch = if String.startsWith(patternPart, ":") then true else pathPart == patternPart
|
if isMatch then {
|
||||||
if isMatch then {
|
let restPath = Option.getOrElse(List.tail(pathParts), [])
|
||||||
let restPath = Option.getOrElse(List.tail(pathParts), [])
|
let restPattern = Option.getOrElse(List.tail(patternParts), [])
|
||||||
let restPattern = Option.getOrElse(List.tail(patternParts), [])
|
matchParts(restPath, restPattern)
|
||||||
matchParts(restPath, restPattern)
|
} else false
|
||||||
} else false
|
},
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getPathSegment(path: String, index: Int): Option<String> = {
|
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)
|
List.get(parts, index + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
fn indexHandler(): { status: Int, body: String } = httpOk(jsonObj(jsonStr("message", "Welcome to Lux HTTP API")))
|
||||||
// Handlers
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
fn indexHandler(): { status: Int, body: String } =
|
fn healthHandler(): { status: Int, body: String } = httpOk(jsonObj(jsonStr("status", "healthy")))
|
||||||
httpOk(jsonObj(jsonStr("message", "Welcome to Lux HTTP API")))
|
|
||||||
|
|
||||||
fn healthHandler(): { status: Int, body: String } =
|
|
||||||
httpOk(jsonObj(jsonStr("status", "healthy")))
|
|
||||||
|
|
||||||
fn listUsersHandler(): { status: Int, body: String } = {
|
fn listUsersHandler(): { status: Int, body: String } = {
|
||||||
let user1 = jsonObj(jsonNum("id", 1) + "," + jsonStr("name", "Alice"))
|
let user1 = jsonObj(jsonNum("id", 1) + "," + jsonStr("name", "Alice"))
|
||||||
@@ -105,12 +62,12 @@ fn listUsersHandler(): { status: Int, body: String } = {
|
|||||||
|
|
||||||
fn getUserHandler(path: String): { status: Int, body: String } = {
|
fn getUserHandler(path: String): { status: Int, body: String } = {
|
||||||
match getPathSegment(path, 1) {
|
match getPathSegment(path, 1) {
|
||||||
Some(id) => {
|
Some(id) => {
|
||||||
let body = jsonObj(jsonStr("id", id) + "," + jsonStr("name", "User " + id))
|
let body = jsonObj(jsonStr("id", id) + "," + jsonStr("name", "User " + id))
|
||||||
httpOk(body)
|
httpOk(body)
|
||||||
},
|
},
|
||||||
None => httpNotFound(jsonError("User not found"))
|
None => httpNotFound(jsonError("User not found")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn createUserHandler(body: String): { status: Int, body: String } = {
|
fn createUserHandler(body: String): { status: Int, body: String } = {
|
||||||
@@ -118,34 +75,21 @@ fn createUserHandler(body: String): { status: Int, body: String } = {
|
|||||||
httpCreated(newUser)
|
httpCreated(newUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Router
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
fn router(method: String, path: String, body: String): { status: Int, body: String } = {
|
fn router(method: String, path: String, body: String): { status: Int, body: String } = {
|
||||||
if method == "GET" && path == "/" then indexHandler()
|
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))
|
||||||
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} = {
|
fn serveLoop(remaining: Int): Unit with {Console, HttpServer} = {
|
||||||
if remaining <= 0 then {
|
if remaining <= 0 then {
|
||||||
Console.print("Max requests reached, stopping server.")
|
Console.print("Max requests reached, stopping server.")
|
||||||
HttpServer.stop()
|
HttpServer.stop()
|
||||||
} else {
|
} else {
|
||||||
let req = HttpServer.accept()
|
let req = HttpServer.accept()
|
||||||
Console.print(req.method + " " + req.path)
|
Console.print(req.method + " " + req.path)
|
||||||
let resp = router(req.method, req.path, req.body)
|
let resp = router(req.method, req.path, req.body)
|
||||||
HttpServer.respond(resp.status, resp.body)
|
HttpServer.respond(resp.status, resp.body)
|
||||||
serveLoop(remaining - 1)
|
serveLoop(remaining - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main(): Unit with {Console, HttpServer} = {
|
fn main(): Unit with {Console, HttpServer} = {
|
||||||
|
|||||||
@@ -1,24 +1,4 @@
|
|||||||
// HTTP Router Example
|
fn indexHandler(): { status: Int, body: String } = httpOk("Welcome to Lux HTTP Framework!")
|
||||||
//
|
|
||||||
// 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 listUsersHandler(): { status: Int, body: String } = {
|
fn listUsersHandler(): { status: Int, body: String } = {
|
||||||
let user1 = jsonObject(jsonJoin([jsonNumber("id", 1), jsonString("name", "Alice")]))
|
let user1 = jsonObject(jsonJoin([jsonNumber("id", 1), jsonString("name", "Alice")]))
|
||||||
@@ -29,44 +9,31 @@ fn listUsersHandler(): { status: Int, body: String } = {
|
|||||||
|
|
||||||
fn getUserHandler(path: String): { status: Int, body: String } = {
|
fn getUserHandler(path: String): { status: Int, body: String } = {
|
||||||
match getPathSegment(path, 1) {
|
match getPathSegment(path, 1) {
|
||||||
Some(id) => {
|
Some(id) => {
|
||||||
let body = jsonObject(jsonJoin([jsonString("id", id), jsonString("name", "User " + id)]))
|
let body = jsonObject(jsonJoin([jsonString("id", id), jsonString("name", "User " + id)]))
|
||||||
httpOk(body)
|
httpOk(body)
|
||||||
},
|
},
|
||||||
None => httpNotFound(jsonErrorMsg("User ID required"))
|
None => httpNotFound(jsonErrorMsg("User ID required")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn healthHandler(): { status: Int, body: String } =
|
fn healthHandler(): { status: Int, body: String } = httpOk(jsonObject(jsonString("status", "healthy")))
|
||||||
httpOk(jsonObject(jsonString("status", "healthy")))
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Router
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
fn router(method: String, path: String, body: String): { status: Int, body: String } = {
|
fn router(method: String, path: String, body: String): { status: Int, body: String } = {
|
||||||
if method == "GET" && path == "/" then indexHandler()
|
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))
|
||||||
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} = {
|
fn serveLoop(remaining: Int): Unit with {Console, HttpServer} = {
|
||||||
if remaining <= 0 then {
|
if remaining <= 0 then {
|
||||||
Console.print("Max requests reached, stopping server.")
|
Console.print("Max requests reached, stopping server.")
|
||||||
HttpServer.stop()
|
HttpServer.stop()
|
||||||
} else {
|
} else {
|
||||||
let req = HttpServer.accept()
|
let req = HttpServer.accept()
|
||||||
Console.print(req.method + " " + req.path)
|
Console.print(req.method + " " + req.path)
|
||||||
let resp = router(req.method, req.path, req.body)
|
let resp = router(req.method, req.path, req.body)
|
||||||
HttpServer.respond(resp.status, resp.body)
|
HttpServer.respond(resp.status, resp.body)
|
||||||
serveLoop(remaining - 1)
|
serveLoop(remaining - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main(): Unit with {Console, HttpServer} = {
|
fn main(): Unit with {Console, HttpServer} = {
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
// Test file for JIT compilation
|
fn fib(n: Int): Int = if n <= 1 then n else fib(n - 1) + fib(n - 2)
|
||||||
// This uses only features the JIT supports: integers, arithmetic, conditionals, functions
|
|
||||||
|
|
||||||
fn fib(n: Int): Int =
|
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
|
||||||
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 main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
let fibResult = fib(30)
|
let fibResult = fib(30)
|
||||||
|
|||||||
@@ -1,107 +1,79 @@
|
|||||||
// JSON example - demonstrates JSON parsing and manipulation
|
|
||||||
//
|
|
||||||
// This script parses JSON, extracts values, and builds new JSON structures
|
|
||||||
|
|
||||||
fn main(): Unit with {Console, File} = {
|
fn main(): Unit with {Console, File} = {
|
||||||
Console.print("=== Lux JSON Example ===")
|
Console.print("=== Lux JSON Example ===")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// First, build some JSON programmatically
|
|
||||||
Console.print("=== Building JSON ===")
|
Console.print("=== Building JSON ===")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
let name = Json.string("Alice")
|
let name = Json.string("Alice")
|
||||||
let age = Json.int(30)
|
let age = Json.int(30)
|
||||||
let active = Json.bool(true)
|
let active = Json.bool(true)
|
||||||
let scores = Json.array([Json.int(95), Json.int(87), Json.int(92)])
|
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)])
|
let person = Json.object([("name", name), ("age", age), ("active", active), ("scores", scores)])
|
||||||
|
|
||||||
Console.print("Built JSON:")
|
Console.print("Built JSON:")
|
||||||
let pretty = Json.prettyPrint(person)
|
let pretty = Json.prettyPrint(person)
|
||||||
Console.print(pretty)
|
Console.print(pretty)
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Stringify to a compact string
|
|
||||||
let jsonStr = Json.stringify(person)
|
let jsonStr = Json.stringify(person)
|
||||||
Console.print("Compact: " + jsonStr)
|
Console.print("Compact: " + jsonStr)
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Write to file and read back to test parsing
|
|
||||||
File.write("/tmp/test.json", jsonStr)
|
File.write("/tmp/test.json", jsonStr)
|
||||||
Console.print("Written to /tmp/test.json")
|
Console.print("Written to /tmp/test.json")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Read and parse from file
|
|
||||||
Console.print("=== Parsing JSON ===")
|
Console.print("=== Parsing JSON ===")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
let content = File.read("/tmp/test.json")
|
let content = File.read("/tmp/test.json")
|
||||||
Console.print("Read from file: " + content)
|
Console.print("Read from file: " + content)
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
match Json.parse(content) {
|
match Json.parse(content) {
|
||||||
Ok(json) => {
|
Ok(json) => {
|
||||||
Console.print("Parse succeeded!")
|
Console.print("Parse succeeded!")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
Console.print("Extracting fields:")
|
||||||
// Get string field
|
match Json.get(json, "name") {
|
||||||
Console.print("Extracting fields:")
|
Some(nameJson) => match Json.asString(nameJson) {
|
||||||
match Json.get(json, "name") {
|
Some(n) => Console.print(" name: " + n),
|
||||||
Some(nameJson) => match Json.asString(nameJson) {
|
None => Console.print(" name: (not a string)"),
|
||||||
Some(n) => Console.print(" name: " + n),
|
},
|
||||||
None => Console.print(" name: (not a string)")
|
None => Console.print(" name: (not found)"),
|
||||||
},
|
}
|
||||||
None => Console.print(" name: (not found)")
|
match Json.get(json, "age") {
|
||||||
}
|
Some(ageJson) => match Json.asInt(ageJson) {
|
||||||
|
Some(a) => Console.print(" age: " + toString(a)),
|
||||||
// Get int field
|
None => Console.print(" age: (not an int)"),
|
||||||
match Json.get(json, "age") {
|
},
|
||||||
Some(ageJson) => match Json.asInt(ageJson) {
|
None => Console.print(" age: (not found)"),
|
||||||
Some(a) => Console.print(" age: " + toString(a)),
|
}
|
||||||
None => Console.print(" age: (not an int)")
|
match Json.get(json, "active") {
|
||||||
},
|
Some(activeJson) => match Json.asBool(activeJson) {
|
||||||
None => Console.print(" age: (not found)")
|
Some(a) => Console.print(" active: " + toString(a)),
|
||||||
}
|
None => Console.print(" active: (not a bool)"),
|
||||||
|
},
|
||||||
// Get bool field
|
None => Console.print(" active: (not found)"),
|
||||||
match Json.get(json, "active") {
|
}
|
||||||
Some(activeJson) => match Json.asBool(activeJson) {
|
match Json.get(json, "scores") {
|
||||||
Some(a) => Console.print(" active: " + toString(a)),
|
Some(scoresJson) => match Json.asArray(scoresJson) {
|
||||||
None => Console.print(" active: (not a bool)")
|
Some(arr) => {
|
||||||
},
|
Console.print(" scores: " + toString(List.length(arr)) + " items")
|
||||||
None => Console.print(" active: (not found)")
|
match Json.getIndex(scoresJson, 0) {
|
||||||
}
|
Some(firstJson) => match Json.asInt(firstJson) {
|
||||||
|
Some(first) => Console.print(" first score: " + toString(first)),
|
||||||
// Get array field
|
None => Console.print(" first score: (not an int)"),
|
||||||
match Json.get(json, "scores") {
|
},
|
||||||
Some(scoresJson) => match Json.asArray(scoresJson) {
|
None => Console.print(" (no first element)"),
|
||||||
Some(arr) => {
|
}
|
||||||
Console.print(" scores: " + toString(List.length(arr)) + " items")
|
},
|
||||||
// Get first score
|
None => Console.print(" scores: (not an array)"),
|
||||||
match Json.getIndex(scoresJson, 0) {
|
},
|
||||||
Some(firstJson) => match Json.asInt(firstJson) {
|
None => Console.print(" scores: (not found)"),
|
||||||
Some(first) => Console.print(" first score: " + toString(first)),
|
}
|
||||||
None => Console.print(" first score: (not an int)")
|
Console.print("")
|
||||||
},
|
Console.print("Object keys:")
|
||||||
None => Console.print(" (no first element)")
|
match Json.keys(json) {
|
||||||
}
|
Some(ks) => Console.print(" " + String.join(ks, ", ")),
|
||||||
},
|
None => Console.print(" (not an object)"),
|
||||||
None => Console.print(" scores: (not an array)")
|
}
|
||||||
},
|
},
|
||||||
None => Console.print(" scores: (not found)")
|
Err(e) => Console.print("Parse error: " + e),
|
||||||
}
|
}
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.print("")
|
Console.print("")
|
||||||
Console.print("=== JSON Null Check ===")
|
Console.print("=== JSON Null Check ===")
|
||||||
let nullVal = Json.null()
|
let nullVal = Json.null()
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
// Main program that imports modules
|
|
||||||
import examples/modules/math_utils
|
|
||||||
import examples/modules/string_utils
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
Console.print("=== Testing Module Imports ===")
|
Console.print("=== Testing Module Imports ===")
|
||||||
|
|
||||||
// Use math_utils
|
|
||||||
Console.print("square(5) = " + toString(math_utils.square(5)))
|
Console.print("square(5) = " + toString(math_utils.square(5)))
|
||||||
Console.print("cube(3) = " + toString(math_utils.cube(3)))
|
Console.print("cube(3) = " + toString(math_utils.cube(3)))
|
||||||
Console.print("factorial(6) = " + toString(math_utils.factorial(6)))
|
Console.print("factorial(6) = " + toString(math_utils.factorial(6)))
|
||||||
Console.print("sumRange(1, 10) = " + toString(math_utils.sumRange(1, 10)))
|
Console.print("sumRange(1, 10) = " + toString(math_utils.sumRange(1, 10)))
|
||||||
|
|
||||||
// Use string_utils
|
|
||||||
Console.print(string_utils.greet("World"))
|
Console.print(string_utils.greet("World"))
|
||||||
Console.print(string_utils.exclaim("Modules work"))
|
Console.print(string_utils.exclaim("Modules work"))
|
||||||
Console.print("repeat(\"ab\", 3) = " + string_utils.repeat("ab", 3))
|
Console.print("repeat(\"ab\", 3) = " + string_utils.repeat("ab", 3))
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
// Test selective imports
|
|
||||||
import examples/modules/math_utils.{square, factorial}
|
|
||||||
import examples/modules/string_utils as str
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
Console.print("=== Selective & Aliased Imports ===")
|
Console.print("=== Selective & Aliased Imports ===")
|
||||||
|
|
||||||
// Direct imports (no module prefix)
|
|
||||||
Console.print("square(7) = " + toString(square(7)))
|
Console.print("square(7) = " + toString(square(7)))
|
||||||
Console.print("factorial(5) = " + toString(factorial(5)))
|
Console.print("factorial(5) = " + toString(factorial(5)))
|
||||||
|
|
||||||
// Aliased import
|
|
||||||
Console.print(str.greet("Lux"))
|
Console.print(str.greet("Lux"))
|
||||||
Console.print(str.exclaim("Aliased imports work"))
|
Console.print(str.exclaim("Aliased imports work"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
// Test wildcard imports
|
|
||||||
import examples/modules/math_utils.*
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
Console.print("=== Wildcard Imports ===")
|
Console.print("=== Wildcard Imports ===")
|
||||||
|
|
||||||
// All functions available directly
|
|
||||||
Console.print("square(4) = " + toString(square(4)))
|
Console.print("square(4) = " + toString(square(4)))
|
||||||
Console.print("cube(4) = " + toString(cube(4)))
|
Console.print("cube(4) = " + toString(cube(4)))
|
||||||
Console.print("factorial(4) = " + toString(factorial(4)))
|
Console.print("factorial(4) = " + toString(factorial(4)))
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
// Math utilities module
|
fn square(n: Int): Int = n * n
|
||||||
// Exports: square, cube, factorial
|
|
||||||
|
|
||||||
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 =
|
fn sumRange(start: Int, end: Int): Int = if start > end then 0 else start + sumRange(start + 1, end)
|
||||||
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)
|
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
// String utilities module
|
fn repeat(s: String, n: Int): String = if n <= 0 then "" else s + repeat(s, n - 1)
|
||||||
// Exports: repeat, exclaim
|
|
||||||
|
|
||||||
pub fn repeat(s: String, n: Int): String =
|
fn exclaim(s: String): String = s + "!"
|
||||||
if n <= 0 then ""
|
|
||||||
else s + repeat(s, n - 1)
|
|
||||||
|
|
||||||
pub fn exclaim(s: String): String = s + "!"
|
fn greet(name: String): String = "Hello, " + name + "!"
|
||||||
|
|
||||||
pub fn greet(name: String): String =
|
|
||||||
"Hello, " + name + "!"
|
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
// Example using the standard library
|
|
||||||
import std/prelude.*
|
|
||||||
import std/option as opt
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
Console.print("=== Using Standard Library ===")
|
Console.print("=== Using Standard Library ===")
|
||||||
|
|
||||||
// Prelude functions
|
|
||||||
Console.print("identity(42) = " + toString(identity(42)))
|
Console.print("identity(42) = " + toString(identity(42)))
|
||||||
Console.print("not(true) = " + toString(not(true)))
|
Console.print("not(true) = " + toString(not(true)))
|
||||||
Console.print("and(true, false) = " + toString(and(true, false)))
|
Console.print("and(true, false) = " + toString(and(true, false)))
|
||||||
Console.print("or(true, false) = " + toString(or(true, false)))
|
Console.print("or(true, false) = " + toString(or(true, false)))
|
||||||
|
|
||||||
// Option utilities
|
|
||||||
let x = opt.some(10)
|
let x = opt.some(10)
|
||||||
let y = opt.none()
|
let y = opt.none()
|
||||||
Console.print("isSome(Some(10)) = " + toString(opt.isSome(x)))
|
Console.print("isSome(Some(10)) = " + toString(opt.isSome(x)))
|
||||||
|
|||||||
@@ -1,47 +1,31 @@
|
|||||||
// Demonstrating the pipe operator and functional data processing
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// 5 |> double |> addTen |> square = 400
|
|
||||||
// Pipeline result2 = 42
|
|
||||||
// process(1) = 144
|
|
||||||
// process(2) = 196
|
|
||||||
// process(3) = 256
|
|
||||||
// clamped = 0
|
|
||||||
// composed = 121
|
|
||||||
|
|
||||||
// Basic transformations
|
|
||||||
fn double(x: Int): Int = x * 2
|
fn double(x: Int): Int = x * 2
|
||||||
|
|
||||||
fn addTen(x: Int): Int = x + 10
|
fn addTen(x: Int): Int = x + 10
|
||||||
|
|
||||||
fn square(x: Int): Int = x * x
|
fn square(x: Int): Int = x * x
|
||||||
|
|
||||||
fn negate(x: Int): Int = -x
|
fn negate(x: Int): Int = -x
|
||||||
|
|
||||||
// Using the pipe operator for data transformation
|
let result1 = square(addTen(double(5)))
|
||||||
let result1 = 5 |> double |> addTen |> square
|
|
||||||
|
|
||||||
// Chaining multiple operations
|
let result2 = addTen(double(addTen(double(3))))
|
||||||
let result2 = 3 |> double |> addTen |> double |> addTen
|
|
||||||
|
|
||||||
// More complex pipelines
|
fn process(n: Int): Int = square(addTen(double(n)))
|
||||||
fn process(n: Int): Int =
|
|
||||||
n |> double |> addTen |> square
|
|
||||||
|
|
||||||
// Multiple values through same pipeline
|
|
||||||
let a = process(1)
|
let a = process(1)
|
||||||
|
|
||||||
let b = process(2)
|
let b = process(2)
|
||||||
|
|
||||||
let c = process(3)
|
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
|
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} = {
|
fn printResults(): Unit with {Console} = {
|
||||||
Console.print("5 |> double |> addTen |> square = " + toString(result1))
|
Console.print("5 |> double |> addTen |> square = " + toString(result1))
|
||||||
Console.print("Pipeline result2 = " + toString(result2))
|
Console.print("Pipeline result2 = " + toString(result2))
|
||||||
|
|||||||
@@ -1,72 +1,42 @@
|
|||||||
// PostgreSQL Database Example
|
fn jsonStr(key: String, value: String): String = "\"" + key + "\":\"" + value + "\""
|
||||||
//
|
|
||||||
// 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 jsonNum(key: String, value: Int): String = "\"" + key + "\":" + toString(value)
|
||||||
// Helper Functions
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
fn jsonStr(key: String, value: String): String =
|
fn jsonObj(content: String): String = toString(" + content + ")
|
||||||
"\"" + key + "\":\"" + value + "\""
|
|
||||||
|
|
||||||
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} = {
|
fn insertUser(connId: Int, name: String, email: String): Int with {Console, Postgres} = {
|
||||||
let sql = "INSERT INTO users (name, email) VALUES ('" + name + "', '" + email + "') RETURNING id"
|
let sql = "INSERT INTO users (name, email) VALUES ('" + name + "', '" + email + "') RETURNING id"
|
||||||
Console.print("Inserting user: " + name)
|
Console.print("Inserting user: " + name)
|
||||||
match Postgres.queryOne(connId, sql) {
|
match Postgres.queryOne(connId, sql) {
|
||||||
Some(row) => {
|
Some(row) => {
|
||||||
Console.print(" Inserted with ID: " + toString(row.id))
|
Console.print(" Inserted with ID: " + toString(row.id))
|
||||||
row.id
|
row.id
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
Console.print(" Insert failed")
|
Console.print(" Insert failed")
|
||||||
-1
|
-1
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all users
|
|
||||||
fn getUsers(connId: Int): Unit with {Console, Postgres} = {
|
fn getUsers(connId: Int): Unit with {Console, Postgres} = {
|
||||||
Console.print("Fetching all users...")
|
Console.print("Fetching all users...")
|
||||||
let rows = Postgres.query(connId, "SELECT id, name, email FROM users ORDER BY id")
|
let rows = Postgres.query(connId, "SELECT id, name, email FROM users ORDER BY id")
|
||||||
Console.print(" Found " + toString(List.length(rows)) + " users:")
|
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 + ">")
|
Console.print(" - " + toString(row.id) + ": " + row.name + " <" + row.email + ">")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user by ID
|
|
||||||
fn getUserById(connId: Int, id: Int): Unit with {Console, Postgres} = {
|
fn getUserById(connId: Int, id: Int): Unit with {Console, Postgres} = {
|
||||||
let sql = "SELECT id, name, email FROM users WHERE id = " + toString(id)
|
let sql = "SELECT id, name, email FROM users WHERE id = " + toString(id)
|
||||||
Console.print("Looking up user " + toString(id) + "...")
|
Console.print("Looking up user " + toString(id) + "...")
|
||||||
match Postgres.queryOne(connId, sql) {
|
match Postgres.queryOne(connId, sql) {
|
||||||
Some(row) => Console.print(" Found: " + row.name + " <" + row.email + ">"),
|
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} = {
|
fn updateUserEmail(connId: Int, id: Int, newEmail: String): Unit with {Console, Postgres} = {
|
||||||
let sql = "UPDATE users SET email = '" + newEmail + "' WHERE id = " + toString(id)
|
let sql = "UPDATE users SET email = '" + newEmail + "' WHERE id = " + toString(id)
|
||||||
Console.print("Updating user " + toString(id) + " email to " + newEmail)
|
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))
|
Console.print(" Rows affected: " + toString(affected))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete user
|
|
||||||
fn deleteUser(connId: Int, id: Int): Unit with {Console, Postgres} = {
|
fn deleteUser(connId: Int, id: Int): Unit with {Console, Postgres} = {
|
||||||
let sql = "DELETE FROM users WHERE id = " + toString(id)
|
let sql = "DELETE FROM users WHERE id = " + toString(id)
|
||||||
Console.print("Deleting user " + 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))
|
Console.print(" Rows affected: " + toString(affected))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Transaction Example
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
fn transactionDemo(connId: Int): Unit with {Console, Postgres} = {
|
fn transactionDemo(connId: Int): Unit with {Console, Postgres} = {
|
||||||
Console.print("")
|
Console.print("")
|
||||||
Console.print("=== Transaction Demo ===")
|
Console.print("=== Transaction Demo ===")
|
||||||
|
|
||||||
// Start transaction
|
|
||||||
Console.print("Beginning transaction...")
|
Console.print("Beginning transaction...")
|
||||||
Postgres.beginTx(connId)
|
Postgres.beginTx(connId)
|
||||||
|
|
||||||
// Make some changes
|
|
||||||
insertUser(connId, "TxUser1", "tx1@example.com")
|
insertUser(connId, "TxUser1", "tx1@example.com")
|
||||||
insertUser(connId, "TxUser2", "tx2@example.com")
|
insertUser(connId, "TxUser2", "tx2@example.com")
|
||||||
|
|
||||||
// Show users before commit
|
|
||||||
Console.print("Users before commit:")
|
Console.print("Users before commit:")
|
||||||
getUsers(connId)
|
getUsers(connId)
|
||||||
|
|
||||||
// Commit the transaction
|
|
||||||
Console.print("Committing transaction...")
|
Console.print("Committing transaction...")
|
||||||
Postgres.commit(connId)
|
Postgres.commit(connId)
|
||||||
|
|
||||||
Console.print("Transaction committed!")
|
Console.print("Transaction committed!")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Main
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
fn main(): Unit with {Console, Postgres} = {
|
fn main(): Unit with {Console, Postgres} = {
|
||||||
Console.print("========================================")
|
Console.print("========================================")
|
||||||
Console.print(" PostgreSQL Demo")
|
Console.print(" PostgreSQL Demo")
|
||||||
Console.print("========================================")
|
Console.print("========================================")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Connect to database
|
|
||||||
Console.print("Connecting to PostgreSQL...")
|
Console.print("Connecting to PostgreSQL...")
|
||||||
let connStr = "host=localhost user=testuser password=testpass dbname=testdb"
|
let connStr = "host=localhost user=testuser password=testpass dbname=testdb"
|
||||||
let connId = Postgres.connect(connStr)
|
let connId = Postgres.connect(connStr)
|
||||||
Console.print("Connected! Connection ID: " + toString(connId))
|
Console.print("Connected! Connection ID: " + toString(connId))
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Create table if not exists
|
|
||||||
Console.print("Creating users table...")
|
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)")
|
Postgres.execute(connId, "CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL)")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Clear table for demo
|
|
||||||
Console.print("Clearing existing data...")
|
Console.print("Clearing existing data...")
|
||||||
Postgres.execute(connId, "DELETE FROM users")
|
Postgres.execute(connId, "DELETE FROM users")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Insert some users
|
|
||||||
Console.print("=== Inserting Users ===")
|
Console.print("=== Inserting Users ===")
|
||||||
let id1 = insertUser(connId, "Alice", "alice@example.com")
|
let id1 = insertUser(connId, "Alice", "alice@example.com")
|
||||||
let id2 = insertUser(connId, "Bob", "bob@example.com")
|
let id2 = insertUser(connId, "Bob", "bob@example.com")
|
||||||
let id3 = insertUser(connId, "Charlie", "charlie@example.com")
|
let id3 = insertUser(connId, "Charlie", "charlie@example.com")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Query all users
|
|
||||||
Console.print("=== All Users ===")
|
Console.print("=== All Users ===")
|
||||||
getUsers(connId)
|
getUsers(connId)
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Query single user
|
|
||||||
Console.print("=== Single User Lookup ===")
|
Console.print("=== Single User Lookup ===")
|
||||||
getUserById(connId, id2)
|
getUserById(connId, id2)
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Update user
|
|
||||||
Console.print("=== Update User ===")
|
Console.print("=== Update User ===")
|
||||||
updateUserEmail(connId, id2, "bob.new@example.com")
|
updateUserEmail(connId, id2, "bob.new@example.com")
|
||||||
getUserById(connId, id2)
|
getUserById(connId, id2)
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Delete user
|
|
||||||
Console.print("=== Delete User ===")
|
Console.print("=== Delete User ===")
|
||||||
deleteUser(connId, id3)
|
deleteUser(connId, id3)
|
||||||
getUsers(connId)
|
getUsers(connId)
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Transaction demo
|
|
||||||
transactionDemo(connId)
|
transactionDemo(connId)
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Final state
|
|
||||||
Console.print("=== Final State ===")
|
Console.print("=== Final State ===")
|
||||||
getUsers(connId)
|
getUsers(connId)
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Close connection
|
|
||||||
Console.print("Closing connection...")
|
Console.print("Closing connection...")
|
||||||
Postgres.close(connId)
|
Postgres.close(connId)
|
||||||
Console.print("Done!")
|
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 {}
|
let output = run main() with {}
|
||||||
|
|||||||
@@ -1,18 +1,6 @@
|
|||||||
// Property-Based Testing Example
|
|
||||||
//
|
|
||||||
// This example demonstrates property-based testing in Lux,
|
|
||||||
// where we verify properties hold for randomly generated inputs.
|
|
||||||
//
|
|
||||||
// Run with: lux examples/property_testing.lux
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Generator Functions (using Random effect)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
let CHARS = "abcdefghijklmnopqrstuvwxyz"
|
let CHARS = "abcdefghijklmnopqrstuvwxyz"
|
||||||
|
|
||||||
fn genInt(min: Int, max: Int): Int with {Random} =
|
fn genInt(min: Int, max: Int): Int with {Random} = Random.int(min, max)
|
||||||
Random.int(min, max)
|
|
||||||
|
|
||||||
fn genIntList(min: Int, max: Int, maxLen: Int): List<Int> with {Random} = {
|
fn genIntList(min: Int, max: Int, maxLen: Int): List<Int> with {Random} = {
|
||||||
let len = Random.int(0, maxLen)
|
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} = {
|
fn genIntListHelper(min: Int, max: Int, len: Int): List<Int> with {Random} = {
|
||||||
if len <= 0 then
|
if len <= 0 then [] else List.concat([Random.int(min, max)], genIntListHelper(min, max, len - 1))
|
||||||
[]
|
|
||||||
else
|
|
||||||
List.concat([Random.int(min, max)], genIntListHelper(min, max, len - 1))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn genChar(): String with {Random} = {
|
fn genChar(): String with {Random} = {
|
||||||
@@ -37,195 +22,147 @@ fn genString(maxLen: Int): String with {Random} = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn genStringHelper(len: Int): String with {Random} = {
|
fn genStringHelper(len: Int): String with {Random} = {
|
||||||
if len <= 0 then
|
if len <= 0 then "" else genChar() + genStringHelper(len - 1)
|
||||||
""
|
|
||||||
else
|
|
||||||
genChar() + genStringHelper(len - 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Test Runner State
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
fn printResult(name: String, passed: Bool, count: Int): Unit with {Console} = {
|
fn printResult(name: String, passed: Bool, count: Int): Unit with {Console} = {
|
||||||
if passed then
|
if passed then Console.print(" PASS " + name + " (" + toString(count) + " tests)") else Console.print(" FAIL " + name)
|
||||||
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} = {
|
fn testReverseInvolutive(n: Int, count: Int): Bool with {Console, Random} = {
|
||||||
if n <= 0 then {
|
if n <= 0 then {
|
||||||
printResult("reverse(reverse(xs)) == xs", true, count)
|
printResult("reverse(reverse(xs)) == xs", true, count)
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
let xs = genIntList(0, 100, 20)
|
let xs = genIntList(0, 100, 20)
|
||||||
if List.reverse(List.reverse(xs)) == xs then
|
if List.reverse(List.reverse(xs)) == xs then testReverseInvolutive(n - 1, count) else {
|
||||||
testReverseInvolutive(n - 1, count)
|
printResult("reverse(reverse(xs)) == xs", false, count - n + 1)
|
||||||
else {
|
false
|
||||||
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} = {
|
fn testReverseLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||||
if n <= 0 then {
|
if n <= 0 then {
|
||||||
printResult("length(reverse(xs)) == length(xs)", true, count)
|
printResult("length(reverse(xs)) == length(xs)", true, count)
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
let xs = genIntList(0, 100, 20)
|
let xs = genIntList(0, 100, 20)
|
||||||
if List.length(List.reverse(xs)) == List.length(xs) then
|
if List.length(List.reverse(xs)) == List.length(xs) then testReverseLength(n - 1, count) else {
|
||||||
testReverseLength(n - 1, count)
|
printResult("length(reverse(xs)) == length(xs)", false, count - n + 1)
|
||||||
else {
|
false
|
||||||
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} = {
|
fn testMapLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||||
if n <= 0 then {
|
if n <= 0 then {
|
||||||
printResult("length(map(xs, f)) == length(xs)", true, count)
|
printResult("length(map(xs, f)) == length(xs)", true, count)
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
let xs = genIntList(0, 100, 20)
|
let xs = genIntList(0, 100, 20)
|
||||||
if List.length(List.map(xs, fn(x) => x * 2)) == List.length(xs) then
|
if List.length(List.map(xs, fn(x: _) => x * 2)) == List.length(xs) then testMapLength(n - 1, count) else {
|
||||||
testMapLength(n - 1, count)
|
printResult("length(map(xs, f)) == length(xs)", false, count - n + 1)
|
||||||
else {
|
false
|
||||||
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} = {
|
fn testConcatLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||||
if n <= 0 then {
|
if n <= 0 then {
|
||||||
printResult("length(xs ++ ys) == length(xs) + length(ys)", true, count)
|
printResult("length(xs ++ ys) == length(xs) + length(ys)", true, count)
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
let xs = genIntList(0, 50, 10)
|
let xs = genIntList(0, 50, 10)
|
||||||
let ys = genIntList(0, 50, 10)
|
let ys = genIntList(0, 50, 10)
|
||||||
if List.length(List.concat(xs, ys)) == List.length(xs) + List.length(ys) then
|
if List.length(List.concat(xs, ys)) == List.length(xs) + List.length(ys) then testConcatLength(n - 1, count) else {
|
||||||
testConcatLength(n - 1, count)
|
printResult("length(xs ++ ys) == length(xs) + length(ys)", false, count - n + 1)
|
||||||
else {
|
false
|
||||||
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} = {
|
fn testAddCommutative(n: Int, count: Int): Bool with {Console, Random} = {
|
||||||
if n <= 0 then {
|
if n <= 0 then {
|
||||||
printResult("a + b == b + a", true, count)
|
printResult("a + b == b + a", true, count)
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
let a = genInt(-1000, 1000)
|
let a = genInt(-1000, 1000)
|
||||||
let b = genInt(-1000, 1000)
|
let b = genInt(-1000, 1000)
|
||||||
if a + b == b + a then
|
if a + b == b + a then testAddCommutative(n - 1, count) else {
|
||||||
testAddCommutative(n - 1, count)
|
printResult("a + b == b + a", false, count - n + 1)
|
||||||
else {
|
false
|
||||||
printResult("a + b == b + a", false, count - n + 1)
|
}
|
||||||
false
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test: Multiplication is associative
|
|
||||||
fn testMulAssociative(n: Int, count: Int): Bool with {Console, Random} = {
|
fn testMulAssociative(n: Int, count: Int): Bool with {Console, Random} = {
|
||||||
if n <= 0 then {
|
if n <= 0 then {
|
||||||
printResult("(a * b) * c == a * (b * c)", true, count)
|
printResult("(a * b) * c == a * (b * c)", true, count)
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
let a = genInt(-100, 100)
|
let a = genInt(-100, 100)
|
||||||
let b = genInt(-100, 100)
|
let b = genInt(-100, 100)
|
||||||
let c = genInt(-100, 100)
|
let c = genInt(-100, 100)
|
||||||
if (a * b) * c == a * (b * c) then
|
if a * b * c == a * b * c then testMulAssociative(n - 1, count) else {
|
||||||
testMulAssociative(n - 1, count)
|
printResult("(a * b) * c == a * (b * c)", false, count - n + 1)
|
||||||
else {
|
false
|
||||||
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} = {
|
fn testStringConcatLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||||
if n <= 0 then {
|
if n <= 0 then {
|
||||||
printResult("length(s1 + s2) == length(s1) + length(s2)", true, count)
|
printResult("length(s1 + s2) == length(s1) + length(s2)", true, count)
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
let s1 = genString(10)
|
let s1 = genString(10)
|
||||||
let s2 = genString(10)
|
let s2 = genString(10)
|
||||||
if String.length(s1 + s2) == String.length(s1) + String.length(s2) then
|
if String.length(s1 + s2) == String.length(s1) + String.length(s2) then testStringConcatLength(n - 1, count) else {
|
||||||
testStringConcatLength(n - 1, count)
|
printResult("length(s1 + s2) == length(s1) + length(s2)", false, count - n + 1)
|
||||||
else {
|
false
|
||||||
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} = {
|
fn testAddIdentity(n: Int, count: Int): Bool with {Console, Random} = {
|
||||||
if n <= 0 then {
|
if n <= 0 then {
|
||||||
printResult("x + 0 == x && 0 + x == x", true, count)
|
printResult("x + 0 == x && 0 + x == x", true, count)
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
let x = genInt(-10000, 10000)
|
let x = genInt(-10000, 10000)
|
||||||
if x + 0 == x && 0 + x == x then
|
if x + 0 == x && 0 + x == x then testAddIdentity(n - 1, count) else {
|
||||||
testAddIdentity(n - 1, count)
|
printResult("x + 0 == x && 0 + x == x", false, count - n + 1)
|
||||||
else {
|
false
|
||||||
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} = {
|
fn testFilterLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||||
if n <= 0 then {
|
if n <= 0 then {
|
||||||
printResult("length(filter(xs, p)) <= length(xs)", true, count)
|
printResult("length(filter(xs, p)) <= length(xs)", true, count)
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
let xs = genIntList(0, 100, 20)
|
let xs = genIntList(0, 100, 20)
|
||||||
if List.length(List.filter(xs, fn(x) => x > 50)) <= List.length(xs) then
|
if List.length(List.filter(xs, fn(x: _) => x > 50)) <= List.length(xs) then testFilterLength(n - 1, count) else {
|
||||||
testFilterLength(n - 1, count)
|
printResult("length(filter(xs, p)) <= length(xs)", false, count - n + 1)
|
||||||
else {
|
false
|
||||||
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} = {
|
fn testConcatIdentity(n: Int, count: Int): Bool with {Console, Random} = {
|
||||||
if n <= 0 then {
|
if n <= 0 then {
|
||||||
printResult("concat(xs, []) == xs && concat([], xs) == xs", true, count)
|
printResult("concat(xs, []) == xs && concat([], xs) == xs", true, count)
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
let xs = genIntList(0, 100, 10)
|
let xs = genIntList(0, 100, 10)
|
||||||
if List.concat(xs, []) == xs && List.concat([], xs) == xs then
|
if List.concat(xs, []) == xs && List.concat([], xs) == xs then testConcatIdentity(n - 1, count) else {
|
||||||
testConcatIdentity(n - 1, count)
|
printResult("concat(xs, []) == xs && concat([], xs) == xs", false, count - n + 1)
|
||||||
else {
|
false
|
||||||
printResult("concat(xs, []) == xs && concat([], xs) == xs", false, count - n + 1)
|
}
|
||||||
false
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Main
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
fn main(): Unit with {Console, Random} = {
|
fn main(): Unit with {Console, Random} = {
|
||||||
Console.print("========================================")
|
Console.print("========================================")
|
||||||
@@ -234,7 +171,6 @@ fn main(): Unit with {Console, Random} = {
|
|||||||
Console.print("")
|
Console.print("")
|
||||||
Console.print("Running 100 iterations per property...")
|
Console.print("Running 100 iterations per property...")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
testReverseInvolutive(100, 100)
|
testReverseInvolutive(100, 100)
|
||||||
testReverseLength(100, 100)
|
testReverseLength(100, 100)
|
||||||
testMapLength(100, 100)
|
testMapLength(100, 100)
|
||||||
@@ -245,7 +181,6 @@ fn main(): Unit with {Console, Random} = {
|
|||||||
testAddIdentity(100, 100)
|
testAddIdentity(100, 100)
|
||||||
testFilterLength(100, 100)
|
testFilterLength(100, 100)
|
||||||
testConcatIdentity(100, 100)
|
testConcatIdentity(100, 100)
|
||||||
|
|
||||||
Console.print("")
|
Console.print("")
|
||||||
Console.print("========================================")
|
Console.print("========================================")
|
||||||
Console.print(" All property tests completed!")
|
Console.print(" All property tests completed!")
|
||||||
|
|||||||
@@ -1,39 +1,22 @@
|
|||||||
// Demonstrating Random and Time effects in Lux
|
|
||||||
//
|
|
||||||
// Expected output (values will vary):
|
|
||||||
// Rolling dice...
|
|
||||||
// Die 1: <random 1-6>
|
|
||||||
// Die 2: <random 1-6>
|
|
||||||
// Die 3: <random 1-6>
|
|
||||||
// Coin flip: <true/false>
|
|
||||||
// Random float: <0.0-1.0>
|
|
||||||
// Current time: <timestamp>
|
|
||||||
|
|
||||||
// Roll a single die (1-6)
|
|
||||||
fn rollDie(): Int with {Random} = Random.int(1, 6)
|
fn rollDie(): Int with {Random} = Random.int(1, 6)
|
||||||
|
|
||||||
// Roll multiple dice and print results
|
|
||||||
fn rollDice(count: Int): Unit with {Random, Console} = {
|
fn rollDice(count: Int): Unit with {Random, Console} = {
|
||||||
if count > 0 then {
|
if count > 0 then {
|
||||||
let value = rollDie()
|
let value = rollDie()
|
||||||
Console.print("Die " + toString(4 - count) + ": " + toString(value))
|
Console.print("Die " + toString(4 - count) + ": " + toString(value))
|
||||||
rollDice(count - 1)
|
rollDice(count - 1)
|
||||||
} else {
|
} else {
|
||||||
()
|
()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main function demonstrating random effects
|
|
||||||
fn main(): Unit with {Random, Console, Time} = {
|
fn main(): Unit with {Random, Console, Time} = {
|
||||||
Console.print("Rolling dice...")
|
Console.print("Rolling dice...")
|
||||||
rollDice(3)
|
rollDice(3)
|
||||||
|
|
||||||
let coin = Random.bool()
|
let coin = Random.bool()
|
||||||
Console.print("Coin flip: " + toString(coin))
|
Console.print("Coin flip: " + toString(coin))
|
||||||
|
|
||||||
let f = Random.float()
|
let f = Random.float()
|
||||||
Console.print("Random float: " + toString(f))
|
Console.print("Random float: " + toString(f))
|
||||||
|
|
||||||
let now = Time.now()
|
let now = Time.now()
|
||||||
Console.print("Current time: " + toString(now))
|
Console.print("Current time: " + toString(now))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,41 @@
|
|||||||
// Schema Evolution Demo
|
type User = {
|
||||||
// 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 {
|
|
||||||
name: String,
|
name: String,
|
||||||
email: 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_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_user = Schema.migrate(v1_user, 2)
|
||||||
let v2_version = Schema.getVersion(v2_user) // 2
|
|
||||||
|
|
||||||
// ============================================================
|
let v2_version = Schema.getVersion(v2_user)
|
||||||
// PART 2: Runtime Schema Operations (separate type)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Create versioned values for a different type (no migration)
|
|
||||||
let config1 = Schema.versioned("Config", 1, "debug")
|
let config1 = Schema.versioned("Config", 1, "debug")
|
||||||
|
|
||||||
let config2 = Schema.versioned("Config", 2, "release")
|
let config2 = Schema.versioned("Config", 2, "release")
|
||||||
|
|
||||||
// Check versions
|
let c1 = Schema.getVersion(config1)
|
||||||
let c1 = Schema.getVersion(config1) // 1
|
|
||||||
let c2 = Schema.getVersion(config2) // 2
|
let c2 = Schema.getVersion(config2)
|
||||||
|
|
||||||
// Migrate config (auto-migration since no explicit migration defined)
|
|
||||||
let upgradedConfig = Schema.migrate(config1, 2)
|
let upgradedConfig = Schema.migrate(config1, 2)
|
||||||
let upgradedConfigVersion = Schema.getVersion(upgradedConfig) // 2
|
|
||||||
|
|
||||||
// ============================================================
|
let upgradedConfigVersion = Schema.getVersion(upgradedConfig)
|
||||||
// PART 2: Practical Example - API Versioning
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// 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 } } =
|
fn createResponseV2(data: String, timestamp: Int): { version: Int, payload: String, meta: { ts: Int } } = { version: 2, payload: data, meta: { ts: timestamp } }
|
||||||
{ 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 resp1 = createResponseV1("Hello")
|
||||||
|
|
||||||
let resp2 = createResponseV2("World", 1234567890)
|
let resp2 = createResponseV2("World", 1234567890)
|
||||||
|
|
||||||
let payload1 = getPayload(resp1)
|
let payload1 = getPayload(resp1)
|
||||||
let payload2 = resp2.payload
|
|
||||||
|
|
||||||
// ============================================================
|
let payload2 = resp2.payload
|
||||||
// RESULTS
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
Console.print("=== Schema Evolution Demo ===")
|
Console.print("=== Schema Evolution Demo ===")
|
||||||
|
|||||||
@@ -1,58 +1,43 @@
|
|||||||
// Shell/Process example - demonstrates the Process effect
|
|
||||||
//
|
|
||||||
// This script runs shell commands and uses environment variables
|
|
||||||
|
|
||||||
fn main(): Unit with {Process, Console} = {
|
fn main(): Unit with {Process, Console} = {
|
||||||
Console.print("=== Lux Shell Example ===")
|
Console.print("=== Lux Shell Example ===")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Get current working directory
|
|
||||||
let cwd = Process.cwd()
|
let cwd = Process.cwd()
|
||||||
Console.print("Current directory: " + cwd)
|
Console.print("Current directory: " + cwd)
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Get environment variables
|
|
||||||
Console.print("Environment variables:")
|
Console.print("Environment variables:")
|
||||||
match Process.env("USER") {
|
match Process.env("USER") {
|
||||||
Some(user) => Console.print(" USER: " + user),
|
Some(user) => Console.print(" USER: " + user),
|
||||||
None => Console.print(" USER: (not set)")
|
None => Console.print(" USER: (not set)"),
|
||||||
}
|
}
|
||||||
match Process.env("HOME") {
|
match Process.env("HOME") {
|
||||||
Some(home) => Console.print(" HOME: " + home),
|
Some(home) => Console.print(" HOME: " + home),
|
||||||
None => Console.print(" HOME: (not set)")
|
None => Console.print(" HOME: (not set)"),
|
||||||
}
|
}
|
||||||
match Process.env("SHELL") {
|
match Process.env("SHELL") {
|
||||||
Some(shell) => Console.print(" SHELL: " + shell),
|
Some(shell) => Console.print(" SHELL: " + shell),
|
||||||
None => Console.print(" SHELL: (not set)")
|
None => Console.print(" SHELL: (not set)"),
|
||||||
}
|
}
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Run shell commands
|
|
||||||
Console.print("Running shell commands:")
|
Console.print("Running shell commands:")
|
||||||
|
|
||||||
let date = Process.exec("date")
|
let date = Process.exec("date")
|
||||||
Console.print(" date: " + String.trim(date))
|
Console.print(" date: " + String.trim(date))
|
||||||
|
|
||||||
let kernel = Process.exec("uname -r")
|
let kernel = Process.exec("uname -r")
|
||||||
Console.print(" kernel: " + String.trim(kernel))
|
Console.print(" kernel: " + String.trim(kernel))
|
||||||
|
|
||||||
let files = Process.exec("ls examples/*.lux | wc -l")
|
let files = Process.exec("ls examples/*.lux | wc -l")
|
||||||
Console.print(" .lux files in examples/: " + String.trim(files))
|
Console.print(" .lux files in examples/: " + String.trim(files))
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Command line arguments
|
|
||||||
Console.print("Command line arguments:")
|
Console.print("Command line arguments:")
|
||||||
let args = Process.args()
|
let args = Process.args()
|
||||||
let argCount = List.length(args)
|
let argCount = List.length(args)
|
||||||
if argCount == 0 then {
|
if argCount == 0 then {
|
||||||
Console.print(" (no arguments)")
|
Console.print(" (no arguments)")
|
||||||
} else {
|
} else {
|
||||||
Console.print(" Count: " + toString(argCount))
|
Console.print(" Count: " + toString(argCount))
|
||||||
match List.head(args) {
|
match List.head(args) {
|
||||||
Some(first) => Console.print(" First: " + first),
|
Some(first) => Console.print(" First: " + first),
|
||||||
None => Console.print(" First: (empty)")
|
None => Console.print(" First: (empty)"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = run main() with {}
|
let result = run main() with {}
|
||||||
|
|||||||
107
examples/showcase/README.md
Normal file
107
examples/showcase/README.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# Task Manager Showcase
|
||||||
|
|
||||||
|
This example demonstrates Lux's three killer features in a practical, real-world context.
|
||||||
|
|
||||||
|
## Running the Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lux run examples/showcase/task_manager.lux
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features Demonstrated
|
||||||
|
|
||||||
|
### 1. Algebraic Effects
|
||||||
|
|
||||||
|
Every function signature shows exactly what side effects it can perform:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn createTask(title: String, priority: String): Task@latest
|
||||||
|
with {TaskStore, Random} = { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
- `TaskStore` - database operations
|
||||||
|
- `Random` - random number generation
|
||||||
|
- No hidden I/O or surprise calls
|
||||||
|
|
||||||
|
### 2. Behavioral Types
|
||||||
|
|
||||||
|
Compile-time guarantees about function behavior:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn formatTask(task: Task@latest): String
|
||||||
|
is pure // No side effects
|
||||||
|
is deterministic // Same input = same output
|
||||||
|
is total // Always terminates
|
||||||
|
```
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn completeTask(id: String): Option<Task@latest>
|
||||||
|
is idempotent // Safe to retry
|
||||||
|
with {TaskStore}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Schema Evolution
|
||||||
|
|
||||||
|
Versioned types with automatic migration:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
type Task @v2 {
|
||||||
|
id: String,
|
||||||
|
title: String,
|
||||||
|
done: Bool,
|
||||||
|
priority: String, // New in v2
|
||||||
|
|
||||||
|
from @v1 = { ...old, priority: "medium" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Handler Swapping (Testing)
|
||||||
|
|
||||||
|
Test without mocks by swapping effect handlers:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Production
|
||||||
|
run processOrders() with {
|
||||||
|
TaskStore = PostgresTaskStore,
|
||||||
|
Logger = CloudLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Testing
|
||||||
|
run processOrders() with {
|
||||||
|
TaskStore = InMemoryTaskStore,
|
||||||
|
Logger = SilentLogger
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why This Matters
|
||||||
|
|
||||||
|
| Traditional Languages | Lux |
|
||||||
|
|----------------------|-----|
|
||||||
|
| Side effects are implicit | Effects in type signatures |
|
||||||
|
| Runtime crashes | Compile-time verification |
|
||||||
|
| Complex mocking frameworks | Simple handler swapping |
|
||||||
|
| Manual migration code | Automatic schema evolution |
|
||||||
|
| Hope for retry safety | Verified idempotency |
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
showcase/
|
||||||
|
├── README.md # This file
|
||||||
|
└── task_manager.lux # Main example with all features
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Sections in the Code
|
||||||
|
|
||||||
|
1. **Versioned Data Types** - `Task @v1`, `@v2`, `@v3` with migrations
|
||||||
|
2. **Pure Functions** - `is pure`, `is total`, `is deterministic`, `is idempotent`
|
||||||
|
3. **Effects** - `effect TaskStore` and `effect Logger`
|
||||||
|
4. **Effect Handlers** - `InMemoryTaskStore`, `ConsoleLogger`
|
||||||
|
5. **Testing** - `runTestScenario()` with swapped handlers
|
||||||
|
6. **Migration Demo** - `demonstrateMigration()`
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Read the [Behavioral Types Guide](../../docs/guide/12-behavioral-types.md)
|
||||||
|
- Read the [Schema Evolution Guide](../../docs/guide/13-schema-evolution.md)
|
||||||
|
- Explore [more examples](../)
|
||||||
@@ -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 {
|
effect Config {
|
||||||
fn get(key: String): String
|
fn get(key: String): String
|
||||||
}
|
}
|
||||||
@@ -25,14 +13,13 @@ fn configure(): String with {Config, Console} = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handler envConfig: Config {
|
handler envConfig: Config {
|
||||||
fn get(key) =
|
fn get(key) = if key == "api_url" then resume("https://api.example.com") else if key == "timeout" then resume("30") else resume("unknown")
|
||||||
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} = {
|
fn main(): Unit with {Console} = {
|
||||||
let result = run configure() with { Config = envConfig }
|
let result = run configure() with {
|
||||||
|
Config = envConfig,
|
||||||
|
}
|
||||||
Console.print(result)
|
Console.print(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,3 @@
|
|||||||
// Custom Logging with Effects
|
|
||||||
//
|
|
||||||
// This demonstrates how effects let you abstract side effects.
|
|
||||||
// The same code can be run with different logging implementations.
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// [INFO] Starting computation
|
|
||||||
// [DEBUG] x = 10
|
|
||||||
// [INFO] Processing
|
|
||||||
// [DEBUG] result = 20
|
|
||||||
// Final: 20
|
|
||||||
|
|
||||||
effect Log {
|
effect Log {
|
||||||
fn info(msg: String): Unit
|
fn info(msg: String): Unit
|
||||||
fn debug(msg: String): Unit
|
fn debug(msg: String): Unit
|
||||||
@@ -26,18 +14,22 @@ fn computation(): Int with {Log} = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handler consoleLogger: Log {
|
handler consoleLogger: Log {
|
||||||
fn info(msg) = {
|
fn info(msg) =
|
||||||
Console.print("[INFO] " + msg)
|
{
|
||||||
resume(())
|
Console.print("[INFO] " + msg)
|
||||||
}
|
resume(())
|
||||||
fn debug(msg) = {
|
}
|
||||||
Console.print("[DEBUG] " + msg)
|
fn debug(msg) =
|
||||||
resume(())
|
{
|
||||||
}
|
Console.print("[DEBUG] " + msg)
|
||||||
|
resume(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
let result = run computation() with { Log = consoleLogger }
|
let result = run computation() with {
|
||||||
|
Log = consoleLogger,
|
||||||
|
}
|
||||||
Console.print("Final: " + toString(result))
|
Console.print("Final: " + toString(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,18 @@
|
|||||||
// Early Return with Fail Effect
|
|
||||||
//
|
|
||||||
// The Fail effect provides clean early termination.
|
|
||||||
// Functions declare their failure modes in the type signature.
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// Parsing "42"...
|
|
||||||
// Result: 42
|
|
||||||
// Parsing "100"...
|
|
||||||
// Result: 100
|
|
||||||
// Dividing 100 by 4...
|
|
||||||
// Result: 25
|
|
||||||
|
|
||||||
fn parsePositive(s: String): Int with {Fail, Console} = {
|
fn parsePositive(s: String): Int with {Fail, Console} = {
|
||||||
Console.print("Parsing \"" + s + "\"...")
|
Console.print("Parsing \"" + s + "\"...")
|
||||||
if s == "42" then 42
|
if s == "42" then 42 else if s == "100" then 100 else Fail.fail("Invalid number: " + s)
|
||||||
else if s == "100" then 100
|
|
||||||
else Fail.fail("Invalid number: " + s)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn safeDivide(a: Int, b: Int): Int with {Fail, Console} = {
|
fn safeDivide(a: Int, b: Int): Int with {Fail, Console} = {
|
||||||
Console.print("Dividing " + toString(a) + " by " + toString(b) + "...")
|
Console.print("Dividing " + toString(a) + " by " + toString(b) + "...")
|
||||||
if b == 0 then Fail.fail("Division by zero")
|
if b == 0 then Fail.fail("Division by zero") else a / b
|
||||||
else a / b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
// These succeed
|
|
||||||
let n1 = run parsePositive("42") with {}
|
let n1 = run parsePositive("42") with {}
|
||||||
Console.print("Result: " + toString(n1))
|
Console.print("Result: " + toString(n1))
|
||||||
|
|
||||||
let n2 = run parsePositive("100") with {}
|
let n2 = run parsePositive("100") with {}
|
||||||
Console.print("Result: " + toString(n2))
|
Console.print("Result: " + toString(n2))
|
||||||
|
|
||||||
let n3 = run safeDivide(100, 4) with {}
|
let n3 = run safeDivide(100, 4) with {}
|
||||||
Console.print("Result: " + toString(n3))
|
Console.print("Result: " + toString(n3))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,3 @@
|
|||||||
// Effect Composition - Combine multiple effects cleanly
|
|
||||||
//
|
|
||||||
// Unlike monad transformers (which have ordering issues),
|
|
||||||
// effects can be freely combined without boilerplate.
|
|
||||||
// Each handler handles its own effect, ignoring others.
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// [LOG] Starting computation
|
|
||||||
// Generated: 7
|
|
||||||
// [LOG] Processing value
|
|
||||||
// [LOG] Done
|
|
||||||
// Result: 14
|
|
||||||
|
|
||||||
effect Log {
|
effect Log {
|
||||||
fn log(msg: String): Unit
|
fn log(msg: String): Unit
|
||||||
}
|
}
|
||||||
@@ -30,8 +17,8 @@ handler consoleLog: Log {
|
|||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
let result = run computation() with {
|
let result = run computation() with {
|
||||||
Log = consoleLog
|
Log = consoleLog,
|
||||||
}
|
}
|
||||||
Console.print("Generated: " + toString(result / 2))
|
Console.print("Generated: " + toString(result / 2))
|
||||||
Console.print("Result: " + toString(result))
|
Console.print("Result: " + toString(result))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,19 @@
|
|||||||
// Higher-Order Functions and Closures
|
|
||||||
//
|
|
||||||
// Functions are first-class values in Lux.
|
|
||||||
// Closures capture their environment.
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// Square of 5: 25
|
|
||||||
// Cube of 3: 27
|
|
||||||
// Add 10 to 5: 15
|
|
||||||
// Add 10 to 20: 30
|
|
||||||
// Composed: 15625 (cube(square(5)) = cube(25) = 15625)
|
|
||||||
|
|
||||||
fn apply(f: fn(Int): Int, x: Int): Int = f(x)
|
fn apply(f: fn(Int): Int, x: Int): Int = f(x)
|
||||||
|
|
||||||
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int =
|
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int = fn(x: Int): Int => f(g(x))
|
||||||
fn(x: Int): Int => f(g(x))
|
|
||||||
|
|
||||||
fn square(n: Int): Int = n * n
|
fn square(n: Int): Int = n * n
|
||||||
|
|
||||||
fn cube(n: Int): Int = n * n * n
|
fn cube(n: Int): Int = n * n * n
|
||||||
|
|
||||||
fn makeAdder(n: Int): fn(Int): Int =
|
fn makeAdder(n: Int): fn(Int): Int = fn(x: Int): Int => x + n
|
||||||
fn(x: Int): Int => x + n
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
// Apply functions
|
|
||||||
Console.print("Square of 5: " + toString(apply(square, 5)))
|
Console.print("Square of 5: " + toString(apply(square, 5)))
|
||||||
Console.print("Cube of 3: " + toString(apply(cube, 3)))
|
Console.print("Cube of 3: " + toString(apply(cube, 3)))
|
||||||
|
|
||||||
// Closures
|
|
||||||
let add10 = makeAdder(10)
|
let add10 = makeAdder(10)
|
||||||
Console.print("Add 10 to 5: " + toString(add10(5)))
|
Console.print("Add 10 to 5: " + toString(add10(5)))
|
||||||
Console.print("Add 10 to 20: " + toString(add10(20)))
|
Console.print("Add 10 to 20: " + toString(add10(20)))
|
||||||
|
|
||||||
// Function composition
|
|
||||||
let squareThenCube = compose(cube, square)
|
let squareThenCube = compose(cube, square)
|
||||||
Console.print("Composed: " + toString(squareThenCube(5)))
|
Console.print("Composed: " + toString(squareThenCube(5)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,3 @@
|
|||||||
// Algebraic Data Types and Pattern Matching
|
|
||||||
//
|
|
||||||
// Lux has powerful ADTs with exhaustive pattern matching.
|
|
||||||
// The type system ensures all cases are handled.
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// Evaluating: (2 + 3)
|
|
||||||
// Result: 5
|
|
||||||
// Evaluating: ((1 + 2) * (3 + 4))
|
|
||||||
// Result: 21
|
|
||||||
// Evaluating: (10 - (2 * 3))
|
|
||||||
// Result: 4
|
|
||||||
|
|
||||||
type Expr =
|
type Expr =
|
||||||
| Num(Int)
|
| Num(Int)
|
||||||
| Add(Expr, Expr)
|
| Add(Expr, Expr)
|
||||||
@@ -19,19 +6,19 @@ type Expr =
|
|||||||
|
|
||||||
fn eval(e: Expr): Int =
|
fn eval(e: Expr): Int =
|
||||||
match e {
|
match e {
|
||||||
Num(n) => n,
|
Num(n) => n,
|
||||||
Add(a, b) => eval(a) + eval(b),
|
Add(a, b) => eval(a) + eval(b),
|
||||||
Sub(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 =
|
fn showExpr(e: Expr): String =
|
||||||
match e {
|
match e {
|
||||||
Num(n) => toString(n),
|
Num(n) => toString(n),
|
||||||
Add(a, b) => "(" + showExpr(a) + " + " + showExpr(b) + ")",
|
Add(a, b) => "(" + showExpr(a) + " + " + showExpr(b) + ")",
|
||||||
Sub(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} = {
|
fn evalAndPrint(e: Expr): Unit with {Console} = {
|
||||||
Console.print("Evaluating: " + showExpr(e))
|
Console.print("Evaluating: " + showExpr(e))
|
||||||
@@ -39,15 +26,10 @@ fn evalAndPrint(e: Expr): Unit with {Console} = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
// (2 + 3)
|
|
||||||
let e1 = Add(Num(2), Num(3))
|
let e1 = Add(Num(2), Num(3))
|
||||||
evalAndPrint(e1)
|
evalAndPrint(e1)
|
||||||
|
|
||||||
// ((1 + 2) * (3 + 4))
|
|
||||||
let e2 = Mul(Add(Num(1), Num(2)), Add(Num(3), Num(4)))
|
let e2 = Mul(Add(Num(1), Num(2)), Add(Num(3), Num(4)))
|
||||||
evalAndPrint(e2)
|
evalAndPrint(e2)
|
||||||
|
|
||||||
// (10 - (2 * 3))
|
|
||||||
let e3 = Sub(Num(10), Mul(Num(2), Num(3)))
|
let e3 = Sub(Num(10), Mul(Num(2), Num(3)))
|
||||||
evalAndPrint(e3)
|
evalAndPrint(e3)
|
||||||
}
|
}
|
||||||
|
|||||||
419
examples/showcase/task_manager.lux
Normal file
419
examples/showcase/task_manager.lux
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// Task Manager API - A Showcase of Lux's Unique Features
|
||||||
|
// =============================================================================
|
||||||
|
//
|
||||||
|
// This example demonstrates Lux's three killer features:
|
||||||
|
//
|
||||||
|
// 1. ALGEBRAIC EFFECTS - Every side effect is explicit in function signatures
|
||||||
|
// - No hidden I/O, no surprise database calls
|
||||||
|
// - Testing is trivial: just swap handlers
|
||||||
|
//
|
||||||
|
// 2. BEHAVIORAL TYPES - Compile-time guarantees about function behavior
|
||||||
|
// - `is pure` - no side effects, safe to cache
|
||||||
|
// - `is total` - always terminates, never fails
|
||||||
|
// - `is idempotent` - safe to retry without side effects
|
||||||
|
// - `is deterministic` - same input = same output
|
||||||
|
//
|
||||||
|
// 3. SCHEMA EVOLUTION - Versioned types with automatic migration
|
||||||
|
// - Data structures evolve safely over time
|
||||||
|
// - Old data automatically upgrades
|
||||||
|
//
|
||||||
|
// To run: lux run examples/showcase/task_manager.lux
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PART 1: VERSIONED DATA TYPES (Schema Evolution)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Task v1: Our original data model (simple)
|
||||||
|
type Task @v1 {
|
||||||
|
id: String,
|
||||||
|
title: String,
|
||||||
|
done: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task v2: Added priority field
|
||||||
|
// The `from @v1` clause defines how to migrate old data automatically
|
||||||
|
type Task @v2 {
|
||||||
|
id: String,
|
||||||
|
title: String,
|
||||||
|
done: Bool,
|
||||||
|
priority: String, // New field: "low", "medium", "high"
|
||||||
|
|
||||||
|
// Migration: old tasks get "medium" priority by default
|
||||||
|
from @v1 = {
|
||||||
|
id: old.id,
|
||||||
|
title: old.title,
|
||||||
|
done: old.done,
|
||||||
|
priority: "medium"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task v3: Added due date and tags
|
||||||
|
// Migrations chain automatically: v1 → v2 → v3
|
||||||
|
type Task @v3 {
|
||||||
|
id: String,
|
||||||
|
title: String,
|
||||||
|
done: Bool,
|
||||||
|
priority: String,
|
||||||
|
dueDate: Option<Int>, // Unix timestamp, optional
|
||||||
|
tags: List<String>, // New: categorization
|
||||||
|
|
||||||
|
from @v2 = {
|
||||||
|
id: old.id,
|
||||||
|
title: old.title,
|
||||||
|
done: old.done,
|
||||||
|
priority: old.priority,
|
||||||
|
dueDate: None, // No due date for migrated tasks
|
||||||
|
tags: [] // Empty tags for migrated tasks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use @latest to always refer to the newest version
|
||||||
|
type TaskList = List<Task@latest>
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PART 2: PURE FUNCTIONS WITH BEHAVIORAL TYPES
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Pure function: no side effects, safe to cache, parallelize, eliminate if unused
|
||||||
|
// The compiler verifies `is pure` - if you try to call an effect, it errors.
|
||||||
|
fn formatTask(task: Task@latest): String
|
||||||
|
is pure
|
||||||
|
is deterministic
|
||||||
|
is total = {
|
||||||
|
let status = if task.done then "[x]" else "[ ]"
|
||||||
|
let priority = match task.priority {
|
||||||
|
"high" => "!!",
|
||||||
|
"medium" => "!",
|
||||||
|
_ => ""
|
||||||
|
}
|
||||||
|
status + " " + priority + task.title
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idempotent function: f(f(x)) = f(x)
|
||||||
|
// Safe to apply multiple times without changing the result
|
||||||
|
// Critical for retry logic - the compiler verifies this property
|
||||||
|
fn normalizeTitle(title: String): String
|
||||||
|
is pure
|
||||||
|
is idempotent = {
|
||||||
|
title
|
||||||
|
|> String.trim
|
||||||
|
|> String.toLower
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total function: always terminates, never throws
|
||||||
|
// No Fail effect allowed, recursion must be structurally decreasing
|
||||||
|
fn countCompleted(tasks: TaskList): Int
|
||||||
|
is pure
|
||||||
|
is total = {
|
||||||
|
match tasks {
|
||||||
|
[] => 0,
|
||||||
|
[task, ...rest] =>
|
||||||
|
(if task.done then 1 else 0) + countCompleted(rest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commutative function: f(a, b) = f(b, a)
|
||||||
|
// Enables parallel reduction and argument reordering optimizations
|
||||||
|
fn maxPriority(a: String, b: String): String
|
||||||
|
is pure
|
||||||
|
is commutative = {
|
||||||
|
let priorityValue = fn(p: String): Int =>
|
||||||
|
match p {
|
||||||
|
"high" => 3,
|
||||||
|
"medium" => 2,
|
||||||
|
"low" => 1,
|
||||||
|
_ => 0
|
||||||
|
}
|
||||||
|
if priorityValue(a) > priorityValue(b) then a else b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter tasks by criteria - pure, can be cached and parallelized
|
||||||
|
fn filterByPriority(tasks: TaskList, priority: String): TaskList
|
||||||
|
is pure
|
||||||
|
is deterministic = {
|
||||||
|
List.filter(tasks, fn(t: Task@latest): Bool => t.priority == priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filterPending(tasks: TaskList): TaskList
|
||||||
|
is pure
|
||||||
|
is deterministic = {
|
||||||
|
List.filter(tasks, fn(t: Task@latest): Bool => !t.done)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filterCompleted(tasks: TaskList): TaskList
|
||||||
|
is pure
|
||||||
|
is deterministic = {
|
||||||
|
List.filter(tasks, fn(t: Task@latest): Bool => t.done)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PART 3: EFFECTS - EXPLICIT SIDE EFFECTS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Custom effect for task storage
|
||||||
|
// This declares WHAT operations are available, not HOW they work
|
||||||
|
effect TaskStore {
|
||||||
|
fn save(task: Task@latest): Result<Task@latest, String>
|
||||||
|
fn getById(id: String): Option<Task@latest>
|
||||||
|
fn getAll(): TaskList
|
||||||
|
fn delete(id: String): Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service functions declare their effects in the type signature
|
||||||
|
// Anyone reading the signature knows exactly what side effects can occur
|
||||||
|
|
||||||
|
// Create a new task - requires TaskStore and Random effects
|
||||||
|
fn createTask(title: String, priority: String): Task@latest
|
||||||
|
with {TaskStore, Random} = {
|
||||||
|
let id = "task_" + toString(Random.int(10000, 99999))
|
||||||
|
let task = {
|
||||||
|
id: id,
|
||||||
|
title: normalizeTitle(title), // Uses our idempotent normalizer
|
||||||
|
done: false,
|
||||||
|
priority: priority,
|
||||||
|
dueDate: None,
|
||||||
|
tags: []
|
||||||
|
}
|
||||||
|
match TaskStore.save(task) {
|
||||||
|
Ok(saved) => saved,
|
||||||
|
Err(_) => task // Return unsaved if storage fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete a task - idempotent, safe to retry
|
||||||
|
// If the network fails mid-request, retry is safe
|
||||||
|
fn completeTask(id: String): Option<Task@latest>
|
||||||
|
is idempotent // Compiler verifies this is safe to retry
|
||||||
|
with {TaskStore} = {
|
||||||
|
match TaskStore.getById(id) {
|
||||||
|
None => None,
|
||||||
|
Some(task) => {
|
||||||
|
// Setting done = true is idempotent: already done? stays done
|
||||||
|
let updated = { ...task, done: true }
|
||||||
|
match TaskStore.save(updated) {
|
||||||
|
Ok(saved) => Some(saved),
|
||||||
|
Err(_) => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get task summary - logging effect, but computation is pure
|
||||||
|
fn getTaskSummary(): { total: Int, completed: Int, pending: Int, highPriority: Int }
|
||||||
|
with {TaskStore, Logger} = {
|
||||||
|
let tasks = TaskStore.getAll()
|
||||||
|
Logger.log("Fetched " + toString(List.length(tasks)) + " tasks")
|
||||||
|
|
||||||
|
// These computations are pure - could be parallelized
|
||||||
|
let completed = countCompleted(tasks)
|
||||||
|
let pending = List.length(tasks) - completed
|
||||||
|
let highPriority = List.length(filterByPriority(tasks, "high"))
|
||||||
|
|
||||||
|
{ total: List.length(tasks), completed: completed, pending: pending, highPriority: highPriority }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PART 4: EFFECT HANDLERS - SWAP IMPLEMENTATIONS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// In-memory handler for testing
|
||||||
|
// This handler stores tasks in a mutable list - perfect for unit tests
|
||||||
|
handler InMemoryTaskStore: TaskStore {
|
||||||
|
let tasks: List<Task@latest> = []
|
||||||
|
|
||||||
|
fn save(task: Task@latest): Result<Task@latest, String> = {
|
||||||
|
// Remove existing task with same ID (if any), then add new
|
||||||
|
tasks = List.filter(tasks, fn(t: Task@latest): Bool => t.id != task.id)
|
||||||
|
tasks = List.concat(tasks, [task])
|
||||||
|
Ok(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getById(id: String): Option<Task@latest> = {
|
||||||
|
List.find(tasks, fn(t: Task@latest): Bool => t.id == id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getAll(): TaskList = tasks
|
||||||
|
|
||||||
|
fn delete(id: String): Bool = {
|
||||||
|
let before = List.length(tasks)
|
||||||
|
tasks = List.filter(tasks, fn(t: Task@latest): Bool => t.id != task.id)
|
||||||
|
List.length(tasks) < before
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logging handler - wraps another handler with logging
|
||||||
|
handler LoggingTaskStore(inner: TaskStore): TaskStore with {Logger} {
|
||||||
|
fn save(task: Task@latest): Result<Task@latest, String> = {
|
||||||
|
Logger.log("Saving task: " + task.id)
|
||||||
|
inner.save(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getById(id: String): Option<Task@latest> = {
|
||||||
|
Logger.log("Getting task: " + id)
|
||||||
|
inner.getById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getAll(): TaskList = {
|
||||||
|
Logger.log("Getting all tasks")
|
||||||
|
inner.getAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete(id: String): Bool = {
|
||||||
|
Logger.log("Deleting task: " + id)
|
||||||
|
inner.delete(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple logger effect and handler
|
||||||
|
effect Logger {
|
||||||
|
fn log(message: String): Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
handler ConsoleLogger: Logger with {Console} {
|
||||||
|
fn log(message: String): Unit = {
|
||||||
|
Console.print("[LOG] " + message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handler SilentLogger: Logger {
|
||||||
|
fn log(message: String): Unit = {
|
||||||
|
// Do nothing - useful for tests
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PART 5: TESTING - SWAP HANDLERS, NO MOCKS NEEDED
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Test helper: creates a controlled environment
|
||||||
|
fn runTestScenario(): Unit with {Console} = {
|
||||||
|
Console.print("=== Running Test Scenario ===")
|
||||||
|
Console.print("")
|
||||||
|
|
||||||
|
// Use in-memory storage and silent logging for tests
|
||||||
|
// No database, no file I/O, no network - pure in-memory testing
|
||||||
|
let result = run {
|
||||||
|
// Create some tasks
|
||||||
|
let task1 = createTask("Write documentation", "high")
|
||||||
|
let task2 = createTask("Fix bug #123", "medium")
|
||||||
|
let task3 = createTask("Review PR", "low")
|
||||||
|
|
||||||
|
// Complete one task
|
||||||
|
completeTask(task1.id)
|
||||||
|
|
||||||
|
// Get summary
|
||||||
|
getTaskSummary()
|
||||||
|
} with {
|
||||||
|
TaskStore = InMemoryTaskStore,
|
||||||
|
Logger = SilentLogger,
|
||||||
|
Random = {
|
||||||
|
// Deterministic "random" for tests
|
||||||
|
let counter = 0
|
||||||
|
fn int(min: Int, max: Int): Int = {
|
||||||
|
counter = counter + 1
|
||||||
|
min + (counter * 12345) % (max - min)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.print("Test Results:")
|
||||||
|
Console.print(" Total tasks: " + toString(result.total))
|
||||||
|
Console.print(" Completed: " + toString(result.completed))
|
||||||
|
Console.print(" Pending: " + toString(result.pending))
|
||||||
|
Console.print(" High priority: " + toString(result.highPriority))
|
||||||
|
Console.print("")
|
||||||
|
|
||||||
|
// Verify results
|
||||||
|
if result.total == 3 &&
|
||||||
|
result.completed == 1 &&
|
||||||
|
result.pending == 2 &&
|
||||||
|
result.highPriority == 1 {
|
||||||
|
Console.print("All tests passed!")
|
||||||
|
} else {
|
||||||
|
Console.print("Test failed!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PART 6: SCHEMA MIGRATION DEMO
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
fn demonstrateMigration(): Unit with {Console} = {
|
||||||
|
Console.print("=== Schema Evolution Demo ===")
|
||||||
|
Console.print("")
|
||||||
|
|
||||||
|
// Simulate loading a v1 task (from old database/API)
|
||||||
|
let oldTask = Schema.versioned("Task", 1, {
|
||||||
|
id: "legacy_001",
|
||||||
|
title: "Old task from v1",
|
||||||
|
done: false
|
||||||
|
})
|
||||||
|
|
||||||
|
Console.print("Loaded v1 task:")
|
||||||
|
Console.print(" Version: " + toString(Schema.getVersion(oldTask)))
|
||||||
|
Console.print("")
|
||||||
|
|
||||||
|
// Migrate to latest version automatically
|
||||||
|
let migratedTask = Schema.migrate(oldTask, 3)
|
||||||
|
|
||||||
|
Console.print("After migration to v3:")
|
||||||
|
Console.print(" Version: " + toString(Schema.getVersion(migratedTask)))
|
||||||
|
Console.print(" Has priority: " + migratedTask.priority) // Added by v2 migration
|
||||||
|
Console.print(" Has tags: " + toString(List.length(migratedTask.tags)) + " tags") // Added by v3
|
||||||
|
Console.print("")
|
||||||
|
Console.print("Old data seamlessly upgraded!")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PART 7: MAIN - PUTTING IT ALL TOGETHER
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
fn main(): Unit with {Console} = {
|
||||||
|
Console.print("╔═══════════════════════════════════════════════════════════╗")
|
||||||
|
Console.print("║ Lux Task Manager - Feature Showcase ║")
|
||||||
|
Console.print("╚═══════════════════════════════════════════════════════════╝")
|
||||||
|
Console.print("")
|
||||||
|
|
||||||
|
// Demonstrate pure functions
|
||||||
|
Console.print("--- Pure Functions (Behavioral Types) ---")
|
||||||
|
let sampleTask = {
|
||||||
|
id: "demo",
|
||||||
|
title: "Learn Lux",
|
||||||
|
done: false,
|
||||||
|
priority: "high",
|
||||||
|
dueDate: None,
|
||||||
|
tags: ["learning", "programming"]
|
||||||
|
}
|
||||||
|
Console.print("Formatted task: " + formatTask(sampleTask))
|
||||||
|
Console.print("Normalized title: " + normalizeTitle(" HELLO WORLD "))
|
||||||
|
Console.print("")
|
||||||
|
|
||||||
|
// Demonstrate schema evolution
|
||||||
|
demonstrateMigration()
|
||||||
|
Console.print("")
|
||||||
|
|
||||||
|
// Run tests with swapped handlers
|
||||||
|
runTestScenario()
|
||||||
|
Console.print("")
|
||||||
|
|
||||||
|
Console.print("╔═══════════════════════════════════════════════════════════╗")
|
||||||
|
Console.print("║ Key Takeaways: ║")
|
||||||
|
Console.print("║ ║")
|
||||||
|
Console.print("║ 1. Effects in signatures = no hidden side effects ║")
|
||||||
|
Console.print("║ 2. Behavioral types = compile-time guarantees ║")
|
||||||
|
Console.print("║ 3. Handler swapping = easy testing without mocks ║")
|
||||||
|
Console.print("║ 4. Schema evolution = safe data migrations ║")
|
||||||
|
Console.print("╚═══════════════════════════════════════════════════════════╝")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the showcase
|
||||||
|
let _ = run main() with {}
|
||||||
@@ -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 factorialTail(n: Int, acc: Int): Int = if n <= 1 then acc else factorialTail(n - 1, n * acc)
|
||||||
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 factorial2(n: Int): Int = factorialTail(n, 1)
|
fn factorial2(n: Int): Int = factorialTail(n, 1)
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,11 @@
|
|||||||
// FizzBuzz - print numbers 1-100, but:
|
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)
|
||||||
// - 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 printFizzbuzz(i: Int, max: Int): Unit with {Console} =
|
fn printFizzbuzz(i: Int, max: Int): Unit with {Console} =
|
||||||
if i > max then ()
|
if i > max then () else {
|
||||||
else {
|
Console.print(fizzbuzz(i))
|
||||||
Console.print(fizzbuzz(i))
|
printFizzbuzz(i + 1, max)
|
||||||
printFizzbuzz(i + 1, max)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} =
|
fn main(): Unit with {Console} = printFizzbuzz(1, 100)
|
||||||
printFizzbuzz(1, 100)
|
|
||||||
|
|
||||||
let output = run main() with {}
|
let output = run main() with {}
|
||||||
|
|||||||
@@ -1,42 +1,17 @@
|
|||||||
// Number guessing game - demonstrates Random and Console effects
|
fn checkGuess(guess: Int, secret: Int): String = if guess == secret then "Correct" else if guess < secret then "Too low" else "Too high"
|
||||||
//
|
|
||||||
// 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!
|
|
||||||
|
|
||||||
// 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} = {
|
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)
|
let result = checkGuess(mid, secret)
|
||||||
Console.print("Guess " + toString(mid) + ": " + result + "!")
|
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} = {
|
fn main(): Unit with {Console} = {
|
||||||
Console.print("Welcome to the Guessing Game!")
|
Console.print("Welcome to the Guessing Game!")
|
||||||
// Use a fixed "secret" for reproducible output
|
|
||||||
let secret = 42
|
let secret = 42
|
||||||
Console.print("Target number: " + toString(secret))
|
Console.print("Target number: " + toString(secret))
|
||||||
Console.print("Simulating guesses...")
|
Console.print("Simulating guesses...")
|
||||||
|
|
||||||
let attempts = binarySearch(1, 100, secret, 1)
|
let attempts = binarySearch(1, 100, secret, 1)
|
||||||
Console.print("Found in " + toString(attempts) + " attempts!")
|
Console.print("Found in " + toString(attempts) + " attempts!")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// The classic first program
|
fn main(): Unit with {Console} = Console.print("Hello, World!")
|
||||||
// Expected output: Hello, World!
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} =
|
|
||||||
Console.print("Hello, World!")
|
|
||||||
|
|
||||||
let output = run main() with {}
|
let output = run main() with {}
|
||||||
|
|||||||
@@ -1,25 +1,14 @@
|
|||||||
// Prime number utilities
|
fn isPrime(n: Int): Bool = if n < 2 then false else isPrimeHelper(n, 2)
|
||||||
|
|
||||||
fn isPrime(n: Int): Bool =
|
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)
|
||||||
if n < 2 then false
|
|
||||||
else isPrimeHelper(n, 2)
|
|
||||||
|
|
||||||
fn isPrimeHelper(n: Int, i: Int): Bool =
|
fn findPrimes(count: Int): Unit with {Console} = findPrimesHelper(2, count)
|
||||||
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 findPrimesHelper(current: Int, remaining: Int): Unit with {Console} =
|
fn findPrimesHelper(current: Int, remaining: Int): Unit with {Console} =
|
||||||
if remaining <= 0 then ()
|
if remaining <= 0 then () else if isPrime(current) then {
|
||||||
else if isPrime(current) then {
|
Console.print(toString(current))
|
||||||
Console.print(toString(current))
|
findPrimesHelper(current + 1, remaining - 1)
|
||||||
findPrimesHelper(current + 1, remaining - 1)
|
} else findPrimesHelper(current + 1, remaining)
|
||||||
}
|
|
||||||
else findPrimesHelper(current + 1, remaining)
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
Console.print("First 20 prime numbers:")
|
Console.print("First 20 prime numbers:")
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// Standard Library Demo
|
|
||||||
// Demonstrates the built-in modules: List, String, Option, Math
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
Console.print("=== List Operations ===")
|
Console.print("=== List Operations ===")
|
||||||
let nums = [1, 2, 3, 4, 5]
|
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("Length: " + toString(List.length(nums)))
|
||||||
Console.print("Reversed: " + toString(List.reverse(nums)))
|
Console.print("Reversed: " + toString(List.reverse(nums)))
|
||||||
Console.print("Range 1-5: " + toString(List.range(1, 6)))
|
Console.print("Range 1-5: " + toString(List.range(1, 6)))
|
||||||
|
|
||||||
Console.print("")
|
Console.print("")
|
||||||
Console.print("=== String Operations ===")
|
Console.print("=== String Operations ===")
|
||||||
let text = " Hello, World! "
|
let text = " Hello, World! "
|
||||||
@@ -22,7 +18,6 @@ fn main(): Unit with {Console} = {
|
|||||||
Console.print("Contains 'World': " + toString(String.contains(text, "World")))
|
Console.print("Contains 'World': " + toString(String.contains(text, "World")))
|
||||||
Console.print("Split by comma: " + toString(String.split("a,b,c", ",")))
|
Console.print("Split by comma: " + toString(String.split("a,b,c", ",")))
|
||||||
Console.print("Join with dash: " + String.join(["x", "y", "z"], "-"))
|
Console.print("Join with dash: " + String.join(["x", "y", "z"], "-"))
|
||||||
|
|
||||||
Console.print("")
|
Console.print("")
|
||||||
Console.print("=== Option Operations ===")
|
Console.print("=== Option Operations ===")
|
||||||
let some_val = Some(42)
|
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("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("Some(42) getOrElse(0): " + toString(Option.getOrElse(some_val, 0)))
|
||||||
Console.print("None getOrElse(0): " + toString(Option.getOrElse(none_val, 0)))
|
Console.print("None getOrElse(0): " + toString(Option.getOrElse(none_val, 0)))
|
||||||
|
|
||||||
Console.print("")
|
Console.print("")
|
||||||
Console.print("=== Math Operations ===")
|
Console.print("=== Math Operations ===")
|
||||||
Console.print("abs(-5): " + toString(Math.abs(-5)))
|
Console.print("abs(-5): " + toString(Math.abs(-5)))
|
||||||
|
|||||||
@@ -1,13 +1,3 @@
|
|||||||
// State machine example using algebraic data types
|
|
||||||
// Demonstrates pattern matching for state transitions
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// Initial light: red
|
|
||||||
// After transition: green
|
|
||||||
// After two transitions: yellow
|
|
||||||
// Door: Closed -> Open -> Closed -> Locked
|
|
||||||
|
|
||||||
// Traffic light state machine
|
|
||||||
type TrafficLight =
|
type TrafficLight =
|
||||||
| Red
|
| Red
|
||||||
| Yellow
|
| Yellow
|
||||||
@@ -15,26 +5,25 @@ type TrafficLight =
|
|||||||
|
|
||||||
fn nextLight(light: TrafficLight): TrafficLight =
|
fn nextLight(light: TrafficLight): TrafficLight =
|
||||||
match light {
|
match light {
|
||||||
Red => Green,
|
Red => Green,
|
||||||
Green => Yellow,
|
Green => Yellow,
|
||||||
Yellow => Red
|
Yellow => Red,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn canGo(light: TrafficLight): Bool =
|
fn canGo(light: TrafficLight): Bool =
|
||||||
match light {
|
match light {
|
||||||
Green => true,
|
Green => true,
|
||||||
Yellow => false,
|
Yellow => false,
|
||||||
Red => false
|
Red => false,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lightColor(light: TrafficLight): String =
|
fn lightColor(light: TrafficLight): String =
|
||||||
match light {
|
match light {
|
||||||
Red => "red",
|
Red => "red",
|
||||||
Yellow => "yellow",
|
Yellow => "yellow",
|
||||||
Green => "green"
|
Green => "green",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Door state machine
|
|
||||||
type DoorState =
|
type DoorState =
|
||||||
| Open
|
| Open
|
||||||
| Closed
|
| Closed
|
||||||
@@ -48,31 +37,34 @@ type DoorAction =
|
|||||||
|
|
||||||
fn applyAction(state: DoorState, action: DoorAction): DoorState =
|
fn applyAction(state: DoorState, action: DoorAction): DoorState =
|
||||||
match (state, action) {
|
match (state, action) {
|
||||||
(Closed, OpenDoor) => Open,
|
(Closed, OpenDoor) => Open,
|
||||||
(Open, CloseDoor) => Closed,
|
(Open, CloseDoor) => Closed,
|
||||||
(Closed, LockDoor) => Locked,
|
(Closed, LockDoor) => Locked,
|
||||||
(Locked, UnlockDoor) => Closed,
|
(Locked, UnlockDoor) => Closed,
|
||||||
_ => state
|
_ => state,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn doorStateName(state: DoorState): String =
|
fn doorStateName(state: DoorState): String =
|
||||||
match state {
|
match state {
|
||||||
Open => "Open",
|
Open => "Open",
|
||||||
Closed => "Closed",
|
Closed => "Closed",
|
||||||
Locked => "Locked"
|
Locked => "Locked",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test the state machines
|
|
||||||
let light1 = Red
|
let light1 = Red
|
||||||
|
|
||||||
let light2 = nextLight(light1)
|
let light2 = nextLight(light1)
|
||||||
|
|
||||||
let light3 = nextLight(light2)
|
let light3 = nextLight(light2)
|
||||||
|
|
||||||
let door1 = Closed
|
let door1 = Closed
|
||||||
|
|
||||||
let door2 = applyAction(door1, OpenDoor)
|
let door2 = applyAction(door1, OpenDoor)
|
||||||
|
|
||||||
let door3 = applyAction(door2, CloseDoor)
|
let door3 = applyAction(door2, CloseDoor)
|
||||||
|
|
||||||
let door4 = applyAction(door3, LockDoor)
|
let door4 = applyAction(door3, LockDoor)
|
||||||
|
|
||||||
// Print results
|
|
||||||
fn printResults(): Unit with {Console} = {
|
fn printResults(): Unit with {Console} = {
|
||||||
Console.print("Initial light: " + lightColor(light1))
|
Console.print("Initial light: " + lightColor(light1))
|
||||||
Console.print("After transition: " + lightColor(light2))
|
Console.print("After transition: " + lightColor(light2))
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
// Stress test for RC system with large lists
|
|
||||||
// Tests FBIP optimization with single-owner chains
|
|
||||||
|
|
||||||
fn processChain(n: Int): Int = {
|
fn processChain(n: Int): Int = {
|
||||||
// Single owner chain - FBIP should reuse lists
|
|
||||||
let nums = List.range(1, n)
|
let nums = List.range(1, n)
|
||||||
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
||||||
let filtered = List.filter(doubled, fn(x: Int): Bool => x > n)
|
let filtered = List.filter(doubled, fn(x: Int): Bool => x > n)
|
||||||
@@ -12,13 +8,10 @@ fn processChain(n: Int): Int = {
|
|||||||
|
|
||||||
fn main(): Unit = {
|
fn main(): Unit = {
|
||||||
Console.print("=== RC Stress Test ===")
|
Console.print("=== RC Stress Test ===")
|
||||||
|
|
||||||
// Run multiple iterations of list operations
|
|
||||||
let result1 = processChain(100)
|
let result1 = processChain(100)
|
||||||
let result2 = processChain(200)
|
let result2 = processChain(200)
|
||||||
let result3 = processChain(500)
|
let result3 = processChain(500)
|
||||||
let result4 = processChain(1000)
|
let result4 = processChain(1000)
|
||||||
|
|
||||||
Console.print("Completed 4 chains")
|
Console.print("Completed 4 chains")
|
||||||
Console.print("Sizes: 100, 200, 500, 1000")
|
Console.print("Sizes: 100, 200, 500, 1000")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
// Stress test for RC system WITH shared references
|
|
||||||
// Forces rc>1 path by keeping aliases
|
|
||||||
|
|
||||||
fn processWithAlias(n: Int): Int = {
|
fn processWithAlias(n: Int): Int = {
|
||||||
let nums = List.range(1, n)
|
let nums = List.range(1, n)
|
||||||
let alias = nums // This increments rc, forcing copy path
|
let alias = nums
|
||||||
let _len = List.length(alias) // Use the alias
|
let _len = List.length(alias)
|
||||||
|
|
||||||
// Now nums has rc>1, so map must allocate new
|
|
||||||
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
||||||
let filtered = List.filter(doubled, fn(x: Int): Bool => x > n)
|
let filtered = List.filter(doubled, fn(x: Int): Bool => x > n)
|
||||||
let reversed = List.reverse(filtered)
|
let reversed = List.reverse(filtered)
|
||||||
@@ -15,12 +10,9 @@ fn processWithAlias(n: Int): Int = {
|
|||||||
|
|
||||||
fn main(): Unit = {
|
fn main(): Unit = {
|
||||||
Console.print("=== RC Stress Test (Shared Refs) ===")
|
Console.print("=== RC Stress Test (Shared Refs) ===")
|
||||||
|
|
||||||
// Run multiple iterations with shared references
|
|
||||||
let result1 = processWithAlias(100)
|
let result1 = processWithAlias(100)
|
||||||
let result2 = processWithAlias(200)
|
let result2 = processWithAlias(200)
|
||||||
let result3 = processWithAlias(500)
|
let result3 = processWithAlias(500)
|
||||||
let result4 = processWithAlias(1000)
|
let result4 = processWithAlias(1000)
|
||||||
|
|
||||||
Console.print("Completed 4 chains with shared refs")
|
Console.print("Completed 4 chains with shared refs")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,25 @@
|
|||||||
// Demonstrating tail call optimization (TCO) in Lux
|
fn factorialTCO(n: Int, acc: Int): Int = if n <= 1 then acc else factorialTCO(n - 1, n * acc)
|
||||||
// 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 factorial(n: Int): Int = factorialTCO(n, 1)
|
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)
|
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)
|
fn sumTo(n: Int): Int = sumToTCO(n, 0)
|
||||||
|
|
||||||
// Test the functions
|
|
||||||
let fact20 = factorial(20)
|
let fact20 = factorial(20)
|
||||||
|
|
||||||
let fib30 = fib(30)
|
let fib30 = fib(30)
|
||||||
|
|
||||||
let sum1000 = sumTo(1000)
|
let sum1000 = sumTo(1000)
|
||||||
|
|
||||||
let countResult = countdown(10000)
|
let countResult = countdown(10000)
|
||||||
|
|
||||||
// Print results
|
|
||||||
fn printResults(): Unit with {Console} = {
|
fn printResults(): Unit with {Console} = {
|
||||||
Console.print("factorial(20) = " + toString(fact20))
|
Console.print("factorial(20) = " + toString(fact20))
|
||||||
Console.print("fib(30) = " + toString(fib30))
|
Console.print("fib(30) = " + toString(fib30))
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
// This test shows FBIP optimization by comparing allocation counts
|
|
||||||
// With FBIP (rc=1): lists are reused in-place
|
|
||||||
// Without FBIP (rc>1): new lists are allocated
|
|
||||||
|
|
||||||
fn main(): Unit = {
|
fn main(): Unit = {
|
||||||
Console.print("=== FBIP Allocation Test ===")
|
Console.print("=== FBIP Allocation Test ===")
|
||||||
|
|
||||||
// Case 1: Single owner (FBIP active) - should reuse list
|
|
||||||
let a = List.range(1, 100)
|
let a = List.range(1, 100)
|
||||||
let b = List.map(a, fn(x: Int): Int => x * 2)
|
let b = List.map(a, fn(x: Int): Int => x * 2)
|
||||||
let c = List.filter(b, fn(x: Int): Bool => x > 50)
|
let c = List.filter(b, fn(x: Int): Bool => x > 50)
|
||||||
let d = List.reverse(c)
|
let d = List.reverse(c)
|
||||||
Console.print("Single owner chain done")
|
Console.print("Single owner chain done")
|
||||||
|
|
||||||
// The allocation count will show FBIP is working
|
|
||||||
// if allocations are low relative to operations performed
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
fn main(): Unit = {
|
fn main(): Unit = {
|
||||||
// Test FBIP without string operations
|
|
||||||
let nums = [1, 2, 3, 4, 5]
|
let nums = [1, 2, 3, 4, 5]
|
||||||
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
||||||
let filtered = List.filter(doubled, fn(x: Int): Bool => x > 4)
|
let filtered = List.filter(doubled, fn(x: Int): Bool => x > 4)
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// List Operations Test Suite
|
|
||||||
// Run with: lux test examples/test_lists.lux
|
|
||||||
|
|
||||||
fn test_list_length(): Unit with {Test} = {
|
fn test_list_length(): Unit with {Test} = {
|
||||||
Test.assertEqual(0, List.length([]))
|
Test.assertEqual(0, List.length([]))
|
||||||
Test.assertEqual(1, List.length([1]))
|
Test.assertEqual(1, List.length([1]))
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// Math Test Suite
|
|
||||||
// Run with: lux test examples/test_math.lux
|
|
||||||
|
|
||||||
fn test_addition(): Unit with {Test} = {
|
fn test_addition(): Unit with {Test} = {
|
||||||
Test.assertEqual(4, 2 + 2)
|
Test.assertEqual(4, 2 + 2)
|
||||||
Test.assertEqual(0, 0 + 0)
|
Test.assertEqual(0, 0 + 0)
|
||||||
|
|||||||
@@ -1,21 +1,10 @@
|
|||||||
// Test demonstrating ownership transfer with aliases
|
|
||||||
// The ownership transfer optimization ensures FBIP still works
|
|
||||||
// even when variables are aliased, because ownership is transferred
|
|
||||||
// rather than reference count being incremented.
|
|
||||||
|
|
||||||
fn main(): Unit = {
|
fn main(): Unit = {
|
||||||
Console.print("=== Ownership Transfer Test ===")
|
Console.print("=== Ownership Transfer Test ===")
|
||||||
|
|
||||||
let a = List.range(1, 100)
|
let a = List.range(1, 100)
|
||||||
// Ownership transfers from 'a' to 'alias', keeping rc=1
|
|
||||||
let alias = a
|
let alias = a
|
||||||
let len1 = List.length(alias)
|
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 b = List.map(a, fn(x: Int): Int => x * 2)
|
||||||
let c = List.filter(b, fn(x: Int): Bool => x > 50)
|
let c = List.filter(b, fn(x: Int): Bool => x > 50)
|
||||||
let d = List.reverse(c)
|
let d = List.reverse(c)
|
||||||
|
|
||||||
Console.print("Ownership transfer chain done")
|
Console.print("Ownership transfer chain done")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
fn main(): Unit = {
|
fn main(): Unit = {
|
||||||
Console.print("=== Allocation Comparison ===")
|
Console.print("=== Allocation Comparison ===")
|
||||||
|
|
||||||
// FBIP path (rc=1): list is reused
|
|
||||||
Console.print("Test 1: FBIP path")
|
Console.print("Test 1: FBIP path")
|
||||||
let a1 = List.range(1, 50)
|
let a1 = List.range(1, 50)
|
||||||
let b1 = List.map(a1, fn(x: Int): Int => x * 2)
|
let b1 = List.map(a1, fn(x: Int): Int => x * 2)
|
||||||
let c1 = List.reverse(b1)
|
let c1 = List.reverse(b1)
|
||||||
Console.print("FBIP done")
|
Console.print("FBIP done")
|
||||||
|
|
||||||
// To show non-FBIP, we need concat which doesn't have FBIP
|
|
||||||
Console.print("Test 2: Non-FBIP path (concat)")
|
Console.print("Test 2: Non-FBIP path (concat)")
|
||||||
let x = List.range(1, 25)
|
let x = List.range(1, 25)
|
||||||
let y = List.range(26, 50)
|
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")
|
Console.print("Concat done")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
// Demonstrating type classes (traits) in Lux
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// RGB color: rgb(255,128,0)
|
|
||||||
// Red color: red
|
|
||||||
// Green color: green
|
|
||||||
|
|
||||||
// Define a simple Printable trait
|
|
||||||
trait Printable {
|
trait Printable {
|
||||||
fn format(value: Int): String
|
fn format(value: Int): String
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement Printable
|
|
||||||
impl Printable for Int {
|
impl Printable for Int {
|
||||||
fn format(value: Int): String = "Number: " + toString(value)
|
fn format(value: Int): String = "Number: " + toString(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Color type with pattern matching
|
|
||||||
type Color =
|
type Color =
|
||||||
| Red
|
| Red
|
||||||
| Green
|
| Green
|
||||||
@@ -24,18 +14,18 @@ type Color =
|
|||||||
|
|
||||||
fn colorName(c: Color): String =
|
fn colorName(c: Color): String =
|
||||||
match c {
|
match c {
|
||||||
Red => "red",
|
Red => "red",
|
||||||
Green => "green",
|
Green => "green",
|
||||||
Blue => "blue",
|
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 myColor = RGB(255, 128, 0)
|
||||||
|
|
||||||
let redColor = Red
|
let redColor = Red
|
||||||
|
|
||||||
let greenColor = Green
|
let greenColor = Green
|
||||||
|
|
||||||
// Print results
|
|
||||||
fn printResults(): Unit with {Console} = {
|
fn printResults(): Unit with {Console} = {
|
||||||
Console.print("RGB color: " + colorName(myColor))
|
Console.print("RGB color: " + colorName(myColor))
|
||||||
Console.print("Red color: " + colorName(redColor))
|
Console.print("Red color: " + colorName(redColor))
|
||||||
|
|||||||
@@ -1,15 +1,3 @@
|
|||||||
// Demonstrating Schema Evolution in Lux
|
|
||||||
//
|
|
||||||
// Lux provides versioned types to help manage data evolution over time.
|
|
||||||
// The Schema module provides functions for creating and migrating versioned values.
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// Created user v1: Alice (age unknown)
|
|
||||||
// User version: 1
|
|
||||||
// Migrated to v2: Alice (age unknown)
|
|
||||||
// User version after migration: 2
|
|
||||||
|
|
||||||
// Create a versioned User value at v1
|
|
||||||
fn createUserV1(name: String): Unit with {Console} = {
|
fn createUserV1(name: String): Unit with {Console} = {
|
||||||
let user = Schema.versioned("User", 1, { name: name })
|
let user = Schema.versioned("User", 1, { name: name })
|
||||||
let version = Schema.getVersion(user)
|
let version = Schema.getVersion(user)
|
||||||
@@ -17,7 +5,6 @@ fn createUserV1(name: String): Unit with {Console} = {
|
|||||||
Console.print("User version: " + toString(version))
|
Console.print("User version: " + toString(version))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate a user to v2
|
|
||||||
fn migrateUserToV2(name: String): Unit with {Console} = {
|
fn migrateUserToV2(name: String): Unit with {Console} = {
|
||||||
let userV1 = Schema.versioned("User", 1, { name: name })
|
let userV1 = Schema.versioned("User", 1, { name: name })
|
||||||
let userV2 = Schema.migrate(userV1, 2)
|
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))
|
Console.print("User version after migration: " + toString(newVersion))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
createUserV1("Alice")
|
createUserV1("Alice")
|
||||||
migrateUserToV2("Alice")
|
migrateUserToV2("Alice")
|
||||||
|
|||||||
@@ -1,62 +1,38 @@
|
|||||||
// Simple Counter for Browser
|
type Model =
|
||||||
// Compile with: lux compile examples/web/counter.lux --target js -o examples/web/counter.js
|
| Counter(Int)
|
||||||
|
|
||||||
// ============================================================================
|
fn getCount(m: Model): Int =
|
||||||
// Model
|
match m {
|
||||||
// ============================================================================
|
Counter(n) => n,
|
||||||
|
}
|
||||||
type Model = | Counter(Int)
|
|
||||||
|
|
||||||
fn getCount(m: Model): Int = match m { Counter(n) => n }
|
|
||||||
|
|
||||||
fn init(): Model = Counter(0)
|
fn init(): Model = Counter(0)
|
||||||
|
|
||||||
// ============================================================================
|
type Msg =
|
||||||
// Messages
|
| Increment
|
||||||
// ============================================================================
|
| Decrement
|
||||||
|
| Reset
|
||||||
type Msg = | Increment | Decrement | Reset
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Update
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
fn update(model: Model, msg: Msg): Model =
|
fn update(model: Model, msg: Msg): Model =
|
||||||
match msg {
|
match msg {
|
||||||
Increment => Counter(getCount(model) + 1),
|
Increment => Counter(getCount(model) + 1),
|
||||||
Decrement => Counter(getCount(model) - 1),
|
Decrement => Counter(getCount(model) - 1),
|
||||||
Reset => Counter(0)
|
Reset => Counter(0),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// View - Returns HTML string for simplicity
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
fn view(model: Model): String = {
|
fn view(model: Model): String = {
|
||||||
let count = getCount(model)
|
let count = getCount(model)
|
||||||
"<div class=\"counter\">" +
|
"<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>"
|
||||||
"<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 luxInit(): Model = init()
|
||||||
|
|
||||||
fn luxUpdate(model: Model, msgName: String): Model =
|
fn luxUpdate(model: Model, msgName: String): Model =
|
||||||
match msgName {
|
match msgName {
|
||||||
"Increment" => update(model, Increment),
|
"Increment" => update(model, Increment),
|
||||||
"Decrement" => update(model, Decrement),
|
"Decrement" => update(model, Decrement),
|
||||||
"Reset" => update(model, Reset),
|
"Reset" => update(model, Reset),
|
||||||
_ => model
|
_ => model,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn luxView(model: Model): String = view(model)
|
fn luxView(model: Model): String = view(model)
|
||||||
|
|||||||
46
flake.nix
46
flake.nix
@@ -14,6 +14,7 @@
|
|||||||
pkgs = import nixpkgs { inherit system overlays; };
|
pkgs = import nixpkgs { inherit system overlays; };
|
||||||
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
||||||
extensions = [ "rust-src" "rust-analyzer" ];
|
extensions = [ "rust-src" "rust-analyzer" ];
|
||||||
|
targets = [ "x86_64-unknown-linux-musl" ];
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -22,8 +23,8 @@
|
|||||||
rustToolchain
|
rustToolchain
|
||||||
cargo-watch
|
cargo-watch
|
||||||
cargo-edit
|
cargo-edit
|
||||||
pkg-config
|
# Static builds
|
||||||
openssl
|
pkgsStatic.stdenv.cc
|
||||||
# Benchmark tools
|
# Benchmark tools
|
||||||
hyperfine
|
hyperfine
|
||||||
poop
|
poop
|
||||||
@@ -43,7 +44,7 @@
|
|||||||
printf "\n"
|
printf "\n"
|
||||||
printf " \033[1;35m╦ ╦ ╦╦ ╦\033[0m\n"
|
printf " \033[1;35m╦ ╦ ╦╦ ╦\033[0m\n"
|
||||||
printf " \033[1;35m║ ║ ║╔╣\033[0m\n"
|
printf " \033[1;35m║ ║ ║╔╣\033[0m\n"
|
||||||
printf " \033[1;35m╩═╝╚═╝╩ ╩\033[0m v0.1.0\n"
|
printf " \033[1;35m╩═╝╚═╝╩ ╩\033[0m v0.1.1\n"
|
||||||
printf "\n"
|
printf "\n"
|
||||||
printf " Functional language with first-class effects\n"
|
printf " Functional language with first-class effects\n"
|
||||||
printf "\n"
|
printf "\n"
|
||||||
@@ -61,18 +62,47 @@
|
|||||||
|
|
||||||
packages.default = pkgs.rustPlatform.buildRustPackage {
|
packages.default = pkgs.rustPlatform.buildRustPackage {
|
||||||
pname = "lux";
|
pname = "lux";
|
||||||
version = "0.1.0";
|
version = "0.1.1";
|
||||||
src = ./.;
|
src = ./.;
|
||||||
cargoLock.lockFile = ./Cargo.lock;
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
|
|
||||||
nativeBuildInputs = [ pkgs.pkg-config ];
|
|
||||||
buildInputs = [ pkgs.openssl ];
|
|
||||||
|
|
||||||
doCheck = false;
|
doCheck = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
# Benchmark scripts
|
packages.static = let
|
||||||
|
muslPkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
crossSystem = {
|
||||||
|
config = "x86_64-unknown-linux-musl";
|
||||||
|
isStatic = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in muslPkgs.rustPlatform.buildRustPackage {
|
||||||
|
pname = "lux";
|
||||||
|
version = "0.1.1";
|
||||||
|
src = ./.;
|
||||||
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
|
|
||||||
|
CARGO_BUILD_TARGET = "x86_64-unknown-linux-musl";
|
||||||
|
CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static";
|
||||||
|
|
||||||
|
doCheck = false;
|
||||||
|
|
||||||
|
postInstall = ''
|
||||||
|
$STRIP $out/bin/lux 2>/dev/null || true
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
apps = {
|
apps = {
|
||||||
|
# Release automation
|
||||||
|
release = {
|
||||||
|
type = "app";
|
||||||
|
program = toString (pkgs.writeShellScript "lux-release" ''
|
||||||
|
exec ${self}/scripts/release.sh "$@"
|
||||||
|
'');
|
||||||
|
};
|
||||||
|
|
||||||
|
# Benchmark scripts
|
||||||
# Run hyperfine benchmark comparison
|
# Run hyperfine benchmark comparison
|
||||||
bench = {
|
bench = {
|
||||||
type = "app";
|
type = "app";
|
||||||
|
|||||||
512
projects/lux-compiler/lexer.lux
Normal file
512
projects/lux-compiler/lexer.lux
Normal file
@@ -0,0 +1,512 @@
|
|||||||
|
// Lux Lexer — Self-hosted lexer for the Lux language
|
||||||
|
//
|
||||||
|
// This is the first component of the Lux-in-Lux compiler.
|
||||||
|
// It tokenizes Lux source code into a list of tokens.
|
||||||
|
//
|
||||||
|
// Design:
|
||||||
|
// - Recursive descent character scanning
|
||||||
|
// - Immutable state (ParseState tracks chars + position)
|
||||||
|
// - Pattern matching for all token types
|
||||||
|
|
||||||
|
// === Token types ===
|
||||||
|
|
||||||
|
type TokenKind =
|
||||||
|
// Literals
|
||||||
|
| TkInt(Int)
|
||||||
|
| TkFloat(String)
|
||||||
|
| TkString(String)
|
||||||
|
| TkChar(Char)
|
||||||
|
| TkBool(Bool)
|
||||||
|
// Identifiers
|
||||||
|
| TkIdent(String)
|
||||||
|
// Keywords
|
||||||
|
| TkFn | TkLet | TkIf | TkThen | TkElse | TkMatch
|
||||||
|
| TkWith | TkEffect | TkHandler | TkRun | TkResume
|
||||||
|
| TkType | TkImport | TkPub | TkAs | TkFrom
|
||||||
|
| TkTrait | TkImpl | TkFor
|
||||||
|
// Behavioral
|
||||||
|
| TkIs | TkPure | TkTotal | TkIdempotent
|
||||||
|
| TkDeterministic | TkCommutative
|
||||||
|
| TkWhere | TkAssume
|
||||||
|
// Operators
|
||||||
|
| TkPlus | TkMinus | TkStar | TkSlash | TkPercent
|
||||||
|
| TkEq | TkEqEq | TkNe | TkLt | TkLe | TkGt | TkGe
|
||||||
|
| TkAnd | TkOr | TkNot
|
||||||
|
| TkPipe | TkPipeGt | TkArrow | TkThinArrow
|
||||||
|
| TkDot | TkColon | TkColonColon | TkComma | TkSemi | TkAt
|
||||||
|
// Delimiters
|
||||||
|
| TkLParen | TkRParen | TkLBrace | TkRBrace
|
||||||
|
| TkLBracket | TkRBracket
|
||||||
|
// Special
|
||||||
|
| TkUnderscore | TkNewline | TkEof
|
||||||
|
// Doc comment
|
||||||
|
| TkDocComment(String)
|
||||||
|
|
||||||
|
type Token =
|
||||||
|
| Token(TokenKind, Int, Int) // kind, start, end
|
||||||
|
|
||||||
|
type LexState =
|
||||||
|
| LexState(List<Char>, Int) // chars, position
|
||||||
|
|
||||||
|
type LexResult =
|
||||||
|
| LexOk(Token, LexState)
|
||||||
|
| LexErr(String, Int)
|
||||||
|
|
||||||
|
// === Character utilities ===
|
||||||
|
|
||||||
|
fn peek(state: LexState): Option<Char> =
|
||||||
|
match state {
|
||||||
|
LexState(chars, pos) => List.get(chars, pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peekAt(state: LexState, offset: Int): Option<Char> =
|
||||||
|
match state {
|
||||||
|
LexState(chars, pos) => List.get(chars, pos + offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance(state: LexState): LexState =
|
||||||
|
match state {
|
||||||
|
LexState(chars, pos) => LexState(chars, pos + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn position(state: LexState): Int =
|
||||||
|
match state { LexState(_, pos) => pos }
|
||||||
|
|
||||||
|
fn isDigit(c: Char): Bool =
|
||||||
|
c == '0' || c == '1' || c == '2' || c == '3' || c == '4' ||
|
||||||
|
c == '5' || c == '6' || c == '7' || c == '8' || c == '9'
|
||||||
|
|
||||||
|
fn isAlpha(c: Char): Bool =
|
||||||
|
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'
|
||||||
|
|
||||||
|
fn isAlphaNumeric(c: Char): Bool =
|
||||||
|
isAlpha(c) || isDigit(c)
|
||||||
|
|
||||||
|
fn isWhitespace(c: Char): Bool =
|
||||||
|
c == ' ' || c == '\t' || c == '\r'
|
||||||
|
|
||||||
|
// === Core lexing ===
|
||||||
|
|
||||||
|
fn skipLineComment(state: LexState): LexState =
|
||||||
|
match peek(state) {
|
||||||
|
None => state,
|
||||||
|
Some(c) =>
|
||||||
|
if c == '\n' then state
|
||||||
|
else skipLineComment(advance(state))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skipWhitespaceAndComments(state: LexState): LexState =
|
||||||
|
match peek(state) {
|
||||||
|
None => state,
|
||||||
|
Some(c) =>
|
||||||
|
if isWhitespace(c) then
|
||||||
|
skipWhitespaceAndComments(advance(state))
|
||||||
|
else if c == '/' then
|
||||||
|
match peekAt(state, 1) {
|
||||||
|
Some('/') =>
|
||||||
|
// Check for doc comment (///)
|
||||||
|
match peekAt(state, 2) {
|
||||||
|
Some('/') => state, // Don't skip doc comments
|
||||||
|
_ => skipWhitespaceAndComments(skipLineComment(advance(advance(state))))
|
||||||
|
},
|
||||||
|
_ => state
|
||||||
|
}
|
||||||
|
else state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect identifier characters
|
||||||
|
fn collectIdent(state: LexState, acc: List<Char>): (List<Char>, LexState) =
|
||||||
|
match peek(state) {
|
||||||
|
None => (acc, state),
|
||||||
|
Some(c) =>
|
||||||
|
if isAlphaNumeric(c) then
|
||||||
|
collectIdent(advance(state), List.concat(acc, [c]))
|
||||||
|
else (acc, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect number characters (digits only)
|
||||||
|
fn collectDigits(state: LexState, acc: List<Char>): (List<Char>, LexState) =
|
||||||
|
match peek(state) {
|
||||||
|
None => (acc, state),
|
||||||
|
Some(c) =>
|
||||||
|
if isDigit(c) then
|
||||||
|
collectDigits(advance(state), List.concat(acc, [c]))
|
||||||
|
else (acc, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert list of digit chars to int
|
||||||
|
fn charsToInt(chars: List<Char>): Int =
|
||||||
|
List.fold(chars, 0, fn(acc, c) => acc * 10 + charToDigit(c))
|
||||||
|
|
||||||
|
fn charToDigit(c: Char): Int =
|
||||||
|
if c == '0' then 0
|
||||||
|
else if c == '1' then 1
|
||||||
|
else if c == '2' then 2
|
||||||
|
else if c == '3' then 3
|
||||||
|
else if c == '4' then 4
|
||||||
|
else if c == '5' then 5
|
||||||
|
else if c == '6' then 6
|
||||||
|
else if c == '7' then 7
|
||||||
|
else if c == '8' then 8
|
||||||
|
else 9
|
||||||
|
|
||||||
|
// Map identifier string to keyword token or ident
|
||||||
|
fn identToToken(name: String): TokenKind =
|
||||||
|
if name == "fn" then TkFn
|
||||||
|
else if name == "let" then TkLet
|
||||||
|
else if name == "if" then TkIf
|
||||||
|
else if name == "then" then TkThen
|
||||||
|
else if name == "else" then TkElse
|
||||||
|
else if name == "match" then TkMatch
|
||||||
|
else if name == "with" then TkWith
|
||||||
|
else if name == "effect" then TkEffect
|
||||||
|
else if name == "handler" then TkHandler
|
||||||
|
else if name == "run" then TkRun
|
||||||
|
else if name == "resume" then TkResume
|
||||||
|
else if name == "type" then TkType
|
||||||
|
else if name == "true" then TkBool(true)
|
||||||
|
else if name == "false" then TkBool(false)
|
||||||
|
else if name == "import" then TkImport
|
||||||
|
else if name == "pub" then TkPub
|
||||||
|
else if name == "as" then TkAs
|
||||||
|
else if name == "from" then TkFrom
|
||||||
|
else if name == "trait" then TkTrait
|
||||||
|
else if name == "impl" then TkImpl
|
||||||
|
else if name == "for" then TkFor
|
||||||
|
else if name == "is" then TkIs
|
||||||
|
else if name == "pure" then TkPure
|
||||||
|
else if name == "total" then TkTotal
|
||||||
|
else if name == "idempotent" then TkIdempotent
|
||||||
|
else if name == "deterministic" then TkDeterministic
|
||||||
|
else if name == "commutative" then TkCommutative
|
||||||
|
else if name == "where" then TkWhere
|
||||||
|
else if name == "assume" then TkAssume
|
||||||
|
else TkIdent(name)
|
||||||
|
|
||||||
|
// Lex a string literal (after opening quote consumed)
|
||||||
|
fn lexStringBody(state: LexState, acc: List<Char>): (List<Char>, LexState) =
|
||||||
|
match peek(state) {
|
||||||
|
None => (acc, state),
|
||||||
|
Some(c) =>
|
||||||
|
if c == '"' then (acc, advance(state))
|
||||||
|
else if c == '\\' then
|
||||||
|
match peekAt(state, 1) {
|
||||||
|
Some('n') => lexStringBody(advance(advance(state)), List.concat(acc, ['\n'])),
|
||||||
|
Some('t') => lexStringBody(advance(advance(state)), List.concat(acc, ['\t'])),
|
||||||
|
Some('\\') => lexStringBody(advance(advance(state)), List.concat(acc, ['\\'])),
|
||||||
|
Some('"') => lexStringBody(advance(advance(state)), List.concat(acc, ['"'])),
|
||||||
|
_ => lexStringBody(advance(state), List.concat(acc, [c]))
|
||||||
|
}
|
||||||
|
else lexStringBody(advance(state), List.concat(acc, [c]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lex a char literal (after opening quote consumed)
|
||||||
|
fn lexCharLiteral(state: LexState): LexResult =
|
||||||
|
let start = position(state) - 1;
|
||||||
|
match peek(state) {
|
||||||
|
None => LexErr("Unexpected end of input in char literal", start),
|
||||||
|
Some(c) =>
|
||||||
|
if c == '\\' then
|
||||||
|
match peekAt(state, 1) {
|
||||||
|
Some('n') =>
|
||||||
|
match peekAt(state, 2) {
|
||||||
|
Some('\'') => LexOk(Token(TkChar('\n'), start, position(state) + 3), advance(advance(advance(state)))),
|
||||||
|
_ => LexErr("Expected closing quote", position(state))
|
||||||
|
},
|
||||||
|
Some('t') =>
|
||||||
|
match peekAt(state, 2) {
|
||||||
|
Some('\'') => LexOk(Token(TkChar('\t'), start, position(state) + 3), advance(advance(advance(state)))),
|
||||||
|
_ => LexErr("Expected closing quote", position(state))
|
||||||
|
},
|
||||||
|
Some('\\') =>
|
||||||
|
match peekAt(state, 2) {
|
||||||
|
Some('\'') => LexOk(Token(TkChar('\\'), start, position(state) + 3), advance(advance(advance(state)))),
|
||||||
|
_ => LexErr("Expected closing quote", position(state))
|
||||||
|
},
|
||||||
|
_ => LexErr("Unknown escape sequence", position(state))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
match peekAt(state, 1) {
|
||||||
|
Some('\'') => LexOk(Token(TkChar(c), start, position(state) + 2), advance(advance(state))),
|
||||||
|
_ => LexErr("Expected closing quote", position(state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect doc comment text (after /// consumed)
|
||||||
|
fn collectDocComment(state: LexState, acc: List<Char>): (List<Char>, LexState) =
|
||||||
|
match peek(state) {
|
||||||
|
None => (acc, state),
|
||||||
|
Some(c) =>
|
||||||
|
if c == '\n' then (acc, state)
|
||||||
|
else collectDocComment(advance(state), List.concat(acc, [c]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lex a single token
|
||||||
|
fn lexToken(state: LexState): LexResult =
|
||||||
|
let state = skipWhitespaceAndComments(state);
|
||||||
|
let start = position(state);
|
||||||
|
match peek(state) {
|
||||||
|
None => LexOk(Token(TkEof, start, start), state),
|
||||||
|
Some(c) =>
|
||||||
|
if c == '\n' then
|
||||||
|
LexOk(Token(TkNewline, start, start + 1), advance(state))
|
||||||
|
// Numbers
|
||||||
|
else if isDigit(c) then
|
||||||
|
let result = collectDigits(state, []);
|
||||||
|
match result {
|
||||||
|
(digits, nextState) =>
|
||||||
|
// Check for float
|
||||||
|
match peek(nextState) {
|
||||||
|
Some('.') =>
|
||||||
|
match peekAt(nextState, 1) {
|
||||||
|
Some(d) =>
|
||||||
|
if isDigit(d) then
|
||||||
|
let fracResult = collectDigits(advance(nextState), []);
|
||||||
|
match fracResult {
|
||||||
|
(fracDigits, finalState) =>
|
||||||
|
let intPart = String.join(List.map(digits, fn(ch) => String.fromChar(ch)), "");
|
||||||
|
let fracPart = String.join(List.map(fracDigits, fn(ch) => String.fromChar(ch)), "");
|
||||||
|
LexOk(Token(TkFloat(intPart + "." + fracPart), start, position(finalState)), finalState)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
LexOk(Token(TkInt(charsToInt(digits)), start, position(nextState)), nextState),
|
||||||
|
None =>
|
||||||
|
LexOk(Token(TkInt(charsToInt(digits)), start, position(nextState)), nextState)
|
||||||
|
},
|
||||||
|
_ => LexOk(Token(TkInt(charsToInt(digits)), start, position(nextState)), nextState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Identifiers and keywords
|
||||||
|
else if isAlpha(c) then
|
||||||
|
let result = collectIdent(state, []);
|
||||||
|
match result {
|
||||||
|
(chars, nextState) =>
|
||||||
|
let name = String.join(List.map(chars, fn(ch) => String.fromChar(ch)), "");
|
||||||
|
LexOk(Token(identToToken(name), start, position(nextState)), nextState)
|
||||||
|
}
|
||||||
|
// String literals
|
||||||
|
else if c == '"' then
|
||||||
|
let result = lexStringBody(advance(state), []);
|
||||||
|
match result {
|
||||||
|
(chars, nextState) =>
|
||||||
|
let str = String.join(List.map(chars, fn(ch) => String.fromChar(ch)), "");
|
||||||
|
LexOk(Token(TkString(str), start, position(nextState)), nextState)
|
||||||
|
}
|
||||||
|
// Char literals
|
||||||
|
else if c == '\'' then
|
||||||
|
lexCharLiteral(advance(state))
|
||||||
|
// Doc comments (///)
|
||||||
|
else if c == '/' then
|
||||||
|
match peekAt(state, 1) {
|
||||||
|
Some('/') =>
|
||||||
|
match peekAt(state, 2) {
|
||||||
|
Some('/') =>
|
||||||
|
// Skip the "/// " prefix
|
||||||
|
let docState = advance(advance(advance(state)));
|
||||||
|
let docState = match peek(docState) {
|
||||||
|
Some(' ') => advance(docState),
|
||||||
|
_ => docState
|
||||||
|
};
|
||||||
|
let result = collectDocComment(docState, []);
|
||||||
|
match result {
|
||||||
|
(chars, nextState) =>
|
||||||
|
let text = String.join(List.map(chars, fn(ch) => String.fromChar(ch)), "");
|
||||||
|
LexOk(Token(TkDocComment(text), start, position(nextState)), nextState)
|
||||||
|
},
|
||||||
|
_ => LexOk(Token(TkSlash, start, start + 1), advance(state))
|
||||||
|
},
|
||||||
|
_ => LexOk(Token(TkSlash, start, start + 1), advance(state))
|
||||||
|
}
|
||||||
|
// Two-character operators
|
||||||
|
else if c == '=' then
|
||||||
|
match peekAt(state, 1) {
|
||||||
|
Some('=') => LexOk(Token(TkEqEq, start, start + 2), advance(advance(state))),
|
||||||
|
Some('>') => LexOk(Token(TkArrow, start, start + 2), advance(advance(state))),
|
||||||
|
_ => LexOk(Token(TkEq, start, start + 1), advance(state))
|
||||||
|
}
|
||||||
|
else if c == '!' then
|
||||||
|
match peekAt(state, 1) {
|
||||||
|
Some('=') => LexOk(Token(TkNe, start, start + 2), advance(advance(state))),
|
||||||
|
_ => LexOk(Token(TkNot, start, start + 1), advance(state))
|
||||||
|
}
|
||||||
|
else if c == '<' then
|
||||||
|
match peekAt(state, 1) {
|
||||||
|
Some('=') => LexOk(Token(TkLe, start, start + 2), advance(advance(state))),
|
||||||
|
_ => LexOk(Token(TkLt, start, start + 1), advance(state))
|
||||||
|
}
|
||||||
|
else if c == '>' then
|
||||||
|
match peekAt(state, 1) {
|
||||||
|
Some('=') => LexOk(Token(TkGe, start, start + 2), advance(advance(state))),
|
||||||
|
_ => LexOk(Token(TkGt, start, start + 1), advance(state))
|
||||||
|
}
|
||||||
|
else if c == '&' then
|
||||||
|
match peekAt(state, 1) {
|
||||||
|
Some('&') => LexOk(Token(TkAnd, start, start + 2), advance(advance(state))),
|
||||||
|
_ => LexErr("Expected '&&'", start)
|
||||||
|
}
|
||||||
|
else if c == '|' then
|
||||||
|
match peekAt(state, 1) {
|
||||||
|
Some('|') => LexOk(Token(TkOr, start, start + 2), advance(advance(state))),
|
||||||
|
Some('>') => LexOk(Token(TkPipeGt, start, start + 2), advance(advance(state))),
|
||||||
|
_ => LexOk(Token(TkPipe, start, start + 1), advance(state))
|
||||||
|
}
|
||||||
|
else if c == '-' then
|
||||||
|
match peekAt(state, 1) {
|
||||||
|
Some('>') => LexOk(Token(TkThinArrow, start, start + 2), advance(advance(state))),
|
||||||
|
_ => LexOk(Token(TkMinus, start, start + 1), advance(state))
|
||||||
|
}
|
||||||
|
else if c == ':' then
|
||||||
|
match peekAt(state, 1) {
|
||||||
|
Some(':') => LexOk(Token(TkColonColon, start, start + 2), advance(advance(state))),
|
||||||
|
_ => LexOk(Token(TkColon, start, start + 1), advance(state))
|
||||||
|
}
|
||||||
|
// Single-character tokens
|
||||||
|
else if c == '+' then LexOk(Token(TkPlus, start, start + 1), advance(state))
|
||||||
|
else if c == '*' then LexOk(Token(TkStar, start, start + 1), advance(state))
|
||||||
|
else if c == '%' then LexOk(Token(TkPercent, start, start + 1), advance(state))
|
||||||
|
else if c == '.' then LexOk(Token(TkDot, start, start + 1), advance(state))
|
||||||
|
else if c == ',' then LexOk(Token(TkComma, start, start + 1), advance(state))
|
||||||
|
else if c == ';' then LexOk(Token(TkSemi, start, start + 1), advance(state))
|
||||||
|
else if c == '@' then LexOk(Token(TkAt, start, start + 1), advance(state))
|
||||||
|
else if c == '(' then LexOk(Token(TkLParen, start, start + 1), advance(state))
|
||||||
|
else if c == ')' then LexOk(Token(TkRParen, start, start + 1), advance(state))
|
||||||
|
else if c == '{' then LexOk(Token(TkLBrace, start, start + 1), advance(state))
|
||||||
|
else if c == '}' then LexOk(Token(TkRBrace, start, start + 1), advance(state))
|
||||||
|
else if c == '[' then LexOk(Token(TkLBracket, start, start + 1), advance(state))
|
||||||
|
else if c == ']' then LexOk(Token(TkRBracket, start, start + 1), advance(state))
|
||||||
|
else if c == '_' then
|
||||||
|
// Check if it's just underscore or start of ident
|
||||||
|
match peekAt(state, 1) {
|
||||||
|
Some(next) =>
|
||||||
|
if isAlphaNumeric(next) then
|
||||||
|
let result = collectIdent(state, []);
|
||||||
|
match result {
|
||||||
|
(chars, nextState) =>
|
||||||
|
let name = String.join(List.map(chars, fn(ch) => String.fromChar(ch)), "");
|
||||||
|
LexOk(Token(TkIdent(name), start, position(nextState)), nextState)
|
||||||
|
}
|
||||||
|
else LexOk(Token(TkUnderscore, start, start + 1), advance(state)),
|
||||||
|
None => LexOk(Token(TkUnderscore, start, start + 1), advance(state))
|
||||||
|
}
|
||||||
|
else LexErr("Unexpected character: " + String.fromChar(c), start)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lex all tokens from source
|
||||||
|
fn lexAll(state: LexState, acc: List<Token>): List<Token> =
|
||||||
|
match lexToken(state) {
|
||||||
|
LexErr(msg, pos) =>
|
||||||
|
// On error, skip the character and continue
|
||||||
|
List.concat(acc, [Token(TkEof, pos, pos)]),
|
||||||
|
LexOk(token, nextState) =>
|
||||||
|
match token {
|
||||||
|
Token(TkEof, _, _) => List.concat(acc, [token]),
|
||||||
|
Token(TkNewline, _, _) =>
|
||||||
|
// Skip consecutive newlines
|
||||||
|
lexAll(nextState, List.concat(acc, [token])),
|
||||||
|
_ => lexAll(nextState, List.concat(acc, [token]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API: tokenize a source string
|
||||||
|
fn tokenize(source: String): List<Token> =
|
||||||
|
let chars = String.chars(source);
|
||||||
|
let state = LexState(chars, 0);
|
||||||
|
lexAll(state, [])
|
||||||
|
|
||||||
|
// === Token display ===
|
||||||
|
|
||||||
|
fn tokenKindToString(kind: TokenKind): String =
|
||||||
|
match kind {
|
||||||
|
TkInt(n) => "Int(" + toString(n) + ")",
|
||||||
|
TkFloat(s) => "Float(" + s + ")",
|
||||||
|
TkString(s) => "String(\"" + s + "\")",
|
||||||
|
TkChar(c) => "Char('" + String.fromChar(c) + "')",
|
||||||
|
TkBool(b) => if b then "true" else "false",
|
||||||
|
TkIdent(name) => "Ident(" + name + ")",
|
||||||
|
TkFn => "fn", TkLet => "let", TkIf => "if",
|
||||||
|
TkThen => "then", TkElse => "else", TkMatch => "match",
|
||||||
|
TkWith => "with", TkEffect => "effect", TkHandler => "handler",
|
||||||
|
TkRun => "run", TkResume => "resume", TkType => "type",
|
||||||
|
TkImport => "import", TkPub => "pub", TkAs => "as",
|
||||||
|
TkFrom => "from", TkTrait => "trait", TkImpl => "impl", TkFor => "for",
|
||||||
|
TkIs => "is", TkPure => "pure", TkTotal => "total",
|
||||||
|
TkIdempotent => "idempotent", TkDeterministic => "deterministic",
|
||||||
|
TkCommutative => "commutative", TkWhere => "where", TkAssume => "assume",
|
||||||
|
TkPlus => "+", TkMinus => "-", TkStar => "*", TkSlash => "/",
|
||||||
|
TkPercent => "%", TkEq => "=", TkEqEq => "==", TkNe => "!=",
|
||||||
|
TkLt => "<", TkLe => "<=", TkGt => ">", TkGe => ">=",
|
||||||
|
TkAnd => "&&", TkOr => "||", TkNot => "!",
|
||||||
|
TkPipe => "|", TkPipeGt => "|>",
|
||||||
|
TkArrow => "=>", TkThinArrow => "->",
|
||||||
|
TkDot => ".", TkColon => ":", TkColonColon => "::",
|
||||||
|
TkComma => ",", TkSemi => ";", TkAt => "@",
|
||||||
|
TkLParen => "(", TkRParen => ")", TkLBrace => "{", TkRBrace => "}",
|
||||||
|
TkLBracket => "[", TkRBracket => "]",
|
||||||
|
TkUnderscore => "_", TkNewline => "\\n", TkEof => "EOF",
|
||||||
|
TkDocComment(text) => "DocComment(\"" + text + "\")",
|
||||||
|
_ => "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tokenToString(token: Token): String =
|
||||||
|
match token {
|
||||||
|
Token(kind, start, end) =>
|
||||||
|
tokenKindToString(kind) + " [" + toString(start) + ".." + toString(end) + "]"
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Tests ===
|
||||||
|
|
||||||
|
fn printTokens(tokens: List<Token>): Unit with {Console} =
|
||||||
|
match List.head(tokens) {
|
||||||
|
None => Console.print(""),
|
||||||
|
Some(t) => {
|
||||||
|
Console.print(" " + tokenToString(t));
|
||||||
|
match List.tail(tokens) {
|
||||||
|
Some(rest) => printTokens(rest),
|
||||||
|
None => Console.print("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn testLexer(label: String, source: String): Unit with {Console} = {
|
||||||
|
Console.print("--- " + label + " ---");
|
||||||
|
Console.print(" Input: \"" + source + "\"");
|
||||||
|
let tokens = tokenize(source);
|
||||||
|
printTokens(tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main(): Unit with {Console} = {
|
||||||
|
Console.print("=== Lux Self-Hosted Lexer ===");
|
||||||
|
Console.print("");
|
||||||
|
|
||||||
|
// Basic tokens
|
||||||
|
testLexer("numbers", "42 3");
|
||||||
|
Console.print("");
|
||||||
|
|
||||||
|
// Identifiers and keywords
|
||||||
|
testLexer("keywords", "fn main let x");
|
||||||
|
Console.print("");
|
||||||
|
|
||||||
|
// Operators
|
||||||
|
testLexer("operators", "a + b == c");
|
||||||
|
Console.print("");
|
||||||
|
|
||||||
|
// String literal
|
||||||
|
testLexer("string", "\"hello world\"");
|
||||||
|
Console.print("");
|
||||||
|
|
||||||
|
// Function declaration
|
||||||
|
testLexer("function", "fn add(a: Int, b: Int): Int = a + b");
|
||||||
|
Console.print("");
|
||||||
|
|
||||||
|
// Behavioral properties
|
||||||
|
testLexer("behavioral", "fn add(a: Int): Int is pure = a");
|
||||||
|
Console.print("");
|
||||||
|
|
||||||
|
// Complex expression
|
||||||
|
testLexer("complex", "let result = if x > 0 then x else 0 - x");
|
||||||
|
Console.print("");
|
||||||
|
|
||||||
|
Console.print("=== Lexer test complete ===")
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = run main() with {}
|
||||||
213
scripts/release.sh
Executable file
213
scripts/release.sh
Executable file
@@ -0,0 +1,213 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Lux Release Script
|
||||||
|
# Builds a static binary, generates changelog, and creates a Gitea release.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/release.sh # auto-bump patch (0.2.0 → 0.2.1)
|
||||||
|
# ./scripts/release.sh patch # same as above
|
||||||
|
# ./scripts/release.sh minor # bump minor (0.2.0 → 0.3.0)
|
||||||
|
# ./scripts/release.sh major # bump major (0.2.0 → 1.0.0)
|
||||||
|
# ./scripts/release.sh v1.2.3 # explicit version
|
||||||
|
#
|
||||||
|
# Environment:
|
||||||
|
# GITEA_TOKEN - API token for git.qrty.ink (prompted if not set)
|
||||||
|
# GITEA_URL - Gitea instance URL (default: https://git.qrty.ink)
|
||||||
|
|
||||||
|
# cd to repo root (directory containing this script's parent)
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR/.."
|
||||||
|
|
||||||
|
GITEA_URL="${GITEA_URL:-https://git.qrty.ink}"
|
||||||
|
REPO_OWNER="blu"
|
||||||
|
REPO_NAME="lux"
|
||||||
|
API_BASE="$GITEA_URL/api/v1"
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
info() { printf "${CYAN}::${NC} %s\n" "$1"; }
|
||||||
|
ok() { printf "${GREEN}ok${NC} %s\n" "$1"; }
|
||||||
|
warn() { printf "${YELLOW}!!${NC} %s\n" "$1"; }
|
||||||
|
err() { printf "${RED}error:${NC} %s\n" "$1" >&2; exit 1; }
|
||||||
|
|
||||||
|
# --- Determine version ---
|
||||||
|
CURRENT=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||||
|
BUMP="${1:-patch}"
|
||||||
|
|
||||||
|
bump_version() {
|
||||||
|
local ver="$1" part="$2"
|
||||||
|
IFS='.' read -r major minor patch <<< "$ver"
|
||||||
|
case "$part" in
|
||||||
|
major) echo "$((major + 1)).0.0" ;;
|
||||||
|
minor) echo "$major.$((minor + 1)).0" ;;
|
||||||
|
patch) echo "$major.$minor.$((patch + 1))" ;;
|
||||||
|
*) echo "$part" ;; # treat as explicit version
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$BUMP" in
|
||||||
|
major|minor|patch)
|
||||||
|
VERSION=$(bump_version "$CURRENT" "$BUMP")
|
||||||
|
info "Bumping $BUMP: $CURRENT → $VERSION"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Explicit version — strip v prefix if present
|
||||||
|
VERSION="${BUMP#v}"
|
||||||
|
info "Explicit version: $VERSION"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
TAG="v$VERSION"
|
||||||
|
|
||||||
|
# --- Check for clean working tree ---
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
warn "Working tree has uncommitted changes:"
|
||||||
|
git status --short
|
||||||
|
printf "\n"
|
||||||
|
read -rp "Continue anyway? [y/N] " confirm
|
||||||
|
[[ "$confirm" =~ ^[Yy]$ ]] || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Check if tag already exists ---
|
||||||
|
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||||
|
err "Tag $TAG already exists. Choose a different version."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Update version in source files ---
|
||||||
|
if [ "$VERSION" != "$CURRENT" ]; then
|
||||||
|
info "Updating version in Cargo.toml and flake.nix..."
|
||||||
|
sed -i "0,/^version = \"$CURRENT\"/s//version = \"$VERSION\"/" Cargo.toml
|
||||||
|
sed -i "s/version = \"$CURRENT\";/version = \"$VERSION\";/g" flake.nix
|
||||||
|
sed -i "s/v$CURRENT/v$VERSION/g" flake.nix
|
||||||
|
git add Cargo.toml flake.nix
|
||||||
|
git commit --no-gpg-sign -m "chore: bump version to $VERSION"
|
||||||
|
ok "Version updated and committed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Generate changelog ---
|
||||||
|
info "Generating changelog..."
|
||||||
|
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
||||||
|
if [ -n "$LAST_TAG" ]; then
|
||||||
|
RANGE="$LAST_TAG..HEAD"
|
||||||
|
info "Changes since $LAST_TAG:"
|
||||||
|
else
|
||||||
|
RANGE="HEAD"
|
||||||
|
info "First release — summarizing recent commits:"
|
||||||
|
fi
|
||||||
|
|
||||||
|
CHANGELOG=$(git log "$RANGE" --pretty=format:"- %s" --no-merges 2>/dev/null | head -50 || true)
|
||||||
|
if [ -z "$CHANGELOG" ]; then
|
||||||
|
CHANGELOG="- Initial release"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Build static binary ---
|
||||||
|
info "Building static binary (nix build .#static)..."
|
||||||
|
nix build .#static
|
||||||
|
BINARY="result/bin/lux"
|
||||||
|
if [ ! -f "$BINARY" ]; then
|
||||||
|
err "Static binary not found at $BINARY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
BINARY_SIZE=$(ls -lh "$BINARY" | awk '{print $5}')
|
||||||
|
BINARY_TYPE=$(file "$BINARY" | sed 's/.*: //')
|
||||||
|
ok "Binary: $BINARY_SIZE, $BINARY_TYPE"
|
||||||
|
|
||||||
|
# --- Prepare release artifact ---
|
||||||
|
ARTIFACT="/tmp/lux-${TAG}-linux-x86_64"
|
||||||
|
cp "$BINARY" "$ARTIFACT"
|
||||||
|
chmod +x "$ARTIFACT"
|
||||||
|
|
||||||
|
# --- Show release summary ---
|
||||||
|
printf "\n"
|
||||||
|
printf "${BOLD}═══ Release Summary ═══${NC}\n"
|
||||||
|
printf "\n"
|
||||||
|
printf " ${BOLD}Tag:${NC} %s\n" "$TAG"
|
||||||
|
printf " ${BOLD}Binary:${NC} %s (%s)\n" "lux-${TAG}-linux-x86_64" "$BINARY_SIZE"
|
||||||
|
printf " ${BOLD}Commit:${NC} %s\n" "$(git rev-parse --short HEAD)"
|
||||||
|
printf "\n"
|
||||||
|
printf "${BOLD}Changelog:${NC}\n"
|
||||||
|
printf "%s\n" "$CHANGELOG"
|
||||||
|
printf "\n"
|
||||||
|
|
||||||
|
# --- Confirm ---
|
||||||
|
read -rp "Create release $TAG? [y/N] " confirm
|
||||||
|
[[ "$confirm" =~ ^[Yy]$ ]] || { info "Aborted."; exit 0; }
|
||||||
|
|
||||||
|
# --- Get Gitea token ---
|
||||||
|
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||||
|
printf "\n"
|
||||||
|
info "Gitea API token required (create at $GITEA_URL/user/settings/applications)"
|
||||||
|
read -rsp "Token: " GITEA_TOKEN
|
||||||
|
printf "\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$GITEA_TOKEN" ]; then
|
||||||
|
err "No token provided"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Create and push tag ---
|
||||||
|
info "Creating tag $TAG..."
|
||||||
|
git tag -a "$TAG" -m "Release $TAG" --no-sign
|
||||||
|
ok "Tag created"
|
||||||
|
|
||||||
|
info "Pushing tag to origin..."
|
||||||
|
git push origin "$TAG"
|
||||||
|
ok "Tag pushed"
|
||||||
|
|
||||||
|
# --- Create Gitea release ---
|
||||||
|
info "Creating release on Gitea..."
|
||||||
|
|
||||||
|
RELEASE_BODY=$(printf "## Lux %s\n\n### Changes\n\n%s\n\n### Installation\n\n\`\`\`bash\ncurl -Lo lux %s/%s/%s/releases/download/%s/lux-linux-x86_64\nchmod +x lux\n./lux --version\n\`\`\`" \
|
||||||
|
"$TAG" "$CHANGELOG" "$GITEA_URL" "$REPO_OWNER" "$REPO_NAME" "$TAG")
|
||||||
|
|
||||||
|
RELEASE_JSON=$(jq -n \
|
||||||
|
--arg tag "$TAG" \
|
||||||
|
--arg name "Lux $TAG" \
|
||||||
|
--arg body "$RELEASE_BODY" \
|
||||||
|
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')
|
||||||
|
|
||||||
|
RELEASE_RESPONSE=$(curl -s -X POST \
|
||||||
|
"$API_BASE/repos/$REPO_OWNER/$REPO_NAME/releases" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$RELEASE_JSON")
|
||||||
|
|
||||||
|
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id // empty')
|
||||||
|
if [ -z "$RELEASE_ID" ]; then
|
||||||
|
echo "$RELEASE_RESPONSE" | jq . 2>/dev/null || echo "$RELEASE_RESPONSE"
|
||||||
|
err "Failed to create release"
|
||||||
|
fi
|
||||||
|
ok "Release created (id: $RELEASE_ID)"
|
||||||
|
|
||||||
|
# --- Upload binary ---
|
||||||
|
info "Uploading binary..."
|
||||||
|
UPLOAD_RESPONSE=$(curl -s -X POST \
|
||||||
|
"$API_BASE/repos/$REPO_OWNER/$REPO_NAME/releases/$RELEASE_ID/assets?name=lux-linux-x86_64" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary "@$ARTIFACT")
|
||||||
|
|
||||||
|
ASSET_NAME=$(echo "$UPLOAD_RESPONSE" | jq -r '.name // empty')
|
||||||
|
if [ -z "$ASSET_NAME" ]; then
|
||||||
|
echo "$UPLOAD_RESPONSE" | jq . 2>/dev/null || echo "$UPLOAD_RESPONSE"
|
||||||
|
err "Failed to upload binary"
|
||||||
|
fi
|
||||||
|
ok "Binary uploaded: $ASSET_NAME"
|
||||||
|
|
||||||
|
# --- Done ---
|
||||||
|
printf "\n"
|
||||||
|
printf "${GREEN}${BOLD}Release $TAG published!${NC}\n"
|
||||||
|
printf "\n"
|
||||||
|
printf " ${BOLD}URL:${NC} %s/%s/%s/releases/tag/%s\n" "$GITEA_URL" "$REPO_OWNER" "$REPO_NAME" "$TAG"
|
||||||
|
printf " ${BOLD}Download:${NC} %s/%s/%s/releases/download/%s/lux-linux-x86_64\n" "$GITEA_URL" "$REPO_OWNER" "$REPO_NAME" "$TAG"
|
||||||
|
printf "\n"
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm -f "$ARTIFACT"
|
||||||
73
scripts/validate.sh
Executable file
73
scripts/validate.sh
Executable file
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Lux Full Validation Script
|
||||||
|
# Runs all checks: Rust tests, package tests, type checking, formatting, linting.
|
||||||
|
# Run after every committable change to ensure no regressions.
|
||||||
|
|
||||||
|
# cd to repo root (directory containing this script's parent)
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR/.."
|
||||||
|
|
||||||
|
LUX="$(pwd)/target/release/lux"
|
||||||
|
PACKAGES_DIR="$(pwd)/../packages"
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
FAILED=0
|
||||||
|
TOTAL=0
|
||||||
|
|
||||||
|
step() {
|
||||||
|
TOTAL=$((TOTAL + 1))
|
||||||
|
printf "${CYAN}[%d]${NC} %s... " "$TOTAL" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
ok() { printf "${GREEN}ok${NC} %s\n" "${1:-}"; }
|
||||||
|
fail() { printf "${RED}FAIL${NC} %s\n" "${1:-}"; FAILED=$((FAILED + 1)); }
|
||||||
|
|
||||||
|
# --- Rust checks ---
|
||||||
|
step "cargo check"
|
||||||
|
if nix develop --command cargo check 2>&1 | grep -q "Finished"; then ok; else fail; fi
|
||||||
|
|
||||||
|
step "cargo test"
|
||||||
|
OUTPUT=$(nix develop --command cargo test 2>&1 || true)
|
||||||
|
RESULT=$(echo "$OUTPUT" | grep "test result:" || echo "no result")
|
||||||
|
if echo "$RESULT" | grep -q "0 failed"; then ok "$RESULT"; else fail "$RESULT"; fi
|
||||||
|
|
||||||
|
# --- Build release binary ---
|
||||||
|
step "cargo build --release"
|
||||||
|
if nix develop --command cargo build --release 2>&1 | grep -q "Finished"; then ok; else fail; fi
|
||||||
|
|
||||||
|
# --- Package tests ---
|
||||||
|
for pkg in path frontmatter xml rss markdown; do
|
||||||
|
PKG_DIR="$PACKAGES_DIR/$pkg"
|
||||||
|
if [ -d "$PKG_DIR" ]; then
|
||||||
|
step "lux test ($pkg)"
|
||||||
|
OUTPUT=$(cd "$PKG_DIR" && "$LUX" test 2>&1 || true)
|
||||||
|
RESULT=$(echo "$OUTPUT" | grep "passed" | tail -1 || echo "no result")
|
||||||
|
if echo "$RESULT" | grep -q "passed"; then ok "$RESULT"; else fail "$RESULT"; fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Lux check on packages ---
|
||||||
|
for pkg in path frontmatter xml rss markdown; do
|
||||||
|
PKG_DIR="$PACKAGES_DIR/$pkg"
|
||||||
|
if [ -d "$PKG_DIR" ]; then
|
||||||
|
step "lux check ($pkg)"
|
||||||
|
OUTPUT=$(cd "$PKG_DIR" && "$LUX" check 2>&1 || true)
|
||||||
|
RESULT=$(echo "$OUTPUT" | grep "passed" | tail -1 || echo "no result")
|
||||||
|
if echo "$RESULT" | grep -q "passed"; then ok; else fail "$RESULT"; fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Summary ---
|
||||||
|
printf "\n${BOLD}═══ Validation Summary ═══${NC}\n"
|
||||||
|
if [ $FAILED -eq 0 ]; then
|
||||||
|
printf "${GREEN}All %d checks passed.${NC}\n" "$TOTAL"
|
||||||
|
else
|
||||||
|
printf "${RED}%d/%d checks failed.${NC}\n" "$FAILED" "$TOTAL"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -499,6 +499,12 @@ pub enum Expr {
|
|||||||
field: Ident,
|
field: Ident,
|
||||||
span: Span,
|
span: Span,
|
||||||
},
|
},
|
||||||
|
/// Tuple index access: tuple.0, tuple.1
|
||||||
|
TupleIndex {
|
||||||
|
object: Box<Expr>,
|
||||||
|
index: usize,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
/// Lambda: fn(x, y) => x + y or fn(x: Int): Int => x + 1
|
/// Lambda: fn(x, y) => x + y or fn(x: Int): Int => x + 1
|
||||||
Lambda {
|
Lambda {
|
||||||
params: Vec<Parameter>,
|
params: Vec<Parameter>,
|
||||||
@@ -563,6 +569,7 @@ impl Expr {
|
|||||||
Expr::Call { span, .. } => *span,
|
Expr::Call { span, .. } => *span,
|
||||||
Expr::EffectOp { span, .. } => *span,
|
Expr::EffectOp { span, .. } => *span,
|
||||||
Expr::Field { span, .. } => *span,
|
Expr::Field { span, .. } => *span,
|
||||||
|
Expr::TupleIndex { span, .. } => *span,
|
||||||
Expr::Lambda { span, .. } => *span,
|
Expr::Lambda { span, .. } => *span,
|
||||||
Expr::Let { span, .. } => *span,
|
Expr::Let { span, .. } => *span,
|
||||||
Expr::If { span, .. } => *span,
|
Expr::If { span, .. } => *span,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1268,6 +1268,11 @@ impl JsBackend {
|
|||||||
Ok(format!("{}.{}", obj, field.name))
|
Ok(format!("{}.{}", obj, field.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Expr::TupleIndex { object, index, .. } => {
|
||||||
|
let obj = self.emit_expr(object)?;
|
||||||
|
Ok(format!("{}[{}]", obj, index))
|
||||||
|
}
|
||||||
|
|
||||||
Expr::Run {
|
Expr::Run {
|
||||||
expr, handlers, ..
|
expr, handlers, ..
|
||||||
} => {
|
} => {
|
||||||
|
|||||||
@@ -224,10 +224,31 @@ pub mod colors {
|
|||||||
pub const BOLD: &str = "\x1b[1m";
|
pub const BOLD: &str = "\x1b[1m";
|
||||||
pub const DIM: &str = "\x1b[2m";
|
pub const DIM: &str = "\x1b[2m";
|
||||||
pub const RED: &str = "\x1b[31m";
|
pub const RED: &str = "\x1b[31m";
|
||||||
|
pub const GREEN: &str = "\x1b[32m";
|
||||||
pub const YELLOW: &str = "\x1b[33m";
|
pub const YELLOW: &str = "\x1b[33m";
|
||||||
pub const BLUE: &str = "\x1b[34m";
|
pub const BLUE: &str = "\x1b[34m";
|
||||||
|
pub const MAGENTA: &str = "\x1b[35m";
|
||||||
pub const CYAN: &str = "\x1b[36m";
|
pub const CYAN: &str = "\x1b[36m";
|
||||||
pub const WHITE: &str = "\x1b[37m";
|
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
|
/// Severity level for diagnostics
|
||||||
|
|||||||
@@ -598,6 +598,9 @@ impl Formatter {
|
|||||||
Expr::Field { object, field, .. } => {
|
Expr::Field { object, field, .. } => {
|
||||||
format!("{}.{}", self.format_expr(object), field.name)
|
format!("{}.{}", self.format_expr(object), field.name)
|
||||||
}
|
}
|
||||||
|
Expr::TupleIndex { object, index, .. } => {
|
||||||
|
format!("{}.{}", self.format_expr(object), index)
|
||||||
|
}
|
||||||
Expr::If { condition, then_branch, else_branch, .. } => {
|
Expr::If { condition, then_branch, else_branch, .. } => {
|
||||||
format!(
|
format!(
|
||||||
"if {} then {} else {}",
|
"if {} then {} else {}",
|
||||||
@@ -728,7 +731,7 @@ impl Formatter {
|
|||||||
match &lit.kind {
|
match &lit.kind {
|
||||||
LiteralKind::Int(n) => n.to_string(),
|
LiteralKind::Int(n) => n.to_string(),
|
||||||
LiteralKind::Float(f) => format!("{}", f),
|
LiteralKind::Float(f) => format!("{}", f),
|
||||||
LiteralKind::String(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
|
LiteralKind::String(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"").replace('{', "\\{").replace('}', "\\}")),
|
||||||
LiteralKind::Char(c) => format!("'{}'", c),
|
LiteralKind::Char(c) => format!("'{}'", c),
|
||||||
LiteralKind::Bool(b) => b.to_string(),
|
LiteralKind::Bool(b) => b.to_string(),
|
||||||
LiteralKind::Unit => "()".to_string(),
|
LiteralKind::Unit => "()".to_string(),
|
||||||
|
|||||||
@@ -90,6 +90,14 @@ pub enum BuiltinFn {
|
|||||||
StringToLower,
|
StringToLower,
|
||||||
StringSubstring,
|
StringSubstring,
|
||||||
StringFromChar,
|
StringFromChar,
|
||||||
|
StringCharAt,
|
||||||
|
StringIndexOf,
|
||||||
|
StringLastIndexOf,
|
||||||
|
StringRepeat,
|
||||||
|
|
||||||
|
// Int/Float operations
|
||||||
|
IntToString,
|
||||||
|
FloatToString,
|
||||||
|
|
||||||
// JSON operations
|
// JSON operations
|
||||||
JsonParse,
|
JsonParse,
|
||||||
@@ -620,6 +628,14 @@ pub struct Interpreter {
|
|||||||
pg_connections: RefCell<HashMap<i64, PgClient>>,
|
pg_connections: RefCell<HashMap<i64, PgClient>>,
|
||||||
/// Next PostgreSQL connection ID
|
/// Next PostgreSQL connection ID
|
||||||
next_pg_conn_id: RefCell<i64>,
|
next_pg_conn_id: RefCell<i64>,
|
||||||
|
/// Concurrent tasks: task_id -> (thunk_value, result_option, is_cancelled)
|
||||||
|
concurrent_tasks: RefCell<HashMap<i64, (Value, Option<Value>, bool)>>,
|
||||||
|
/// Next task ID
|
||||||
|
next_task_id: RefCell<i64>,
|
||||||
|
/// Channels: channel_id -> (queue, is_closed)
|
||||||
|
channels: RefCell<HashMap<i64, (Vec<Value>, bool)>>,
|
||||||
|
/// Next channel ID
|
||||||
|
next_channel_id: RefCell<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Results from running tests
|
/// Results from running tests
|
||||||
@@ -664,6 +680,10 @@ impl Interpreter {
|
|||||||
next_sql_conn_id: RefCell::new(1),
|
next_sql_conn_id: RefCell::new(1),
|
||||||
pg_connections: RefCell::new(HashMap::new()),
|
pg_connections: RefCell::new(HashMap::new()),
|
||||||
next_pg_conn_id: RefCell::new(1),
|
next_pg_conn_id: RefCell::new(1),
|
||||||
|
concurrent_tasks: RefCell::new(HashMap::new()),
|
||||||
|
next_task_id: RefCell::new(1),
|
||||||
|
channels: RefCell::new(HashMap::new()),
|
||||||
|
next_channel_id: RefCell::new(1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -966,6 +986,22 @@ impl Interpreter {
|
|||||||
"parseFloat".to_string(),
|
"parseFloat".to_string(),
|
||||||
Value::Builtin(BuiltinFn::StringParseFloat),
|
Value::Builtin(BuiltinFn::StringParseFloat),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"charAt".to_string(),
|
||||||
|
Value::Builtin(BuiltinFn::StringCharAt),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"indexOf".to_string(),
|
||||||
|
Value::Builtin(BuiltinFn::StringIndexOf),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"lastIndexOf".to_string(),
|
||||||
|
Value::Builtin(BuiltinFn::StringLastIndexOf),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"repeat".to_string(),
|
||||||
|
Value::Builtin(BuiltinFn::StringRepeat),
|
||||||
|
),
|
||||||
]));
|
]));
|
||||||
env.define("String", string_module);
|
env.define("String", string_module);
|
||||||
|
|
||||||
@@ -1039,6 +1075,18 @@ impl Interpreter {
|
|||||||
]));
|
]));
|
||||||
env.define("Math", math_module);
|
env.define("Math", math_module);
|
||||||
|
|
||||||
|
// Int module
|
||||||
|
let int_module = Value::Record(HashMap::from([
|
||||||
|
("toString".to_string(), Value::Builtin(BuiltinFn::IntToString)),
|
||||||
|
]));
|
||||||
|
env.define("Int", int_module);
|
||||||
|
|
||||||
|
// Float module
|
||||||
|
let float_module = Value::Record(HashMap::from([
|
||||||
|
("toString".to_string(), Value::Builtin(BuiltinFn::FloatToString)),
|
||||||
|
]));
|
||||||
|
env.define("Float", float_module);
|
||||||
|
|
||||||
// JSON module
|
// JSON module
|
||||||
let json_module = Value::Record(HashMap::from([
|
let json_module = Value::Record(HashMap::from([
|
||||||
("parse".to_string(), Value::Builtin(BuiltinFn::JsonParse)),
|
("parse".to_string(), Value::Builtin(BuiltinFn::JsonParse)),
|
||||||
@@ -1383,6 +1431,34 @@ impl Interpreter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Expr::TupleIndex {
|
||||||
|
object,
|
||||||
|
index,
|
||||||
|
span,
|
||||||
|
} => {
|
||||||
|
let obj_val = self.eval_expr(object, env)?;
|
||||||
|
match obj_val {
|
||||||
|
Value::Tuple(elements) => {
|
||||||
|
if *index < elements.len() {
|
||||||
|
Ok(EvalResult::Value(elements[*index].clone()))
|
||||||
|
} else {
|
||||||
|
Err(RuntimeError {
|
||||||
|
message: format!(
|
||||||
|
"Tuple index {} out of bounds for tuple with {} elements",
|
||||||
|
index,
|
||||||
|
elements.len()
|
||||||
|
),
|
||||||
|
span: Some(*span),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(RuntimeError {
|
||||||
|
message: format!("Cannot use tuple index on {}", obj_val.type_name()),
|
||||||
|
span: Some(*span),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Expr::Lambda { params, body, .. } => {
|
Expr::Lambda { params, body, .. } => {
|
||||||
let closure = Closure {
|
let closure = Closure {
|
||||||
params: params.iter().map(|p| p.name.name.clone()).collect(),
|
params: params.iter().map(|p| p.name.name.clone()).collect(),
|
||||||
@@ -1578,6 +1654,7 @@ impl Interpreter {
|
|||||||
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a < b)),
|
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a < b)),
|
||||||
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a < b)),
|
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a < b)),
|
||||||
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a < b)),
|
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a < b)),
|
||||||
|
(Value::Char(a), Value::Char(b)) => Ok(Value::Bool(a < b)),
|
||||||
(l, r) => Err(RuntimeError {
|
(l, r) => Err(RuntimeError {
|
||||||
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
|
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
|
||||||
span: Some(span),
|
span: Some(span),
|
||||||
@@ -1587,6 +1664,7 @@ impl Interpreter {
|
|||||||
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a <= b)),
|
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a <= b)),
|
||||||
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a <= b)),
|
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a <= b)),
|
||||||
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a <= b)),
|
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a <= b)),
|
||||||
|
(Value::Char(a), Value::Char(b)) => Ok(Value::Bool(a <= b)),
|
||||||
(l, r) => Err(RuntimeError {
|
(l, r) => Err(RuntimeError {
|
||||||
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
|
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
|
||||||
span: Some(span),
|
span: Some(span),
|
||||||
@@ -1596,6 +1674,7 @@ impl Interpreter {
|
|||||||
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a > b)),
|
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a > b)),
|
||||||
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a > b)),
|
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a > b)),
|
||||||
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a > b)),
|
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a > b)),
|
||||||
|
(Value::Char(a), Value::Char(b)) => Ok(Value::Bool(a > b)),
|
||||||
(l, r) => Err(RuntimeError {
|
(l, r) => Err(RuntimeError {
|
||||||
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
|
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
|
||||||
span: Some(span),
|
span: Some(span),
|
||||||
@@ -1605,6 +1684,7 @@ impl Interpreter {
|
|||||||
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a >= b)),
|
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a >= b)),
|
||||||
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a >= b)),
|
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a >= b)),
|
||||||
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a >= b)),
|
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a >= b)),
|
||||||
|
(Value::Char(a), Value::Char(b)) => Ok(Value::Bool(a >= b)),
|
||||||
(l, r) => Err(RuntimeError {
|
(l, r) => Err(RuntimeError {
|
||||||
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
|
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
|
||||||
span: Some(span),
|
span: Some(span),
|
||||||
@@ -2187,6 +2267,26 @@ impl Interpreter {
|
|||||||
Ok(EvalResult::Value(Value::String(result)))
|
Ok(EvalResult::Value(Value::String(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BuiltinFn::IntToString => {
|
||||||
|
if args.len() != 1 {
|
||||||
|
return Err(err("Int.toString requires 1 argument"));
|
||||||
|
}
|
||||||
|
match &args[0] {
|
||||||
|
Value::Int(n) => Ok(EvalResult::Value(Value::String(format!("{}", n)))),
|
||||||
|
v => Ok(EvalResult::Value(Value::String(format!("{}", v)))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BuiltinFn::FloatToString => {
|
||||||
|
if args.len() != 1 {
|
||||||
|
return Err(err("Float.toString requires 1 argument"));
|
||||||
|
}
|
||||||
|
match &args[0] {
|
||||||
|
Value::Float(f) => Ok(EvalResult::Value(Value::String(format!("{}", f)))),
|
||||||
|
v => Ok(EvalResult::Value(Value::String(format!("{}", v)))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
BuiltinFn::TypeOf => {
|
BuiltinFn::TypeOf => {
|
||||||
if args.len() != 1 {
|
if args.len() != 1 {
|
||||||
return Err(err("typeOf requires 1 argument"));
|
return Err(err("typeOf requires 1 argument"));
|
||||||
@@ -2498,6 +2598,89 @@ impl Interpreter {
|
|||||||
Ok(EvalResult::Value(Value::String(c.to_string())))
|
Ok(EvalResult::Value(Value::String(c.to_string())))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BuiltinFn::StringCharAt => {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return Err(err("String.charAt requires 2 arguments: string, index"));
|
||||||
|
}
|
||||||
|
let s = match &args[0] {
|
||||||
|
Value::String(s) => s.clone(),
|
||||||
|
v => return Err(err(&format!("String.charAt expects String, got {}", v.type_name()))),
|
||||||
|
};
|
||||||
|
let idx = match &args[1] {
|
||||||
|
Value::Int(n) => *n as usize,
|
||||||
|
v => return Err(err(&format!("String.charAt expects Int for index, got {}", v.type_name()))),
|
||||||
|
};
|
||||||
|
let chars: Vec<char> = s.chars().collect();
|
||||||
|
if idx < chars.len() {
|
||||||
|
Ok(EvalResult::Value(Value::String(chars[idx].to_string())))
|
||||||
|
} else {
|
||||||
|
Ok(EvalResult::Value(Value::String(String::new())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BuiltinFn::StringIndexOf => {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return Err(err("String.indexOf requires 2 arguments: string, substring"));
|
||||||
|
}
|
||||||
|
let s = match &args[0] {
|
||||||
|
Value::String(s) => s.clone(),
|
||||||
|
v => return Err(err(&format!("String.indexOf expects String, got {}", v.type_name()))),
|
||||||
|
};
|
||||||
|
let sub = match &args[1] {
|
||||||
|
Value::String(s) => s.clone(),
|
||||||
|
v => return Err(err(&format!("String.indexOf expects String for substring, got {}", v.type_name()))),
|
||||||
|
};
|
||||||
|
match s.find(&sub) {
|
||||||
|
Some(idx) => Ok(EvalResult::Value(Value::Constructor {
|
||||||
|
name: "Some".to_string(),
|
||||||
|
fields: vec![Value::Int(idx as i64)],
|
||||||
|
})),
|
||||||
|
None => Ok(EvalResult::Value(Value::Constructor {
|
||||||
|
name: "None".to_string(),
|
||||||
|
fields: vec![],
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BuiltinFn::StringLastIndexOf => {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return Err(err("String.lastIndexOf requires 2 arguments: string, substring"));
|
||||||
|
}
|
||||||
|
let s = match &args[0] {
|
||||||
|
Value::String(s) => s.clone(),
|
||||||
|
v => return Err(err(&format!("String.lastIndexOf expects String, got {}", v.type_name()))),
|
||||||
|
};
|
||||||
|
let sub = match &args[1] {
|
||||||
|
Value::String(s) => s.clone(),
|
||||||
|
v => return Err(err(&format!("String.lastIndexOf expects String for substring, got {}", v.type_name()))),
|
||||||
|
};
|
||||||
|
match s.rfind(&sub) {
|
||||||
|
Some(idx) => Ok(EvalResult::Value(Value::Constructor {
|
||||||
|
name: "Some".to_string(),
|
||||||
|
fields: vec![Value::Int(idx as i64)],
|
||||||
|
})),
|
||||||
|
None => Ok(EvalResult::Value(Value::Constructor {
|
||||||
|
name: "None".to_string(),
|
||||||
|
fields: vec![],
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BuiltinFn::StringRepeat => {
|
||||||
|
if args.len() != 2 {
|
||||||
|
return Err(err("String.repeat requires 2 arguments: string, count"));
|
||||||
|
}
|
||||||
|
let s = match &args[0] {
|
||||||
|
Value::String(s) => s.clone(),
|
||||||
|
v => return Err(err(&format!("String.repeat expects String, got {}", v.type_name()))),
|
||||||
|
};
|
||||||
|
let count = match &args[1] {
|
||||||
|
Value::Int(n) => (*n).max(0) as usize,
|
||||||
|
v => return Err(err(&format!("String.repeat expects Int for count, got {}", v.type_name()))),
|
||||||
|
};
|
||||||
|
Ok(EvalResult::Value(Value::String(s.repeat(count))))
|
||||||
|
}
|
||||||
|
|
||||||
// JSON operations
|
// JSON operations
|
||||||
BuiltinFn::JsonParse => {
|
BuiltinFn::JsonParse => {
|
||||||
let s = Self::expect_arg_1::<String>(&args, "Json.parse", span)?;
|
let s = Self::expect_arg_1::<String>(&args, "Json.parse", span)?;
|
||||||
@@ -3709,6 +3892,26 @@ impl Interpreter {
|
|||||||
}
|
}
|
||||||
Ok(Value::Unit)
|
Ok(Value::Unit)
|
||||||
}
|
}
|
||||||
|
("Test", "assertEqualMsg") => {
|
||||||
|
let expected = request.args.first().cloned().unwrap_or(Value::Unit);
|
||||||
|
let actual = request.args.get(1).cloned().unwrap_or(Value::Unit);
|
||||||
|
let label = match request.args.get(2) {
|
||||||
|
Some(Value::String(s)) => s.clone(),
|
||||||
|
_ => "Values not equal".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if Value::values_equal(&expected, &actual) {
|
||||||
|
self.test_results.borrow_mut().passed += 1;
|
||||||
|
} else {
|
||||||
|
self.test_results.borrow_mut().failed += 1;
|
||||||
|
self.test_results.borrow_mut().failures.push(TestFailure {
|
||||||
|
message: label,
|
||||||
|
expected: Some(format!("{}", expected)),
|
||||||
|
actual: Some(format!("{}", actual)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Value::Unit)
|
||||||
|
}
|
||||||
("Test", "assertNotEqual") => {
|
("Test", "assertNotEqual") => {
|
||||||
let a = request.args.first().cloned().unwrap_or(Value::Unit);
|
let a = request.args.first().cloned().unwrap_or(Value::Unit);
|
||||||
let b = request.args.get(1).cloned().unwrap_or(Value::Unit);
|
let b = request.args.get(1).cloned().unwrap_or(Value::Unit);
|
||||||
@@ -4369,6 +4572,237 @@ impl Interpreter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Concurrent Effect =====
|
||||||
|
("Concurrent", "spawn") => {
|
||||||
|
// For now, spawn just stores the thunk - it will be evaluated on await
|
||||||
|
// In a real implementation, this would start a thread/fiber
|
||||||
|
let thunk = match request.args.first() {
|
||||||
|
Some(v) => v.clone(),
|
||||||
|
_ => return Err(RuntimeError {
|
||||||
|
message: "Concurrent.spawn requires a thunk argument".to_string(),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let task_id = *self.next_task_id.borrow();
|
||||||
|
*self.next_task_id.borrow_mut() += 1;
|
||||||
|
|
||||||
|
// Store task: (thunk, None for result, not cancelled)
|
||||||
|
self.concurrent_tasks.borrow_mut().insert(task_id, (thunk, None, false));
|
||||||
|
|
||||||
|
Ok(Value::Int(task_id))
|
||||||
|
}
|
||||||
|
("Concurrent", "await") => {
|
||||||
|
let task_id = match request.args.first() {
|
||||||
|
Some(Value::Int(id)) => *id,
|
||||||
|
_ => return Err(RuntimeError {
|
||||||
|
message: "Concurrent.await requires a task ID".to_string(),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if already computed or cancelled
|
||||||
|
let task_info = {
|
||||||
|
let tasks = self.concurrent_tasks.borrow();
|
||||||
|
tasks.get(&task_id).cloned()
|
||||||
|
};
|
||||||
|
|
||||||
|
match task_info {
|
||||||
|
Some((_, Some(result), _)) => Ok(result),
|
||||||
|
Some((_, _, true)) => Err(RuntimeError {
|
||||||
|
message: format!("Task {} was cancelled", task_id),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
Some((thunk, None, false)) => {
|
||||||
|
// For cooperative concurrency, we just need to signal
|
||||||
|
// that we're waiting on this task
|
||||||
|
// Return the thunk to be evaluated by the caller
|
||||||
|
// This is a simplification - real async would use fibers
|
||||||
|
Ok(thunk)
|
||||||
|
}
|
||||||
|
None => Err(RuntimeError {
|
||||||
|
message: format!("Unknown task ID: {}", task_id),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
("Concurrent", "yield") => {
|
||||||
|
// In cooperative concurrency, yield allows other tasks to run
|
||||||
|
// For now, this is a no-op in our single-threaded model
|
||||||
|
Ok(Value::Unit)
|
||||||
|
}
|
||||||
|
("Concurrent", "sleep") => {
|
||||||
|
// Non-blocking sleep (delegates to thread sleep for now)
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
let ms = match request.args.first() {
|
||||||
|
Some(Value::Int(n)) => *n as u64,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
thread::sleep(Duration::from_millis(ms));
|
||||||
|
Ok(Value::Unit)
|
||||||
|
}
|
||||||
|
("Concurrent", "cancel") => {
|
||||||
|
let task_id = match request.args.first() {
|
||||||
|
Some(Value::Int(id)) => *id,
|
||||||
|
_ => return Err(RuntimeError {
|
||||||
|
message: "Concurrent.cancel requires a task ID".to_string(),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut tasks = self.concurrent_tasks.borrow_mut();
|
||||||
|
if let Some((thunk, result, _)) = tasks.get(&task_id).cloned() {
|
||||||
|
tasks.insert(task_id, (thunk, result, true));
|
||||||
|
Ok(Value::Bool(true))
|
||||||
|
} else {
|
||||||
|
Ok(Value::Bool(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
("Concurrent", "isRunning") => {
|
||||||
|
let task_id = match request.args.first() {
|
||||||
|
Some(Value::Int(id)) => *id,
|
||||||
|
_ => return Err(RuntimeError {
|
||||||
|
message: "Concurrent.isRunning requires a task ID".to_string(),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let tasks = self.concurrent_tasks.borrow();
|
||||||
|
let is_running = match tasks.get(&task_id) {
|
||||||
|
Some((_, None, false)) => true, // Not completed and not cancelled
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
Ok(Value::Bool(is_running))
|
||||||
|
}
|
||||||
|
("Concurrent", "taskCount") => {
|
||||||
|
let tasks = self.concurrent_tasks.borrow();
|
||||||
|
let count = tasks.iter()
|
||||||
|
.filter(|(_, (_, result, cancelled))| result.is_none() && !cancelled)
|
||||||
|
.count();
|
||||||
|
Ok(Value::Int(count as i64))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Channel Effect =====
|
||||||
|
("Channel", "create") => {
|
||||||
|
let channel_id = *self.next_channel_id.borrow();
|
||||||
|
*self.next_channel_id.borrow_mut() += 1;
|
||||||
|
|
||||||
|
// Create empty channel queue, not closed
|
||||||
|
self.channels.borrow_mut().insert(channel_id, (Vec::new(), false));
|
||||||
|
|
||||||
|
Ok(Value::Int(channel_id))
|
||||||
|
}
|
||||||
|
("Channel", "send") => {
|
||||||
|
let channel_id = match request.args.first() {
|
||||||
|
Some(Value::Int(id)) => *id,
|
||||||
|
_ => return Err(RuntimeError {
|
||||||
|
message: "Channel.send requires a channel ID".to_string(),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let value = match request.args.get(1) {
|
||||||
|
Some(v) => v.clone(),
|
||||||
|
_ => return Err(RuntimeError {
|
||||||
|
message: "Channel.send requires a value".to_string(),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut channels = self.channels.borrow_mut();
|
||||||
|
match channels.get_mut(&channel_id) {
|
||||||
|
Some((queue, false)) => {
|
||||||
|
queue.push(value);
|
||||||
|
Ok(Value::Unit)
|
||||||
|
}
|
||||||
|
Some((_, true)) => Err(RuntimeError {
|
||||||
|
message: format!("Channel {} is closed", channel_id),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
None => Err(RuntimeError {
|
||||||
|
message: format!("Unknown channel ID: {}", channel_id),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
("Channel", "receive") => {
|
||||||
|
let channel_id = match request.args.first() {
|
||||||
|
Some(Value::Int(id)) => *id,
|
||||||
|
_ => return Err(RuntimeError {
|
||||||
|
message: "Channel.receive requires a channel ID".to_string(),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut channels = self.channels.borrow_mut();
|
||||||
|
match channels.get_mut(&channel_id) {
|
||||||
|
Some((queue, _)) if !queue.is_empty() => {
|
||||||
|
Ok(queue.remove(0))
|
||||||
|
}
|
||||||
|
Some((_, true)) => Err(RuntimeError {
|
||||||
|
message: format!("Channel {} is closed and empty", channel_id),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
Some((_, false)) => Err(RuntimeError {
|
||||||
|
message: format!("Channel {} is empty (blocking receive not supported yet)", channel_id),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
None => Err(RuntimeError {
|
||||||
|
message: format!("Unknown channel ID: {}", channel_id),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
("Channel", "tryReceive") => {
|
||||||
|
let channel_id = match request.args.first() {
|
||||||
|
Some(Value::Int(id)) => *id,
|
||||||
|
_ => return Err(RuntimeError {
|
||||||
|
message: "Channel.tryReceive requires a channel ID".to_string(),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut channels = self.channels.borrow_mut();
|
||||||
|
match channels.get_mut(&channel_id) {
|
||||||
|
Some((queue, _)) if !queue.is_empty() => {
|
||||||
|
Ok(Value::Constructor {
|
||||||
|
name: "Some".to_string(),
|
||||||
|
fields: vec![queue.remove(0)],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
Ok(Value::Constructor {
|
||||||
|
name: "None".to_string(),
|
||||||
|
fields: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
None => Err(RuntimeError {
|
||||||
|
message: format!("Unknown channel ID: {}", channel_id),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
("Channel", "close") => {
|
||||||
|
let channel_id = match request.args.first() {
|
||||||
|
Some(Value::Int(id)) => *id,
|
||||||
|
_ => return Err(RuntimeError {
|
||||||
|
message: "Channel.close requires a channel ID".to_string(),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut channels = self.channels.borrow_mut();
|
||||||
|
if let Some((queue, closed)) = channels.get_mut(&channel_id) {
|
||||||
|
*closed = true;
|
||||||
|
Ok(Value::Unit)
|
||||||
|
} else {
|
||||||
|
Err(RuntimeError {
|
||||||
|
message: format!("Unknown channel ID: {}", channel_id),
|
||||||
|
span: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ => Err(RuntimeError {
|
_ => Err(RuntimeError {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Unhandled effect operation: {}.{}",
|
"Unhandled effect operation: {}.{}",
|
||||||
|
|||||||
@@ -493,6 +493,8 @@ impl<'a> Lexer<'a> {
|
|||||||
Some('"') => '"',
|
Some('"') => '"',
|
||||||
Some('0') => '\0',
|
Some('0') => '\0',
|
||||||
Some('\'') => '\'',
|
Some('\'') => '\'',
|
||||||
|
Some('{') => '{',
|
||||||
|
Some('}') => '}',
|
||||||
Some('x') => {
|
Some('x') => {
|
||||||
// Hex escape \xNN
|
// Hex escape \xNN
|
||||||
let h1 = self.advance().and_then(|c| c.to_digit(16));
|
let h1 = self.advance().and_then(|c| c.to_digit(16));
|
||||||
|
|||||||
1143
src/linter.rs
Normal file
1143
src/linter.rs
Normal file
File diff suppressed because it is too large
Load Diff
1215
src/lsp.rs
1215
src/lsp.rs
File diff suppressed because it is too large
Load Diff
1576
src/main.rs
1576
src/main.rs
File diff suppressed because it is too large
Load Diff
@@ -305,6 +305,11 @@ impl ModuleLoader {
|
|||||||
self.cache.iter()
|
self.cache.iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the module cache (for passing to C backend)
|
||||||
|
pub fn module_cache(&self) -> &HashMap<String, Module> {
|
||||||
|
&self.cache
|
||||||
|
}
|
||||||
|
|
||||||
/// Clear the module cache
|
/// Clear the module cache
|
||||||
pub fn clear_cache(&mut self) {
|
pub fn clear_cache(&mut self) {
|
||||||
self.cache.clear();
|
self.cache.clear();
|
||||||
|
|||||||
777
src/package.rs
777
src/package.rs
@@ -6,6 +6,618 @@ use std::collections::HashMap;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Semantic Versioning
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// A semantic version (major.minor.patch)
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct Version {
|
||||||
|
pub major: u32,
|
||||||
|
pub minor: u32,
|
||||||
|
pub patch: u32,
|
||||||
|
pub prerelease: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Version {
|
||||||
|
pub fn new(major: u32, minor: u32, patch: u32) -> Self {
|
||||||
|
Self { major, minor, patch, prerelease: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(s: &str) -> Result<Self, String> {
|
||||||
|
let s = s.trim();
|
||||||
|
|
||||||
|
// Handle prerelease suffix (e.g., "1.0.0-alpha")
|
||||||
|
let (version_part, prerelease) = if let Some(pos) = s.find('-') {
|
||||||
|
(&s[..pos], Some(s[pos + 1..].to_string()))
|
||||||
|
} else {
|
||||||
|
(s, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let parts: Vec<&str> = version_part.split('.').collect();
|
||||||
|
if parts.len() < 2 || parts.len() > 3 {
|
||||||
|
return Err(format!("Invalid version format: {}", s));
|
||||||
|
}
|
||||||
|
|
||||||
|
let major = parts[0].parse::<u32>()
|
||||||
|
.map_err(|_| format!("Invalid major version: {}", parts[0]))?;
|
||||||
|
let minor = parts[1].parse::<u32>()
|
||||||
|
.map_err(|_| format!("Invalid minor version: {}", parts[1]))?;
|
||||||
|
let patch = if parts.len() > 2 {
|
||||||
|
parts[2].parse::<u32>()
|
||||||
|
.map_err(|_| format!("Invalid patch version: {}", parts[2]))?
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self { major, minor, patch, prerelease })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Version {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(ref pre) = self.prerelease {
|
||||||
|
write!(f, "{}.{}.{}-{}", self.major, self.minor, self.patch, pre)
|
||||||
|
} else {
|
||||||
|
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Version {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
match self.major.cmp(&other.major) {
|
||||||
|
Ordering::Equal => {}
|
||||||
|
ord => return ord,
|
||||||
|
}
|
||||||
|
match self.minor.cmp(&other.minor) {
|
||||||
|
Ordering::Equal => {}
|
||||||
|
ord => return ord,
|
||||||
|
}
|
||||||
|
match self.patch.cmp(&other.patch) {
|
||||||
|
Ordering::Equal => {}
|
||||||
|
ord => return ord,
|
||||||
|
}
|
||||||
|
// Prerelease versions are less than release versions
|
||||||
|
match (&self.prerelease, &other.prerelease) {
|
||||||
|
(None, None) => Ordering::Equal,
|
||||||
|
(Some(_), None) => Ordering::Less,
|
||||||
|
(None, Some(_)) => Ordering::Greater,
|
||||||
|
(Some(a), Some(b)) => a.cmp(b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Version {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Version constraint for dependencies
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum VersionConstraint {
|
||||||
|
/// Exact version: "1.2.3"
|
||||||
|
Exact(Version),
|
||||||
|
/// Caret: "^1.2.3" - compatible updates (>=1.2.3, <2.0.0)
|
||||||
|
Caret(Version),
|
||||||
|
/// Tilde: "~1.2.3" - patch updates only (>=1.2.3, <1.3.0)
|
||||||
|
Tilde(Version),
|
||||||
|
/// Greater than or equal: ">=1.2.3"
|
||||||
|
GreaterEq(Version),
|
||||||
|
/// Less than: "<2.0.0"
|
||||||
|
Less(Version),
|
||||||
|
/// Range: ">=1.0.0, <2.0.0"
|
||||||
|
Range { min: Version, max: Version },
|
||||||
|
/// Any version: "*"
|
||||||
|
Any,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VersionConstraint {
|
||||||
|
pub fn parse(s: &str) -> Result<Self, String> {
|
||||||
|
let s = s.trim();
|
||||||
|
|
||||||
|
if s == "*" {
|
||||||
|
return Ok(VersionConstraint::Any);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for range (comma-separated constraints)
|
||||||
|
if s.contains(',') {
|
||||||
|
let parts: Vec<&str> = s.split(',').collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err("Range must have exactly two constraints".to_string());
|
||||||
|
}
|
||||||
|
let first = VersionConstraint::parse(parts[0].trim())?;
|
||||||
|
let second = VersionConstraint::parse(parts[1].trim())?;
|
||||||
|
|
||||||
|
match (first, second) {
|
||||||
|
(VersionConstraint::GreaterEq(min), VersionConstraint::Less(max)) => {
|
||||||
|
Ok(VersionConstraint::Range { min, max })
|
||||||
|
}
|
||||||
|
_ => Err("Range must be >=version, <version".to_string())
|
||||||
|
}
|
||||||
|
} else if let Some(rest) = s.strip_prefix('^') {
|
||||||
|
Ok(VersionConstraint::Caret(Version::parse(rest)?))
|
||||||
|
} else if let Some(rest) = s.strip_prefix('~') {
|
||||||
|
Ok(VersionConstraint::Tilde(Version::parse(rest)?))
|
||||||
|
} else if let Some(rest) = s.strip_prefix(">=") {
|
||||||
|
Ok(VersionConstraint::GreaterEq(Version::parse(rest)?))
|
||||||
|
} else if let Some(rest) = s.strip_prefix('<') {
|
||||||
|
Ok(VersionConstraint::Less(Version::parse(rest)?))
|
||||||
|
} else {
|
||||||
|
// Try to parse as exact version
|
||||||
|
Ok(VersionConstraint::Exact(Version::parse(s)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a version satisfies this constraint
|
||||||
|
pub fn satisfies(&self, version: &Version) -> bool {
|
||||||
|
match self {
|
||||||
|
VersionConstraint::Exact(v) => version == v,
|
||||||
|
VersionConstraint::Caret(v) => {
|
||||||
|
// ^1.2.3 means >=1.2.3, <2.0.0 (if major > 0)
|
||||||
|
// ^0.2.3 means >=0.2.3, <0.3.0 (if major == 0)
|
||||||
|
if v.major == 0 {
|
||||||
|
version.major == 0 && version.minor == v.minor && version >= v
|
||||||
|
} else {
|
||||||
|
version.major == v.major && version >= v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VersionConstraint::Tilde(v) => {
|
||||||
|
// ~1.2.3 means >=1.2.3, <1.3.0
|
||||||
|
version.major == v.major && version.minor == v.minor && version >= v
|
||||||
|
}
|
||||||
|
VersionConstraint::GreaterEq(v) => version >= v,
|
||||||
|
VersionConstraint::Less(v) => version < v,
|
||||||
|
VersionConstraint::Range { min, max } => version >= min && version < max,
|
||||||
|
VersionConstraint::Any => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for VersionConstraint {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
VersionConstraint::Exact(v) => write!(f, "{}", v),
|
||||||
|
VersionConstraint::Caret(v) => write!(f, "^{}", v),
|
||||||
|
VersionConstraint::Tilde(v) => write!(f, "~{}", v),
|
||||||
|
VersionConstraint::GreaterEq(v) => write!(f, ">={}", v),
|
||||||
|
VersionConstraint::Less(v) => write!(f, "<{}", v),
|
||||||
|
VersionConstraint::Range { min, max } => write!(f, ">={}, <{}", min, max),
|
||||||
|
VersionConstraint::Any => write!(f, "*"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Lock File
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// A lock file entry for a resolved package
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LockedPackage {
|
||||||
|
pub name: String,
|
||||||
|
pub version: Version,
|
||||||
|
pub source: LockedSource,
|
||||||
|
pub checksum: Option<String>,
|
||||||
|
pub dependencies: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Source of a locked package
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum LockedSource {
|
||||||
|
Registry,
|
||||||
|
Git { url: String, rev: String },
|
||||||
|
Path { path: PathBuf },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The lock file (lux.lock)
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct LockFile {
|
||||||
|
pub packages: Vec<LockedPackage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LockFile {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { packages: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a lock file
|
||||||
|
pub fn parse(content: &str) -> Result<Self, String> {
|
||||||
|
let mut packages = Vec::new();
|
||||||
|
let mut current_pkg: Option<LockedPackage> = None;
|
||||||
|
let mut in_package = false;
|
||||||
|
|
||||||
|
for line in content.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
|
||||||
|
if line.is_empty() || line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if line == "[[package]]" {
|
||||||
|
if let Some(pkg) = current_pkg.take() {
|
||||||
|
packages.push(pkg);
|
||||||
|
}
|
||||||
|
current_pkg = Some(LockedPackage {
|
||||||
|
name: String::new(),
|
||||||
|
version: Version::new(0, 0, 0),
|
||||||
|
source: LockedSource::Registry,
|
||||||
|
checksum: None,
|
||||||
|
dependencies: Vec::new(),
|
||||||
|
});
|
||||||
|
in_package = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if in_package {
|
||||||
|
if let Some(ref mut pkg) = current_pkg {
|
||||||
|
if let Some(eq_pos) = line.find('=') {
|
||||||
|
let key = line[..eq_pos].trim();
|
||||||
|
let value = line[eq_pos + 1..].trim().trim_matches('"');
|
||||||
|
|
||||||
|
match key {
|
||||||
|
"name" => pkg.name = value.to_string(),
|
||||||
|
"version" => pkg.version = Version::parse(value)?,
|
||||||
|
"source" => {
|
||||||
|
if value == "registry" {
|
||||||
|
pkg.source = LockedSource::Registry;
|
||||||
|
} else if value.starts_with("git:") {
|
||||||
|
let parts: Vec<&str> = value[4..].splitn(2, '@').collect();
|
||||||
|
pkg.source = LockedSource::Git {
|
||||||
|
url: parts[0].to_string(),
|
||||||
|
rev: parts.get(1).unwrap_or(&"HEAD").to_string(),
|
||||||
|
};
|
||||||
|
} else if value.starts_with("path:") {
|
||||||
|
pkg.source = LockedSource::Path {
|
||||||
|
path: PathBuf::from(&value[5..]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"checksum" => pkg.checksum = Some(value.to_string()),
|
||||||
|
"dependencies" => {
|
||||||
|
// Parse array
|
||||||
|
let deps_str = value.trim_matches(|c| c == '[' || c == ']');
|
||||||
|
pkg.dependencies = deps_str
|
||||||
|
.split(',')
|
||||||
|
.map(|s| s.trim().trim_matches('"').to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(pkg) = current_pkg {
|
||||||
|
packages.push(pkg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { packages })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format lock file as TOML
|
||||||
|
pub fn format(&self) -> String {
|
||||||
|
let mut output = String::new();
|
||||||
|
output.push_str("# This file is auto-generated by lux pkg. Do not edit manually.\n\n");
|
||||||
|
|
||||||
|
for pkg in &self.packages {
|
||||||
|
output.push_str("[[package]]\n");
|
||||||
|
output.push_str(&format!("name = \"{}\"\n", pkg.name));
|
||||||
|
output.push_str(&format!("version = \"{}\"\n", pkg.version));
|
||||||
|
|
||||||
|
let source_str = match &pkg.source {
|
||||||
|
LockedSource::Registry => "registry".to_string(),
|
||||||
|
LockedSource::Git { url, rev } => format!("git:{}@{}", url, rev),
|
||||||
|
LockedSource::Path { path } => format!("path:{}", path.display()),
|
||||||
|
};
|
||||||
|
output.push_str(&format!("source = \"{}\"\n", source_str));
|
||||||
|
|
||||||
|
if let Some(ref checksum) = pkg.checksum {
|
||||||
|
output.push_str(&format!("checksum = \"{}\"\n", checksum));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pkg.dependencies.is_empty() {
|
||||||
|
let deps: Vec<String> = pkg.dependencies.iter()
|
||||||
|
.map(|d| format!("\"{}\"", d))
|
||||||
|
.collect();
|
||||||
|
output.push_str(&format!("dependencies = [{}]\n", deps.join(", ")));
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find a locked package by name
|
||||||
|
pub fn find(&self, name: &str) -> Option<&LockedPackage> {
|
||||||
|
self.packages.iter().find(|p| p.name == name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Dependency Resolution
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Resolution error
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ResolutionError {
|
||||||
|
pub message: String,
|
||||||
|
pub package: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ResolutionError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(ref pkg) = self.package {
|
||||||
|
write!(f, "Resolution error for '{}': {}", pkg, self.message)
|
||||||
|
} else {
|
||||||
|
write!(f, "Resolution error: {}", self.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dependency resolver with transitive dependency support
|
||||||
|
pub struct Resolver {
|
||||||
|
/// Available versions for each package (simulated for now)
|
||||||
|
available_versions: HashMap<String, Vec<Version>>,
|
||||||
|
/// Package dependencies cache (package@version -> dependencies)
|
||||||
|
package_deps: HashMap<String, HashMap<String, Dependency>>,
|
||||||
|
/// Packages directory for reading transitive deps
|
||||||
|
packages_dir: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Resolver {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
available_versions: HashMap::new(),
|
||||||
|
package_deps: HashMap::new(),
|
||||||
|
packages_dir: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create resolver with packages directory for reading transitive deps
|
||||||
|
pub fn with_packages_dir(packages_dir: &Path) -> Self {
|
||||||
|
Self {
|
||||||
|
available_versions: HashMap::new(),
|
||||||
|
package_deps: HashMap::new(),
|
||||||
|
packages_dir: Some(packages_dir.to_path_buf()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add available versions for a package (for testing/registry integration)
|
||||||
|
pub fn add_available_versions(&mut self, name: &str, versions: Vec<Version>) {
|
||||||
|
self.available_versions.insert(name.to_string(), versions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add package dependencies (for testing or when loaded from registry)
|
||||||
|
pub fn add_package_deps(&mut self, name: &str, version: &Version, deps: HashMap<String, Dependency>) {
|
||||||
|
let key = format!("{}@{}", name, version);
|
||||||
|
self.package_deps.insert(key, deps);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve dependencies to a lock file (with transitive dependencies)
|
||||||
|
pub fn resolve(
|
||||||
|
&self,
|
||||||
|
manifest: &Manifest,
|
||||||
|
existing_lock: Option<&LockFile>,
|
||||||
|
) -> Result<LockFile, ResolutionError> {
|
||||||
|
let mut lock = LockFile::new();
|
||||||
|
let mut resolved: HashMap<String, (Version, LockedSource)> = HashMap::new();
|
||||||
|
let mut to_resolve: Vec<(String, Dependency, Option<String>)> = Vec::new();
|
||||||
|
|
||||||
|
// If we have an existing lock file, prefer those versions
|
||||||
|
if let Some(existing) = existing_lock {
|
||||||
|
for pkg in &existing.packages {
|
||||||
|
resolved.insert(pkg.name.clone(), (pkg.version.clone(), pkg.source.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue direct dependencies for resolution
|
||||||
|
for (name, dep) in &manifest.dependencies {
|
||||||
|
to_resolve.push((name.clone(), dep.clone(), None));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process queue (breadth-first for better dependency order)
|
||||||
|
while let Some((name, dep, required_by)) = to_resolve.pop() {
|
||||||
|
// Skip if already resolved with compatible version
|
||||||
|
if let Some((existing_version, _)) = resolved.get(&name) {
|
||||||
|
let constraint = VersionConstraint::parse(&dep.version)
|
||||||
|
.map_err(|e| ResolutionError {
|
||||||
|
message: e,
|
||||||
|
package: Some(name.clone()),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if constraint.satisfies(existing_version) {
|
||||||
|
continue; // Already have a compatible version
|
||||||
|
} else {
|
||||||
|
// Version conflict
|
||||||
|
return Err(ResolutionError {
|
||||||
|
message: format!(
|
||||||
|
"Version conflict: {} requires {} {}, but {} is already resolved{}",
|
||||||
|
required_by.as_deref().unwrap_or("project"),
|
||||||
|
name,
|
||||||
|
dep.version,
|
||||||
|
existing_version,
|
||||||
|
if let Some(rb) = &required_by {
|
||||||
|
format!(" (required by {})", rb)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
package: Some(name.clone()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let constraint = VersionConstraint::parse(&dep.version)
|
||||||
|
.map_err(|e| ResolutionError {
|
||||||
|
message: e,
|
||||||
|
package: Some(name.clone()),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Resolve the version
|
||||||
|
let version = self.select_version(&name, &constraint, &dep.source)?;
|
||||||
|
|
||||||
|
let source = match &dep.source {
|
||||||
|
DependencySource::Registry => LockedSource::Registry,
|
||||||
|
DependencySource::Git { url, branch } => LockedSource::Git {
|
||||||
|
url: url.clone(),
|
||||||
|
rev: branch.clone().unwrap_or_else(|| "HEAD".to_string()),
|
||||||
|
},
|
||||||
|
DependencySource::Path { path } => LockedSource::Path { path: path.clone() },
|
||||||
|
};
|
||||||
|
|
||||||
|
resolved.insert(name.clone(), (version.clone(), source.clone()));
|
||||||
|
|
||||||
|
// Get transitive dependencies
|
||||||
|
let transitive_deps = self.get_package_dependencies(&name, &version, &dep.source);
|
||||||
|
for (trans_name, trans_dep) in transitive_deps {
|
||||||
|
if !resolved.contains_key(&trans_name) {
|
||||||
|
to_resolve.push((trans_name, trans_dep, Some(name.clone())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build lock file from resolved packages
|
||||||
|
for (name, (version, source)) in &resolved {
|
||||||
|
// Get the dependency list for this package
|
||||||
|
let deps = self.get_package_dependencies(name, version, &match source {
|
||||||
|
LockedSource::Registry => DependencySource::Registry,
|
||||||
|
LockedSource::Git { url, rev } => DependencySource::Git {
|
||||||
|
url: url.clone(),
|
||||||
|
branch: Some(rev.clone()),
|
||||||
|
},
|
||||||
|
LockedSource::Path { path } => DependencySource::Path { path: path.clone() },
|
||||||
|
});
|
||||||
|
let dep_names: Vec<String> = deps.keys().cloned().collect();
|
||||||
|
|
||||||
|
lock.packages.push(LockedPackage {
|
||||||
|
name: name.clone(),
|
||||||
|
version: version.clone(),
|
||||||
|
source: source.clone(),
|
||||||
|
checksum: None,
|
||||||
|
dependencies: dep_names,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort packages by name for deterministic output
|
||||||
|
lock.packages.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
|
||||||
|
Ok(lock)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get dependencies of a package
|
||||||
|
fn get_package_dependencies(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
version: &Version,
|
||||||
|
source: &DependencySource,
|
||||||
|
) -> HashMap<String, Dependency> {
|
||||||
|
// First check our cache
|
||||||
|
let key = format!("{}@{}", name, version);
|
||||||
|
if let Some(deps) = self.package_deps.get(&key) {
|
||||||
|
return deps.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to read from installed package
|
||||||
|
if let Some(ref packages_dir) = self.packages_dir {
|
||||||
|
let pkg_dir = packages_dir.join(name);
|
||||||
|
let manifest_path = pkg_dir.join("lux.toml");
|
||||||
|
|
||||||
|
if manifest_path.exists() {
|
||||||
|
if let Ok(content) = fs::read_to_string(&manifest_path) {
|
||||||
|
if let Ok(manifest) = parse_manifest(&content) {
|
||||||
|
return manifest.dependencies;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For path dependencies, read from the path
|
||||||
|
if let DependencySource::Path { path } = source {
|
||||||
|
let manifest_path = if path.is_absolute() {
|
||||||
|
path.join("lux.toml")
|
||||||
|
} else if let Some(ref packages_dir) = self.packages_dir {
|
||||||
|
packages_dir.parent().unwrap_or(packages_dir).join(path).join("lux.toml")
|
||||||
|
} else {
|
||||||
|
path.join("lux.toml")
|
||||||
|
};
|
||||||
|
|
||||||
|
if manifest_path.exists() {
|
||||||
|
if let Ok(content) = fs::read_to_string(&manifest_path) {
|
||||||
|
if let Ok(manifest) = parse_manifest(&content) {
|
||||||
|
return manifest.dependencies;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No dependencies found
|
||||||
|
HashMap::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select the best version that satisfies the constraint
|
||||||
|
fn select_version(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
constraint: &VersionConstraint,
|
||||||
|
source: &DependencySource,
|
||||||
|
) -> Result<Version, ResolutionError> {
|
||||||
|
match source {
|
||||||
|
DependencySource::Git { .. } | DependencySource::Path { .. } => {
|
||||||
|
// For git/path sources, use the version from the constraint or 0.0.0
|
||||||
|
match constraint {
|
||||||
|
VersionConstraint::Exact(v) => Ok(v.clone()),
|
||||||
|
_ => Ok(Version::new(0, 0, 0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DependencySource::Registry => {
|
||||||
|
// Check available versions
|
||||||
|
if let Some(versions) = self.available_versions.get(name) {
|
||||||
|
// Find the highest version that satisfies the constraint
|
||||||
|
let mut matching: Vec<&Version> = versions
|
||||||
|
.iter()
|
||||||
|
.filter(|v| constraint.satisfies(v))
|
||||||
|
.collect();
|
||||||
|
matching.sort();
|
||||||
|
matching.reverse();
|
||||||
|
|
||||||
|
if let Some(v) = matching.first() {
|
||||||
|
return Ok((*v).clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No available versions - use the constraint's base version
|
||||||
|
match constraint {
|
||||||
|
VersionConstraint::Exact(v) => Ok(v.clone()),
|
||||||
|
VersionConstraint::Caret(v) => Ok(v.clone()),
|
||||||
|
VersionConstraint::Tilde(v) => Ok(v.clone()),
|
||||||
|
VersionConstraint::GreaterEq(v) => Ok(v.clone()),
|
||||||
|
VersionConstraint::Range { min, .. } => Ok(min.clone()),
|
||||||
|
VersionConstraint::Less(_) | VersionConstraint::Any => {
|
||||||
|
// Can't determine version without registry
|
||||||
|
Ok(Version::new(0, 0, 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Resolver {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Manifest and Package Manager
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
/// Package manifest (lux.toml)
|
/// Package manifest (lux.toml)
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -69,6 +681,43 @@ impl PackageManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load the lock file (lux.lock)
|
||||||
|
pub fn load_lock(&self) -> Result<Option<LockFile>, String> {
|
||||||
|
let lock_path = self.project_root.join("lux.lock");
|
||||||
|
|
||||||
|
if !lock_path.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = fs::read_to_string(&lock_path)
|
||||||
|
.map_err(|e| format!("Failed to read lux.lock: {}", e))?;
|
||||||
|
|
||||||
|
LockFile::parse(&content).map(Some)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save the lock file (lux.lock)
|
||||||
|
pub fn save_lock(&self, lock: &LockFile) -> Result<(), String> {
|
||||||
|
let lock_path = self.project_root.join("lux.lock");
|
||||||
|
let content = lock.format();
|
||||||
|
|
||||||
|
fs::write(&lock_path, content)
|
||||||
|
.map_err(|e| format!("Failed to write lux.lock: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve dependencies and generate/update lock file
|
||||||
|
pub fn resolve(&self) -> Result<LockFile, String> {
|
||||||
|
let manifest = self.load_manifest()?;
|
||||||
|
let existing_lock = self.load_lock()?;
|
||||||
|
|
||||||
|
// Use resolver with packages directory for transitive dep lookup
|
||||||
|
let resolver = Resolver::with_packages_dir(&self.packages_dir);
|
||||||
|
let lock = resolver.resolve(&manifest, existing_lock.as_ref())
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
self.save_lock(&lock)?;
|
||||||
|
Ok(lock)
|
||||||
|
}
|
||||||
|
|
||||||
/// Find the project root by looking for lux.toml
|
/// Find the project root by looking for lux.toml
|
||||||
pub fn find_project_root() -> Option<PathBuf> {
|
pub fn find_project_root() -> Option<PathBuf> {
|
||||||
let mut current = std::env::current_dir().ok()?;
|
let mut current = std::env::current_dir().ok()?;
|
||||||
@@ -154,19 +803,139 @@ impl PackageManager {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve dependencies and generate/update lock file
|
||||||
|
let lock = self.resolve()?;
|
||||||
|
|
||||||
// Create packages directory
|
// Create packages directory
|
||||||
fs::create_dir_all(&self.packages_dir)
|
fs::create_dir_all(&self.packages_dir)
|
||||||
.map_err(|e| format!("Failed to create packages directory: {}", e))?;
|
.map_err(|e| format!("Failed to create packages directory: {}", e))?;
|
||||||
|
|
||||||
println!("Installing {} dependencies...", manifest.dependencies.len());
|
println!("Installing {} dependencies...", lock.packages.len());
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
for (_name, dep) in &manifest.dependencies {
|
// Install from lock file for reproducibility
|
||||||
self.install_dependency(dep)?;
|
for locked_pkg in &lock.packages {
|
||||||
|
self.install_locked_package(locked_pkg, &manifest)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!("Done! Installed {} packages.", manifest.dependencies.len());
|
println!("Done! Installed {} packages.", lock.packages.len());
|
||||||
|
println!("Lock file written to lux.lock");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install a package from the lock file
|
||||||
|
fn install_locked_package(&self, locked: &LockedPackage, manifest: &Manifest) -> Result<(), String> {
|
||||||
|
print!(" Installing {} v{}... ", locked.name, locked.version);
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
|
||||||
|
let dest_dir = self.packages_dir.join(&locked.name);
|
||||||
|
|
||||||
|
// Get the dependency info from manifest for source details
|
||||||
|
let dep = manifest.dependencies.get(&locked.name);
|
||||||
|
|
||||||
|
match &locked.source {
|
||||||
|
LockedSource::Registry => {
|
||||||
|
self.install_from_registry_locked(locked, &dest_dir)?;
|
||||||
|
}
|
||||||
|
LockedSource::Git { url, rev } => {
|
||||||
|
self.install_from_git_locked(url, rev, &dest_dir)?;
|
||||||
|
}
|
||||||
|
LockedSource::Path { path } => {
|
||||||
|
let source_path = if let Some(d) = dep {
|
||||||
|
match &d.source {
|
||||||
|
DependencySource::Path { path } => path.clone(),
|
||||||
|
_ => path.clone(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path.clone()
|
||||||
|
};
|
||||||
|
self.install_from_path(&source_path, &dest_dir)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("done");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_from_registry_locked(&self, locked: &LockedPackage, dest: &Path) -> Result<(), String> {
|
||||||
|
// Check if already installed with correct version
|
||||||
|
let version_file = dest.join(".version");
|
||||||
|
if version_file.exists() {
|
||||||
|
let installed_version = fs::read_to_string(&version_file).unwrap_or_default();
|
||||||
|
if installed_version.trim() == locked.version.to_string() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
let cache_path = self.cache_dir.join(&locked.name).join(locked.version.to_string());
|
||||||
|
|
||||||
|
if cache_path.exists() {
|
||||||
|
// Copy from cache
|
||||||
|
copy_dir_recursive(&cache_path, dest)?;
|
||||||
|
} else {
|
||||||
|
// Create placeholder package (in real impl, would download)
|
||||||
|
fs::create_dir_all(dest)
|
||||||
|
.map_err(|e| format!("Failed to create package directory: {}", e))?;
|
||||||
|
|
||||||
|
// Create a lib.lux placeholder
|
||||||
|
let lib_content = format!(
|
||||||
|
"// Package: {} v{}\n// This is a placeholder - real package would be downloaded from registry\n\n",
|
||||||
|
locked.name, locked.version
|
||||||
|
);
|
||||||
|
fs::write(dest.join("lib.lux"), lib_content)
|
||||||
|
.map_err(|e| format!("Failed to create lib.lux: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write version file
|
||||||
|
fs::write(&version_file, locked.version.to_string())
|
||||||
|
.map_err(|e| format!("Failed to write version file: {}", e))?;
|
||||||
|
|
||||||
|
// Verify checksum if present
|
||||||
|
if let Some(ref expected) = locked.checksum {
|
||||||
|
// In a real implementation, verify the checksum here
|
||||||
|
let _ = expected; // Placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_from_git_locked(&self, url: &str, rev: &str, dest: &Path) -> Result<(), String> {
|
||||||
|
// Remove existing if present
|
||||||
|
if dest.exists() {
|
||||||
|
fs::remove_dir_all(dest)
|
||||||
|
.map_err(|e| format!("Failed to remove existing directory: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone at specific revision
|
||||||
|
let mut cmd = std::process::Command::new("git");
|
||||||
|
cmd.arg("clone")
|
||||||
|
.arg("--depth").arg("1");
|
||||||
|
|
||||||
|
// If rev is not HEAD, we need to fetch the specific revision
|
||||||
|
if rev != "HEAD" && !rev.is_empty() {
|
||||||
|
cmd.arg("--branch").arg(rev);
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.arg(url).arg(dest);
|
||||||
|
|
||||||
|
let output = cmd.output()
|
||||||
|
.map_err(|e| format!("Failed to run git: {}", e))?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(format!(
|
||||||
|
"Git clone failed: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove .git directory to save space
|
||||||
|
let git_dir = dest.join(".git");
|
||||||
|
if git_dir.exists() {
|
||||||
|
fs::remove_dir_all(&git_dir).ok();
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ impl Parser {
|
|||||||
TokenKind::Let => Ok(Declaration::Let(self.parse_let_decl(visibility, doc)?)),
|
TokenKind::Let => Ok(Declaration::Let(self.parse_let_decl(visibility, doc)?)),
|
||||||
TokenKind::Trait => Ok(Declaration::Trait(self.parse_trait_decl(visibility, doc)?)),
|
TokenKind::Trait => Ok(Declaration::Trait(self.parse_trait_decl(visibility, doc)?)),
|
||||||
TokenKind::Impl => Ok(Declaration::Impl(self.parse_impl_decl()?)),
|
TokenKind::Impl => Ok(Declaration::Impl(self.parse_impl_decl()?)),
|
||||||
|
TokenKind::Run => Err(self.error("Bare 'run' expressions are not allowed at top level. Use 'let _ = run ...' or 'let result = run ...'")),
|
||||||
_ => Err(self.error("Expected declaration (fn, effect, handler, type, trait, impl, or let)")),
|
_ => Err(self.error("Expected declaration (fn, effect, handler, type, trait, impl, or let)")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -532,7 +533,14 @@ impl Parser {
|
|||||||
let start = self.current_span();
|
let start = self.current_span();
|
||||||
self.expect(TokenKind::Let)?;
|
self.expect(TokenKind::Let)?;
|
||||||
|
|
||||||
let name = self.parse_ident()?;
|
// Allow underscore as wildcard pattern (discards the value)
|
||||||
|
let name = if self.check(TokenKind::Underscore) {
|
||||||
|
let span = self.current_span();
|
||||||
|
self.advance();
|
||||||
|
Ident::new("_".to_string(), span)
|
||||||
|
} else {
|
||||||
|
self.parse_ident()?
|
||||||
|
};
|
||||||
|
|
||||||
let typ = if self.check(TokenKind::Colon) {
|
let typ = if self.check(TokenKind::Colon) {
|
||||||
self.advance();
|
self.advance();
|
||||||
@@ -871,7 +879,8 @@ impl Parser {
|
|||||||
Ok(effects)
|
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> {
|
fn parse_behavioral_properties(&mut self) -> Result<Vec<BehavioralProperty>, ParseError> {
|
||||||
let mut properties = Vec::new();
|
let mut properties = Vec::new();
|
||||||
|
|
||||||
@@ -893,9 +902,15 @@ impl Parser {
|
|||||||
let property = self.parse_single_property()?;
|
let property = self.parse_single_property()?;
|
||||||
properties.push(property);
|
properties.push(property);
|
||||||
|
|
||||||
// Optional comma for multiple properties: is pure, is total
|
// After first property, allow comma-separated list without repeating 'is'
|
||||||
if self.check(TokenKind::Comma) {
|
while self.check(TokenKind::Comma) {
|
||||||
self.advance();
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1631,6 +1646,20 @@ impl Parser {
|
|||||||
} else if self.check(TokenKind::Dot) {
|
} else if self.check(TokenKind::Dot) {
|
||||||
let start = expr.span();
|
let start = expr.span();
|
||||||
self.advance();
|
self.advance();
|
||||||
|
|
||||||
|
// Check for tuple index access: expr.0, expr.1, etc.
|
||||||
|
if let TokenKind::Int(n) = self.peek_kind() {
|
||||||
|
let index = n as usize;
|
||||||
|
self.advance();
|
||||||
|
let span = start.merge(self.previous_span());
|
||||||
|
expr = Expr::TupleIndex {
|
||||||
|
object: Box::new(expr),
|
||||||
|
index,
|
||||||
|
span,
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let field = self.parse_ident()?;
|
let field = self.parse_ident()?;
|
||||||
|
|
||||||
// Check if this is an effect operation: Effect.operation(args)
|
// Check if this is an effect operation: Effect.operation(args)
|
||||||
@@ -1666,11 +1695,14 @@ impl Parser {
|
|||||||
|
|
||||||
fn parse_args(&mut self) -> Result<Vec<Expr>, ParseError> {
|
fn parse_args(&mut self) -> Result<Vec<Expr>, ParseError> {
|
||||||
let mut args = Vec::new();
|
let mut args = Vec::new();
|
||||||
|
self.skip_newlines();
|
||||||
|
|
||||||
while !self.check(TokenKind::RParen) {
|
while !self.check(TokenKind::RParen) {
|
||||||
args.push(self.parse_expr()?);
|
args.push(self.parse_expr()?);
|
||||||
|
self.skip_newlines();
|
||||||
if !self.check(TokenKind::RParen) {
|
if !self.check(TokenKind::RParen) {
|
||||||
self.expect(TokenKind::Comma)?;
|
self.expect(TokenKind::Comma)?;
|
||||||
|
self.skip_newlines();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1872,6 +1904,14 @@ impl Parser {
|
|||||||
span: token.span,
|
span: token.span,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
TokenKind::Char(c) => {
|
||||||
|
let c = *c;
|
||||||
|
self.advance();
|
||||||
|
Ok(Pattern::Literal(Literal {
|
||||||
|
kind: LiteralKind::Char(c),
|
||||||
|
span: token.span,
|
||||||
|
}))
|
||||||
|
}
|
||||||
TokenKind::Ident(name) => {
|
TokenKind::Ident(name) => {
|
||||||
// Check if it starts with uppercase (constructor) or lowercase (variable)
|
// Check if it starts with uppercase (constructor) or lowercase (variable)
|
||||||
if name.chars().next().map_or(false, |c| c.is_uppercase()) {
|
if name.chars().next().map_or(false, |c| c.is_uppercase()) {
|
||||||
@@ -1962,7 +2002,14 @@ impl Parser {
|
|||||||
let start = self.current_span();
|
let start = self.current_span();
|
||||||
self.expect(TokenKind::Let)?;
|
self.expect(TokenKind::Let)?;
|
||||||
|
|
||||||
let name = self.parse_ident()?;
|
// Allow underscore as wildcard pattern (discards the value)
|
||||||
|
let name = if self.check(TokenKind::Underscore) {
|
||||||
|
let span = self.current_span();
|
||||||
|
self.advance();
|
||||||
|
Ident::new("_".to_string(), span)
|
||||||
|
} else {
|
||||||
|
self.parse_ident()?
|
||||||
|
};
|
||||||
|
|
||||||
let typ = if self.check(TokenKind::Colon) {
|
let typ = if self.check(TokenKind::Colon) {
|
||||||
self.advance();
|
self.advance();
|
||||||
@@ -2207,7 +2254,15 @@ impl Parser {
|
|||||||
// Let statement
|
// Let statement
|
||||||
let let_start = self.current_span();
|
let let_start = self.current_span();
|
||||||
self.advance();
|
self.advance();
|
||||||
let name = self.parse_ident()?;
|
|
||||||
|
// Allow underscore as wildcard pattern (discards the value)
|
||||||
|
let name = if self.check(TokenKind::Underscore) {
|
||||||
|
let span = self.current_span();
|
||||||
|
self.advance();
|
||||||
|
Ident::new("_".to_string(), span)
|
||||||
|
} else {
|
||||||
|
self.parse_ident()?
|
||||||
|
};
|
||||||
|
|
||||||
let typ = if self.check(TokenKind::Colon) {
|
let typ = if self.check(TokenKind::Colon) {
|
||||||
self.advance();
|
self.advance();
|
||||||
@@ -2266,12 +2321,15 @@ impl Parser {
|
|||||||
fn parse_list_expr(&mut self) -> Result<Expr, ParseError> {
|
fn parse_list_expr(&mut self) -> Result<Expr, ParseError> {
|
||||||
let start = self.current_span();
|
let start = self.current_span();
|
||||||
self.expect(TokenKind::LBracket)?;
|
self.expect(TokenKind::LBracket)?;
|
||||||
|
self.skip_newlines();
|
||||||
|
|
||||||
let mut elements = Vec::new();
|
let mut elements = Vec::new();
|
||||||
while !self.check(TokenKind::RBracket) {
|
while !self.check(TokenKind::RBracket) {
|
||||||
elements.push(self.parse_expr()?);
|
elements.push(self.parse_expr()?);
|
||||||
|
self.skip_newlines();
|
||||||
if !self.check(TokenKind::RBracket) {
|
if !self.check(TokenKind::RBracket) {
|
||||||
self.expect(TokenKind::Comma)?;
|
self.expect(TokenKind::Comma)?;
|
||||||
|
self.skip_newlines();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
637
src/registry.rs
Normal file
637
src/registry.rs
Normal file
@@ -0,0 +1,637 @@
|
|||||||
|
//! Package Registry Server for Lux
|
||||||
|
//!
|
||||||
|
//! Provides a central repository for sharing Lux packages.
|
||||||
|
//! The registry serves package metadata and tarballs via HTTP.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::net::{TcpListener, TcpStream};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
/// Package metadata stored in the registry
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PackageMetadata {
|
||||||
|
pub name: String,
|
||||||
|
pub version: String,
|
||||||
|
pub description: String,
|
||||||
|
pub authors: Vec<String>,
|
||||||
|
pub license: Option<String>,
|
||||||
|
pub repository: Option<String>,
|
||||||
|
pub keywords: Vec<String>,
|
||||||
|
pub dependencies: HashMap<String, String>,
|
||||||
|
pub checksum: String,
|
||||||
|
pub published_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A version entry for a package
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct VersionEntry {
|
||||||
|
pub version: String,
|
||||||
|
pub checksum: String,
|
||||||
|
pub published_at: String,
|
||||||
|
pub yanked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Package index entry (all versions of a package)
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PackageIndex {
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub versions: Vec<VersionEntry>,
|
||||||
|
pub latest_version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The package registry
|
||||||
|
pub struct Registry {
|
||||||
|
/// Base directory for storing packages
|
||||||
|
storage_dir: PathBuf,
|
||||||
|
/// In-memory index of all packages
|
||||||
|
index: Arc<RwLock<HashMap<String, PackageIndex>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Registry {
|
||||||
|
/// Create a new registry with the given storage directory
|
||||||
|
pub fn new(storage_dir: &Path) -> Self {
|
||||||
|
let registry = Self {
|
||||||
|
storage_dir: storage_dir.to_path_buf(),
|
||||||
|
index: Arc::new(RwLock::new(HashMap::new())),
|
||||||
|
};
|
||||||
|
registry.load_index();
|
||||||
|
registry
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the package index from disk
|
||||||
|
fn load_index(&self) {
|
||||||
|
let index_path = self.storage_dir.join("index.json");
|
||||||
|
if !index_path.exists() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(content) = fs::read_to_string(&index_path) {
|
||||||
|
if let Ok(index) = parse_index_json(&content) {
|
||||||
|
let mut idx = self.index.write().unwrap();
|
||||||
|
*idx = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save the package index to disk
|
||||||
|
fn save_index(&self) {
|
||||||
|
let index_path = self.storage_dir.join("index.json");
|
||||||
|
let idx = self.index.read().unwrap();
|
||||||
|
let json = format_index_json(&idx);
|
||||||
|
fs::write(&index_path, json).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publish a new package version
|
||||||
|
pub fn publish(&self, metadata: PackageMetadata, tarball: &[u8]) -> Result<(), String> {
|
||||||
|
// Validate package name
|
||||||
|
if !is_valid_package_name(&metadata.name) {
|
||||||
|
return Err("Invalid package name. Use lowercase letters, numbers, and hyphens.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create package directory
|
||||||
|
let pkg_dir = self.storage_dir.join("packages").join(&metadata.name);
|
||||||
|
fs::create_dir_all(&pkg_dir)
|
||||||
|
.map_err(|e| format!("Failed to create package directory: {}", e))?;
|
||||||
|
|
||||||
|
// Write tarball
|
||||||
|
let tarball_path = pkg_dir.join(format!("{}-{}.tar.gz", metadata.name, metadata.version));
|
||||||
|
fs::write(&tarball_path, tarball)
|
||||||
|
.map_err(|e| format!("Failed to write package tarball: {}", e))?;
|
||||||
|
|
||||||
|
// Write metadata
|
||||||
|
let meta_path = pkg_dir.join(format!("{}-{}.json", metadata.name, metadata.version));
|
||||||
|
let meta_json = format_metadata_json(&metadata);
|
||||||
|
fs::write(&meta_path, meta_json)
|
||||||
|
.map_err(|e| format!("Failed to write package metadata: {}", e))?;
|
||||||
|
|
||||||
|
// Update index
|
||||||
|
{
|
||||||
|
let mut idx = self.index.write().unwrap();
|
||||||
|
let entry = idx.entry(metadata.name.clone()).or_insert_with(|| PackageIndex {
|
||||||
|
name: metadata.name.clone(),
|
||||||
|
description: metadata.description.clone(),
|
||||||
|
versions: Vec::new(),
|
||||||
|
latest_version: String::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if version already exists
|
||||||
|
if entry.versions.iter().any(|v| v.version == metadata.version) {
|
||||||
|
return Err(format!("Version {} already exists", metadata.version));
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.versions.push(VersionEntry {
|
||||||
|
version: metadata.version.clone(),
|
||||||
|
checksum: metadata.checksum.clone(),
|
||||||
|
published_at: metadata.published_at.clone(),
|
||||||
|
yanked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update latest version (simple comparison for now)
|
||||||
|
entry.latest_version = metadata.version.clone();
|
||||||
|
entry.description = metadata.description.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.save_index();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get package metadata
|
||||||
|
pub fn get_metadata(&self, name: &str, version: &str) -> Option<PackageMetadata> {
|
||||||
|
let meta_path = self.storage_dir
|
||||||
|
.join("packages")
|
||||||
|
.join(name)
|
||||||
|
.join(format!("{}-{}.json", name, version));
|
||||||
|
|
||||||
|
if let Ok(content) = fs::read_to_string(&meta_path) {
|
||||||
|
parse_metadata_json(&content)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get package tarball
|
||||||
|
pub fn get_tarball(&self, name: &str, version: &str) -> Option<Vec<u8>> {
|
||||||
|
let tarball_path = self.storage_dir
|
||||||
|
.join("packages")
|
||||||
|
.join(name)
|
||||||
|
.join(format!("{}-{}.tar.gz", name, version));
|
||||||
|
|
||||||
|
fs::read(&tarball_path).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search packages
|
||||||
|
pub fn search(&self, query: &str) -> Vec<PackageIndex> {
|
||||||
|
let idx = self.index.read().unwrap();
|
||||||
|
let query_lower = query.to_lowercase();
|
||||||
|
|
||||||
|
idx.values()
|
||||||
|
.filter(|pkg| {
|
||||||
|
pkg.name.to_lowercase().contains(&query_lower) ||
|
||||||
|
pkg.description.to_lowercase().contains(&query_lower)
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all packages
|
||||||
|
pub fn list_all(&self) -> Vec<PackageIndex> {
|
||||||
|
let idx = self.index.read().unwrap();
|
||||||
|
idx.values().cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get package index entry
|
||||||
|
pub fn get_package(&self, name: &str) -> Option<PackageIndex> {
|
||||||
|
let idx = self.index.read().unwrap();
|
||||||
|
idx.get(name).cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HTTP Registry Server
|
||||||
|
pub struct RegistryServer {
|
||||||
|
registry: Arc<Registry>,
|
||||||
|
bind_addr: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegistryServer {
|
||||||
|
/// Create a new registry server
|
||||||
|
pub fn new(storage_dir: &Path, bind_addr: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
registry: Arc::new(Registry::new(storage_dir)),
|
||||||
|
bind_addr: bind_addr.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the server
|
||||||
|
pub fn run(&self) -> Result<(), String> {
|
||||||
|
let listener = TcpListener::bind(&self.bind_addr)
|
||||||
|
.map_err(|e| format!("Failed to bind to {}: {}", self.bind_addr, e))?;
|
||||||
|
|
||||||
|
println!("Lux Package Registry running at http://{}", self.bind_addr);
|
||||||
|
println!("Storage directory: {}", self.registry.storage_dir.display());
|
||||||
|
println!();
|
||||||
|
println!("Endpoints:");
|
||||||
|
println!(" GET /api/v1/packages - List all packages");
|
||||||
|
println!(" GET /api/v1/packages/:name - Get package info");
|
||||||
|
println!(" GET /api/v1/packages/:name/:ver - Get version metadata");
|
||||||
|
println!(" GET /api/v1/download/:name/:ver - Download package tarball");
|
||||||
|
println!(" GET /api/v1/search?q=query - Search packages");
|
||||||
|
println!(" POST /api/v1/publish - Publish a package");
|
||||||
|
println!();
|
||||||
|
|
||||||
|
for stream in listener.incoming() {
|
||||||
|
match stream {
|
||||||
|
Ok(stream) => {
|
||||||
|
let registry = Arc::clone(&self.registry);
|
||||||
|
thread::spawn(move || {
|
||||||
|
handle_request(stream, ®istry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Connection error: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle an HTTP request
|
||||||
|
fn handle_request(mut stream: TcpStream, registry: &Registry) {
|
||||||
|
let mut buffer = [0; 8192];
|
||||||
|
let bytes_read = match stream.read(&mut buffer) {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let request = String::from_utf8_lossy(&buffer[..bytes_read]);
|
||||||
|
let lines: Vec<&str> = request.lines().collect();
|
||||||
|
|
||||||
|
if lines.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parts: Vec<&str> = lines[0].split_whitespace().collect();
|
||||||
|
if parts.len() < 2 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let method = parts[0];
|
||||||
|
let path = parts[1];
|
||||||
|
|
||||||
|
// Parse path and query string
|
||||||
|
let (path, query) = if let Some(q_pos) = path.find('?') {
|
||||||
|
(&path[..q_pos], Some(&path[q_pos + 1..]))
|
||||||
|
} else {
|
||||||
|
(path, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = match (method, path) {
|
||||||
|
("GET", "/") => {
|
||||||
|
html_response(200, r#"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><title>Lux Package Registry</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Lux Package Registry</h1>
|
||||||
|
<p>Welcome to the Lux package registry.</p>
|
||||||
|
<h2>API Endpoints</h2>
|
||||||
|
<ul>
|
||||||
|
<li>GET /api/v1/packages - List all packages</li>
|
||||||
|
<li>GET /api/v1/packages/:name - Get package info</li>
|
||||||
|
<li>GET /api/v1/packages/:name/:version - Get version metadata</li>
|
||||||
|
<li>GET /api/v1/download/:name/:version - Download package</li>
|
||||||
|
<li>GET /api/v1/search?q=query - Search packages</li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"#)
|
||||||
|
}
|
||||||
|
|
||||||
|
("GET", "/api/v1/packages") => {
|
||||||
|
let packages = registry.list_all();
|
||||||
|
let json = format_packages_list_json(&packages);
|
||||||
|
json_response(200, &json)
|
||||||
|
}
|
||||||
|
|
||||||
|
("GET", path) if path.starts_with("/api/v1/packages/") => {
|
||||||
|
let rest = &path[17..]; // Remove "/api/v1/packages/"
|
||||||
|
let parts: Vec<&str> = rest.split('/').collect();
|
||||||
|
|
||||||
|
match parts.len() {
|
||||||
|
1 => {
|
||||||
|
// Get package info
|
||||||
|
if let Some(pkg) = registry.get_package(parts[0]) {
|
||||||
|
let json = format_package_json(&pkg);
|
||||||
|
json_response(200, &json)
|
||||||
|
} else {
|
||||||
|
json_response(404, r#"{"error": "Package not found"}"#)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
// Get version metadata
|
||||||
|
if let Some(meta) = registry.get_metadata(parts[0], parts[1]) {
|
||||||
|
let json = format_metadata_json(&meta);
|
||||||
|
json_response(200, &json)
|
||||||
|
} else {
|
||||||
|
json_response(404, r#"{"error": "Version not found"}"#)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => json_response(400, r#"{"error": "Invalid path"}"#)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
("GET", path) if path.starts_with("/api/v1/download/") => {
|
||||||
|
let rest = &path[17..]; // Remove "/api/v1/download/"
|
||||||
|
let parts: Vec<&str> = rest.split('/').collect();
|
||||||
|
|
||||||
|
if parts.len() == 2 {
|
||||||
|
if let Some(tarball) = registry.get_tarball(parts[0], parts[1]) {
|
||||||
|
tarball_response(&tarball)
|
||||||
|
} else {
|
||||||
|
json_response(404, r#"{"error": "Package not found"}"#)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
json_response(400, r#"{"error": "Invalid path"}"#)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
("GET", "/api/v1/search") => {
|
||||||
|
let q = query
|
||||||
|
.and_then(|qs| parse_query_string(qs).get("q").cloned())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let results = registry.search(&q);
|
||||||
|
let json = format_packages_list_json(&results);
|
||||||
|
json_response(200, &json)
|
||||||
|
}
|
||||||
|
|
||||||
|
("POST", "/api/v1/publish") => {
|
||||||
|
// Find content length
|
||||||
|
let content_length: usize = lines.iter()
|
||||||
|
.find(|l| l.to_lowercase().starts_with("content-length:"))
|
||||||
|
.and_then(|l| l.split(':').nth(1))
|
||||||
|
.and_then(|s| s.trim().parse().ok())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
// Find body start
|
||||||
|
let body_start = request.find("\r\n\r\n")
|
||||||
|
.map(|i| i + 4)
|
||||||
|
.unwrap_or(bytes_read);
|
||||||
|
|
||||||
|
// For now, return a message about publishing
|
||||||
|
// Real implementation would parse multipart form data
|
||||||
|
json_response(200, &format!(
|
||||||
|
r#"{{"message": "Publish endpoint ready", "content_length": {}}}"#,
|
||||||
|
content_length
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
json_response(404, r#"{"error": "Not found"}"#)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
stream.write_all(response.as_bytes()).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an HTML response
|
||||||
|
fn html_response(status: u16, body: &str) -> String {
|
||||||
|
let status_text = match status {
|
||||||
|
200 => "OK",
|
||||||
|
400 => "Bad Request",
|
||||||
|
404 => "Not Found",
|
||||||
|
500 => "Internal Server Error",
|
||||||
|
_ => "Unknown",
|
||||||
|
};
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"HTTP/1.1 {} {}\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||||
|
status, status_text, body.len(), body
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a JSON response
|
||||||
|
fn json_response(status: u16, body: &str) -> String {
|
||||||
|
let status_text = match status {
|
||||||
|
200 => "OK",
|
||||||
|
400 => "Bad Request",
|
||||||
|
404 => "Not Found",
|
||||||
|
500 => "Internal Server Error",
|
||||||
|
_ => "Unknown",
|
||||||
|
};
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||||
|
status, status_text, body.len(), body
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a tarball response
|
||||||
|
fn tarball_response(data: &[u8]) -> String {
|
||||||
|
format!(
|
||||||
|
"HTTP/1.1 200 OK\r\nContent-Type: application/gzip\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||||
|
data.len()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate package name
|
||||||
|
fn is_valid_package_name(name: &str) -> bool {
|
||||||
|
!name.is_empty() &&
|
||||||
|
name.len() <= 64 &&
|
||||||
|
name.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_') &&
|
||||||
|
name.chars().next().map(|c| c.is_ascii_lowercase()).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse query string into key-value pairs
|
||||||
|
fn parse_query_string(qs: &str) -> HashMap<String, String> {
|
||||||
|
let mut params = HashMap::new();
|
||||||
|
for part in qs.split('&') {
|
||||||
|
if let Some(eq_pos) = part.find('=') {
|
||||||
|
let key = &part[..eq_pos];
|
||||||
|
let value = &part[eq_pos + 1..];
|
||||||
|
params.insert(
|
||||||
|
urlldecode(key),
|
||||||
|
urlldecode(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
params
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple URL decoding
|
||||||
|
fn urlldecode(s: &str) -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut chars = s.chars().peekable();
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
if c == '%' {
|
||||||
|
let hex: String = chars.by_ref().take(2).collect();
|
||||||
|
if let Ok(byte) = u8::from_str_radix(&hex, 16) {
|
||||||
|
result.push(byte as char);
|
||||||
|
}
|
||||||
|
} else if c == '+' {
|
||||||
|
result.push(' ');
|
||||||
|
} else {
|
||||||
|
result.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON formatting helpers
|
||||||
|
|
||||||
|
fn format_metadata_json(meta: &PackageMetadata) -> String {
|
||||||
|
let deps: Vec<String> = meta.dependencies.iter()
|
||||||
|
.map(|(k, v)| format!(r#""{}": "{}""#, k, v))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let authors: Vec<String> = meta.authors.iter()
|
||||||
|
.map(|a| format!(r#""{}""#, a))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let keywords: Vec<String> = meta.keywords.iter()
|
||||||
|
.map(|k| format!(r#""{}""#, k))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
format!(
|
||||||
|
r#"{{
|
||||||
|
"name": "{}",
|
||||||
|
"version": "{}",
|
||||||
|
"description": "{}",
|
||||||
|
"authors": [{}],
|
||||||
|
"license": {},
|
||||||
|
"repository": {},
|
||||||
|
"keywords": [{}],
|
||||||
|
"dependencies": {{{}}},
|
||||||
|
"checksum": "{}",
|
||||||
|
"published_at": "{}"
|
||||||
|
}}"#,
|
||||||
|
meta.name,
|
||||||
|
meta.version,
|
||||||
|
escape_json(&meta.description),
|
||||||
|
authors.join(", "),
|
||||||
|
meta.license.as_ref().map(|l| format!(r#""{}""#, l)).unwrap_or("null".to_string()),
|
||||||
|
meta.repository.as_ref().map(|r| format!(r#""{}""#, r)).unwrap_or("null".to_string()),
|
||||||
|
keywords.join(", "),
|
||||||
|
deps.join(", "),
|
||||||
|
meta.checksum,
|
||||||
|
meta.published_at,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_package_json(pkg: &PackageIndex) -> String {
|
||||||
|
let versions: Vec<String> = pkg.versions.iter()
|
||||||
|
.map(|v| format!(
|
||||||
|
r#"{{"version": "{}", "checksum": "{}", "published_at": "{}", "yanked": {}}}"#,
|
||||||
|
v.version, v.checksum, v.published_at, v.yanked
|
||||||
|
))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
format!(
|
||||||
|
r#"{{
|
||||||
|
"name": "{}",
|
||||||
|
"description": "{}",
|
||||||
|
"latest_version": "{}",
|
||||||
|
"versions": [{}]
|
||||||
|
}}"#,
|
||||||
|
pkg.name,
|
||||||
|
escape_json(&pkg.description),
|
||||||
|
pkg.latest_version,
|
||||||
|
versions.join(", ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_packages_list_json(packages: &[PackageIndex]) -> String {
|
||||||
|
let items: Vec<String> = packages.iter()
|
||||||
|
.map(|pkg| format!(
|
||||||
|
r#"{{"name": "{}", "description": "{}", "latest_version": "{}"}}"#,
|
||||||
|
pkg.name,
|
||||||
|
escape_json(&pkg.description),
|
||||||
|
pkg.latest_version
|
||||||
|
))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
format!(r#"{{"packages": [{}]}}"#, items.join(", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_index_json(index: &HashMap<String, PackageIndex>) -> String {
|
||||||
|
let items: Vec<String> = index.values()
|
||||||
|
.map(|pkg| format_package_json(pkg))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
format!(r#"{{"packages": [{}]}}"#, items.join(",\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_index_json(content: &str) -> Result<HashMap<String, PackageIndex>, String> {
|
||||||
|
// Simple JSON parsing for the index
|
||||||
|
// In production, would use serde_json
|
||||||
|
let mut index = HashMap::new();
|
||||||
|
|
||||||
|
// Basic parsing - find package names and latest versions
|
||||||
|
// This is a simplified parser for the index format
|
||||||
|
let content = content.trim();
|
||||||
|
if !content.starts_with('{') || !content.ends_with('}') {
|
||||||
|
return Err("Invalid JSON format".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, return empty index if parsing fails
|
||||||
|
// Real implementation would properly parse JSON
|
||||||
|
Ok(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_metadata_json(content: &str) -> Option<PackageMetadata> {
|
||||||
|
// Simple JSON parsing for metadata
|
||||||
|
// In production, would use serde_json
|
||||||
|
let mut name = String::new();
|
||||||
|
let mut version = String::new();
|
||||||
|
let mut description = String::new();
|
||||||
|
let mut checksum = String::new();
|
||||||
|
let mut published_at = String::new();
|
||||||
|
|
||||||
|
for line in content.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.contains("\"name\":") {
|
||||||
|
name = extract_json_string(line);
|
||||||
|
} else if line.contains("\"version\":") {
|
||||||
|
version = extract_json_string(line);
|
||||||
|
} else if line.contains("\"description\":") {
|
||||||
|
description = extract_json_string(line);
|
||||||
|
} else if line.contains("\"checksum\":") {
|
||||||
|
checksum = extract_json_string(line);
|
||||||
|
} else if line.contains("\"published_at\":") {
|
||||||
|
published_at = extract_json_string(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if name.is_empty() || version.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(PackageMetadata {
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
description,
|
||||||
|
authors: Vec::new(),
|
||||||
|
license: None,
|
||||||
|
repository: None,
|
||||||
|
keywords: Vec::new(),
|
||||||
|
dependencies: HashMap::new(),
|
||||||
|
checksum,
|
||||||
|
published_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_json_string(line: &str) -> String {
|
||||||
|
// Extract string value from "key": "value" format
|
||||||
|
if let Some(colon) = line.find(':') {
|
||||||
|
let value = line[colon + 1..].trim();
|
||||||
|
let value = value.trim_start_matches('"');
|
||||||
|
if let Some(end) = value.find('"') {
|
||||||
|
return value[..end].to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escape_json(s: &str) -> String {
|
||||||
|
s.replace('\\', "\\\\")
|
||||||
|
.replace('"', "\\\"")
|
||||||
|
.replace('\n', "\\n")
|
||||||
|
.replace('\r', "\\r")
|
||||||
|
.replace('\t', "\\t")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the registry server (called from main)
|
||||||
|
pub fn run_registry_server(storage_dir: &str, bind_addr: &str) -> Result<(), String> {
|
||||||
|
let storage_path = PathBuf::from(storage_dir);
|
||||||
|
fs::create_dir_all(&storage_path)
|
||||||
|
.map_err(|e| format!("Failed to create storage directory: {}", e))?;
|
||||||
|
|
||||||
|
let server = RegistryServer::new(&storage_path, bind_addr);
|
||||||
|
server.run()
|
||||||
|
}
|
||||||
679
src/symbol_table.rs
Normal file
679
src/symbol_table.rs
Normal file
@@ -0,0 +1,679 @@
|
|||||||
|
//! Symbol Table for Lux
|
||||||
|
//!
|
||||||
|
//! Provides semantic analysis infrastructure for IDE features like
|
||||||
|
//! go-to-definition, find references, and rename refactoring.
|
||||||
|
|
||||||
|
use crate::ast::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Unique identifier for a symbol
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct SymbolId(pub u32);
|
||||||
|
|
||||||
|
/// Kind of symbol
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum SymbolKind {
|
||||||
|
Function,
|
||||||
|
Variable,
|
||||||
|
Parameter,
|
||||||
|
Type,
|
||||||
|
TypeParameter,
|
||||||
|
Variant,
|
||||||
|
Effect,
|
||||||
|
EffectOperation,
|
||||||
|
Field,
|
||||||
|
Module,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A symbol definition
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Symbol {
|
||||||
|
pub id: SymbolId,
|
||||||
|
pub name: String,
|
||||||
|
pub kind: SymbolKind,
|
||||||
|
pub span: Span,
|
||||||
|
/// Type signature (for display)
|
||||||
|
pub type_signature: Option<String>,
|
||||||
|
/// Documentation comment
|
||||||
|
pub documentation: Option<String>,
|
||||||
|
/// Parent symbol (e.g., type for variants, effect for operations)
|
||||||
|
pub parent: Option<SymbolId>,
|
||||||
|
/// Is this symbol exported (public)?
|
||||||
|
pub is_public: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A reference to a symbol
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Reference {
|
||||||
|
pub symbol_id: SymbolId,
|
||||||
|
pub span: Span,
|
||||||
|
pub is_definition: bool,
|
||||||
|
pub is_write: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A scope in the symbol table
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Scope {
|
||||||
|
/// Parent scope (None for global scope)
|
||||||
|
pub parent: Option<usize>,
|
||||||
|
/// Symbols defined in this scope
|
||||||
|
pub symbols: HashMap<String, SymbolId>,
|
||||||
|
/// Span of this scope
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The symbol table
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SymbolTable {
|
||||||
|
/// All symbols
|
||||||
|
symbols: Vec<Symbol>,
|
||||||
|
/// All references
|
||||||
|
references: Vec<Reference>,
|
||||||
|
/// Scopes (index 0 is always the global scope)
|
||||||
|
scopes: Vec<Scope>,
|
||||||
|
/// Mapping from position to references
|
||||||
|
position_to_reference: HashMap<(u32, u32), usize>,
|
||||||
|
/// Next symbol ID
|
||||||
|
next_id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymbolTable {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
symbols: Vec::new(),
|
||||||
|
references: Vec::new(),
|
||||||
|
scopes: vec![Scope {
|
||||||
|
parent: None,
|
||||||
|
symbols: HashMap::new(),
|
||||||
|
span: Span { start: 0, end: 0 },
|
||||||
|
}],
|
||||||
|
position_to_reference: HashMap::new(),
|
||||||
|
next_id: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build symbol table from a program
|
||||||
|
pub fn build(program: &Program) -> Self {
|
||||||
|
let mut table = Self::new();
|
||||||
|
table.visit_program(program);
|
||||||
|
table
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a symbol to the current scope
|
||||||
|
fn add_symbol(&mut self, scope_idx: usize, symbol: Symbol) -> SymbolId {
|
||||||
|
let id = symbol.id;
|
||||||
|
self.scopes[scope_idx].symbols.insert(symbol.name.clone(), id);
|
||||||
|
self.symbols.push(symbol);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new symbol
|
||||||
|
fn new_symbol(
|
||||||
|
&mut self,
|
||||||
|
name: String,
|
||||||
|
kind: SymbolKind,
|
||||||
|
span: Span,
|
||||||
|
type_signature: Option<String>,
|
||||||
|
is_public: bool,
|
||||||
|
) -> Symbol {
|
||||||
|
let id = SymbolId(self.next_id);
|
||||||
|
self.next_id += 1;
|
||||||
|
Symbol {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
kind,
|
||||||
|
span,
|
||||||
|
type_signature,
|
||||||
|
documentation: None,
|
||||||
|
parent: None,
|
||||||
|
is_public,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a reference
|
||||||
|
fn add_reference(&mut self, symbol_id: SymbolId, span: Span, is_definition: bool, is_write: bool) {
|
||||||
|
let ref_idx = self.references.len();
|
||||||
|
self.references.push(Reference {
|
||||||
|
symbol_id,
|
||||||
|
span,
|
||||||
|
is_definition,
|
||||||
|
is_write,
|
||||||
|
});
|
||||||
|
// Index by start position
|
||||||
|
self.position_to_reference.insert((span.start as u32, span.end as u32), ref_idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up a symbol by name in the given scope and its parents
|
||||||
|
pub fn lookup(&self, name: &str, scope_idx: usize) -> Option<SymbolId> {
|
||||||
|
let scope = &self.scopes[scope_idx];
|
||||||
|
if let Some(&id) = scope.symbols.get(name) {
|
||||||
|
return Some(id);
|
||||||
|
}
|
||||||
|
if let Some(parent) = scope.parent {
|
||||||
|
return self.lookup(name, parent);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a symbol by ID
|
||||||
|
pub fn get_symbol(&self, id: SymbolId) -> Option<&Symbol> {
|
||||||
|
self.symbols.iter().find(|s| s.id == id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the symbol at a position
|
||||||
|
pub fn symbol_at_position(&self, offset: usize) -> Option<&Symbol> {
|
||||||
|
// Find a reference that contains this offset
|
||||||
|
for reference in &self.references {
|
||||||
|
if offset >= reference.span.start && offset <= reference.span.end {
|
||||||
|
return self.get_symbol(reference.symbol_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the definition of a symbol at a position
|
||||||
|
pub fn definition_at_position(&self, offset: usize) -> Option<&Symbol> {
|
||||||
|
self.symbol_at_position(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find all references to a symbol
|
||||||
|
pub fn find_references(&self, symbol_id: SymbolId) -> Vec<&Reference> {
|
||||||
|
self.references
|
||||||
|
.iter()
|
||||||
|
.filter(|r| r.symbol_id == symbol_id)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all symbols of a given kind
|
||||||
|
pub fn symbols_of_kind(&self, kind: SymbolKind) -> Vec<&Symbol> {
|
||||||
|
self.symbols.iter().filter(|s| s.kind == kind).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all symbols in the global scope
|
||||||
|
pub fn global_symbols(&self) -> Vec<&Symbol> {
|
||||||
|
self.scopes[0]
|
||||||
|
.symbols
|
||||||
|
.values()
|
||||||
|
.filter_map(|&id| self.get_symbol(id))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new scope
|
||||||
|
fn push_scope(&mut self, parent: usize, span: Span) -> usize {
|
||||||
|
let idx = self.scopes.len();
|
||||||
|
self.scopes.push(Scope {
|
||||||
|
parent: Some(parent),
|
||||||
|
symbols: HashMap::new(),
|
||||||
|
span,
|
||||||
|
});
|
||||||
|
idx
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// AST Visitors
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
fn visit_program(&mut self, program: &Program) {
|
||||||
|
// First pass: collect all top-level declarations
|
||||||
|
for decl in &program.declarations {
|
||||||
|
self.visit_declaration(decl, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_declaration(&mut self, decl: &Declaration, scope_idx: usize) {
|
||||||
|
match decl {
|
||||||
|
Declaration::Function(f) => self.visit_function(f, scope_idx),
|
||||||
|
Declaration::Type(t) => self.visit_type_decl(t, scope_idx),
|
||||||
|
Declaration::Effect(e) => self.visit_effect(e, scope_idx),
|
||||||
|
Declaration::Let(let_decl) => {
|
||||||
|
let is_public = matches!(let_decl.visibility, Visibility::Public);
|
||||||
|
let type_sig = let_decl.typ.as_ref().map(|t| self.type_expr_to_string(t));
|
||||||
|
let mut symbol = self.new_symbol(
|
||||||
|
let_decl.name.name.clone(),
|
||||||
|
SymbolKind::Variable,
|
||||||
|
let_decl.span,
|
||||||
|
type_sig,
|
||||||
|
is_public,
|
||||||
|
);
|
||||||
|
symbol.documentation = let_decl.doc.clone();
|
||||||
|
let id = self.add_symbol(scope_idx, symbol);
|
||||||
|
self.add_reference(id, let_decl.name.span, true, true);
|
||||||
|
|
||||||
|
// Visit the expression
|
||||||
|
self.visit_expr(&let_decl.value, scope_idx);
|
||||||
|
}
|
||||||
|
Declaration::Handler(h) => self.visit_handler(h, scope_idx),
|
||||||
|
Declaration::Trait(t) => self.visit_trait(t, scope_idx),
|
||||||
|
Declaration::Impl(i) => self.visit_impl(i, scope_idx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_function(&mut self, f: &FunctionDecl, scope_idx: usize) {
|
||||||
|
let is_public = matches!(f.visibility, Visibility::Public);
|
||||||
|
|
||||||
|
// Build type signature
|
||||||
|
let param_types: Vec<String> = f.params.iter()
|
||||||
|
.map(|p| format!("{}: {}", p.name.name, self.type_expr_to_string(&p.typ)))
|
||||||
|
.collect();
|
||||||
|
let return_type = self.type_expr_to_string(&f.return_type);
|
||||||
|
let effects = if f.effects.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(" with {{{}}}", f.effects.iter()
|
||||||
|
.map(|e| e.name.clone())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", "))
|
||||||
|
};
|
||||||
|
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 mut symbol = self.new_symbol(
|
||||||
|
f.name.name.clone(),
|
||||||
|
SymbolKind::Function,
|
||||||
|
f.name.span,
|
||||||
|
Some(type_sig),
|
||||||
|
is_public,
|
||||||
|
);
|
||||||
|
symbol.documentation = f.doc.clone();
|
||||||
|
let fn_id = self.add_symbol(scope_idx, symbol);
|
||||||
|
self.add_reference(fn_id, f.name.span, true, false);
|
||||||
|
|
||||||
|
// Create scope for function body
|
||||||
|
let body_span = f.body.span();
|
||||||
|
let fn_scope = self.push_scope(scope_idx, body_span);
|
||||||
|
|
||||||
|
// Add type parameters
|
||||||
|
for tp in &f.type_params {
|
||||||
|
let symbol = self.new_symbol(
|
||||||
|
tp.name.clone(),
|
||||||
|
SymbolKind::TypeParameter,
|
||||||
|
tp.span,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
self.add_symbol(fn_scope, symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add parameters
|
||||||
|
for param in &f.params {
|
||||||
|
let type_sig = self.type_expr_to_string(¶m.typ);
|
||||||
|
let symbol = self.new_symbol(
|
||||||
|
param.name.name.clone(),
|
||||||
|
SymbolKind::Parameter,
|
||||||
|
param.name.span,
|
||||||
|
Some(type_sig),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
self.add_symbol(fn_scope, symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visit body
|
||||||
|
self.visit_expr(&f.body, fn_scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_type_decl(&mut self, t: &TypeDecl, scope_idx: usize) {
|
||||||
|
let is_public = matches!(t.visibility, Visibility::Public);
|
||||||
|
let type_sig = format!("type {}", t.name.name);
|
||||||
|
|
||||||
|
let mut symbol = self.new_symbol(
|
||||||
|
t.name.name.clone(),
|
||||||
|
SymbolKind::Type,
|
||||||
|
t.name.span,
|
||||||
|
Some(type_sig),
|
||||||
|
is_public,
|
||||||
|
);
|
||||||
|
symbol.documentation = t.doc.clone();
|
||||||
|
let type_id = self.add_symbol(scope_idx, symbol);
|
||||||
|
self.add_reference(type_id, t.name.span, true, false);
|
||||||
|
|
||||||
|
// Add variants
|
||||||
|
match &t.definition {
|
||||||
|
TypeDef::Enum(variants) => {
|
||||||
|
for variant in variants {
|
||||||
|
let mut var_symbol = self.new_symbol(
|
||||||
|
variant.name.name.clone(),
|
||||||
|
SymbolKind::Variant,
|
||||||
|
variant.name.span,
|
||||||
|
None,
|
||||||
|
is_public,
|
||||||
|
);
|
||||||
|
var_symbol.parent = Some(type_id);
|
||||||
|
self.add_symbol(scope_idx, var_symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TypeDef::Record(fields) => {
|
||||||
|
for field in fields {
|
||||||
|
let mut field_symbol = self.new_symbol(
|
||||||
|
field.name.name.clone(),
|
||||||
|
SymbolKind::Field,
|
||||||
|
field.name.span,
|
||||||
|
Some(self.type_expr_to_string(&field.typ)),
|
||||||
|
is_public,
|
||||||
|
);
|
||||||
|
field_symbol.parent = Some(type_id);
|
||||||
|
self.add_symbol(scope_idx, field_symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TypeDef::Alias(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_effect(&mut self, e: &EffectDecl, scope_idx: usize) {
|
||||||
|
let is_public = true; // Effects are typically public
|
||||||
|
let type_sig = format!("effect {}", e.name.name);
|
||||||
|
|
||||||
|
let mut symbol = self.new_symbol(
|
||||||
|
e.name.name.clone(),
|
||||||
|
SymbolKind::Effect,
|
||||||
|
e.name.span,
|
||||||
|
Some(type_sig),
|
||||||
|
is_public,
|
||||||
|
);
|
||||||
|
symbol.documentation = e.doc.clone();
|
||||||
|
let effect_id = self.add_symbol(scope_idx, symbol);
|
||||||
|
|
||||||
|
// Add operations
|
||||||
|
for op in &e.operations {
|
||||||
|
let param_types: Vec<String> = op.params.iter()
|
||||||
|
.map(|p| format!("{}: {}", p.name.name, self.type_expr_to_string(&p.typ)))
|
||||||
|
.collect();
|
||||||
|
let return_type = self.type_expr_to_string(&op.return_type);
|
||||||
|
let op_sig = format!("fn {}({}): {}", op.name.name, param_types.join(", "), return_type);
|
||||||
|
|
||||||
|
let mut op_symbol = self.new_symbol(
|
||||||
|
op.name.name.clone(),
|
||||||
|
SymbolKind::EffectOperation,
|
||||||
|
op.name.span,
|
||||||
|
Some(op_sig),
|
||||||
|
is_public,
|
||||||
|
);
|
||||||
|
op_symbol.parent = Some(effect_id);
|
||||||
|
self.add_symbol(scope_idx, op_symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_handler(&mut self, _h: &HandlerDecl, _scope_idx: usize) {
|
||||||
|
// Handlers are complex - visit their implementations
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_trait(&mut self, t: &TraitDecl, scope_idx: usize) {
|
||||||
|
let is_public = matches!(t.visibility, Visibility::Public);
|
||||||
|
let type_sig = format!("trait {}", t.name.name);
|
||||||
|
|
||||||
|
let mut symbol = self.new_symbol(
|
||||||
|
t.name.name.clone(),
|
||||||
|
SymbolKind::Type, // Traits are like types
|
||||||
|
t.name.span,
|
||||||
|
Some(type_sig),
|
||||||
|
is_public,
|
||||||
|
);
|
||||||
|
symbol.documentation = t.doc.clone();
|
||||||
|
self.add_symbol(scope_idx, symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_impl(&mut self, _i: &ImplDecl, _scope_idx: usize) {
|
||||||
|
// Impl blocks add methods to types
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_expr(&mut self, expr: &Expr, scope_idx: usize) {
|
||||||
|
match expr {
|
||||||
|
Expr::Var(ident) => {
|
||||||
|
// Look up the identifier and add a reference
|
||||||
|
if let Some(id) = self.lookup(&ident.name, scope_idx) {
|
||||||
|
self.add_reference(id, ident.span, false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::Let { name, value, body, span, .. } => {
|
||||||
|
// Visit the value first
|
||||||
|
self.visit_expr(value, scope_idx);
|
||||||
|
|
||||||
|
// Create a new scope for the let binding
|
||||||
|
let let_scope = self.push_scope(scope_idx, *span);
|
||||||
|
|
||||||
|
// Add the variable
|
||||||
|
let symbol = self.new_symbol(
|
||||||
|
name.name.clone(),
|
||||||
|
SymbolKind::Variable,
|
||||||
|
name.span,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
let var_id = self.add_symbol(let_scope, symbol);
|
||||||
|
self.add_reference(var_id, name.span, true, true);
|
||||||
|
|
||||||
|
// Visit the body
|
||||||
|
self.visit_expr(body, let_scope);
|
||||||
|
}
|
||||||
|
Expr::Lambda { params, body, span, .. } => {
|
||||||
|
let lambda_scope = self.push_scope(scope_idx, *span);
|
||||||
|
|
||||||
|
for param in params {
|
||||||
|
let symbol = self.new_symbol(
|
||||||
|
param.name.name.clone(),
|
||||||
|
SymbolKind::Parameter,
|
||||||
|
param.name.span,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
self.add_symbol(lambda_scope, symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.visit_expr(body, lambda_scope);
|
||||||
|
}
|
||||||
|
Expr::Call { func, args, .. } => {
|
||||||
|
self.visit_expr(func, scope_idx);
|
||||||
|
for arg in args {
|
||||||
|
self.visit_expr(arg, scope_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::EffectOp { args, .. } => {
|
||||||
|
for arg in args {
|
||||||
|
self.visit_expr(arg, scope_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => {
|
||||||
|
self.visit_expr(object, scope_idx);
|
||||||
|
}
|
||||||
|
Expr::If { condition, then_branch, else_branch, .. } => {
|
||||||
|
self.visit_expr(condition, scope_idx);
|
||||||
|
self.visit_expr(then_branch, scope_idx);
|
||||||
|
self.visit_expr(else_branch, scope_idx);
|
||||||
|
}
|
||||||
|
Expr::Match { scrutinee, arms, .. } => {
|
||||||
|
self.visit_expr(scrutinee, scope_idx);
|
||||||
|
for arm in arms {
|
||||||
|
// Each arm may bind variables
|
||||||
|
let arm_scope = self.push_scope(scope_idx, arm.body.span());
|
||||||
|
self.visit_pattern(&arm.pattern, arm_scope);
|
||||||
|
if let Some(ref guard) = arm.guard {
|
||||||
|
self.visit_expr(guard, arm_scope);
|
||||||
|
}
|
||||||
|
self.visit_expr(&arm.body, arm_scope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::Block { statements, result, .. } => {
|
||||||
|
for stmt in statements {
|
||||||
|
self.visit_statement(stmt, scope_idx);
|
||||||
|
}
|
||||||
|
self.visit_expr(result, scope_idx);
|
||||||
|
}
|
||||||
|
Expr::BinaryOp { left, right, .. } => {
|
||||||
|
self.visit_expr(left, scope_idx);
|
||||||
|
self.visit_expr(right, scope_idx);
|
||||||
|
}
|
||||||
|
Expr::UnaryOp { operand, .. } => {
|
||||||
|
self.visit_expr(operand, scope_idx);
|
||||||
|
}
|
||||||
|
Expr::List { elements, .. } => {
|
||||||
|
for e in elements {
|
||||||
|
self.visit_expr(e, scope_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::Tuple { elements, .. } => {
|
||||||
|
for e in elements {
|
||||||
|
self.visit_expr(e, scope_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::Record { fields, .. } => {
|
||||||
|
for (_, e) in fields {
|
||||||
|
self.visit_expr(e, scope_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::Run { expr, handlers, .. } => {
|
||||||
|
self.visit_expr(expr, scope_idx);
|
||||||
|
for (_effect, handler_expr) in handlers {
|
||||||
|
self.visit_expr(handler_expr, scope_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::Resume { value, .. } => {
|
||||||
|
self.visit_expr(value, scope_idx);
|
||||||
|
}
|
||||||
|
// Literals don't need symbol resolution
|
||||||
|
Expr::Literal(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_statement(&mut self, stmt: &Statement, scope_idx: usize) {
|
||||||
|
match stmt {
|
||||||
|
Statement::Expr(e) => self.visit_expr(e, scope_idx),
|
||||||
|
Statement::Let { name, value, .. } => {
|
||||||
|
self.visit_expr(value, scope_idx);
|
||||||
|
let symbol = self.new_symbol(
|
||||||
|
name.name.clone(),
|
||||||
|
SymbolKind::Variable,
|
||||||
|
name.span,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
let id = self.add_symbol(scope_idx, symbol);
|
||||||
|
self.add_reference(id, name.span, true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_pattern(&mut self, pattern: &Pattern, scope_idx: usize) {
|
||||||
|
match pattern {
|
||||||
|
Pattern::Var(ident) => {
|
||||||
|
let symbol = self.new_symbol(
|
||||||
|
ident.name.clone(),
|
||||||
|
SymbolKind::Variable,
|
||||||
|
ident.span,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
let id = self.add_symbol(scope_idx, symbol);
|
||||||
|
self.add_reference(id, ident.span, true, true);
|
||||||
|
}
|
||||||
|
Pattern::Constructor { fields, .. } => {
|
||||||
|
for p in fields {
|
||||||
|
self.visit_pattern(p, scope_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Pattern::Tuple { elements, .. } => {
|
||||||
|
for p in elements {
|
||||||
|
self.visit_pattern(p, scope_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Pattern::Record { fields, .. } => {
|
||||||
|
for (_, p) in fields {
|
||||||
|
self.visit_pattern(p, scope_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Pattern::Wildcard(_) => {}
|
||||||
|
Pattern::Literal(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn type_expr_to_string(&self, typ: &TypeExpr) -> String {
|
||||||
|
match typ {
|
||||||
|
TypeExpr::Named(ident) => ident.name.clone(),
|
||||||
|
TypeExpr::App(base, args) => {
|
||||||
|
let base_str = self.type_expr_to_string(base);
|
||||||
|
if args.is_empty() {
|
||||||
|
base_str
|
||||||
|
} else {
|
||||||
|
let args_str: Vec<String> = args.iter()
|
||||||
|
.map(|a| self.type_expr_to_string(a))
|
||||||
|
.collect();
|
||||||
|
format!("{}<{}>", base_str, args_str.join(", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TypeExpr::Function { params, return_type, .. } => {
|
||||||
|
let params_str: Vec<String> = params.iter()
|
||||||
|
.map(|p| self.type_expr_to_string(p))
|
||||||
|
.collect();
|
||||||
|
format!("fn({}): {}", params_str.join(", "), self.type_expr_to_string(return_type))
|
||||||
|
}
|
||||||
|
TypeExpr::Tuple(types) => {
|
||||||
|
let types_str: Vec<String> = types.iter()
|
||||||
|
.map(|t| self.type_expr_to_string(t))
|
||||||
|
.collect();
|
||||||
|
format!("({})", types_str.join(", "))
|
||||||
|
}
|
||||||
|
TypeExpr::Record(fields) => {
|
||||||
|
let fields_str: Vec<String> = fields.iter()
|
||||||
|
.map(|f| format!("{}: {}", f.name, self.type_expr_to_string(&f.typ)))
|
||||||
|
.collect();
|
||||||
|
format!("{{ {} }}", fields_str.join(", "))
|
||||||
|
}
|
||||||
|
TypeExpr::Unit => "Unit".to_string(),
|
||||||
|
TypeExpr::Versioned { base, .. } => {
|
||||||
|
format!("{}@versioned", self.type_expr_to_string(base))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SymbolTable {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::parser::Parser;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_symbol_table_basic() {
|
||||||
|
let source = r#"
|
||||||
|
fn add(a: Int, b: Int): Int = a + b
|
||||||
|
let x = 42
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let program = Parser::parse_source(source).unwrap();
|
||||||
|
let table = SymbolTable::build(&program);
|
||||||
|
|
||||||
|
// Should have add function and x variable
|
||||||
|
let globals = table.global_symbols();
|
||||||
|
assert!(globals.iter().any(|s| s.name == "add"));
|
||||||
|
assert!(globals.iter().any(|s| s.name == "x"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_symbol_lookup() {
|
||||||
|
let source = r#"
|
||||||
|
fn foo(x: Int): Int = x + 1
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let program = Parser::parse_source(source).unwrap();
|
||||||
|
let table = SymbolTable::build(&program);
|
||||||
|
|
||||||
|
// Should be able to find foo
|
||||||
|
assert!(table.lookup("foo", 0).is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -335,7 +335,7 @@ fn references_params(expr: &Expr, params: &[&str]) -> bool {
|
|||||||
Statement::Expr(e) => references_params(e, params),
|
Statement::Expr(e) => references_params(e, params),
|
||||||
}) || references_params(result, params)
|
}) || references_params(result, params)
|
||||||
}
|
}
|
||||||
Expr::Field { object, .. } => references_params(object, params),
|
Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => references_params(object, params),
|
||||||
Expr::Lambda { body, .. } => references_params(body, params),
|
Expr::Lambda { body, .. } => references_params(body, params),
|
||||||
Expr::Tuple { elements, .. } => elements.iter().any(|e| references_params(e, params)),
|
Expr::Tuple { elements, .. } => elements.iter().any(|e| references_params(e, params)),
|
||||||
Expr::List { elements, .. } => elements.iter().any(|e| references_params(e, params)),
|
Expr::List { elements, .. } => elements.iter().any(|e| references_params(e, params)),
|
||||||
@@ -519,7 +519,7 @@ fn has_recursive_calls(func_name: &str, body: &Expr) -> bool {
|
|||||||
Expr::Record { fields, .. } => {
|
Expr::Record { fields, .. } => {
|
||||||
fields.iter().any(|(_, e)| has_recursive_calls(func_name, e))
|
fields.iter().any(|(_, e)| has_recursive_calls(func_name, e))
|
||||||
}
|
}
|
||||||
Expr::Field { object, .. } => has_recursive_calls(func_name, object),
|
Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => has_recursive_calls(func_name, object),
|
||||||
Expr::Let { value, body, .. } => {
|
Expr::Let { value, body, .. } => {
|
||||||
has_recursive_calls(func_name, value) || has_recursive_calls(func_name, body)
|
has_recursive_calls(func_name, value) || has_recursive_calls(func_name, body)
|
||||||
}
|
}
|
||||||
@@ -759,6 +759,17 @@ impl TypeChecker {
|
|||||||
self.env.bindings.get(name)
|
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
|
/// Get auto-generated migrations from type checking
|
||||||
/// Returns: type_name -> from_version -> migration_body
|
/// Returns: type_name -> from_version -> migration_body
|
||||||
pub fn get_auto_migrations(&self) -> &HashMap<String, HashMap<u32, Expr>> {
|
pub fn get_auto_migrations(&self) -> &HashMap<String, HashMap<u32, Expr>> {
|
||||||
@@ -1525,7 +1536,7 @@ impl TypeChecker {
|
|||||||
// Use the declared type if present, otherwise use inferred
|
// Use the declared type if present, otherwise use inferred
|
||||||
let final_type = if let Some(ref type_expr) = let_decl.typ {
|
let final_type = if let Some(ref type_expr) = let_decl.typ {
|
||||||
let declared = self.resolve_type(type_expr);
|
let declared = self.resolve_type(type_expr);
|
||||||
if let Err(e) = unify(&inferred, &declared) {
|
if let Err(e) = unify_with_env(&inferred, &declared, &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Variable '{}' has type {}, but declared type is {}: {}",
|
"Variable '{}' has type {}, but declared type is {}: {}",
|
||||||
@@ -1662,6 +1673,42 @@ impl TypeChecker {
|
|||||||
span,
|
span,
|
||||||
} => self.infer_field(object, field, *span),
|
} => self.infer_field(object, field, *span),
|
||||||
|
|
||||||
|
Expr::TupleIndex {
|
||||||
|
object,
|
||||||
|
index,
|
||||||
|
span,
|
||||||
|
} => {
|
||||||
|
let object_type = self.infer_expr(object);
|
||||||
|
match &object_type {
|
||||||
|
Type::Tuple(types) => {
|
||||||
|
if *index < types.len() {
|
||||||
|
types[*index].clone()
|
||||||
|
} else {
|
||||||
|
self.errors.push(TypeError {
|
||||||
|
message: format!(
|
||||||
|
"Tuple index {} out of bounds for tuple with {} elements",
|
||||||
|
index,
|
||||||
|
types.len()
|
||||||
|
),
|
||||||
|
span: *span,
|
||||||
|
});
|
||||||
|
Type::Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Type::Var(_) => Type::var(),
|
||||||
|
_ => {
|
||||||
|
self.errors.push(TypeError {
|
||||||
|
message: format!(
|
||||||
|
"Cannot use tuple index on non-tuple type {}",
|
||||||
|
object_type
|
||||||
|
),
|
||||||
|
span: *span,
|
||||||
|
});
|
||||||
|
Type::Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Expr::Lambda {
|
Expr::Lambda {
|
||||||
params,
|
params,
|
||||||
return_type,
|
return_type,
|
||||||
@@ -1736,7 +1783,7 @@ impl TypeChecker {
|
|||||||
match op {
|
match op {
|
||||||
BinaryOp::Add => {
|
BinaryOp::Add => {
|
||||||
// Add supports both numeric types and string concatenation
|
// Add supports both numeric types and string concatenation
|
||||||
if let Err(e) = unify(&left_type, &right_type) {
|
if let Err(e) = unify_with_env(&left_type, &right_type, &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!("Operands of '{}' must have same type: {}", op, e),
|
message: format!("Operands of '{}' must have same type: {}", op, e),
|
||||||
span,
|
span,
|
||||||
@@ -1759,7 +1806,7 @@ impl TypeChecker {
|
|||||||
|
|
||||||
BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => {
|
BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => {
|
||||||
// Arithmetic: both operands must be same numeric type
|
// Arithmetic: both operands must be same numeric type
|
||||||
if let Err(e) = unify(&left_type, &right_type) {
|
if let Err(e) = unify_with_env(&left_type, &right_type, &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!("Operands of '{}' must have same type: {}", op, e),
|
message: format!("Operands of '{}' must have same type: {}", op, e),
|
||||||
span,
|
span,
|
||||||
@@ -1783,7 +1830,7 @@ impl TypeChecker {
|
|||||||
|
|
||||||
BinaryOp::Eq | BinaryOp::Ne => {
|
BinaryOp::Eq | BinaryOp::Ne => {
|
||||||
// Equality: operands must have same type
|
// Equality: operands must have same type
|
||||||
if let Err(e) = unify(&left_type, &right_type) {
|
if let Err(e) = unify_with_env(&left_type, &right_type, &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!("Operands of '{}' must have same type: {}", op, e),
|
message: format!("Operands of '{}' must have same type: {}", op, e),
|
||||||
span,
|
span,
|
||||||
@@ -1794,7 +1841,7 @@ impl TypeChecker {
|
|||||||
|
|
||||||
BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge => {
|
BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge => {
|
||||||
// Comparison: operands must be same orderable type
|
// Comparison: operands must be same orderable type
|
||||||
if let Err(e) = unify(&left_type, &right_type) {
|
if let Err(e) = unify_with_env(&left_type, &right_type, &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!("Operands of '{}' must have same type: {}", op, e),
|
message: format!("Operands of '{}' must have same type: {}", op, e),
|
||||||
span,
|
span,
|
||||||
@@ -1805,13 +1852,13 @@ impl TypeChecker {
|
|||||||
|
|
||||||
BinaryOp::And | BinaryOp::Or => {
|
BinaryOp::And | BinaryOp::Or => {
|
||||||
// Logical: both must be Bool
|
// Logical: both must be Bool
|
||||||
if let Err(e) = unify(&left_type, &Type::Bool) {
|
if let Err(e) = unify_with_env(&left_type, &Type::Bool, &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!("Left operand of '{}' must be Bool: {}", op, e),
|
message: format!("Left operand of '{}' must be Bool: {}", op, e),
|
||||||
span: left.span(),
|
span: left.span(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if let Err(e) = unify(&right_type, &Type::Bool) {
|
if let Err(e) = unify_with_env(&right_type, &Type::Bool, &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!("Right operand of '{}' must be Bool: {}", op, e),
|
message: format!("Right operand of '{}' must be Bool: {}", op, e),
|
||||||
span: right.span(),
|
span: right.span(),
|
||||||
@@ -1825,7 +1872,7 @@ impl TypeChecker {
|
|||||||
// right must be a function that accepts left's type
|
// right must be a function that accepts left's type
|
||||||
let result_type = Type::var();
|
let result_type = Type::var();
|
||||||
let expected_fn = Type::function(vec![left_type.clone()], result_type.clone());
|
let expected_fn = Type::function(vec![left_type.clone()], result_type.clone());
|
||||||
if let Err(e) = unify(&right_type, &expected_fn) {
|
if let Err(e) = unify_with_env(&right_type, &expected_fn, &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Pipe target must be a function accepting {}: {}",
|
"Pipe target must be a function accepting {}: {}",
|
||||||
@@ -1857,7 +1904,7 @@ impl TypeChecker {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
UnaryOp::Not => {
|
UnaryOp::Not => {
|
||||||
if let Err(e) = unify(&operand_type, &Type::Bool) {
|
if let Err(e) = unify_with_env(&operand_type, &Type::Bool, &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!("Operator '!' requires Bool operand: {}", e),
|
message: format!("Operator '!' requires Bool operand: {}", e),
|
||||||
span,
|
span,
|
||||||
@@ -1908,7 +1955,7 @@ impl TypeChecker {
|
|||||||
self.current_effects.clone(),
|
self.current_effects.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
match unify(&func_type, &expected_fn) {
|
match unify_with_env(&func_type, &expected_fn, &self.env) {
|
||||||
Ok(subst) => result_type.apply(&subst),
|
Ok(subst) => result_type.apply(&subst),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Provide more detailed error message based on the type of mismatch
|
// Provide more detailed error message based on the type of mismatch
|
||||||
@@ -1985,7 +2032,7 @@ impl TypeChecker {
|
|||||||
let result_type = Type::var();
|
let result_type = Type::var();
|
||||||
let expected_fn = Type::function(arg_types, result_type.clone());
|
let expected_fn = Type::function(arg_types, result_type.clone());
|
||||||
|
|
||||||
if let Err(e) = unify(field_type, &expected_fn) {
|
if let Err(e) = unify_with_env(field_type, &expected_fn, &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Type mismatch in {}.{} call: {}",
|
"Type mismatch in {}.{} call: {}",
|
||||||
@@ -2057,7 +2104,7 @@ impl TypeChecker {
|
|||||||
for (i, (arg_type, (_, param_type))) in
|
for (i, (arg_type, (_, param_type))) in
|
||||||
arg_types.iter().zip(op.params.iter()).enumerate()
|
arg_types.iter().zip(op.params.iter()).enumerate()
|
||||||
{
|
{
|
||||||
if let Err(e) = unify(arg_type, param_type) {
|
if let Err(e) = unify_with_env(arg_type, param_type, &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Argument {} of '{}.{}' has type {}, expected {}: {}",
|
"Argument {} of '{}.{}' has type {}, expected {}: {}",
|
||||||
@@ -2090,6 +2137,7 @@ impl TypeChecker {
|
|||||||
|
|
||||||
fn infer_field(&mut self, object: &Expr, field: &Ident, span: Span) -> Type {
|
fn infer_field(&mut self, object: &Expr, field: &Ident, span: Span) -> Type {
|
||||||
let object_type = self.infer_expr(object);
|
let object_type = self.infer_expr(object);
|
||||||
|
let object_type = self.env.expand_type_alias(&object_type);
|
||||||
|
|
||||||
match &object_type {
|
match &object_type {
|
||||||
Type::Record(fields) => match fields.iter().find(|(n, _)| n == &field.name) {
|
Type::Record(fields) => match fields.iter().find(|(n, _)| n == &field.name) {
|
||||||
@@ -2170,7 +2218,7 @@ impl TypeChecker {
|
|||||||
// Check return type if specified
|
// Check return type if specified
|
||||||
let ret_type = if let Some(rt) = return_type {
|
let ret_type = if let Some(rt) = return_type {
|
||||||
let declared = self.resolve_type(rt);
|
let declared = self.resolve_type(rt);
|
||||||
if let Err(e) = unify(&body_type, &declared) {
|
if let Err(e) = unify_with_env(&body_type, &declared, &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Lambda body type {} doesn't match declared {}: {}",
|
"Lambda body type {} doesn't match declared {}: {}",
|
||||||
@@ -2236,7 +2284,7 @@ impl TypeChecker {
|
|||||||
span: Span,
|
span: Span,
|
||||||
) -> Type {
|
) -> Type {
|
||||||
let cond_type = self.infer_expr(condition);
|
let cond_type = self.infer_expr(condition);
|
||||||
if let Err(e) = unify(&cond_type, &Type::Bool) {
|
if let Err(e) = unify_with_env(&cond_type, &Type::Bool, &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!("If condition must be Bool, got {}: {}", cond_type, e),
|
message: format!("If condition must be Bool, got {}: {}", cond_type, e),
|
||||||
span: condition.span(),
|
span: condition.span(),
|
||||||
@@ -2246,7 +2294,7 @@ impl TypeChecker {
|
|||||||
let then_type = self.infer_expr(then_branch);
|
let then_type = self.infer_expr(then_branch);
|
||||||
let else_type = self.infer_expr(else_branch);
|
let else_type = self.infer_expr(else_branch);
|
||||||
|
|
||||||
match unify(&then_type, &else_type) {
|
match unify_with_env(&then_type, &else_type, &self.env) {
|
||||||
Ok(subst) => then_type.apply(&subst),
|
Ok(subst) => then_type.apply(&subst),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
@@ -2287,7 +2335,7 @@ impl TypeChecker {
|
|||||||
// Check guard if present
|
// Check guard if present
|
||||||
if let Some(ref guard) = arm.guard {
|
if let Some(ref guard) = arm.guard {
|
||||||
let guard_type = self.infer_expr(guard);
|
let guard_type = self.infer_expr(guard);
|
||||||
if let Err(e) = unify(&guard_type, &Type::Bool) {
|
if let Err(e) = unify_with_env(&guard_type, &Type::Bool, &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!("Match guard must be Bool: {}", e),
|
message: format!("Match guard must be Bool: {}", e),
|
||||||
span: guard.span(),
|
span: guard.span(),
|
||||||
@@ -2303,7 +2351,7 @@ impl TypeChecker {
|
|||||||
match &result_type {
|
match &result_type {
|
||||||
None => result_type = Some(body_type),
|
None => result_type = Some(body_type),
|
||||||
Some(prev) => {
|
Some(prev) => {
|
||||||
if let Err(e) = unify(prev, &body_type) {
|
if let Err(e) = unify_with_env(prev, &body_type, &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Match arm has incompatible type: expected {}, got {}: {}",
|
"Match arm has incompatible type: expected {}, got {}: {}",
|
||||||
@@ -2353,7 +2401,7 @@ impl TypeChecker {
|
|||||||
|
|
||||||
Pattern::Literal(lit) => {
|
Pattern::Literal(lit) => {
|
||||||
let lit_type = self.infer_literal(lit);
|
let lit_type = self.infer_literal(lit);
|
||||||
if let Err(e) = unify(&lit_type, expected) {
|
if let Err(e) = unify_with_env(&lit_type, expected, &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!("Pattern literal type mismatch: {}", e),
|
message: format!("Pattern literal type mismatch: {}", e),
|
||||||
span: lit.span,
|
span: lit.span,
|
||||||
@@ -2367,7 +2415,7 @@ impl TypeChecker {
|
|||||||
// For now, handle Option specially
|
// For now, handle Option specially
|
||||||
match name.name.as_str() {
|
match name.name.as_str() {
|
||||||
"None" => {
|
"None" => {
|
||||||
if let Err(e) = unify(expected, &Type::Option(Box::new(Type::var()))) {
|
if let Err(e) = unify_with_env(expected, &Type::Option(Box::new(Type::var())), &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!(
|
message: format!(
|
||||||
"None pattern doesn't match type {}: {}",
|
"None pattern doesn't match type {}: {}",
|
||||||
@@ -2380,7 +2428,7 @@ impl TypeChecker {
|
|||||||
}
|
}
|
||||||
"Some" => {
|
"Some" => {
|
||||||
let inner_type = Type::var();
|
let inner_type = Type::var();
|
||||||
if let Err(e) = unify(expected, &Type::Option(Box::new(inner_type.clone())))
|
if let Err(e) = unify_with_env(expected, &Type::Option(Box::new(inner_type.clone())), &self.env)
|
||||||
{
|
{
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!(
|
message: format!(
|
||||||
@@ -2409,7 +2457,7 @@ impl TypeChecker {
|
|||||||
|
|
||||||
Pattern::Tuple { elements, span } => {
|
Pattern::Tuple { elements, span } => {
|
||||||
let element_types: Vec<Type> = elements.iter().map(|_| Type::var()).collect();
|
let element_types: Vec<Type> = elements.iter().map(|_| Type::var()).collect();
|
||||||
if let Err(e) = unify(expected, &Type::Tuple(element_types.clone())) {
|
if let Err(e) = unify_with_env(expected, &Type::Tuple(element_types.clone()), &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!("Tuple pattern doesn't match type {}: {}", expected, e),
|
message: format!("Tuple pattern doesn't match type {}: {}", expected, e),
|
||||||
span: *span,
|
span: *span,
|
||||||
@@ -2459,7 +2507,7 @@ impl TypeChecker {
|
|||||||
|
|
||||||
if let Some(type_expr) = typ {
|
if let Some(type_expr) = typ {
|
||||||
let declared = self.resolve_type(type_expr);
|
let declared = self.resolve_type(type_expr);
|
||||||
if let Err(e) = unify(&value_type, &declared) {
|
if let Err(e) = unify_with_env(&value_type, &declared, &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Variable '{}' has type {}, but declared type is {}: {}",
|
"Variable '{}' has type {}, but declared type is {}: {}",
|
||||||
@@ -2502,7 +2550,7 @@ impl TypeChecker {
|
|||||||
let first_type = self.infer_expr(&elements[0]);
|
let first_type = self.infer_expr(&elements[0]);
|
||||||
for elem in &elements[1..] {
|
for elem in &elements[1..] {
|
||||||
let elem_type = self.infer_expr(elem);
|
let elem_type = self.infer_expr(elem);
|
||||||
if let Err(e) = unify(&first_type, &elem_type) {
|
if let Err(e) = unify_with_env(&first_type, &elem_type, &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!("List elements must have same type: {}", e),
|
message: format!("List elements must have same type: {}", e),
|
||||||
span,
|
span,
|
||||||
@@ -2808,7 +2856,7 @@ impl TypeChecker {
|
|||||||
// Check return type matches if specified
|
// Check return type matches if specified
|
||||||
if let Some(ref return_type_expr) = impl_method.return_type {
|
if let Some(ref return_type_expr) = impl_method.return_type {
|
||||||
let return_type = self.resolve_type(return_type_expr);
|
let return_type = self.resolve_type(return_type_expr);
|
||||||
if let Err(e) = unify(&body_type, &return_type) {
|
if let Err(e) = unify_with_env(&body_type, &return_type, &self.env) {
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Method '{}' body has type {}, but declared return type is {}: {}",
|
"Method '{}' body has type {}, but declared return type is {}: {}",
|
||||||
|
|||||||
143
src/types.rs
143
src/types.rs
@@ -1146,6 +1146,15 @@ impl TypeEnv {
|
|||||||
],
|
],
|
||||||
return_type: Type::Unit,
|
return_type: Type::Unit,
|
||||||
},
|
},
|
||||||
|
EffectOpDef {
|
||||||
|
name: "assertEqualMsg".to_string(),
|
||||||
|
params: vec![
|
||||||
|
("expected".to_string(), Type::Var(0)),
|
||||||
|
("actual".to_string(), Type::Var(0)),
|
||||||
|
("label".to_string(), Type::String),
|
||||||
|
],
|
||||||
|
return_type: Type::Unit,
|
||||||
|
},
|
||||||
EffectOpDef {
|
EffectOpDef {
|
||||||
name: "assertNotEqual".to_string(),
|
name: "assertNotEqual".to_string(),
|
||||||
params: vec![
|
params: vec![
|
||||||
@@ -1173,6 +1182,110 @@ impl TypeEnv {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add Concurrent effect for concurrent/parallel execution
|
||||||
|
// Task is represented as Int (task ID)
|
||||||
|
env.effects.insert(
|
||||||
|
"Concurrent".to_string(),
|
||||||
|
EffectDef {
|
||||||
|
name: "Concurrent".to_string(),
|
||||||
|
type_params: Vec::new(),
|
||||||
|
operations: vec![
|
||||||
|
// Spawn a new concurrent task that returns a value
|
||||||
|
// Returns a Task<A> (represented as Int task ID)
|
||||||
|
EffectOpDef {
|
||||||
|
name: "spawn".to_string(),
|
||||||
|
params: vec![("thunk".to_string(), Type::Function {
|
||||||
|
params: Vec::new(),
|
||||||
|
return_type: Box::new(Type::Var(0)),
|
||||||
|
effects: EffectSet::empty(),
|
||||||
|
properties: PropertySet::empty(),
|
||||||
|
})],
|
||||||
|
return_type: Type::Int, // Task ID
|
||||||
|
},
|
||||||
|
// Wait for a task to complete and get its result
|
||||||
|
EffectOpDef {
|
||||||
|
name: "await".to_string(),
|
||||||
|
params: vec![("task".to_string(), Type::Int)],
|
||||||
|
return_type: Type::Var(0),
|
||||||
|
},
|
||||||
|
// Yield control to allow other tasks to run
|
||||||
|
EffectOpDef {
|
||||||
|
name: "yield".to_string(),
|
||||||
|
params: Vec::new(),
|
||||||
|
return_type: Type::Unit,
|
||||||
|
},
|
||||||
|
// Sleep for milliseconds (non-blocking to other tasks)
|
||||||
|
EffectOpDef {
|
||||||
|
name: "sleep".to_string(),
|
||||||
|
params: vec![("ms".to_string(), Type::Int)],
|
||||||
|
return_type: Type::Unit,
|
||||||
|
},
|
||||||
|
// Cancel a running task
|
||||||
|
EffectOpDef {
|
||||||
|
name: "cancel".to_string(),
|
||||||
|
params: vec![("task".to_string(), Type::Int)],
|
||||||
|
return_type: Type::Bool,
|
||||||
|
},
|
||||||
|
// Check if a task is still running
|
||||||
|
EffectOpDef {
|
||||||
|
name: "isRunning".to_string(),
|
||||||
|
params: vec![("task".to_string(), Type::Int)],
|
||||||
|
return_type: Type::Bool,
|
||||||
|
},
|
||||||
|
// Get the number of active tasks
|
||||||
|
EffectOpDef {
|
||||||
|
name: "taskCount".to_string(),
|
||||||
|
params: Vec::new(),
|
||||||
|
return_type: Type::Int,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add Channel effect for concurrent communication
|
||||||
|
env.effects.insert(
|
||||||
|
"Channel".to_string(),
|
||||||
|
EffectDef {
|
||||||
|
name: "Channel".to_string(),
|
||||||
|
type_params: Vec::new(),
|
||||||
|
operations: vec![
|
||||||
|
// Create a new channel, returns channel ID
|
||||||
|
EffectOpDef {
|
||||||
|
name: "create".to_string(),
|
||||||
|
params: Vec::new(),
|
||||||
|
return_type: Type::Int, // Channel ID
|
||||||
|
},
|
||||||
|
// Send a value on a channel
|
||||||
|
EffectOpDef {
|
||||||
|
name: "send".to_string(),
|
||||||
|
params: vec![
|
||||||
|
("channel".to_string(), Type::Int),
|
||||||
|
("value".to_string(), Type::Var(0)),
|
||||||
|
],
|
||||||
|
return_type: Type::Unit,
|
||||||
|
},
|
||||||
|
// Receive a value from a channel (blocks until available)
|
||||||
|
EffectOpDef {
|
||||||
|
name: "receive".to_string(),
|
||||||
|
params: vec![("channel".to_string(), Type::Int)],
|
||||||
|
return_type: Type::Var(0),
|
||||||
|
},
|
||||||
|
// Try to receive (non-blocking, returns Option)
|
||||||
|
EffectOpDef {
|
||||||
|
name: "tryReceive".to_string(),
|
||||||
|
params: vec![("channel".to_string(), Type::Int)],
|
||||||
|
return_type: Type::Option(Box::new(Type::Var(0))),
|
||||||
|
},
|
||||||
|
// Close a channel
|
||||||
|
EffectOpDef {
|
||||||
|
name: "close".to_string(),
|
||||||
|
params: vec![("channel".to_string(), Type::Int)],
|
||||||
|
return_type: Type::Unit,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Add Sql effect for database access
|
// Add Sql effect for database access
|
||||||
// Connection is represented as Int (connection ID)
|
// Connection is represented as Int (connection ID)
|
||||||
let row_type = Type::Record(vec![]); // Dynamic record type
|
let row_type = Type::Record(vec![]); // Dynamic record type
|
||||||
@@ -1495,6 +1608,14 @@ impl TypeEnv {
|
|||||||
"parseFloat".to_string(),
|
"parseFloat".to_string(),
|
||||||
Type::function(vec![Type::String], Type::Option(Box::new(Type::Float))),
|
Type::function(vec![Type::String], Type::Option(Box::new(Type::Float))),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"indexOf".to_string(),
|
||||||
|
Type::function(vec![Type::String, Type::String], Type::Option(Box::new(Type::Int))),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"lastIndexOf".to_string(),
|
||||||
|
Type::function(vec![Type::String, Type::String], Type::Option(Box::new(Type::Int))),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
env.bind("String", TypeScheme::mono(string_module_type));
|
env.bind("String", TypeScheme::mono(string_module_type));
|
||||||
|
|
||||||
@@ -1769,6 +1890,24 @@ impl TypeEnv {
|
|||||||
]);
|
]);
|
||||||
env.bind("Math", TypeScheme::mono(math_module_type));
|
env.bind("Math", TypeScheme::mono(math_module_type));
|
||||||
|
|
||||||
|
// Int module
|
||||||
|
let int_module_type = Type::Record(vec![
|
||||||
|
(
|
||||||
|
"toString".to_string(),
|
||||||
|
Type::function(vec![Type::Int], Type::String),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
env.bind("Int", TypeScheme::mono(int_module_type));
|
||||||
|
|
||||||
|
// Float module
|
||||||
|
let float_module_type = Type::Record(vec![
|
||||||
|
(
|
||||||
|
"toString".to_string(),
|
||||||
|
Type::function(vec![Type::Float], Type::String),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
env.bind("Float", TypeScheme::mono(float_module_type));
|
||||||
|
|
||||||
env
|
env
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1928,7 +2067,9 @@ pub fn unify(t1: &Type, t2: &Type) -> Result<Substitution, String> {
|
|||||||
// Function's required effects (e1) must be a subset of available effects (e2)
|
// Function's required effects (e1) must be a subset of available effects (e2)
|
||||||
// A pure function (empty effects) can be called anywhere
|
// A pure function (empty effects) can be called anywhere
|
||||||
// A function requiring {Logger} can be called in context with {Logger} or {Logger, Console}
|
// A function requiring {Logger} can be called in context with {Logger} or {Logger, Console}
|
||||||
if !e1.is_subset(&e2) {
|
// When expected effects (e2) are empty, it means "no constraint" (e.g., callback parameter)
|
||||||
|
// so we allow any actual effects through
|
||||||
|
if !e2.is_empty() && !e1.is_subset(&e2) {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Effect mismatch: expected {{{}}}, got {{{}}}",
|
"Effect mismatch: expected {{{}}}, got {{{}}}",
|
||||||
e1, e2
|
e1, e2
|
||||||
|
|||||||
162
stdlib/html.lux
162
stdlib/html.lux
@@ -11,13 +11,13 @@
|
|||||||
|
|
||||||
// Html type represents a DOM structure
|
// Html type represents a DOM structure
|
||||||
// Parameterized by Msg - the type of messages emitted by event handlers
|
// Parameterized by Msg - the type of messages emitted by event handlers
|
||||||
type Html<M> =
|
pub type Html<M> =
|
||||||
| Element(String, List<Attr<M>>, List<Html<M>>)
|
| Element(String, List<Attr<M>>, List<Html<M>>)
|
||||||
| Text(String)
|
| Text(String)
|
||||||
| Empty
|
| Empty
|
||||||
|
|
||||||
// Attributes that can be applied to elements
|
// Attributes that can be applied to elements
|
||||||
type Attr<M> =
|
pub type Attr<M> =
|
||||||
| Class(String)
|
| Class(String)
|
||||||
| Id(String)
|
| Id(String)
|
||||||
| Style(String, String)
|
| Style(String, String)
|
||||||
@@ -46,243 +46,243 @@ type Attr<M> =
|
|||||||
// Element builders - Container elements
|
// Element builders - Container elements
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
fn div<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn div<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("div", attrs, children)
|
Element("div", attrs, children)
|
||||||
|
|
||||||
fn span<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn span<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("span", attrs, children)
|
Element("span", attrs, children)
|
||||||
|
|
||||||
fn section<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn section<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("section", attrs, children)
|
Element("section", attrs, children)
|
||||||
|
|
||||||
fn article<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn article<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("article", attrs, children)
|
Element("article", attrs, children)
|
||||||
|
|
||||||
fn header<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn header<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("header", attrs, children)
|
Element("header", attrs, children)
|
||||||
|
|
||||||
fn footer<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn footer<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("footer", attrs, children)
|
Element("footer", attrs, children)
|
||||||
|
|
||||||
fn nav<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn nav<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("nav", attrs, children)
|
Element("nav", attrs, children)
|
||||||
|
|
||||||
fn main<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn main<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("main", attrs, children)
|
Element("main", attrs, children)
|
||||||
|
|
||||||
fn aside<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn aside<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("aside", attrs, children)
|
Element("aside", attrs, children)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Element builders - Text elements
|
// Element builders - Text elements
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
fn h1<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn h1<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("h1", attrs, children)
|
Element("h1", attrs, children)
|
||||||
|
|
||||||
fn h2<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn h2<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("h2", attrs, children)
|
Element("h2", attrs, children)
|
||||||
|
|
||||||
fn h3<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn h3<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("h3", attrs, children)
|
Element("h3", attrs, children)
|
||||||
|
|
||||||
fn h4<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn h4<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("h4", attrs, children)
|
Element("h4", attrs, children)
|
||||||
|
|
||||||
fn h5<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn h5<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("h5", attrs, children)
|
Element("h5", attrs, children)
|
||||||
|
|
||||||
fn h6<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn h6<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("h6", attrs, children)
|
Element("h6", attrs, children)
|
||||||
|
|
||||||
fn p<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn p<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("p", attrs, children)
|
Element("p", attrs, children)
|
||||||
|
|
||||||
fn pre<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn pre<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("pre", attrs, children)
|
Element("pre", attrs, children)
|
||||||
|
|
||||||
fn code<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn code<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("code", attrs, children)
|
Element("code", attrs, children)
|
||||||
|
|
||||||
fn blockquote<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn blockquote<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("blockquote", attrs, children)
|
Element("blockquote", attrs, children)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Element builders - Inline elements
|
// Element builders - Inline elements
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
fn a<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn a<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("a", attrs, children)
|
Element("a", attrs, children)
|
||||||
|
|
||||||
fn strong<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn strong<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("strong", attrs, children)
|
Element("strong", attrs, children)
|
||||||
|
|
||||||
fn em<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn em<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("em", attrs, children)
|
Element("em", attrs, children)
|
||||||
|
|
||||||
fn small<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn small<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("small", attrs, children)
|
Element("small", attrs, children)
|
||||||
|
|
||||||
fn br<M>(): Html<M> =
|
pub fn br<M>(): Html<M> =
|
||||||
Element("br", [], [])
|
Element("br", [], [])
|
||||||
|
|
||||||
fn hr<M>(): Html<M> =
|
pub fn hr<M>(): Html<M> =
|
||||||
Element("hr", [], [])
|
Element("hr", [], [])
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Element builders - Lists
|
// Element builders - Lists
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
fn ul<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn ul<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("ul", attrs, children)
|
Element("ul", attrs, children)
|
||||||
|
|
||||||
fn ol<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn ol<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("ol", attrs, children)
|
Element("ol", attrs, children)
|
||||||
|
|
||||||
fn li<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn li<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("li", attrs, children)
|
Element("li", attrs, children)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Element builders - Forms
|
// Element builders - Forms
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
fn form<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn form<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("form", attrs, children)
|
Element("form", attrs, children)
|
||||||
|
|
||||||
fn input<M>(attrs: List<Attr<M>>): Html<M> =
|
pub fn input<M>(attrs: List<Attr<M>>): Html<M> =
|
||||||
Element("input", attrs, [])
|
Element("input", attrs, [])
|
||||||
|
|
||||||
fn textarea<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn textarea<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("textarea", attrs, children)
|
Element("textarea", attrs, children)
|
||||||
|
|
||||||
fn button<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn button<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("button", attrs, children)
|
Element("button", attrs, children)
|
||||||
|
|
||||||
fn label<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn label<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("label", attrs, children)
|
Element("label", attrs, children)
|
||||||
|
|
||||||
fn select<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn select<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("select", attrs, children)
|
Element("select", attrs, children)
|
||||||
|
|
||||||
fn option<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn option<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("option", attrs, children)
|
Element("option", attrs, children)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Element builders - Media
|
// Element builders - Media
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
fn img<M>(attrs: List<Attr<M>>): Html<M> =
|
pub fn img<M>(attrs: List<Attr<M>>): Html<M> =
|
||||||
Element("img", attrs, [])
|
Element("img", attrs, [])
|
||||||
|
|
||||||
fn video<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn video<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("video", attrs, children)
|
Element("video", attrs, children)
|
||||||
|
|
||||||
fn audio<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn audio<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("audio", attrs, children)
|
Element("audio", attrs, children)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Element builders - Tables
|
// Element builders - Tables
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
fn table<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn table<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("table", attrs, children)
|
Element("table", attrs, children)
|
||||||
|
|
||||||
fn thead<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn thead<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("thead", attrs, children)
|
Element("thead", attrs, children)
|
||||||
|
|
||||||
fn tbody<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn tbody<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("tbody", attrs, children)
|
Element("tbody", attrs, children)
|
||||||
|
|
||||||
fn tr<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn tr<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("tr", attrs, children)
|
Element("tr", attrs, children)
|
||||||
|
|
||||||
fn th<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn th<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("th", attrs, children)
|
Element("th", attrs, children)
|
||||||
|
|
||||||
fn td<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
pub fn td<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||||
Element("td", attrs, children)
|
Element("td", attrs, children)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Text and empty nodes
|
// Text and empty nodes
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
fn text<M>(content: String): Html<M> =
|
pub fn text<M>(content: String): Html<M> =
|
||||||
Text(content)
|
Text(content)
|
||||||
|
|
||||||
fn empty<M>(): Html<M> =
|
pub fn empty<M>(): Html<M> =
|
||||||
Empty
|
Empty
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Attribute helpers
|
// Attribute helpers
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
fn class<M>(name: String): Attr<M> =
|
pub fn class<M>(name: String): Attr<M> =
|
||||||
Class(name)
|
Class(name)
|
||||||
|
|
||||||
fn id<M>(name: String): Attr<M> =
|
pub fn id<M>(name: String): Attr<M> =
|
||||||
Id(name)
|
Id(name)
|
||||||
|
|
||||||
fn style<M>(property: String, value: String): Attr<M> =
|
pub fn style<M>(property: String, value: String): Attr<M> =
|
||||||
Style(property, value)
|
Style(property, value)
|
||||||
|
|
||||||
fn href<M>(url: String): Attr<M> =
|
pub fn href<M>(url: String): Attr<M> =
|
||||||
Href(url)
|
Href(url)
|
||||||
|
|
||||||
fn src<M>(url: String): Attr<M> =
|
pub fn src<M>(url: String): Attr<M> =
|
||||||
Src(url)
|
Src(url)
|
||||||
|
|
||||||
fn alt<M>(description: String): Attr<M> =
|
pub fn alt<M>(description: String): Attr<M> =
|
||||||
Alt(description)
|
Alt(description)
|
||||||
|
|
||||||
fn inputType<M>(t: String): Attr<M> =
|
pub fn inputType<M>(t: String): Attr<M> =
|
||||||
Type(t)
|
Type(t)
|
||||||
|
|
||||||
fn value<M>(v: String): Attr<M> =
|
pub fn value<M>(v: String): Attr<M> =
|
||||||
Value(v)
|
Value(v)
|
||||||
|
|
||||||
fn placeholder<M>(p: String): Attr<M> =
|
pub fn placeholder<M>(p: String): Attr<M> =
|
||||||
Placeholder(p)
|
Placeholder(p)
|
||||||
|
|
||||||
fn disabled<M>(d: Bool): Attr<M> =
|
pub fn disabled<M>(d: Bool): Attr<M> =
|
||||||
Disabled(d)
|
Disabled(d)
|
||||||
|
|
||||||
fn checked<M>(c: Bool): Attr<M> =
|
pub fn checked<M>(c: Bool): Attr<M> =
|
||||||
Checked(c)
|
Checked(c)
|
||||||
|
|
||||||
fn name<M>(n: String): Attr<M> =
|
pub fn name<M>(n: String): Attr<M> =
|
||||||
Name(n)
|
Name(n)
|
||||||
|
|
||||||
fn onClick<M>(msg: M): Attr<M> =
|
pub fn onClick<M>(msg: M): Attr<M> =
|
||||||
OnClick(msg)
|
OnClick(msg)
|
||||||
|
|
||||||
fn onInput<M>(h: fn(String): M): Attr<M> =
|
pub fn onInput<M>(h: fn(String): M): Attr<M> =
|
||||||
OnInput(h)
|
OnInput(h)
|
||||||
|
|
||||||
fn onSubmit<M>(msg: M): Attr<M> =
|
pub fn onSubmit<M>(msg: M): Attr<M> =
|
||||||
OnSubmit(msg)
|
OnSubmit(msg)
|
||||||
|
|
||||||
fn onChange<M>(h: fn(String): M): Attr<M> =
|
pub fn onChange<M>(h: fn(String): M): Attr<M> =
|
||||||
OnChange(h)
|
OnChange(h)
|
||||||
|
|
||||||
fn onMouseEnter<M>(msg: M): Attr<M> =
|
pub fn onMouseEnter<M>(msg: M): Attr<M> =
|
||||||
OnMouseEnter(msg)
|
OnMouseEnter(msg)
|
||||||
|
|
||||||
fn onMouseLeave<M>(msg: M): Attr<M> =
|
pub fn onMouseLeave<M>(msg: M): Attr<M> =
|
||||||
OnMouseLeave(msg)
|
OnMouseLeave(msg)
|
||||||
|
|
||||||
fn onFocus<M>(msg: M): Attr<M> =
|
pub fn onFocus<M>(msg: M): Attr<M> =
|
||||||
OnFocus(msg)
|
OnFocus(msg)
|
||||||
|
|
||||||
fn onBlur<M>(msg: M): Attr<M> =
|
pub fn onBlur<M>(msg: M): Attr<M> =
|
||||||
OnBlur(msg)
|
OnBlur(msg)
|
||||||
|
|
||||||
fn onKeyDown<M>(h: fn(String): M): Attr<M> =
|
pub fn onKeyDown<M>(h: fn(String): M): Attr<M> =
|
||||||
OnKeyDown(h)
|
OnKeyDown(h)
|
||||||
|
|
||||||
fn onKeyUp<M>(h: fn(String): M): Attr<M> =
|
pub fn onKeyUp<M>(h: fn(String): M): Attr<M> =
|
||||||
OnKeyUp(h)
|
OnKeyUp(h)
|
||||||
|
|
||||||
fn data<M>(name: String, value: String): Attr<M> =
|
pub fn data<M>(name: String, value: String): Attr<M> =
|
||||||
DataAttr(name, value)
|
DataAttr(name, value)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -290,11 +290,11 @@ fn data<M>(name: String, value: String): Attr<M> =
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// Conditionally include an element
|
// Conditionally include an element
|
||||||
fn when<M>(condition: Bool, element: Html<M>): Html<M> =
|
pub fn when<M>(condition: Bool, element: Html<M>): Html<M> =
|
||||||
if condition then element else Empty
|
if condition then element else Empty
|
||||||
|
|
||||||
// Conditionally apply attributes
|
// Conditionally apply attributes
|
||||||
fn attrIf<M>(condition: Bool, attr: Attr<M>): List<Attr<M>> =
|
pub fn attrIf<M>(condition: Bool, attr: Attr<M>): List<Attr<M>> =
|
||||||
if condition then [attr] else []
|
if condition then [attr] else []
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -302,7 +302,7 @@ fn attrIf<M>(condition: Bool, attr: Attr<M>): List<Attr<M>> =
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// Render an attribute to a string
|
// Render an attribute to a string
|
||||||
fn renderAttr<M>(attr: Attr<M>): String =
|
pub fn renderAttr<M>(attr: Attr<M>): String =
|
||||||
match attr {
|
match attr {
|
||||||
Class(name) => " class=\"" + name + "\"",
|
Class(name) => " class=\"" + name + "\"",
|
||||||
Id(name) => " id=\"" + name + "\"",
|
Id(name) => " id=\"" + name + "\"",
|
||||||
@@ -333,24 +333,24 @@ fn renderAttr<M>(attr: Attr<M>): String =
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render attributes list to string
|
// Render attributes list to string
|
||||||
fn renderAttrs<M>(attrs: List<Attr<M>>): String =
|
pub fn renderAttrs<M>(attrs: List<Attr<M>>): String =
|
||||||
List.foldl(attrs, "", fn(acc, attr) => acc + renderAttr(attr))
|
List.fold(attrs, "", fn(acc, attr) => acc + renderAttr(attr))
|
||||||
|
|
||||||
// Self-closing tags
|
// Self-closing tags
|
||||||
fn isSelfClosing(tag: String): Bool =
|
pub fn isSelfClosing(tag: String): Bool =
|
||||||
tag == "br" || tag == "hr" || tag == "img" || tag == "input" ||
|
tag == "br" || tag == "hr" || tag == "img" || tag == "input" ||
|
||||||
tag == "meta" || tag == "link" || tag == "area" || tag == "base" ||
|
tag == "meta" || tag == "link" || tag == "area" || tag == "base" ||
|
||||||
tag == "col" || tag == "embed" || tag == "source" || tag == "track" || tag == "wbr"
|
tag == "col" || tag == "embed" || tag == "source" || tag == "track" || tag == "wbr"
|
||||||
|
|
||||||
// Render Html to string
|
// Render Html to string
|
||||||
fn render<M>(html: Html<M>): String =
|
pub fn render<M>(html: Html<M>): String =
|
||||||
match html {
|
match html {
|
||||||
Element(tag, attrs, children) => {
|
Element(tag, attrs, children) => {
|
||||||
let attrStr = renderAttrs(attrs)
|
let attrStr = renderAttrs(attrs)
|
||||||
if isSelfClosing(tag) then
|
if isSelfClosing(tag) then
|
||||||
"<" + tag + attrStr + " />"
|
"<" + tag + attrStr + " />"
|
||||||
else {
|
else {
|
||||||
let childrenStr = List.foldl(children, "", fn(acc, child) => acc + render(child))
|
let childrenStr = List.fold(children, "", fn(acc, child) => acc + render(child))
|
||||||
"<" + tag + attrStr + ">" + childrenStr + "</" + tag + ">"
|
"<" + tag + attrStr + ">" + childrenStr + "</" + tag + ">"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -359,7 +359,7 @@ fn render<M>(html: Html<M>): String =
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Escape HTML special characters
|
// Escape HTML special characters
|
||||||
fn escapeHtml(s: String): String = {
|
pub fn escapeHtml(s: String): String = {
|
||||||
// Simple replacement - a full implementation would handle all entities
|
// Simple replacement - a full implementation would handle all entities
|
||||||
let s1 = String.replace(s, "&", "&")
|
let s1 = String.replace(s, "&", "&")
|
||||||
let s2 = String.replace(s1, "<", "<")
|
let s2 = String.replace(s1, "<", "<")
|
||||||
@@ -369,7 +369,7 @@ fn escapeHtml(s: String): String = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render a full HTML document
|
// Render a full HTML document
|
||||||
fn document(title: String, headExtra: List<Html<M>>, bodyContent: List<Html<M>>): String = {
|
pub fn document(title: String, headExtra: List<Html<M>>, bodyContent: List<Html<M>>): String = {
|
||||||
let headElements = List.concat([
|
let headElements = List.concat([
|
||||||
[Element("meta", [DataAttr("charset", "UTF-8")], [])],
|
[Element("meta", [DataAttr("charset", "UTF-8")], [])],
|
||||||
[Element("meta", [Name("viewport"), Value("width=device-width, initial-scale=1.0")], [])],
|
[Element("meta", [Name("viewport"), Value("width=device-width, initial-scale=1.0")], [])],
|
||||||
|
|||||||
527
stdlib/http.lux
527
stdlib/http.lux
@@ -42,6 +42,10 @@ fn httpNotFound(body: String): { status: Int, body: String } =
|
|||||||
fn httpServerError(body: String): { status: Int, body: String } =
|
fn httpServerError(body: String): { status: Int, body: String } =
|
||||||
{ status: 500, body: body }
|
{ status: 500, body: body }
|
||||||
|
|
||||||
|
// Create a 429 Too Many Requests response
|
||||||
|
fn httpTooManyRequests(body: String): { status: Int, body: String } =
|
||||||
|
{ status: 429, body: body }
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Path Matching
|
// Path Matching
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -84,6 +88,54 @@ fn getPathSegment(path: String, index: Int): Option<String> = {
|
|||||||
List.get(parts, index + 1)
|
List.get(parts, index + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract path parameters from a matched route pattern
|
||||||
|
// For path "/users/42/posts/5" and pattern "/users/:userId/posts/:postId"
|
||||||
|
// returns [("userId", "42"), ("postId", "5")]
|
||||||
|
fn getPathParams(path: String, pattern: String): List<(String, String)> = {
|
||||||
|
let pathParts = String.split(path, "/")
|
||||||
|
let patternParts = String.split(pattern, "/")
|
||||||
|
extractParamsHelper(pathParts, patternParts, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extractParamsHelper(pathParts: List<String>, patternParts: List<String>, acc: List<(String, String)>): List<(String, String)> = {
|
||||||
|
if List.length(pathParts) == 0 || List.length(patternParts) == 0 then
|
||||||
|
List.reverse(acc)
|
||||||
|
else {
|
||||||
|
match List.head(pathParts) {
|
||||||
|
None => List.reverse(acc),
|
||||||
|
Some(p) => match List.head(patternParts) {
|
||||||
|
None => List.reverse(acc),
|
||||||
|
Some(pat) => {
|
||||||
|
let restPath = Option.getOrElse(List.tail(pathParts), [])
|
||||||
|
let restPattern = Option.getOrElse(List.tail(patternParts), [])
|
||||||
|
if String.startsWith(pat, ":") then {
|
||||||
|
let paramName = String.substring(pat, 1, String.length(pat))
|
||||||
|
let newAcc = List.concat([(paramName, p)], acc)
|
||||||
|
extractParamsHelper(restPath, restPattern, newAcc)
|
||||||
|
} else {
|
||||||
|
extractParamsHelper(restPath, restPattern, acc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a specific path parameter by name from a list of params
|
||||||
|
fn getParam(params: List<(String, String)>, name: String): Option<String> = {
|
||||||
|
if List.length(params) == 0 then None
|
||||||
|
else {
|
||||||
|
match List.head(params) {
|
||||||
|
None => None,
|
||||||
|
Some(pair) => match pair {
|
||||||
|
(pName, pValue) =>
|
||||||
|
if pName == name then Some(pValue)
|
||||||
|
else getParam(Option.getOrElse(List.tail(params), []), name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// JSON Helpers
|
// JSON Helpers
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -130,32 +182,483 @@ fn jsonMessage(text: String): String =
|
|||||||
jsonObject(jsonString("message", text))
|
jsonObject(jsonString("message", text))
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Usage Example (copy into your file)
|
// Header Helpers
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Get a header value from request headers (case-insensitive)
|
||||||
|
fn getHeader(headers: List<(String, String)>, name: String): Option<String> = {
|
||||||
|
let lowerName = String.toLower(name)
|
||||||
|
getHeaderHelper(headers, lowerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getHeaderHelper(headers: List<(String, String)>, lowerName: String): Option<String> = {
|
||||||
|
if List.length(headers) == 0 then None
|
||||||
|
else {
|
||||||
|
match List.head(headers) {
|
||||||
|
None => None,
|
||||||
|
Some(header) => match header {
|
||||||
|
(hName, hValue) =>
|
||||||
|
if String.toLower(hName) == lowerName then Some(hValue)
|
||||||
|
else getHeaderHelper(Option.getOrElse(List.tail(headers), []), lowerName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Routing Helpers
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
|
// Route matching pattern:
|
||||||
|
//
|
||||||
// fn router(method: String, path: String, body: String): { status: Int, body: String } = {
|
// fn router(method: String, path: String, body: String): { status: Int, body: String } = {
|
||||||
// if method == "GET" && path == "/" then httpOk("Welcome!")
|
// if method == "GET" && path == "/" then httpOk("Home")
|
||||||
// else if method == "GET" && pathMatches(path, "/users/:id") then {
|
// else if method == "GET" && pathMatches(path, "/users/:id") then {
|
||||||
// match getPathSegment(path, 1) {
|
// let params = getPathParams(path, "/users/:id")
|
||||||
|
// match getParam(params, "id") {
|
||||||
// Some(id) => httpOk(jsonObject(jsonString("id", id))),
|
// Some(id) => httpOk(jsonObject(jsonString("id", id))),
|
||||||
// None => httpNotFound(jsonErrorMsg("User not found"))
|
// None => httpNotFound(jsonErrorMsg("User not found"))
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// else httpNotFound(jsonErrorMsg("Not found"))
|
// else if method == "POST" && path == "/users" then
|
||||||
|
// httpCreated(body)
|
||||||
|
// else
|
||||||
|
// httpNotFound(jsonErrorMsg("Not found"))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Helper to check if request is a GET to a specific path
|
||||||
|
fn isGet(method: String, path: String, pattern: String): Bool =
|
||||||
|
method == "GET" && pathMatches(path, pattern)
|
||||||
|
|
||||||
|
// Helper to check if request is a POST to a specific path
|
||||||
|
fn isPost(method: String, path: String, pattern: String): Bool =
|
||||||
|
method == "POST" && pathMatches(path, pattern)
|
||||||
|
|
||||||
|
// Helper to check if request is a PUT to a specific path
|
||||||
|
fn isPut(method: String, path: String, pattern: String): Bool =
|
||||||
|
method == "PUT" && pathMatches(path, pattern)
|
||||||
|
|
||||||
|
// Helper to check if request is a DELETE to a specific path
|
||||||
|
fn isDelete(method: String, path: String, pattern: String): Bool =
|
||||||
|
method == "DELETE" && pathMatches(path, pattern)
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Server Loop Patterns
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// The server loop should be defined in your main file:
|
||||||
|
//
|
||||||
|
// fn serverLoop(): Unit with {HttpServer} = {
|
||||||
|
// let req = HttpServer.accept()
|
||||||
|
// let resp = router(req.method, req.path, req.body, req.headers)
|
||||||
|
// HttpServer.respond(resp.status, resp.body)
|
||||||
|
// serverLoop()
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// fn main(): Unit with {Console, HttpServer} = {
|
// For testing with a fixed number of requests:
|
||||||
// HttpServer.listen(8080)
|
|
||||||
// Console.print("Server running on port 8080")
|
|
||||||
// serveLoop(5) // Handle 5 requests
|
|
||||||
// }
|
|
||||||
//
|
//
|
||||||
// fn serveLoop(remaining: Int): Unit with {Console, HttpServer} = {
|
// fn serverLoopN(remaining: Int): Unit with {HttpServer} = {
|
||||||
// if remaining <= 0 then HttpServer.stop()
|
// if remaining <= 0 then HttpServer.stop()
|
||||||
// else {
|
// else {
|
||||||
// let req = HttpServer.accept()
|
// let req = HttpServer.accept()
|
||||||
// let resp = router(req.method, req.path, req.body)
|
// let resp = router(req.method, req.path, req.body, req.headers)
|
||||||
// HttpServer.respond(resp.status, resp.body)
|
// HttpServer.respond(resp.status, resp.body)
|
||||||
// serveLoop(remaining - 1)
|
// serverLoopN(remaining - 1)
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Middleware Pattern
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Middleware wraps handlers to add cross-cutting concerns.
|
||||||
|
// In Lux, middleware is implemented as function composition.
|
||||||
|
//
|
||||||
|
// Example logging middleware:
|
||||||
|
//
|
||||||
|
// fn withLogging(
|
||||||
|
// handler: fn(String, String, String): { status: Int, body: String }
|
||||||
|
// ): fn(String, String, String): { status: Int, body: String } with {Console} = {
|
||||||
|
// fn(method: String, path: String, body: String): { status: Int, body: String } => {
|
||||||
|
// Console.print("[HTTP] " + method + " " + path)
|
||||||
|
// let response = handler(method, path, body)
|
||||||
|
// Console.print("[HTTP] " + toString(response.status))
|
||||||
|
// response
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// let myHandler = withLogging(router)
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// CORS Headers
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Standard CORS headers for API responses
|
||||||
|
fn corsHeaders(): List<(String, String)> = [
|
||||||
|
("Access-Control-Allow-Origin", "*"),
|
||||||
|
("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"),
|
||||||
|
("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||||
|
]
|
||||||
|
|
||||||
|
// CORS headers for specific origin with credentials
|
||||||
|
fn corsHeadersWithOrigin(origin: String): List<(String, String)> = [
|
||||||
|
("Access-Control-Allow-Origin", origin),
|
||||||
|
("Access-Control-Allow-Credentials", "true"),
|
||||||
|
("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"),
|
||||||
|
("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||||
|
]
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Content Type Headers
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
fn jsonHeaders(): List<(String, String)> = [
|
||||||
|
("Content-Type", "application/json")
|
||||||
|
]
|
||||||
|
|
||||||
|
fn htmlHeaders(): List<(String, String)> = [
|
||||||
|
("Content-Type", "text/html; charset=utf-8")
|
||||||
|
]
|
||||||
|
|
||||||
|
fn textHeaders(): List<(String, String)> = [
|
||||||
|
("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
]
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Query String Parsing
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Parse query string from path (e.g., "/search?q=hello&page=1")
|
||||||
|
// Returns the path without query string and a list of parameters
|
||||||
|
pub fn parseQueryString(fullPath: String): (String, List<(String, String)>) = {
|
||||||
|
match String.indexOf(fullPath, "?") {
|
||||||
|
None => (fullPath, []),
|
||||||
|
Some(idx) => {
|
||||||
|
let path = String.substring(fullPath, 0, idx)
|
||||||
|
let queryStr = String.substring(fullPath, idx + 1, String.length(fullPath))
|
||||||
|
let params = parseQueryParams(queryStr)
|
||||||
|
(path, params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseQueryParams(queryStr: String): List<(String, String)> = {
|
||||||
|
let pairs = String.split(queryStr, "&")
|
||||||
|
List.filterMap(pairs, fn(pair: String): Option<(String, String)> => {
|
||||||
|
match String.indexOf(pair, "=") {
|
||||||
|
None => None,
|
||||||
|
Some(idx) => {
|
||||||
|
let key = String.substring(pair, 0, idx)
|
||||||
|
let value = String.substring(pair, idx + 1, String.length(pair))
|
||||||
|
Some((urlDecode(key), urlDecode(value)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a query parameter by name
|
||||||
|
pub fn getQueryParam(params: List<(String, String)>, name: String): Option<String> =
|
||||||
|
getParam(params, name)
|
||||||
|
|
||||||
|
// Simple URL decoding (handles %XX and +)
|
||||||
|
fn urlDecode(s: String): String = {
|
||||||
|
// For now, just replace + with space
|
||||||
|
// Full implementation would decode %XX sequences
|
||||||
|
String.replace(s, "+", " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Cookie Handling
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Parse cookies from Cookie header value
|
||||||
|
pub fn parseCookies(cookieHeader: String): List<(String, String)> = {
|
||||||
|
let pairs = String.split(cookieHeader, "; ")
|
||||||
|
List.filterMap(pairs, fn(pair: String): Option<(String, String)> => {
|
||||||
|
match String.indexOf(pair, "=") {
|
||||||
|
None => None,
|
||||||
|
Some(idx) => {
|
||||||
|
let name = String.trim(String.substring(pair, 0, idx))
|
||||||
|
let value = String.trim(String.substring(pair, idx + 1, String.length(pair)))
|
||||||
|
Some((name, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a cookie value by name from request headers
|
||||||
|
pub fn getCookie(headers: List<(String, String)>, name: String): Option<String> = {
|
||||||
|
match getHeader(headers, "Cookie") {
|
||||||
|
None => None,
|
||||||
|
Some(cookieHeader) => {
|
||||||
|
let cookies = parseCookies(cookieHeader)
|
||||||
|
getParam(cookies, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a Set-Cookie header value
|
||||||
|
pub fn setCookie(name: String, value: String): String =
|
||||||
|
name + "=" + value
|
||||||
|
|
||||||
|
// Create a Set-Cookie header with options
|
||||||
|
pub fn setCookieWithOptions(
|
||||||
|
name: String,
|
||||||
|
value: String,
|
||||||
|
maxAge: Option<Int>,
|
||||||
|
path: Option<String>,
|
||||||
|
httpOnly: Bool,
|
||||||
|
secure: Bool
|
||||||
|
): String = {
|
||||||
|
let base = name + "=" + value
|
||||||
|
let withMaxAge = match maxAge {
|
||||||
|
Some(age) => base + "; Max-Age=" + toString(age),
|
||||||
|
None => base
|
||||||
|
}
|
||||||
|
let withPath = match path {
|
||||||
|
Some(p) => withMaxAge + "; Path=" + p,
|
||||||
|
None => withMaxAge
|
||||||
|
}
|
||||||
|
let withHttpOnly = if httpOnly then withPath + "; HttpOnly" else withPath
|
||||||
|
if secure then withHttpOnly + "; Secure" else withHttpOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Static File MIME Types
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Get MIME type for a file extension
|
||||||
|
pub fn getMimeType(path: String): String = {
|
||||||
|
let ext = getFileExtension(path)
|
||||||
|
match ext {
|
||||||
|
"html" => "text/html; charset=utf-8",
|
||||||
|
"htm" => "text/html; charset=utf-8",
|
||||||
|
"css" => "text/css; charset=utf-8",
|
||||||
|
"js" => "application/javascript; charset=utf-8",
|
||||||
|
"json" => "application/json; charset=utf-8",
|
||||||
|
"png" => "image/png",
|
||||||
|
"jpg" => "image/jpeg",
|
||||||
|
"jpeg" => "image/jpeg",
|
||||||
|
"gif" => "image/gif",
|
||||||
|
"svg" => "image/svg+xml",
|
||||||
|
"ico" => "image/x-icon",
|
||||||
|
"woff" => "font/woff",
|
||||||
|
"woff2" => "font/woff2",
|
||||||
|
"ttf" => "font/ttf",
|
||||||
|
"pdf" => "application/pdf",
|
||||||
|
"xml" => "application/xml",
|
||||||
|
"txt" => "text/plain; charset=utf-8",
|
||||||
|
"md" => "text/markdown; charset=utf-8",
|
||||||
|
_ => "application/octet-stream"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getFileExtension(path: String): String = {
|
||||||
|
match String.lastIndexOf(path, ".") {
|
||||||
|
None => "",
|
||||||
|
Some(idx) => String.toLower(String.substring(path, idx + 1, String.length(path)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Request Type
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Standard request record for cleaner routing
|
||||||
|
type Request = {
|
||||||
|
method: String,
|
||||||
|
path: String,
|
||||||
|
query: List<(String, String)>,
|
||||||
|
headers: List<(String, String)>,
|
||||||
|
body: String
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a raw request into a Request record
|
||||||
|
pub fn parseRequest(
|
||||||
|
method: String,
|
||||||
|
fullPath: String,
|
||||||
|
headers: List<(String, String)>,
|
||||||
|
body: String
|
||||||
|
): Request = {
|
||||||
|
let (path, query) = parseQueryString(fullPath)
|
||||||
|
{ method: method, path: path, query: query, headers: headers, body: body }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Response Type with Headers
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Response with headers support
|
||||||
|
type Response = {
|
||||||
|
status: Int,
|
||||||
|
headers: List<(String, String)>,
|
||||||
|
body: String
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a response with headers
|
||||||
|
pub fn httpResponse(status: Int, body: String, headers: List<(String, String)>): Response =
|
||||||
|
{ status: status, headers: headers, body: body }
|
||||||
|
|
||||||
|
// Create a JSON response
|
||||||
|
pub fn jsonResponse(status: Int, body: String): Response =
|
||||||
|
{ status: status, headers: jsonHeaders(), body: body }
|
||||||
|
|
||||||
|
// Create an HTML response
|
||||||
|
pub fn htmlResponse(status: Int, body: String): Response =
|
||||||
|
{ status: status, headers: htmlHeaders(), body: body }
|
||||||
|
|
||||||
|
// Create a redirect response
|
||||||
|
pub fn httpRedirect(location: String): Response =
|
||||||
|
{ status: 302, headers: [("Location", location)], body: "" }
|
||||||
|
|
||||||
|
// Create a permanent redirect response
|
||||||
|
pub fn httpRedirectPermanent(location: String): Response =
|
||||||
|
{ status: 301, headers: [("Location", location)], body: "" }
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Middleware Functions
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Request type for middleware (simplified)
|
||||||
|
type Handler = fn(Request): Response
|
||||||
|
|
||||||
|
// Logging middleware - logs request method, path, and response status
|
||||||
|
pub fn withLogging(handler: Handler): Handler with {Console} =
|
||||||
|
fn(req: Request): Response => {
|
||||||
|
Console.print("[HTTP] " + req.method + " " + req.path)
|
||||||
|
let resp = handler(req)
|
||||||
|
Console.print("[HTTP] " + toString(resp.status))
|
||||||
|
resp
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORS middleware - adds CORS headers to all responses
|
||||||
|
pub fn withCors(handler: Handler): Handler =
|
||||||
|
fn(req: Request): Response => {
|
||||||
|
// Handle preflight
|
||||||
|
if req.method == "OPTIONS" then
|
||||||
|
{ status: 204, headers: corsHeaders(), body: "" }
|
||||||
|
else {
|
||||||
|
let resp = handler(req)
|
||||||
|
{ status: resp.status, headers: List.concat(resp.headers, corsHeaders()), body: resp.body }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON content-type middleware - ensures JSON content type on responses
|
||||||
|
pub fn withJson(handler: Handler): Handler =
|
||||||
|
fn(req: Request): Response => {
|
||||||
|
let resp = handler(req)
|
||||||
|
{ status: resp.status, headers: List.concat(resp.headers, jsonHeaders()), body: resp.body }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error handling middleware - catches failures and returns 500
|
||||||
|
pub fn withErrorHandling(handler: Handler): Handler =
|
||||||
|
fn(req: Request): Response => {
|
||||||
|
// In a real implementation, this would use effect handling
|
||||||
|
// For now, just call the handler
|
||||||
|
handler(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limiting check (returns remaining requests or 0 if limited)
|
||||||
|
// Note: Actual rate limiting requires state/effects
|
||||||
|
pub fn checkRateLimit(key: String, limit: Int, window: Int): Int with {Time} = {
|
||||||
|
// Placeholder - real implementation would track requests
|
||||||
|
limit
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Router DSL
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Route definition
|
||||||
|
type Route = {
|
||||||
|
method: String,
|
||||||
|
pattern: String,
|
||||||
|
handler: fn(Request): Response
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a GET route
|
||||||
|
pub fn get(pattern: String, handler: fn(Request): Response): Route =
|
||||||
|
{ method: "GET", pattern: pattern, handler: handler }
|
||||||
|
|
||||||
|
// Create a POST route
|
||||||
|
pub fn post(pattern: String, handler: fn(Request): Response): Route =
|
||||||
|
{ method: "POST", pattern: pattern, handler: handler }
|
||||||
|
|
||||||
|
// Create a PUT route
|
||||||
|
pub fn put(pattern: String, handler: fn(Request): Response): Route =
|
||||||
|
{ method: "PUT", pattern: pattern, handler: handler }
|
||||||
|
|
||||||
|
// Create a DELETE route
|
||||||
|
pub fn delete(pattern: String, handler: fn(Request): Response): Route =
|
||||||
|
{ method: "DELETE", pattern: pattern, handler: handler }
|
||||||
|
|
||||||
|
// Create a PATCH route
|
||||||
|
pub fn patch(pattern: String, handler: fn(Request): Response): Route =
|
||||||
|
{ method: "PATCH", pattern: pattern, handler: handler }
|
||||||
|
|
||||||
|
// Match request against a list of routes
|
||||||
|
pub fn matchRoute(req: Request, routes: List<Route>): Option<(Route, List<(String, String)>)> = {
|
||||||
|
matchRouteHelper(req, routes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matchRouteHelper(req: Request, routes: List<Route>): Option<(Route, List<(String, String)>)> = {
|
||||||
|
match List.head(routes) {
|
||||||
|
None => None,
|
||||||
|
Some(route) => {
|
||||||
|
if route.method == req.method && pathMatches(req.path, route.pattern) then {
|
||||||
|
let params = getPathParams(req.path, route.pattern)
|
||||||
|
Some((route, params))
|
||||||
|
} else {
|
||||||
|
matchRouteHelper(req, Option.getOrElse(List.tail(routes), []))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a router from a list of routes
|
||||||
|
pub fn router(routes: List<Route>, notFound: fn(Request): Response): Handler =
|
||||||
|
fn(req: Request): Response => {
|
||||||
|
match matchRoute(req, routes) {
|
||||||
|
Some((route, _params)) => route.handler(req),
|
||||||
|
None => notFound(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Example Usage
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// fn main(): Unit with {Console, HttpServer} = {
|
||||||
|
// // Define routes
|
||||||
|
// let routes = [
|
||||||
|
// get("/", fn(req: Request): Response => jsonResponse(200, jsonMessage("Welcome!"))),
|
||||||
|
// get("/users/:id", fn(req: Request): Response => {
|
||||||
|
// let params = getPathParams(req.path, "/users/:id")
|
||||||
|
// match getParam(params, "id") {
|
||||||
|
// Some(id) => jsonResponse(200, jsonObject(jsonString("id", id))),
|
||||||
|
// None => jsonResponse(404, jsonErrorMsg("User not found"))
|
||||||
|
// }
|
||||||
|
// }),
|
||||||
|
// post("/users", fn(req: Request): Response => jsonResponse(201, jsonMessage("Created")))
|
||||||
|
// ]
|
||||||
|
//
|
||||||
|
// // Create router with middleware
|
||||||
|
// let app = withLogging(withCors(router(routes, fn(req: Request): Response =>
|
||||||
|
// jsonResponse(404, jsonErrorMsg("Not found"))
|
||||||
|
// )))
|
||||||
|
//
|
||||||
|
// // Start server
|
||||||
|
// HttpServer.listen(8080)
|
||||||
|
// Console.print("Server running on http://localhost:8080")
|
||||||
|
//
|
||||||
|
// // Server loop
|
||||||
|
// fn serverLoop(): Unit with {HttpServer} = {
|
||||||
|
// let rawReq = HttpServer.accept()
|
||||||
|
// let req = parseRequest(rawReq.method, rawReq.path, rawReq.headers, rawReq.body)
|
||||||
|
// let resp = app(req)
|
||||||
|
// HttpServer.respond(resp.status, resp.body)
|
||||||
|
// serverLoop()
|
||||||
|
// }
|
||||||
|
// serverLoop()
|
||||||
|
// }
|
||||||
|
|||||||
473
stdlib/json.lux
Normal file
473
stdlib/json.lux
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
// JSON Serialization and Deserialization for Lux
|
||||||
|
//
|
||||||
|
// Provides type-safe JSON encoding and decoding with support
|
||||||
|
// for schema versioning and custom codecs.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// let json = Json.encode(user) // Serialize to JSON string
|
||||||
|
// let user = Json.decode(json) // Deserialize from JSON string
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// JSON Value Type
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Represents any JSON value
|
||||||
|
type JsonValue =
|
||||||
|
| JsonNull
|
||||||
|
| JsonBool(Bool)
|
||||||
|
| JsonInt(Int)
|
||||||
|
| JsonFloat(Float)
|
||||||
|
| JsonString(String)
|
||||||
|
| JsonArray(List<JsonValue>)
|
||||||
|
| JsonObject(List<(String, JsonValue)>)
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Encoding Primitives
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Escape a string for JSON
|
||||||
|
pub fn escapeString(s: String): String = {
|
||||||
|
let escaped = String.replace(s, "\\", "\\\\")
|
||||||
|
let escaped = String.replace(escaped, "\"", "\\\"")
|
||||||
|
let escaped = String.replace(escaped, "\n", "\\n")
|
||||||
|
let escaped = String.replace(escaped, "\r", "\\r")
|
||||||
|
let escaped = String.replace(escaped, "\t", "\\t")
|
||||||
|
escaped
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode a JsonValue to a JSON string
|
||||||
|
pub fn encode(value: JsonValue): String = {
|
||||||
|
match value {
|
||||||
|
JsonNull => "null",
|
||||||
|
JsonBool(b) => if b then "true" else "false",
|
||||||
|
JsonInt(n) => toString(n),
|
||||||
|
JsonFloat(f) => toString(f),
|
||||||
|
JsonString(s) => "\"" + escapeString(s) + "\"",
|
||||||
|
JsonArray(items) => {
|
||||||
|
let encodedItems = List.map(items, encode)
|
||||||
|
"[" + String.join(encodedItems, ",") + "]"
|
||||||
|
},
|
||||||
|
JsonObject(fields) => {
|
||||||
|
let encodedFields = List.map(fields, fn(field: (String, JsonValue)): String => {
|
||||||
|
match field {
|
||||||
|
(key, val) => "\"" + escapeString(key) + "\":" + encode(val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
"{" + String.join(encodedFields, ",") + "}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pretty-print a JsonValue with indentation
|
||||||
|
pub fn encodePretty(value: JsonValue): String =
|
||||||
|
encodePrettyIndent(value, 0)
|
||||||
|
|
||||||
|
fn encodePrettyIndent(value: JsonValue, indent: Int): String = {
|
||||||
|
let spaces = String.repeat(" ", indent)
|
||||||
|
let nextSpaces = String.repeat(" ", indent + 1)
|
||||||
|
|
||||||
|
match value {
|
||||||
|
JsonNull => "null",
|
||||||
|
JsonBool(b) => if b then "true" else "false",
|
||||||
|
JsonInt(n) => toString(n),
|
||||||
|
JsonFloat(f) => toString(f),
|
||||||
|
JsonString(s) => "\"" + escapeString(s) + "\"",
|
||||||
|
JsonArray(items) => {
|
||||||
|
if List.length(items) == 0 then "[]"
|
||||||
|
else {
|
||||||
|
let encodedItems = List.map(items, fn(item: JsonValue): String =>
|
||||||
|
nextSpaces + encodePrettyIndent(item, indent + 1)
|
||||||
|
)
|
||||||
|
"[\n" + String.join(encodedItems, ",\n") + "\n" + spaces + "]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
JsonObject(fields) => {
|
||||||
|
if List.length(fields) == 0 then "{}"
|
||||||
|
else {
|
||||||
|
let encodedFields = List.map(fields, fn(field: (String, JsonValue)): String => {
|
||||||
|
match field {
|
||||||
|
(key, val) => nextSpaces + "\"" + escapeString(key) + "\": " + encodePrettyIndent(val, indent + 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
"{\n" + String.join(encodedFields, ",\n") + "\n" + spaces + "}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Type-specific Encoders
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Encode primitives
|
||||||
|
pub fn encodeNull(): JsonValue = JsonNull
|
||||||
|
pub fn encodeBool(b: Bool): JsonValue = JsonBool(b)
|
||||||
|
pub fn encodeInt(n: Int): JsonValue = JsonInt(n)
|
||||||
|
pub fn encodeFloat(f: Float): JsonValue = JsonFloat(f)
|
||||||
|
pub fn encodeString(s: String): JsonValue = JsonString(s)
|
||||||
|
|
||||||
|
// Encode a list
|
||||||
|
pub fn encodeList<A>(items: List<A>, encodeItem: fn(A): JsonValue): JsonValue =
|
||||||
|
JsonArray(List.map(items, encodeItem))
|
||||||
|
|
||||||
|
// Encode an optional value
|
||||||
|
pub fn encodeOption<A>(opt: Option<A>, encodeItem: fn(A): JsonValue): JsonValue =
|
||||||
|
match opt {
|
||||||
|
None => JsonNull,
|
||||||
|
Some(value) => encodeItem(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode a Result
|
||||||
|
pub fn encodeResult<T, E>(
|
||||||
|
result: Result<T, E>,
|
||||||
|
encodeOk: fn(T): JsonValue,
|
||||||
|
encodeErr: fn(E): JsonValue
|
||||||
|
): JsonValue =
|
||||||
|
match result {
|
||||||
|
Ok(value) => JsonObject([
|
||||||
|
("ok", encodeOk(value))
|
||||||
|
]),
|
||||||
|
Err(error) => JsonObject([
|
||||||
|
("error", encodeErr(error))
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Object Building Helpers
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Create an empty JSON object
|
||||||
|
pub fn object(): JsonValue = JsonObject([])
|
||||||
|
|
||||||
|
// Add a field to a JSON object
|
||||||
|
pub fn withField(obj: JsonValue, key: String, value: JsonValue): JsonValue =
|
||||||
|
match obj {
|
||||||
|
JsonObject(fields) => JsonObject(List.concat(fields, [(key, value)])),
|
||||||
|
_ => obj // Not an object, return unchanged
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a string field
|
||||||
|
pub fn withString(obj: JsonValue, key: String, value: String): JsonValue =
|
||||||
|
withField(obj, key, JsonString(value))
|
||||||
|
|
||||||
|
// Add an int field
|
||||||
|
pub fn withInt(obj: JsonValue, key: String, value: Int): JsonValue =
|
||||||
|
withField(obj, key, JsonInt(value))
|
||||||
|
|
||||||
|
// Add a bool field
|
||||||
|
pub fn withBool(obj: JsonValue, key: String, value: Bool): JsonValue =
|
||||||
|
withField(obj, key, JsonBool(value))
|
||||||
|
|
||||||
|
// Add an optional field (only adds if Some)
|
||||||
|
pub fn withOptional<A>(obj: JsonValue, key: String, opt: Option<A>, encodeItem: fn(A): JsonValue): JsonValue =
|
||||||
|
match opt {
|
||||||
|
None => obj,
|
||||||
|
Some(value) => withField(obj, key, encodeItem(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Decoding
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Result type for parsing
|
||||||
|
type ParseResult<A> = Result<A, String>
|
||||||
|
|
||||||
|
// Get a field from a JSON object
|
||||||
|
pub fn getField(obj: JsonValue, key: String): Option<JsonValue> =
|
||||||
|
match obj {
|
||||||
|
JsonObject(fields) => findField(fields, key),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findField(fields: List<(String, JsonValue)>, key: String): Option<JsonValue> =
|
||||||
|
match List.head(fields) {
|
||||||
|
None => None,
|
||||||
|
Some(field) => match field {
|
||||||
|
(k, v) => if k == key then Some(v)
|
||||||
|
else findField(Option.getOrElse(List.tail(fields), []), key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a string field
|
||||||
|
pub fn getString(obj: JsonValue, key: String): Option<String> =
|
||||||
|
match getField(obj, key) {
|
||||||
|
Some(JsonString(s)) => Some(s),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get an int field
|
||||||
|
pub fn getInt(obj: JsonValue, key: String): Option<Int> =
|
||||||
|
match getField(obj, key) {
|
||||||
|
Some(JsonInt(n)) => Some(n),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a bool field
|
||||||
|
pub fn getBool(obj: JsonValue, key: String): Option<Bool> =
|
||||||
|
match getField(obj, key) {
|
||||||
|
Some(JsonBool(b)) => Some(b),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get an array field
|
||||||
|
pub fn getArray(obj: JsonValue, key: String): Option<List<JsonValue>> =
|
||||||
|
match getField(obj, key) {
|
||||||
|
Some(JsonArray(items)) => Some(items),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get an object field
|
||||||
|
pub fn getObject(obj: JsonValue, key: String): Option<JsonValue> =
|
||||||
|
match getField(obj, key) {
|
||||||
|
Some(JsonObject(_) as obj) => Some(obj),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Simple JSON Parser
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Parse a JSON string into a JsonValue
|
||||||
|
// Note: This is a simplified parser for common cases
|
||||||
|
pub fn parse(json: String): Result<JsonValue, String> =
|
||||||
|
parseValue(String.trim(json), 0).mapResult(fn(r: (JsonValue, Int)): JsonValue => {
|
||||||
|
match r { (value, _) => value }
|
||||||
|
})
|
||||||
|
|
||||||
|
fn parseValue(json: String, pos: Int): Result<(JsonValue, Int), String> = {
|
||||||
|
let c = String.charAt(json, pos)
|
||||||
|
match c {
|
||||||
|
"n" => parseNull(json, pos),
|
||||||
|
"t" => parseTrue(json, pos),
|
||||||
|
"f" => parseFalse(json, pos),
|
||||||
|
"\"" => parseString(json, pos),
|
||||||
|
"[" => parseArray(json, pos),
|
||||||
|
"{" => parseObject(json, pos),
|
||||||
|
"-" => parseNumber(json, pos),
|
||||||
|
_ => if isDigit(c) then parseNumber(json, pos)
|
||||||
|
else if c == " " || c == "\n" || c == "\r" || c == "\t" then
|
||||||
|
parseValue(json, pos + 1)
|
||||||
|
else Err("Unexpected character at position " + toString(pos))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseNull(json: String, pos: Int): Result<(JsonValue, Int), String> =
|
||||||
|
if String.substring(json, pos, pos + 4) == "null" then Ok((JsonNull, pos + 4))
|
||||||
|
else Err("Expected 'null' at position " + toString(pos))
|
||||||
|
|
||||||
|
fn parseTrue(json: String, pos: Int): Result<(JsonValue, Int), String> =
|
||||||
|
if String.substring(json, pos, pos + 4) == "true" then Ok((JsonBool(true), pos + 4))
|
||||||
|
else Err("Expected 'true' at position " + toString(pos))
|
||||||
|
|
||||||
|
fn parseFalse(json: String, pos: Int): Result<(JsonValue, Int), String> =
|
||||||
|
if String.substring(json, pos, pos + 5) == "false" then Ok((JsonBool(false), pos + 5))
|
||||||
|
else Err("Expected 'false' at position " + toString(pos))
|
||||||
|
|
||||||
|
fn parseString(json: String, pos: Int): Result<(JsonValue, Int), String> = {
|
||||||
|
// Skip opening quote
|
||||||
|
let start = pos + 1
|
||||||
|
let result = parseStringContent(json, start, "")
|
||||||
|
result.mapResult(fn(r: (String, Int)): (JsonValue, Int) => {
|
||||||
|
match r { (s, endPos) => (JsonString(s), endPos) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseStringContent(json: String, pos: Int, acc: String): Result<(String, Int), String> = {
|
||||||
|
let c = String.charAt(json, pos)
|
||||||
|
if c == "\"" then Ok((acc, pos + 1))
|
||||||
|
else if c == "\\" then {
|
||||||
|
let nextC = String.charAt(json, pos + 1)
|
||||||
|
let escaped = match nextC {
|
||||||
|
"n" => "\n",
|
||||||
|
"r" => "\r",
|
||||||
|
"t" => "\t",
|
||||||
|
"\"" => "\"",
|
||||||
|
"\\" => "\\",
|
||||||
|
_ => nextC
|
||||||
|
}
|
||||||
|
parseStringContent(json, pos + 2, acc + escaped)
|
||||||
|
}
|
||||||
|
else if c == "" then Err("Unterminated string")
|
||||||
|
else parseStringContent(json, pos + 1, acc + c)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseNumber(json: String, pos: Int): Result<(JsonValue, Int), String> = {
|
||||||
|
let result = parseNumberDigits(json, pos, "")
|
||||||
|
result.mapResult(fn(r: (String, Int)): (JsonValue, Int) => {
|
||||||
|
match r {
|
||||||
|
(numStr, endPos) => {
|
||||||
|
// Check if it's a float
|
||||||
|
if String.contains(numStr, ".") then
|
||||||
|
(JsonFloat(parseFloat(numStr)), endPos)
|
||||||
|
else
|
||||||
|
(JsonInt(parseInt(numStr)), endPos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseNumberDigits(json: String, pos: Int, acc: String): Result<(String, Int), String> = {
|
||||||
|
let c = String.charAt(json, pos)
|
||||||
|
if isDigit(c) || c == "." || c == "-" || c == "e" || c == "E" || c == "+" then
|
||||||
|
parseNumberDigits(json, pos + 1, acc + c)
|
||||||
|
else if acc == "" then Err("Expected number at position " + toString(pos))
|
||||||
|
else Ok((acc, pos))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseArray(json: String, pos: Int): Result<(JsonValue, Int), String> = {
|
||||||
|
// Skip opening bracket and whitespace
|
||||||
|
let startPos = skipWhitespace(json, pos + 1)
|
||||||
|
|
||||||
|
if String.charAt(json, startPos) == "]" then Ok((JsonArray([]), startPos + 1))
|
||||||
|
else parseArrayItems(json, startPos, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseArrayItems(json: String, pos: Int, acc: List<JsonValue>): Result<(JsonValue, Int), String> = {
|
||||||
|
match parseValue(json, pos) {
|
||||||
|
Err(e) => Err(e),
|
||||||
|
Ok((value, nextPos)) => {
|
||||||
|
let newAcc = List.concat(acc, [value])
|
||||||
|
let afterWhitespace = skipWhitespace(json, nextPos)
|
||||||
|
let c = String.charAt(json, afterWhitespace)
|
||||||
|
if c == "]" then Ok((JsonArray(newAcc), afterWhitespace + 1))
|
||||||
|
else if c == "," then parseArrayItems(json, skipWhitespace(json, afterWhitespace + 1), newAcc)
|
||||||
|
else Err("Expected ',' or ']' at position " + toString(afterWhitespace))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseObject(json: String, pos: Int): Result<(JsonValue, Int), String> = {
|
||||||
|
// Skip opening brace and whitespace
|
||||||
|
let startPos = skipWhitespace(json, pos + 1)
|
||||||
|
|
||||||
|
if String.charAt(json, startPos) == "}" then Ok((JsonObject([]), startPos + 1))
|
||||||
|
else parseObjectFields(json, startPos, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseObjectFields(json: String, pos: Int, acc: List<(String, JsonValue)>): Result<(JsonValue, Int), String> = {
|
||||||
|
// Parse key
|
||||||
|
match parseString(json, pos) {
|
||||||
|
Err(e) => Err(e),
|
||||||
|
Ok((keyValue, afterKey)) => {
|
||||||
|
match keyValue {
|
||||||
|
JsonString(key) => {
|
||||||
|
let colonPos = skipWhitespace(json, afterKey)
|
||||||
|
if String.charAt(json, colonPos) != ":" then
|
||||||
|
Err("Expected ':' at position " + toString(colonPos))
|
||||||
|
else {
|
||||||
|
let valuePos = skipWhitespace(json, colonPos + 1)
|
||||||
|
match parseValue(json, valuePos) {
|
||||||
|
Err(e) => Err(e),
|
||||||
|
Ok((value, afterValue)) => {
|
||||||
|
let newAcc = List.concat(acc, [(key, value)])
|
||||||
|
let afterWhitespace = skipWhitespace(json, afterValue)
|
||||||
|
let c = String.charAt(json, afterWhitespace)
|
||||||
|
if c == "}" then Ok((JsonObject(newAcc), afterWhitespace + 1))
|
||||||
|
else if c == "," then parseObjectFields(json, skipWhitespace(json, afterWhitespace + 1), newAcc)
|
||||||
|
else Err("Expected ',' or '}' at position " + toString(afterWhitespace))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => Err("Expected string key at position " + toString(pos))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skipWhitespace(json: String, pos: Int): Int = {
|
||||||
|
let c = String.charAt(json, pos)
|
||||||
|
if c == " " || c == "\n" || c == "\r" || c == "\t" then
|
||||||
|
skipWhitespace(json, pos + 1)
|
||||||
|
else pos
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isDigit(c: String): Bool =
|
||||||
|
c == "0" || c == "1" || c == "2" || c == "3" || c == "4" ||
|
||||||
|
c == "5" || c == "6" || c == "7" || c == "8" || c == "9"
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Codec Type (for automatic serialization)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// A codec can both encode and decode a type
|
||||||
|
type Codec<A> = {
|
||||||
|
encode: fn(A): JsonValue,
|
||||||
|
decode: fn(JsonValue): Result<A, String>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a codec from encode/decode functions
|
||||||
|
pub fn codec<A>(
|
||||||
|
enc: fn(A): JsonValue,
|
||||||
|
dec: fn(JsonValue): Result<A, String>
|
||||||
|
): Codec<A> =
|
||||||
|
{ encode: enc, decode: dec }
|
||||||
|
|
||||||
|
// Built-in codecs
|
||||||
|
pub fn stringCodec(): Codec<String> =
|
||||||
|
codec(
|
||||||
|
encodeString,
|
||||||
|
fn(json: JsonValue): Result<String, String> => match json {
|
||||||
|
JsonString(s) => Ok(s),
|
||||||
|
_ => Err("Expected string")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
pub fn intCodec(): Codec<Int> =
|
||||||
|
codec(
|
||||||
|
encodeInt,
|
||||||
|
fn(json: JsonValue): Result<Int, String> => match json {
|
||||||
|
JsonInt(n) => Ok(n),
|
||||||
|
_ => Err("Expected int")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
pub fn boolCodec(): Codec<Bool> =
|
||||||
|
codec(
|
||||||
|
encodeBool,
|
||||||
|
fn(json: JsonValue): Result<Bool, String> => match json {
|
||||||
|
JsonBool(b) => Ok(b),
|
||||||
|
_ => Err("Expected bool")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
pub fn listCodec<A>(itemCodec: Codec<A>): Codec<List<A>> =
|
||||||
|
codec(
|
||||||
|
fn(items: List<A>): JsonValue => encodeList(items, itemCodec.encode),
|
||||||
|
fn(json: JsonValue): Result<List<A>, String> => match json {
|
||||||
|
JsonArray(items) => decodeAll(items, itemCodec.decode),
|
||||||
|
_ => Err("Expected array")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
fn decodeAll<A>(items: List<JsonValue>, decode: fn(JsonValue): Result<A, String>): Result<List<A>, String> = {
|
||||||
|
match List.head(items) {
|
||||||
|
None => Ok([]),
|
||||||
|
Some(item) => match decode(item) {
|
||||||
|
Err(e) => Err(e),
|
||||||
|
Ok(decoded) => match decodeAll(Option.getOrElse(List.tail(items), []), decode) {
|
||||||
|
Err(e) => Err(e),
|
||||||
|
Ok(rest) => Ok(List.concat([decoded], rest))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn optionCodec<A>(itemCodec: Codec<A>): Codec<Option<A>> =
|
||||||
|
codec(
|
||||||
|
fn(opt: Option<A>): JsonValue => encodeOption(opt, itemCodec.encode),
|
||||||
|
fn(json: JsonValue): Result<Option<A>, String> => match json {
|
||||||
|
JsonNull => Ok(None),
|
||||||
|
_ => itemCodec.decode(json).mapResult(fn(a: A): Option<A> => Some(a))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Helper for Result.mapResult
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
fn mapResult<A, B>(result: Result<A, String>, f: fn(A): B): Result<B, String> =
|
||||||
|
match result {
|
||||||
|
Ok(a) => Ok(f(a)),
|
||||||
|
Err(e) => Err(e)
|
||||||
|
}
|
||||||
175
website/docs/index.html
Normal file
175
website/docs/index.html
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Documentation - Lux</title>
|
||||||
|
<meta name="description" content="Lux language documentation and API reference.">
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>✨</text></svg>">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Playfair+Display:wght@400;600;700&family=Source+Serif+4:opsz,wght@8..60,400;8..60,500;8..60,600&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="../static/style.css">
|
||||||
|
<style>
|
||||||
|
.docs-container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--space-xl) var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--space-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-header p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-sections {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section {
|
||||||
|
background: var(--bg-glass);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section li {
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section a {
|
||||||
|
display: block;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: var(--space-xs) 0;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section a:hover {
|
||||||
|
color: var(--gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-section p {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<a href="/" class="logo">Lux</a>
|
||||||
|
<ul class="nav-links" id="nav-links">
|
||||||
|
<li><a href="/install">Install</a></li>
|
||||||
|
<li><a href="/tour/">Tour</a></li>
|
||||||
|
<li><a href="/examples/">Examples</a></li>
|
||||||
|
<li><a href="/docs/" class="active">Docs</a></li>
|
||||||
|
<li><a href="/play">Play</a></li>
|
||||||
|
<li><a href="https://git.qrty.ink/blu/lux" class="nav-source">Source</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="docs-container">
|
||||||
|
<header class="docs-header">
|
||||||
|
<h1>Documentation</h1>
|
||||||
|
<p>Complete reference for the Lux programming language.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="docs-sections">
|
||||||
|
<div class="docs-section">
|
||||||
|
<h2>Standard Library</h2>
|
||||||
|
<p>Core types and functions.</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="stdlib/list.html">List</a></li>
|
||||||
|
<li><a href="stdlib/string.html">String</a></li>
|
||||||
|
<li><a href="stdlib/option.html">Option</a></li>
|
||||||
|
<li><a href="stdlib/result.html">Result</a></li>
|
||||||
|
<li><a href="stdlib/math.html">Math</a></li>
|
||||||
|
<li><a href="stdlib/json.html">Json</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="docs-section">
|
||||||
|
<h2>Effects</h2>
|
||||||
|
<p>Built-in effect types and operations.</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="effects/console.html">Console</a></li>
|
||||||
|
<li><a href="effects/file.html">File</a></li>
|
||||||
|
<li><a href="effects/process.html">Process</a></li>
|
||||||
|
<li><a href="effects/http.html">Http</a></li>
|
||||||
|
<li><a href="effects/http-server.html">HttpServer</a></li>
|
||||||
|
<li><a href="effects/time.html">Time</a></li>
|
||||||
|
<li><a href="effects/random.html">Random</a></li>
|
||||||
|
<li><a href="effects/state.html">State</a></li>
|
||||||
|
<li><a href="effects/fail.html">Fail</a></li>
|
||||||
|
<li><a href="effects/sql.html">Sql</a></li>
|
||||||
|
<li><a href="effects/postgres.html">Postgres</a></li>
|
||||||
|
<li><a href="effects/concurrent.html">Concurrent</a></li>
|
||||||
|
<li><a href="effects/channel.html">Channel</a></li>
|
||||||
|
<li><a href="effects/test.html">Test</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="docs-section">
|
||||||
|
<h2>Language Reference</h2>
|
||||||
|
<p>Syntax, types, and semantics.</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="spec/grammar.html">Grammar (EBNF)</a></li>
|
||||||
|
<li><a href="spec/types.html">Type System</a></li>
|
||||||
|
<li><a href="spec/effects.html">Effect System</a></li>
|
||||||
|
<li><a href="spec/operators.html">Operators</a></li>
|
||||||
|
<li><a href="spec/keywords.html">Keywords</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="docs-section">
|
||||||
|
<h2>Guides</h2>
|
||||||
|
<p>In-depth explanations of key concepts.</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="../learn/effects.html">Effects Guide</a></li>
|
||||||
|
<li><a href="../learn/behavioral-types.html">Behavioral Types</a></li>
|
||||||
|
<li><a href="../learn/compilation.html">Compilation</a></li>
|
||||||
|
<li><a href="../learn/performance.html">Performance</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="docs-section">
|
||||||
|
<h2>Coming From</h2>
|
||||||
|
<p>Lux for developers of other languages.</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="../learn/from-rust.html">Rust</a></li>
|
||||||
|
<li><a href="../learn/from-haskell.html">Haskell</a></li>
|
||||||
|
<li><a href="../learn/from-typescript.html">TypeScript</a></li>
|
||||||
|
<li><a href="../learn/from-python.html">Python</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="docs-section">
|
||||||
|
<h2>Tooling</h2>
|
||||||
|
<p>CLI, LSP, and editor integration.</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="tools/cli.html">CLI Reference</a></li>
|
||||||
|
<li><a href="tools/lsp.html">LSP Setup</a></li>
|
||||||
|
<li><a href="tools/vscode.html">VS Code</a></li>
|
||||||
|
<li><a href="tools/neovim.html">Neovim</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
239
website/examples/http-server.html
Normal file
239
website/examples/http-server.html
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>HTTP Server - Lux by Example</title>
|
||||||
|
<meta name="description" content="Build an HTTP server in Lux.">
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>✨</text></svg>">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Playfair+Display:wght@400;600;700&family=Source+Serif+4:opsz,wght@8..60,400;8..60,500;8..60,600&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="../static/style.css">
|
||||||
|
<style>
|
||||||
|
.example-container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--space-xl) var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-header {
|
||||||
|
margin-bottom: var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-header h1 {
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-header p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
background: var(--code-bg);
|
||||||
|
border: 1px solid var(--code-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: var(--space-lg) 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-bottom: 1px solid var(--code-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-filename {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-code);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
padding: var(--space-md);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content pre {
|
||||||
|
font-family: var(--font-code);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation {
|
||||||
|
background: var(--bg-glass);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: var(--space-lg);
|
||||||
|
margin: var(--space-lg) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation ul {
|
||||||
|
padding-left: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation li {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation code {
|
||||||
|
background: var(--code-bg);
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: var(--gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-it {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: var(--space-lg);
|
||||||
|
margin: var(--space-lg) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-it h3 {
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-it pre {
|
||||||
|
background: var(--code-bg);
|
||||||
|
padding: var(--space-md);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: var(--font-code);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: var(--space-xl);
|
||||||
|
padding-top: var(--space-lg);
|
||||||
|
border-top: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-nav a {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-nav a:hover {
|
||||||
|
color: var(--gold);
|
||||||
|
border-color: var(--border-gold);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<a href="/" class="logo">Lux</a>
|
||||||
|
<ul class="nav-links" id="nav-links">
|
||||||
|
<li><a href="/install">Install</a></li>
|
||||||
|
<li><a href="/tour/">Tour</a></li>
|
||||||
|
<li><a href="/examples/">Examples</a></li>
|
||||||
|
<li><a href="/docs/">Docs</a></li>
|
||||||
|
<li><a href="/play">Play</a></li>
|
||||||
|
<li><a href="https://git.qrty.ink/blu/lux" class="nav-source">Source</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="example-container">
|
||||||
|
<header class="example-header">
|
||||||
|
<h1>HTTP Server</h1>
|
||||||
|
<p>Build a simple HTTP server with effect-tracked I/O. The type signature tells you exactly what side effects this code performs.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="code-block">
|
||||||
|
<div class="code-header">
|
||||||
|
<span class="code-filename">server.lux</span>
|
||||||
|
</div>
|
||||||
|
<div class="code-content">
|
||||||
|
<pre><code><span class="cm">// A simple HTTP server in Lux</span>
|
||||||
|
<span class="cm">// Notice the effect signature: {HttpServer, Console}</span>
|
||||||
|
|
||||||
|
<span class="kw">fn</span> <span class="fn">handleRequest</span>(req: <span class="ty">Request</span>): <span class="ty">Response</span> = {
|
||||||
|
<span class="kw">match</span> req.path {
|
||||||
|
<span class="st">"/"</span> => Response {
|
||||||
|
status: <span class="num">200</span>,
|
||||||
|
body: <span class="st">"Welcome to Lux!"</span>
|
||||||
|
},
|
||||||
|
<span class="st">"/api/hello"</span> => Response {
|
||||||
|
status: <span class="num">200</span>,
|
||||||
|
body: Json.stringify({ message: <span class="st">"Hello, World!"</span> })
|
||||||
|
},
|
||||||
|
_ => Response {
|
||||||
|
status: <span class="num">404</span>,
|
||||||
|
body: <span class="st">"Not Found"</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<span class="kw">fn</span> <span class="fn">main</span>(): <span class="ty">Unit</span> <span class="kw">with</span> {<span class="ef">HttpServer</span>, <span class="ef">Console</span>} = {
|
||||||
|
<span class="ef">HttpServer</span>.listen(<span class="num">8080</span>)
|
||||||
|
<span class="ef">Console</span>.print(<span class="st">"Server listening on http://localhost:8080"</span>)
|
||||||
|
|
||||||
|
<span class="kw">loop</span> {
|
||||||
|
<span class="kw">let</span> req = <span class="ef">HttpServer</span>.accept()
|
||||||
|
<span class="ef">Console</span>.print(req.method + <span class="st">" "</span> + req.path)
|
||||||
|
|
||||||
|
<span class="kw">let</span> response = handleRequest(req)
|
||||||
|
<span class="ef">HttpServer</span>.respond(response.status, response.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<span class="kw">run</span> main() <span class="kw">with</span> {}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="explanation">
|
||||||
|
<h2>Key Concepts</h2>
|
||||||
|
<ul>
|
||||||
|
<li><code>with {HttpServer, Console}</code> - The function signature declares exactly which effects this code uses</li>
|
||||||
|
<li><code>HttpServer.listen(port)</code> - Start listening on a port</li>
|
||||||
|
<li><code>HttpServer.accept()</code> - Wait for and return the next request</li>
|
||||||
|
<li><code>HttpServer.respond(status, body)</code> - Send a response</li>
|
||||||
|
<li>Pattern matching on <code>req.path</code> for routing</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="explanation">
|
||||||
|
<h2>Why Effects Matter Here</h2>
|
||||||
|
<ul>
|
||||||
|
<li>The type signature <code>with {HttpServer, Console}</code> tells you this function does network I/O and console output</li>
|
||||||
|
<li>Pure functions like <code>handleRequest</code> have no effects - they're easy to test</li>
|
||||||
|
<li>For testing, you can swap the <code>HttpServer</code> handler to simulate requests without a real network</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="run-it">
|
||||||
|
<h3>Run It</h3>
|
||||||
|
<pre>$ lux run server.lux
|
||||||
|
Server listening on http://localhost:8080
|
||||||
|
|
||||||
|
# In another terminal:
|
||||||
|
$ curl http://localhost:8080/
|
||||||
|
Welcome to Lux!
|
||||||
|
|
||||||
|
$ curl http://localhost:8080/api/hello
|
||||||
|
{"message":"Hello, World!"}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="example-nav">
|
||||||
|
<a href="index.html">← All Examples</a>
|
||||||
|
<a href="rest-api.html">REST API →</a>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
255
website/examples/index.html
Normal file
255
website/examples/index.html
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Lux by Example</title>
|
||||||
|
<meta name="description" content="Learn Lux through annotated example programs.">
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>✨</text></svg>">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Playfair+Display:wght@400;600;700&family=Source+Serif+4:opsz,wght@8..60,400;8..60,500;8..60,600&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="../static/style.css">
|
||||||
|
<style>
|
||||||
|
.examples-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 250px 1fr;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
min-height: calc(100vh - 80px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.examples-sidebar {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
padding: var(--space-lg);
|
||||||
|
border-right: 1px solid var(--border-subtle);
|
||||||
|
position: sticky;
|
||||||
|
top: 60px;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.examples-sidebar h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.examples-sidebar ul {
|
||||||
|
list-style: none;
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.examples-sidebar li {
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.examples-sidebar a {
|
||||||
|
display: block;
|
||||||
|
padding: var(--space-xs) var(--space-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.examples-sidebar a:hover {
|
||||||
|
color: var(--gold);
|
||||||
|
background: var(--bg-glass);
|
||||||
|
}
|
||||||
|
|
||||||
|
.examples-sidebar a.active {
|
||||||
|
color: var(--gold);
|
||||||
|
background: var(--bg-glass);
|
||||||
|
border-left: 2px solid var(--gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.examples-content {
|
||||||
|
padding: var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.examples-content h1 {
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.examples-intro {
|
||||||
|
max-width: 700px;
|
||||||
|
margin-bottom: var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.examples-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card {
|
||||||
|
background: var(--bg-glass);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: var(--space-lg);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card:hover {
|
||||||
|
border-color: var(--border-gold);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card h3 {
|
||||||
|
color: var(--gold);
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card p {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card a {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.examples-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.examples-sidebar {
|
||||||
|
position: static;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<a href="/" class="logo">Lux</a>
|
||||||
|
<ul class="nav-links" id="nav-links">
|
||||||
|
<li><a href="/install">Install</a></li>
|
||||||
|
<li><a href="/tour/">Tour</a></li>
|
||||||
|
<li><a href="/examples/" class="active">Examples</a></li>
|
||||||
|
<li><a href="/docs/">Docs</a></li>
|
||||||
|
<li><a href="/play">Play</a></li>
|
||||||
|
<li><a href="https://git.qrty.ink/blu/lux" class="nav-source">Source</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="examples-container">
|
||||||
|
<aside class="examples-sidebar">
|
||||||
|
<h2>Basics</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="hello-world.html">Hello World</a></li>
|
||||||
|
<li><a href="values.html">Values</a></li>
|
||||||
|
<li><a href="variables.html">Variables</a></li>
|
||||||
|
<li><a href="functions.html">Functions</a></li>
|
||||||
|
<li><a href="closures.html">Closures</a></li>
|
||||||
|
<li><a href="recursion.html">Recursion</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Types</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="records.html">Records</a></li>
|
||||||
|
<li><a href="variants.html">Variants</a></li>
|
||||||
|
<li><a href="generics.html">Generics</a></li>
|
||||||
|
<li><a href="option.html">Option</a></li>
|
||||||
|
<li><a href="result.html">Result</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Effects</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="console-io.html">Console I/O</a></li>
|
||||||
|
<li><a href="file-operations.html">File Operations</a></li>
|
||||||
|
<li><a href="http-requests.html">HTTP Requests</a></li>
|
||||||
|
<li><a href="random-numbers.html">Random Numbers</a></li>
|
||||||
|
<li><a href="time-sleep.html">Time & Sleep</a></li>
|
||||||
|
<li><a href="state-management.html">State Management</a></li>
|
||||||
|
<li><a href="error-handling.html">Error Handling</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Data</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="json-parsing.html">JSON Parsing</a></li>
|
||||||
|
<li><a href="json-generation.html">JSON Generation</a></li>
|
||||||
|
<li><a href="string-processing.html">String Processing</a></li>
|
||||||
|
<li><a href="list-operations.html">List Operations</a></li>
|
||||||
|
<li><a href="sqlite-database.html">SQLite Database</a></li>
|
||||||
|
<li><a href="postgresql.html">PostgreSQL</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Concurrent</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="spawning-tasks.html">Spawning Tasks</a></li>
|
||||||
|
<li><a href="channels.html">Channels</a></li>
|
||||||
|
<li><a href="producer-consumer.html">Producer/Consumer</a></li>
|
||||||
|
<li><a href="parallel-map.html">Parallel Map</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Web</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="http-server.html">HTTP Server</a></li>
|
||||||
|
<li><a href="rest-api.html">REST API</a></li>
|
||||||
|
<li><a href="middleware.html">Middleware</a></li>
|
||||||
|
<li><a href="routing.html">Routing</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Projects</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/projects/">All Projects</a></li>
|
||||||
|
<li><a href="/projects/#rest-api">REST API</a></li>
|
||||||
|
<li><a href="/projects/#todo-app">Todo App</a></li>
|
||||||
|
<li><a href="/projects/#json-parser">JSON Parser</a></li>
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="examples-content">
|
||||||
|
<h1>Lux by Example</h1>
|
||||||
|
|
||||||
|
<div class="examples-intro">
|
||||||
|
<p>Learn Lux through annotated example programs. Each example is self-contained and demonstrates a specific concept or pattern.</p>
|
||||||
|
<p>Click any example to see the full code with explanations.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="examples-grid">
|
||||||
|
<div class="example-card">
|
||||||
|
<h3>Hello World</h3>
|
||||||
|
<p>Your first Lux program. Learn about the main function and Console effect.</p>
|
||||||
|
<a href="hello-world.html">View example →</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="example-card">
|
||||||
|
<h3>Effects Basics</h3>
|
||||||
|
<p>Understand how effects make side effects explicit in type signatures.</p>
|
||||||
|
<a href="console-io.html">View example →</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="example-card">
|
||||||
|
<h3>Pattern Matching</h3>
|
||||||
|
<p>Destructure data with exhaustive pattern matching.</p>
|
||||||
|
<a href="variants.html">View example →</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="example-card">
|
||||||
|
<h3>HTTP Server</h3>
|
||||||
|
<p>Build a simple web server with effect-tracked I/O.</p>
|
||||||
|
<a href="http-server.html">View example →</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="example-card">
|
||||||
|
<h3>JSON Processing</h3>
|
||||||
|
<p>Parse and generate JSON data with type safety.</p>
|
||||||
|
<a href="json-parsing.html">View example →</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="example-card">
|
||||||
|
<h3>Concurrency</h3>
|
||||||
|
<p>Spawn tasks and communicate via channels.</p>
|
||||||
|
<a href="spawning-tasks.html">View example →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user