Compare commits
71 Commits
552e7a4972
...
v0.1.9
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f8babfd8b | |||
| 582d603513 | |||
| fbb7ddb6c3 | |||
| 400acc3f35 | |||
| ea3a7ca2dd | |||
| 7b40421a6a | |||
| 26b94935e9 | |||
| 018a799c05 | |||
| ec78286165 | |||
| f2688072ac | |||
| 746643527d | |||
| 091ff1e422 | |||
| 1fc472a54c | |||
| caabaeeb9c | |||
| 4e43d3d50d | |||
| fd5ed53b29 | |||
| 2800ce4e2d | |||
| ec365ebb3f | |||
| 52dcc88051 | |||
| 1842b668e5 | |||
| c67e3f31c3 | |||
| b0ccde749c | |||
| 4ba7a23ae3 | |||
| 89741b4a32 | |||
| 3a2376cd49 | |||
| 4dfb04a1b6 | |||
| 3cdde02eb2 | |||
| a5762d0397 | |||
| 1132c621c6 | |||
| a0fff1814e | |||
| 4e9e823246 | |||
| 6a2e4a7ac1 | |||
| 3d706cb32b | |||
| 7c3bfa9301 | |||
| b56c5461f1 | |||
| 61e1469845 | |||
| bb0a288210 | |||
| 5d7f4633e1 | |||
| d05b13d840 | |||
| 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
|
||||
/result
|
||||
|
||||
# Claude Code project instructions
|
||||
CLAUDE.md
|
||||
|
||||
# Build output
|
||||
_site/
|
||||
docs/*.html
|
||||
docs/*.css
|
||||
|
||||
# Test binaries
|
||||
hello
|
||||
test_rc
|
||||
|
||||
177
CLAUDE.md
Normal file
177
CLAUDE.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# 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.** This is NOT optional — every fix, feature, or change MUST be committed right away. 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.
|
||||
|
||||
**Commit workflow:**
|
||||
1. Make the change
|
||||
2. Run `./scripts/validate.sh` (all 13 checks must pass)
|
||||
3. `git add` the relevant files
|
||||
4. `git commit --no-gpg-sign -m "type: description"` (use conventional commits: fix/feat/chore/docs)
|
||||
5. Move on to the next task
|
||||
|
||||
**Never skip committing.** If you fixed a bug, commit it. If you added a feature, commit it. If you updated docs, commit it. Do not batch unrelated changes into one commit.
|
||||
|
||||
**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
|
||||
217
Cargo.lock
generated
217
Cargo.lock
generated
@@ -135,16 +135,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
@@ -297,21 +287,6 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
@@ -417,6 +392,12 @@ dependencies = [
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.27"
|
||||
@@ -552,16 +533,17 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.5.0"
|
||||
name = "hyper-rustls"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"hyper",
|
||||
"native-tls",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -794,8 +776,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lux"
|
||||
version = "0.1.0"
|
||||
version = "0.1.8"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"lsp-server",
|
||||
"lsp-types",
|
||||
"postgres",
|
||||
@@ -843,23 +826,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "nibble_vec"
|
||||
version = "0.1.0"
|
||||
@@ -905,50 +871,6 @@ version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@@ -1203,15 +1125,15 @@ dependencies = [
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"hyper-tls",
|
||||
"hyper-rustls",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1219,15 +1141,30 @@ dependencies = [
|
||||
"sync_wrapper",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"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]]
|
||||
name = "rusqlite"
|
||||
version = "0.31.0"
|
||||
@@ -1255,6 +1192,18 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.4"
|
||||
@@ -1264,6 +1213,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
@@ -1298,15 +1257,6 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -1314,26 +1264,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.6.0"
|
||||
name = "sct"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38"
|
||||
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"core-foundation 0.10.1",
|
||||
"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",
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1521,7 +1458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
@@ -1619,16 +1556,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tokio-postgres"
|
||||
version = "0.7.16"
|
||||
@@ -1655,6 +1582,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
@@ -1750,6 +1687,12 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
@@ -1941,6 +1884,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.25.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "2.1.1"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lux"
|
||||
version = "0.1.0"
|
||||
version = "0.1.9"
|
||||
edition = "2021"
|
||||
description = "A functional programming language with first-class effects, schema evolution, and behavioral types"
|
||||
license = "MIT"
|
||||
@@ -13,10 +13,11 @@ lsp-types = "0.94"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
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"
|
||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||
postgres = "0.19"
|
||||
glob = "0.3"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
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.
|
||||
|
||||
## 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)
|
||||
2. **Data Evolution** — Types change, data persists. (Manual migrations, runtime failures)
|
||||
3. **Behavioral Properties** — Is this idempotent? Does it terminate? (Comments and hope)
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
@@ -144,6 +151,7 @@ fn main(): Unit with {Console} =
|
||||
- String, List, Option, Result, Math, JSON modules
|
||||
- Console, File, Http, Random, Time, Process effects
|
||||
- SQL effect (SQLite with transactions)
|
||||
- PostgreSQL effect (connection pooling ready)
|
||||
- DOM effect (40+ browser operations)
|
||||
|
||||
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 |
|
||||
| Transaction effect | P2 | 1 week | ✅ Complete |
|
||||
| Connection pooling | P2 | 1 week | ❌ Missing |
|
||||
| PostgreSQL support | P1 | 2 weeks | ✅ Complete |
|
||||
|
||||
### Phase 1.3: Web Server Framework
|
||||
|
||||
@@ -207,8 +208,11 @@
|
||||
|------|----------|--------|--------|
|
||||
| Package manager (lux pkg) | P1 | 3 weeks | ✅ Complete |
|
||||
| Module loader integration | P1 | 1 week | ✅ Complete |
|
||||
| Package registry | P2 | 2 weeks | ✅ Complete (server + CLI commands) |
|
||||
| Dependency resolution | P2 | 2 weeks | ❌ Missing |
|
||||
| Package registry server | P2 | 2 weeks | ✅ Complete |
|
||||
| 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:**
|
||||
- `lux pkg init` - Initialize project with lux.toml
|
||||
@@ -300,6 +304,8 @@
|
||||
- ✅ Random effect (int, float, range, bool)
|
||||
- ✅ Time effect (now, sleep)
|
||||
- ✅ Test effect (assert, assertEqual, assertTrue, assertFalse)
|
||||
- ✅ SQL effect (SQLite with transactions)
|
||||
- ✅ Postgres effect (PostgreSQL connections)
|
||||
|
||||
**Module System:**
|
||||
- ✅ Imports, exports, aliases
|
||||
@@ -319,7 +325,7 @@
|
||||
- ✅ C backend (functions, closures, pattern matching, lists)
|
||||
- ✅ JS backend (full language support, browser & Node.js)
|
||||
- ✅ REPL with history
|
||||
- ✅ Basic LSP server
|
||||
- ✅ LSP server (diagnostics, hover, completions, go-to-definition, references, symbols)
|
||||
- ✅ Formatter
|
||||
- ✅ Watch mode
|
||||
- ✅ 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 |
|
||||
| `Http` | `get`, `post`, `put`, `delete` | HTTP client |
|
||||
| `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 |
|
||||
|
||||
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
|
||||
|
||||
Native testing framework:
|
||||
@@ -360,6 +468,10 @@ fn main(): Unit with {Console} = {
|
||||
| Random | int, float, bool |
|
||||
| State | get, put |
|
||||
| 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 |
|
||||
|
||||
## 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
|
||||
// Behavioral properties are compile-time guarantees about function behavior
|
||||
//
|
||||
// Expected output:
|
||||
// add(5, 3) = 8
|
||||
// factorial(5) = 120
|
||||
// multiply(7, 6) = 42
|
||||
// abs(-5) = 5
|
||||
fn add(a: Int, b: Int): Int is pure = a + b
|
||||
|
||||
// A pure function - no side effects, same input always gives same output
|
||||
fn add(a: Int, b: Int): Int is pure =
|
||||
a + b
|
||||
fn factorial(n: Int): Int is deterministic = if n <= 1 then 1 else n * factorial(n - 1)
|
||||
|
||||
// A deterministic function - same input always gives same output
|
||||
fn factorial(n: Int): Int is deterministic =
|
||||
if n <= 1 then 1
|
||||
else n * factorial(n - 1)
|
||||
fn multiply(a: Int, b: Int): Int is commutative = a * b
|
||||
|
||||
// A commutative function - order of arguments doesn't matter
|
||||
fn multiply(a: Int, b: Int): Int is commutative =
|
||||
a * b
|
||||
fn abs(x: Int): Int is idempotent = if x < 0 then 0 - x else x
|
||||
|
||||
// An idempotent function - absolute value
|
||||
fn abs(x: Int): Int is idempotent =
|
||||
if x < 0 then 0 - x else x
|
||||
|
||||
// Test the functions
|
||||
let sumResult = add(5, 3)
|
||||
|
||||
let factResult = factorial(5)
|
||||
|
||||
let productResult = multiply(7, 6)
|
||||
|
||||
let absResult = abs(0 - 5)
|
||||
|
||||
// Print results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("add(5, 3) = " + toString(sumResult))
|
||||
Console.print("factorial(5) = " + toString(factResult))
|
||||
|
||||
@@ -1,82 +1,42 @@
|
||||
// Behavioral Types Demo
|
||||
// Demonstrates compile-time verification of function properties
|
||||
|
||||
// ============================================================
|
||||
// PART 1: Pure Functions
|
||||
// ============================================================
|
||||
|
||||
// Pure functions have no side effects
|
||||
fn add(a: Int, b: Int): Int is pure = a + b
|
||||
|
||||
fn subtract(a: Int, b: Int): Int is pure = a - b
|
||||
|
||||
// ============================================================
|
||||
// PART 2: Commutative Functions
|
||||
// ============================================================
|
||||
|
||||
// Commutative functions: f(a, b) = f(b, a)
|
||||
fn multiply(a: Int, b: Int): Int is commutative = a * b
|
||||
|
||||
fn sum(a: Int, b: Int): Int is commutative = a + b
|
||||
|
||||
// ============================================================
|
||||
// PART 3: Idempotent Functions
|
||||
// ============================================================
|
||||
|
||||
// Idempotent functions: f(f(x)) = f(x)
|
||||
fn abs(x: Int): Int is idempotent =
|
||||
if x < 0 then 0 - x else x
|
||||
fn abs(x: Int): Int is idempotent = if x < 0 then 0 - x else x
|
||||
|
||||
fn identity(x: Int): Int is idempotent = x
|
||||
|
||||
// ============================================================
|
||||
// PART 4: Deterministic Functions
|
||||
// ============================================================
|
||||
fn factorial(n: Int): Int is deterministic = if n <= 1 then 1 else n * factorial(n - 1)
|
||||
|
||||
// Deterministic functions always produce the same output for the same input
|
||||
fn factorial(n: Int): Int is deterministic =
|
||||
if n <= 1 then 1 else n * factorial(n - 1)
|
||||
fn fib(n: Int): Int is deterministic = if n <= 1 then n else fib(n - 1) + fib(n - 2)
|
||||
|
||||
fn fib(n: Int): Int is deterministic =
|
||||
if n <= 1 then n else fib(n - 1) + fib(n - 2)
|
||||
fn sumTo(n: Int): Int is total = if n <= 0 then 0 else n + sumTo(n - 1)
|
||||
|
||||
// ============================================================
|
||||
// PART 5: Total Functions
|
||||
// ============================================================
|
||||
|
||||
// Total functions are defined for all inputs (no infinite loops, no exceptions)
|
||||
fn sumTo(n: Int): Int is total =
|
||||
if n <= 0 then 0 else n + sumTo(n - 1)
|
||||
|
||||
fn power(base: Int, exp: Int): Int is total =
|
||||
if exp <= 0 then 1 else base * power(base, exp - 1)
|
||||
|
||||
// ============================================================
|
||||
// RESULTS
|
||||
// ============================================================
|
||||
fn power(base: Int, exp: Int): Int is total = if exp <= 0 then 1 else base * power(base, exp - 1)
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Behavioral Types Demo ===")
|
||||
Console.print("")
|
||||
|
||||
Console.print("Part 1: Pure functions")
|
||||
Console.print(" add(5, 3) = " + toString(add(5, 3)))
|
||||
Console.print(" subtract(10, 4) = " + toString(subtract(10, 4)))
|
||||
Console.print("")
|
||||
|
||||
Console.print("Part 2: Commutative functions")
|
||||
Console.print(" multiply(7, 6) = " + toString(multiply(7, 6)))
|
||||
Console.print(" sum(10, 20) = " + toString(sum(10, 20)))
|
||||
Console.print("")
|
||||
|
||||
Console.print("Part 3: Idempotent functions")
|
||||
Console.print(" abs(-42) = " + toString(abs(0 - 42)))
|
||||
Console.print(" identity(100) = " + toString(identity(100)))
|
||||
Console.print("")
|
||||
|
||||
Console.print("Part 4: Deterministic functions")
|
||||
Console.print(" factorial(5) = " + toString(factorial(5)))
|
||||
Console.print(" fib(10) = " + toString(fib(10)))
|
||||
Console.print("")
|
||||
|
||||
Console.print("Part 5: Total functions")
|
||||
Console.print(" sumTo(10) = " + toString(sumTo(10)))
|
||||
Console.print(" power(2, 8) = " + toString(power(2, 8)))
|
||||
|
||||
@@ -1,31 +1,7 @@
|
||||
// Demonstrating built-in effects in Lux
|
||||
//
|
||||
// Lux provides several built-in effects:
|
||||
// - Console: print and read from terminal
|
||||
// - Fail: early termination with error
|
||||
// - State: get/put mutable state (requires runtime initialization)
|
||||
// - Reader: read-only environment access (requires runtime initialization)
|
||||
//
|
||||
// This example demonstrates Console and Fail effects.
|
||||
//
|
||||
// Expected output:
|
||||
// Starting computation...
|
||||
// Step 1: validating input
|
||||
// Step 2: processing
|
||||
// Result: 42
|
||||
// Done!
|
||||
fn safeDivide(a: Int, b: Int): Int with {Fail} = if b == 0 then Fail.fail("Division by zero") else a / b
|
||||
|
||||
// A function that can fail
|
||||
fn safeDivide(a: Int, b: Int): Int with {Fail} =
|
||||
if b == 0 then Fail.fail("Division by zero")
|
||||
else a / b
|
||||
fn validatePositive(n: Int): Int with {Fail} = if n < 0 then Fail.fail("Negative number not allowed") else n
|
||||
|
||||
// A function that validates input
|
||||
fn validatePositive(n: Int): Int with {Fail} =
|
||||
if n < 0 then Fail.fail("Negative number not allowed")
|
||||
else n
|
||||
|
||||
// A computation that uses multiple effects
|
||||
fn compute(input: Int): Int with {Console, Fail} = {
|
||||
Console.print("Starting computation...")
|
||||
Console.print("Step 1: validating input")
|
||||
@@ -36,7 +12,6 @@ fn compute(input: Int): Int with {Console, Fail} = {
|
||||
result
|
||||
}
|
||||
|
||||
// Main function
|
||||
fn main(): Unit with {Console} = {
|
||||
let result = run compute(21) with {}
|
||||
Console.print("Done!")
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
// Counter Example - A simple interactive counter using TEA pattern
|
||||
//
|
||||
// This example demonstrates:
|
||||
// - Model-View-Update architecture (TEA)
|
||||
// - Html DSL for describing UI (inline version)
|
||||
// - Message-based state updates
|
||||
|
||||
// ============================================================================
|
||||
// Html Types (subset of stdlib/html)
|
||||
// ============================================================================
|
||||
|
||||
type Html<M> =
|
||||
| Element(String, List<Attr<M>>, List<Html<M>>)
|
||||
| Text(String)
|
||||
@@ -19,86 +8,56 @@ type Attr<M> =
|
||||
| Id(String)
|
||||
| OnClick(M)
|
||||
|
||||
// Html builder helpers
|
||||
fn div<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("div", attrs, children)
|
||||
fn div<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = Element("div", attrs, children)
|
||||
|
||||
fn span<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("span", attrs, children)
|
||||
fn span<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = Element("span", attrs, children)
|
||||
|
||||
fn h1<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("h1", attrs, children)
|
||||
fn h1<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = Element("h1", attrs, children)
|
||||
|
||||
fn button<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("button", attrs, children)
|
||||
fn button<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = Element("button", attrs, children)
|
||||
|
||||
fn text<M>(content: String): Html<M> =
|
||||
Text(content)
|
||||
fn text<M>(content: String): Html<M> = Text(content)
|
||||
|
||||
fn class<M>(name: String): Attr<M> =
|
||||
Class(name)
|
||||
fn class<M>(name: String): Attr<M> = Class(name)
|
||||
|
||||
fn onClick<M>(msg: M): Attr<M> =
|
||||
OnClick(msg)
|
||||
|
||||
// ============================================================================
|
||||
// Model - The application state (using ADT wrapper)
|
||||
// ============================================================================
|
||||
fn onClick<M>(msg: M): Attr<M> = OnClick(msg)
|
||||
|
||||
type Model =
|
||||
| Counter(Int)
|
||||
|
||||
fn getCount(model: Model): Int =
|
||||
match model {
|
||||
Counter(n) => n
|
||||
}
|
||||
Counter(n) => n,
|
||||
}
|
||||
|
||||
fn init(): Model = Counter(0)
|
||||
|
||||
// ============================================================================
|
||||
// Messages - Events that can occur
|
||||
// ============================================================================
|
||||
|
||||
type Msg =
|
||||
| Increment
|
||||
| Decrement
|
||||
| Reset
|
||||
|
||||
// ============================================================================
|
||||
// Update - State transitions
|
||||
// ============================================================================
|
||||
|
||||
fn update(model: Model, msg: Msg): Model =
|
||||
match msg {
|
||||
Increment => Counter(getCount(model) + 1),
|
||||
Decrement => Counter(getCount(model) - 1),
|
||||
Reset => Counter(0)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// View - Render the UI
|
||||
// ============================================================================
|
||||
Reset => Counter(0),
|
||||
}
|
||||
|
||||
fn viewCounter(count: Int): Html<Msg> = {
|
||||
let countText = text(toString(count))
|
||||
let countSpan = span([class("count")], [countText])
|
||||
let displayDiv = div([class("counter-display")], [countSpan])
|
||||
|
||||
let minusBtn = button([onClick(Decrement), class("btn")], [text("-")])
|
||||
let resetBtn = button([onClick(Reset), class("btn btn-reset")], [text("Reset")])
|
||||
let plusBtn = button([onClick(Increment), class("btn")], [text("+")])
|
||||
let buttonsDiv = div([class("counter-buttons")], [minusBtn, resetBtn, plusBtn])
|
||||
|
||||
let title = h1([], [text("Counter")])
|
||||
div([class("counter-app")], [title, displayDiv, buttonsDiv])
|
||||
}
|
||||
|
||||
fn view(model: Model): Html<Msg> = viewCounter(getCount(model))
|
||||
|
||||
// ============================================================================
|
||||
// Debug: Print Html structure
|
||||
// ============================================================================
|
||||
|
||||
fn showAttr(attr: Attr<Msg>): String =
|
||||
match attr {
|
||||
Class(s) => "class=\"" + s + "\"",
|
||||
@@ -106,27 +65,27 @@ fn showAttr(attr: Attr<Msg>): String =
|
||||
OnClick(msg) => match msg {
|
||||
Increment => "onclick=\"Increment\"",
|
||||
Decrement => "onclick=\"Decrement\"",
|
||||
Reset => "onclick=\"Reset\""
|
||||
}
|
||||
}
|
||||
Reset => "onclick=\"Reset\"",
|
||||
},
|
||||
}
|
||||
|
||||
fn showAttrs(attrs: List<Attr<Msg>>): String =
|
||||
match List.head(attrs) {
|
||||
None => "",
|
||||
Some(a) => match List.tail(attrs) {
|
||||
None => showAttr(a),
|
||||
Some(rest) => showAttr(a) + " " + showAttrs(rest)
|
||||
}
|
||||
}
|
||||
Some(rest) => showAttr(a) + " " + showAttrs(rest),
|
||||
},
|
||||
}
|
||||
|
||||
fn showChildren(children: List<Html<Msg>>, indent: Int): String =
|
||||
match List.head(children) {
|
||||
None => "",
|
||||
Some(c) => match List.tail(children) {
|
||||
None => showHtml(c, indent),
|
||||
Some(rest) => showHtml(c, indent) + showChildren(rest, indent)
|
||||
}
|
||||
}
|
||||
Some(rest) => showHtml(c, indent) + showChildren(rest, indent),
|
||||
},
|
||||
}
|
||||
|
||||
fn showHtml(html: Html<Msg>, indent: Int): String =
|
||||
match html {
|
||||
@@ -137,12 +96,8 @@ fn showHtml(html: Html<Msg>, indent: Int): String =
|
||||
let attrPart = if String.length(attrStr) > 0 then " " + attrStr else ""
|
||||
let childStr = showChildren(children, indent + 2)
|
||||
"<" + tag + attrPart + ">" + childStr + "</" + tag + ">"
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Entry point
|
||||
// ============================================================================
|
||||
},
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
let model = init()
|
||||
@@ -150,24 +105,19 @@ fn main(): Unit with {Console} = {
|
||||
Console.print("")
|
||||
Console.print("Initial count: " + toString(getCount(model)))
|
||||
Console.print("")
|
||||
|
||||
let m1 = update(model, Increment)
|
||||
Console.print("After Increment: " + toString(getCount(m1)))
|
||||
|
||||
let m2 = update(m1, Increment)
|
||||
Console.print("After Increment: " + toString(getCount(m2)))
|
||||
|
||||
let m3 = update(m2, Increment)
|
||||
Console.print("After Increment: " + toString(getCount(m3)))
|
||||
|
||||
let m4 = update(m3, Decrement)
|
||||
Console.print("After Decrement: " + toString(getCount(m4)))
|
||||
|
||||
let m5 = update(m4, Reset)
|
||||
Console.print("After Reset: " + toString(getCount(m5)))
|
||||
|
||||
Console.print("")
|
||||
Console.print("=== View (HTML Structure) ===")
|
||||
Console.print(showHtml(view(m2), 0))
|
||||
}
|
||||
|
||||
let output = run main() with {}
|
||||
|
||||
@@ -1,57 +1,37 @@
|
||||
// Demonstrating algebraic data types and pattern matching
|
||||
//
|
||||
// Expected output:
|
||||
// Tree sum: 8
|
||||
// Tree depth: 3
|
||||
// Safe divide 10/2: Result: 5
|
||||
// Safe divide 10/0: Division by zero!
|
||||
|
||||
// Define a binary tree
|
||||
type Tree =
|
||||
| Leaf(Int)
|
||||
| Node(Tree, Tree)
|
||||
|
||||
// Sum all values in a tree
|
||||
fn sumTree(tree: Tree): Int =
|
||||
match tree {
|
||||
Leaf(n) => n,
|
||||
Node(left, right) => sumTree(left) + sumTree(right)
|
||||
}
|
||||
Node(left, right) => sumTree(left) + sumTree(right),
|
||||
}
|
||||
|
||||
// Find the depth of a tree
|
||||
fn depth(tree: Tree): Int =
|
||||
match tree {
|
||||
Leaf(_) => 1,
|
||||
Node(left, right) => {
|
||||
let leftDepth = depth(left)
|
||||
let rightDepth = depth(right)
|
||||
1 + (if leftDepth > rightDepth then leftDepth else rightDepth)
|
||||
}
|
||||
}
|
||||
|
||||
// Example tree:
|
||||
// Node
|
||||
// / \
|
||||
// Node Leaf(5)
|
||||
// / \
|
||||
// Leaf(1) Leaf(2)
|
||||
1 + if leftDepth > rightDepth then leftDepth else rightDepth
|
||||
},
|
||||
}
|
||||
|
||||
let myTree = Node(Node(Leaf(1), Leaf(2)), Leaf(5))
|
||||
|
||||
let treeSum = sumTree(myTree)
|
||||
|
||||
let treeDepth = depth(myTree)
|
||||
|
||||
// Option type example
|
||||
fn safeDivide(a: Int, b: Int): Option<Int> =
|
||||
if b == 0 then None
|
||||
else Some(a / b)
|
||||
fn safeDivide(a: Int, b: Int): Option<Int> = if b == 0 then None else Some(a / b)
|
||||
|
||||
fn showResult(result: Option<Int>): String =
|
||||
match result {
|
||||
None => "Division by zero!",
|
||||
Some(n) => "Result: " + toString(n)
|
||||
}
|
||||
Some(n) => "Result: " + toString(n),
|
||||
}
|
||||
|
||||
// Print results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("Tree sum: " + toString(treeSum))
|
||||
Console.print("Tree depth: " + toString(treeDepth))
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
// Demonstrating algebraic effects in Lux
|
||||
//
|
||||
// Expected output:
|
||||
// [info] Processing data...
|
||||
// [debug] Result computed
|
||||
// Final result: 42
|
||||
|
||||
// Define a custom logging effect
|
||||
effect Logger {
|
||||
fn log(level: String, msg: String): Unit
|
||||
fn getLevel(): String
|
||||
}
|
||||
|
||||
// A function that uses the Logger effect
|
||||
fn processData(data: Int): Int with {Logger} = {
|
||||
Logger.log("info", "Processing data...")
|
||||
let result = data * 2
|
||||
@@ -19,17 +10,15 @@ fn processData(data: Int): Int with {Logger} = {
|
||||
result
|
||||
}
|
||||
|
||||
// A handler that prints logs to console
|
||||
handler consoleLogger: Logger {
|
||||
fn log(level, msg) = Console.print("[" + level + "] " + msg)
|
||||
fn getLevel() = "debug"
|
||||
}
|
||||
|
||||
// Run and print
|
||||
fn main(): Unit with {Console} = {
|
||||
let result = run processData(21) with {
|
||||
Logger = consoleLogger
|
||||
}
|
||||
Logger = consoleLogger,
|
||||
}
|
||||
Console.print("Final result: " + toString(result))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
// Factorial function demonstrating recursion
|
||||
//
|
||||
// Expected output: 10! = 3628800
|
||||
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
|
||||
|
||||
fn factorial(n: Int): Int =
|
||||
if n <= 1 then 1
|
||||
else n * factorial(n - 1)
|
||||
|
||||
// Calculate factorial of 10
|
||||
let result = factorial(10)
|
||||
|
||||
// Print result using Console effect
|
||||
fn showResult(): Unit with {Console} =
|
||||
Console.print("10! = " + toString(result))
|
||||
fn showResult(): Unit with {Console} = Console.print("10! = " + toString(result))
|
||||
|
||||
let output = run showResult() with {}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
// File I/O example - demonstrates the File effect
|
||||
//
|
||||
// This script reads a file, counts lines/words, and writes a report
|
||||
|
||||
fn countLines(content: String): Int = {
|
||||
let lines = String.split(content, "\n")
|
||||
let lines = String.split(content, "
|
||||
")
|
||||
List.length(lines)
|
||||
}
|
||||
|
||||
@@ -14,35 +11,28 @@ fn countWords(content: String): Int = {
|
||||
|
||||
fn analyzeFile(path: String): Unit with {File, Console} = {
|
||||
Console.print("Analyzing file: " + path)
|
||||
|
||||
if File.exists(path) then {
|
||||
let content = File.read(path)
|
||||
let lines = countLines(content)
|
||||
let words = countWords(content)
|
||||
let chars = String.length(content)
|
||||
|
||||
Console.print(" Lines: " + toString(lines))
|
||||
Console.print(" Words: " + toString(words))
|
||||
Console.print(" Chars: " + toString(chars))
|
||||
} else {
|
||||
} else {
|
||||
Console.print(" Error: File not found!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {File, Console} = {
|
||||
Console.print("=== Lux File Analyzer ===")
|
||||
Console.print("")
|
||||
|
||||
// Analyze this file itself
|
||||
analyzeFile("examples/file_io.lux")
|
||||
Console.print("")
|
||||
|
||||
// Analyze hello.lux
|
||||
analyzeFile("examples/hello.lux")
|
||||
Console.print("")
|
||||
|
||||
// Write a report
|
||||
let report = "File analysis complete.\nAnalyzed 2 files."
|
||||
let report = "File analysis complete.
|
||||
Analyzed 2 files."
|
||||
File.write("/tmp/lux_report.txt", report)
|
||||
Console.print("Report written to /tmp/lux_report.txt")
|
||||
}
|
||||
|
||||
@@ -1,55 +1,39 @@
|
||||
// Demonstrating functional programming features
|
||||
//
|
||||
// Expected output:
|
||||
// apply(double, 21) = 42
|
||||
// compose(addOne, double)(5) = 11
|
||||
// pipe: 5 |> double |> addOne |> square = 121
|
||||
// curried add5(10) = 15
|
||||
// partial times3(7) = 21
|
||||
// record transform = 30
|
||||
|
||||
// Higher-order functions
|
||||
fn apply(f: fn(Int): Int, x: Int): Int = f(x)
|
||||
|
||||
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int =
|
||||
fn(x: Int): Int => f(g(x))
|
||||
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int = fn(x: Int): Int => f(g(x))
|
||||
|
||||
// Basic functions
|
||||
fn double(x: Int): Int = x * 2
|
||||
|
||||
fn addOne(x: Int): Int = x + 1
|
||||
|
||||
fn square(x: Int): Int = x * x
|
||||
|
||||
// Using apply
|
||||
let result1 = apply(double, 21)
|
||||
|
||||
// Using compose
|
||||
let doubleAndAddOne = compose(addOne, double)
|
||||
|
||||
let result2 = doubleAndAddOne(5)
|
||||
|
||||
// Using the pipe operator
|
||||
let result3 = 5 |> double |> addOne |> square
|
||||
let result3 = square(addOne(double(5)))
|
||||
|
||||
// Currying example
|
||||
fn add(a: Int): fn(Int): Int =
|
||||
fn(b: Int): Int => a + b
|
||||
fn add(a: Int): fn(Int): Int = fn(b: Int): Int => a + b
|
||||
|
||||
let add5 = add(5)
|
||||
|
||||
let result4 = add5(10)
|
||||
|
||||
// Partial application simulation
|
||||
fn multiply(a: Int, b: Int): Int = a * b
|
||||
|
||||
let times3 = fn(x: Int): Int => multiply(3, x)
|
||||
|
||||
let result5 = times3(7)
|
||||
|
||||
// Working with records
|
||||
let transform = fn(record: { x: Int, y: Int }): Int =>
|
||||
record.x + record.y
|
||||
let transform = fn(record: { x: Int, y: Int }): Int => record.x + record.y
|
||||
|
||||
let point = { x: 10, y: 20 }
|
||||
|
||||
let recordSum = transform(point)
|
||||
|
||||
// Print all results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("apply(double, 21) = " + toString(result1))
|
||||
Console.print("compose(addOne, double)(5) = " + toString(result2))
|
||||
|
||||
@@ -1,45 +1,34 @@
|
||||
// Demonstrating generic type parameters in Lux
|
||||
//
|
||||
// Expected output:
|
||||
// identity(42) = 42
|
||||
// identity("hello") = hello
|
||||
// first(MkPair(1, "one")) = 1
|
||||
// second(MkPair(1, "one")) = one
|
||||
// map(Some(21), double) = Some(42)
|
||||
|
||||
// Generic identity function
|
||||
fn identity<T>(x: T): T = x
|
||||
|
||||
// Generic pair type
|
||||
type Pair<A, B> =
|
||||
| MkPair(A, B)
|
||||
|
||||
fn first<A, B>(p: Pair<A, B>): A =
|
||||
match p {
|
||||
MkPair(a, _) => a
|
||||
}
|
||||
MkPair(a, _) => a,
|
||||
}
|
||||
|
||||
fn second<A, B>(p: Pair<A, B>): B =
|
||||
match p {
|
||||
MkPair(_, b) => b
|
||||
}
|
||||
MkPair(_, b) => b,
|
||||
}
|
||||
|
||||
// Generic map function for Option
|
||||
fn mapOption<T, U>(opt: Option<T>, f: fn(T): U): Option<U> =
|
||||
match opt {
|
||||
None => None,
|
||||
Some(x) => Some(f(x))
|
||||
}
|
||||
Some(x) => Some(f(x)),
|
||||
}
|
||||
|
||||
// Helper function for testing
|
||||
fn double(x: Int): Int = x * 2
|
||||
|
||||
// Test usage
|
||||
let id_int = identity(42)
|
||||
|
||||
let id_str = identity("hello")
|
||||
|
||||
let pair = MkPair(1, "one")
|
||||
|
||||
let fst = first(pair)
|
||||
|
||||
let snd = second(pair)
|
||||
|
||||
let doubled = mapOption(Some(21), double)
|
||||
@@ -47,8 +36,8 @@ let doubled = mapOption(Some(21), double)
|
||||
fn showOption(opt: Option<Int>): String =
|
||||
match opt {
|
||||
None => "None",
|
||||
Some(x) => "Some(" + toString(x) + ")"
|
||||
}
|
||||
Some(x) => "Some(" + toString(x) + ")",
|
||||
}
|
||||
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("identity(42) = " + toString(id_int))
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
// Demonstrating resumable effect handlers in Lux
|
||||
//
|
||||
// Handlers can use `resume(value)` to return a value to the effect call site
|
||||
// and continue the computation. This enables powerful control flow patterns.
|
||||
//
|
||||
// Expected output:
|
||||
// [INFO] Starting computation
|
||||
// [DEBUG] Intermediate result: 10
|
||||
// [INFO] Computation complete
|
||||
// Final result: 20
|
||||
|
||||
// Define a custom logging effect
|
||||
effect Logger {
|
||||
fn log(level: String, msg: String): Unit
|
||||
fn getLogLevel(): String
|
||||
}
|
||||
|
||||
// A function that uses the Logger effect
|
||||
fn compute(): Int with {Logger} = {
|
||||
Logger.log("INFO", "Starting computation")
|
||||
let x = 10
|
||||
@@ -25,20 +12,19 @@ fn compute(): Int with {Logger} = {
|
||||
result
|
||||
}
|
||||
|
||||
// A handler that prints logs with brackets and resumes with Unit
|
||||
handler prettyLogger: Logger {
|
||||
fn log(level, msg) = {
|
||||
fn log(level, msg) =
|
||||
{
|
||||
Console.print("[" + level + "] " + msg)
|
||||
resume(())
|
||||
}
|
||||
}
|
||||
fn getLogLevel() = resume("DEBUG")
|
||||
}
|
||||
|
||||
// Main function
|
||||
fn main(): Unit with {Console} = {
|
||||
let result = run compute() with {
|
||||
Logger = prettyLogger
|
||||
}
|
||||
Logger = prettyLogger,
|
||||
}
|
||||
Console.print("Final result: " + toString(result))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
// Hello World in Lux
|
||||
// Demonstrates basic effect usage
|
||||
//
|
||||
// Expected output: Hello, World!
|
||||
fn greet(): Unit with {Console} = Console.print("Hello, World!")
|
||||
|
||||
fn greet(): Unit with {Console} =
|
||||
Console.print("Hello, World!")
|
||||
|
||||
// Run the greeting with the Console effect
|
||||
let output = run greet() with {}
|
||||
|
||||
@@ -1,91 +1,72 @@
|
||||
// HTTP example - demonstrates the Http effect
|
||||
//
|
||||
// This script makes HTTP requests and parses JSON responses
|
||||
|
||||
fn main(): Unit with {Console, Http} = {
|
||||
Console.print("=== Lux HTTP Example ===")
|
||||
Console.print("")
|
||||
|
||||
// Make a GET request to a public API
|
||||
Console.print("Fetching data from httpbin.org...")
|
||||
Console.print("")
|
||||
|
||||
match Http.get("https://httpbin.org/get") {
|
||||
Ok(response) => {
|
||||
Console.print("GET request successful!")
|
||||
Console.print(" Status: " + toString(response.status))
|
||||
Console.print(" Body length: " + toString(String.length(response.body)) + " bytes")
|
||||
Console.print("")
|
||||
|
||||
// Parse the JSON response
|
||||
match Json.parse(response.body) {
|
||||
Ok(json) => {
|
||||
Console.print("Parsed JSON response:")
|
||||
match Json.get(json, "origin") {
|
||||
Some(origin) => match Json.asString(origin) {
|
||||
Some(ip) => Console.print(" Your IP: " + ip),
|
||||
None => Console.print(" origin: (not a string)")
|
||||
},
|
||||
None => Console.print(" origin: (not found)")
|
||||
}
|
||||
None => Console.print(" origin: (not a string)"),
|
||||
},
|
||||
None => Console.print(" origin: (not found)"),
|
||||
}
|
||||
match Json.get(json, "url") {
|
||||
Some(url) => match Json.asString(url) {
|
||||
Some(u) => Console.print(" URL: " + u),
|
||||
None => Console.print(" url: (not a string)")
|
||||
},
|
||||
None => Console.print(" url: (not found)")
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("JSON parse error: " + e)
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("GET request failed: " + e)
|
||||
}
|
||||
|
||||
None => Console.print(" url: (not a string)"),
|
||||
},
|
||||
None => Console.print(" url: (not found)"),
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("JSON parse error: " + e),
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("GET request failed: " + e),
|
||||
}
|
||||
Console.print("")
|
||||
Console.print("--- POST Request ---")
|
||||
Console.print("")
|
||||
|
||||
// Make a POST request with JSON body
|
||||
let requestBody = Json.object([("message", Json.string("Hello from Lux!")), ("version", Json.int(1))])
|
||||
Console.print("Sending POST with JSON body...")
|
||||
Console.print(" Body: " + Json.stringify(requestBody))
|
||||
Console.print("")
|
||||
|
||||
match Http.postJson("https://httpbin.org/post", requestBody) {
|
||||
Ok(response) => {
|
||||
Console.print("POST request successful!")
|
||||
Console.print(" Status: " + toString(response.status))
|
||||
|
||||
// Parse and extract what we sent
|
||||
match Json.parse(response.body) {
|
||||
Ok(json) => match Json.get(json, "json") {
|
||||
Some(sentJson) => {
|
||||
Console.print(" Server received:")
|
||||
Console.print(" " + Json.stringify(sentJson))
|
||||
},
|
||||
None => Console.print(" (no json field in response)")
|
||||
},
|
||||
Err(e) => Console.print("JSON parse error: " + e)
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("POST request failed: " + e)
|
||||
}
|
||||
|
||||
},
|
||||
None => Console.print(" (no json field in response)"),
|
||||
},
|
||||
Err(e) => Console.print("JSON parse error: " + e),
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("POST request failed: " + e),
|
||||
}
|
||||
Console.print("")
|
||||
Console.print("--- Headers ---")
|
||||
Console.print("")
|
||||
|
||||
// Show response headers
|
||||
match Http.get("https://httpbin.org/headers") {
|
||||
Ok(response) => {
|
||||
Console.print("Response headers (first 5):")
|
||||
let count = 0
|
||||
// Note: Can't easily iterate with effects in callbacks, so just show count
|
||||
Console.print(" Total headers: " + toString(List.length(response.headers)))
|
||||
},
|
||||
Err(e) => Console.print("Request failed: " + e)
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("Request failed: " + e),
|
||||
}
|
||||
}
|
||||
|
||||
let result = run main() with {}
|
||||
|
||||
@@ -1,68 +1,31 @@
|
||||
// HTTP API Example
|
||||
//
|
||||
// A complete REST API demonstrating:
|
||||
// - Route matching with path parameters
|
||||
// - Response builders
|
||||
// - JSON construction
|
||||
//
|
||||
// Run with: lux examples/http_api.lux
|
||||
// Test with:
|
||||
// curl http://localhost:8080/
|
||||
// curl http://localhost:8080/users
|
||||
// curl http://localhost:8080/users/42
|
||||
fn httpOk(body: String): { status: Int, body: String } = { status: 200, body: body }
|
||||
|
||||
// ============================================================
|
||||
// Response Helpers
|
||||
// ============================================================
|
||||
fn httpCreated(body: String): { status: Int, body: String } = { status: 201, body: body }
|
||||
|
||||
fn httpOk(body: String): { status: Int, body: String } =
|
||||
{ status: 200, body: body }
|
||||
fn httpNotFound(body: String): { status: Int, body: String } = { status: 404, body: body }
|
||||
|
||||
fn httpCreated(body: String): { status: Int, body: String } =
|
||||
{ status: 201, body: body }
|
||||
fn httpBadRequest(body: String): { status: Int, body: String } = { status: 400, body: body }
|
||||
|
||||
fn httpNotFound(body: String): { status: Int, body: String } =
|
||||
{ status: 404, body: body }
|
||||
fn jsonEscape(s: String): String = String.replace(String.replace(s, "\\", "\\\\"), "\"", "\\\"")
|
||||
|
||||
fn httpBadRequest(body: String): { status: Int, body: String } =
|
||||
{ status: 400, body: body }
|
||||
fn jsonStr(key: String, value: String): String = "\"" + jsonEscape(key) + "\":\"" + jsonEscape(value) + "\""
|
||||
|
||||
// ============================================================
|
||||
// JSON Helpers
|
||||
// ============================================================
|
||||
fn jsonNum(key: String, value: Int): String = "\"" + jsonEscape(key) + "\":" + toString(value)
|
||||
|
||||
fn jsonEscape(s: String): String =
|
||||
String.replace(String.replace(s, "\\", "\\\\"), "\"", "\\\"")
|
||||
fn jsonObj(content: String): String = toString(" + content + ")
|
||||
|
||||
fn jsonStr(key: String, value: String): String =
|
||||
"\"" + jsonEscape(key) + "\":\"" + jsonEscape(value) + "\""
|
||||
fn jsonArr(content: String): String = "[" + content + "]"
|
||||
|
||||
fn jsonNum(key: String, value: Int): String =
|
||||
"\"" + jsonEscape(key) + "\":" + toString(value)
|
||||
|
||||
fn jsonObj(content: String): String =
|
||||
"{" + content + "}"
|
||||
|
||||
fn jsonArr(content: String): String =
|
||||
"[" + content + "]"
|
||||
|
||||
fn jsonError(message: String): String =
|
||||
jsonObj(jsonStr("error", message))
|
||||
|
||||
// ============================================================
|
||||
// Path Matching
|
||||
// ============================================================
|
||||
fn jsonError(message: String): String = jsonObj(jsonStr("error", message))
|
||||
|
||||
fn pathMatches(path: String, pattern: String): Bool = {
|
||||
let pathParts = String.split(path, "/")
|
||||
let patternParts = String.split(pattern, "/")
|
||||
if List.length(pathParts) != List.length(patternParts) then false
|
||||
else matchParts(pathParts, patternParts)
|
||||
if List.length(pathParts) != List.length(patternParts) then false else matchParts(pathParts, patternParts)
|
||||
}
|
||||
|
||||
fn matchParts(pathParts: List<String>, patternParts: List<String>): Bool = {
|
||||
if List.length(pathParts) == 0 then true
|
||||
else {
|
||||
if List.length(pathParts) == 0 then true else {
|
||||
match List.head(pathParts) {
|
||||
None => true,
|
||||
Some(pathPart) => {
|
||||
@@ -74,12 +37,12 @@ fn matchParts(pathParts: List<String>, patternParts: List<String>): Bool = {
|
||||
let restPath = Option.getOrElse(List.tail(pathParts), [])
|
||||
let restPattern = Option.getOrElse(List.tail(patternParts), [])
|
||||
matchParts(restPath, restPattern)
|
||||
} else false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else false
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn getPathSegment(path: String, index: Int): Option<String> = {
|
||||
@@ -87,15 +50,9 @@ fn getPathSegment(path: String, index: Int): Option<String> = {
|
||||
List.get(parts, index + 1)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Handlers
|
||||
// ============================================================
|
||||
fn indexHandler(): { status: Int, body: String } = httpOk(jsonObj(jsonStr("message", "Welcome to Lux HTTP API")))
|
||||
|
||||
fn indexHandler(): { status: Int, body: String } =
|
||||
httpOk(jsonObj(jsonStr("message", "Welcome to Lux HTTP API")))
|
||||
|
||||
fn healthHandler(): { status: Int, body: String } =
|
||||
httpOk(jsonObj(jsonStr("status", "healthy")))
|
||||
fn healthHandler(): { status: Int, body: String } = httpOk(jsonObj(jsonStr("status", "healthy")))
|
||||
|
||||
fn listUsersHandler(): { status: Int, body: String } = {
|
||||
let user1 = jsonObj(jsonNum("id", 1) + "," + jsonStr("name", "Alice"))
|
||||
@@ -108,9 +65,9 @@ fn getUserHandler(path: String): { status: Int, body: String } = {
|
||||
Some(id) => {
|
||||
let body = jsonObj(jsonStr("id", id) + "," + jsonStr("name", "User " + id))
|
||||
httpOk(body)
|
||||
},
|
||||
None => httpNotFound(jsonError("User not found"))
|
||||
}
|
||||
},
|
||||
None => httpNotFound(jsonError("User not found")),
|
||||
}
|
||||
}
|
||||
|
||||
fn createUserHandler(body: String): { status: Int, body: String } = {
|
||||
@@ -118,34 +75,21 @@ fn createUserHandler(body: String): { status: Int, body: String } = {
|
||||
httpCreated(newUser)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Router
|
||||
// ============================================================
|
||||
|
||||
fn router(method: String, path: String, body: String): { status: Int, body: String } = {
|
||||
if method == "GET" && path == "/" then indexHandler()
|
||||
else if method == "GET" && path == "/health" then healthHandler()
|
||||
else if method == "GET" && path == "/users" then listUsersHandler()
|
||||
else if method == "GET" && pathMatches(path, "/users/:id") then getUserHandler(path)
|
||||
else if method == "POST" && path == "/users" then createUserHandler(body)
|
||||
else httpNotFound(jsonError("Not found: " + path))
|
||||
if method == "GET" && path == "/" then indexHandler() else if method == "GET" && path == "/health" then healthHandler() else if method == "GET" && path == "/users" then listUsersHandler() else if method == "GET" && pathMatches(path, "/users/:id") then getUserHandler(path) else if method == "POST" && path == "/users" then createUserHandler(body) else httpNotFound(jsonError("Not found: " + path))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Server
|
||||
// ============================================================
|
||||
|
||||
fn serveLoop(remaining: Int): Unit with {Console, HttpServer} = {
|
||||
if remaining <= 0 then {
|
||||
Console.print("Max requests reached, stopping server.")
|
||||
HttpServer.stop()
|
||||
} else {
|
||||
} else {
|
||||
let req = HttpServer.accept()
|
||||
Console.print(req.method + " " + req.path)
|
||||
let resp = router(req.method, req.path, req.body)
|
||||
HttpServer.respond(resp.status, resp.body)
|
||||
serveLoop(remaining - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console, HttpServer} = {
|
||||
|
||||
@@ -1,24 +1,4 @@
|
||||
// HTTP Router Example
|
||||
//
|
||||
// Demonstrates the HTTP helper library with:
|
||||
// - Path pattern matching
|
||||
// - Response builders
|
||||
// - JSON helpers
|
||||
//
|
||||
// Run with: lux examples/http_router.lux
|
||||
// Test with:
|
||||
// curl http://localhost:8080/
|
||||
// curl http://localhost:8080/users
|
||||
// curl http://localhost:8080/users/42
|
||||
|
||||
import stdlib/http
|
||||
|
||||
// ============================================================
|
||||
// Route Handlers
|
||||
// ============================================================
|
||||
|
||||
fn indexHandler(): { status: Int, body: String } =
|
||||
httpOk("Welcome to Lux HTTP Framework!")
|
||||
fn indexHandler(): { status: Int, body: String } = httpOk("Welcome to Lux HTTP Framework!")
|
||||
|
||||
fn listUsersHandler(): { status: Int, body: String } = {
|
||||
let user1 = jsonObject(jsonJoin([jsonNumber("id", 1), jsonString("name", "Alice")]))
|
||||
@@ -32,41 +12,28 @@ fn getUserHandler(path: String): { status: Int, body: String } = {
|
||||
Some(id) => {
|
||||
let body = jsonObject(jsonJoin([jsonString("id", id), jsonString("name", "User " + id)]))
|
||||
httpOk(body)
|
||||
},
|
||||
None => httpNotFound(jsonErrorMsg("User ID required"))
|
||||
}
|
||||
},
|
||||
None => httpNotFound(jsonErrorMsg("User ID required")),
|
||||
}
|
||||
}
|
||||
|
||||
fn healthHandler(): { status: Int, body: String } =
|
||||
httpOk(jsonObject(jsonString("status", "healthy")))
|
||||
|
||||
// ============================================================
|
||||
// Router
|
||||
// ============================================================
|
||||
fn healthHandler(): { status: Int, body: String } = httpOk(jsonObject(jsonString("status", "healthy")))
|
||||
|
||||
fn router(method: String, path: String, body: String): { status: Int, body: String } = {
|
||||
if method == "GET" && path == "/" then indexHandler()
|
||||
else if method == "GET" && path == "/health" then healthHandler()
|
||||
else if method == "GET" && path == "/users" then listUsersHandler()
|
||||
else if method == "GET" && pathMatches(path, "/users/:id") then getUserHandler(path)
|
||||
else httpNotFound(jsonErrorMsg("Not found: " + path))
|
||||
if method == "GET" && path == "/" then indexHandler() else if method == "GET" && path == "/health" then healthHandler() else if method == "GET" && path == "/users" then listUsersHandler() else if method == "GET" && pathMatches(path, "/users/:id") then getUserHandler(path) else httpNotFound(jsonErrorMsg("Not found: " + path))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Server
|
||||
// ============================================================
|
||||
|
||||
fn serveLoop(remaining: Int): Unit with {Console, HttpServer} = {
|
||||
if remaining <= 0 then {
|
||||
Console.print("Max requests reached, stopping server.")
|
||||
HttpServer.stop()
|
||||
} else {
|
||||
} else {
|
||||
let req = HttpServer.accept()
|
||||
Console.print(req.method + " " + req.path)
|
||||
let resp = router(req.method, req.path, req.body)
|
||||
HttpServer.respond(resp.status, resp.body)
|
||||
serveLoop(remaining - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console, HttpServer} = {
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
// Test file for JIT compilation
|
||||
// This uses only features the JIT supports: integers, arithmetic, conditionals, functions
|
||||
fn fib(n: Int): Int = if n <= 1 then n else fib(n - 1) + fib(n - 2)
|
||||
|
||||
fn fib(n: Int): Int =
|
||||
if n <= 1 then n
|
||||
else fib(n - 1) + fib(n - 2)
|
||||
|
||||
fn factorial(n: Int): Int =
|
||||
if n <= 1 then 1
|
||||
else n * factorial(n - 1)
|
||||
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
let fibResult = fib(30)
|
||||
|
||||
@@ -1,107 +1,79 @@
|
||||
// JSON example - demonstrates JSON parsing and manipulation
|
||||
//
|
||||
// This script parses JSON, extracts values, and builds new JSON structures
|
||||
|
||||
fn main(): Unit with {Console, File} = {
|
||||
Console.print("=== Lux JSON Example ===")
|
||||
Console.print("")
|
||||
|
||||
// First, build some JSON programmatically
|
||||
Console.print("=== Building JSON ===")
|
||||
Console.print("")
|
||||
|
||||
let name = Json.string("Alice")
|
||||
let age = Json.int(30)
|
||||
let active = Json.bool(true)
|
||||
let scores = Json.array([Json.int(95), Json.int(87), Json.int(92)])
|
||||
|
||||
let person = Json.object([("name", name), ("age", age), ("active", active), ("scores", scores)])
|
||||
|
||||
Console.print("Built JSON:")
|
||||
let pretty = Json.prettyPrint(person)
|
||||
Console.print(pretty)
|
||||
Console.print("")
|
||||
|
||||
// Stringify to a compact string
|
||||
let jsonStr = Json.stringify(person)
|
||||
Console.print("Compact: " + jsonStr)
|
||||
Console.print("")
|
||||
|
||||
// Write to file and read back to test parsing
|
||||
File.write("/tmp/test.json", jsonStr)
|
||||
Console.print("Written to /tmp/test.json")
|
||||
Console.print("")
|
||||
|
||||
// Read and parse from file
|
||||
Console.print("=== Parsing JSON ===")
|
||||
Console.print("")
|
||||
let content = File.read("/tmp/test.json")
|
||||
Console.print("Read from file: " + content)
|
||||
Console.print("")
|
||||
|
||||
match Json.parse(content) {
|
||||
Ok(json) => {
|
||||
Console.print("Parse succeeded!")
|
||||
Console.print("")
|
||||
|
||||
// Get string field
|
||||
Console.print("Extracting fields:")
|
||||
match Json.get(json, "name") {
|
||||
Some(nameJson) => match Json.asString(nameJson) {
|
||||
Some(n) => Console.print(" name: " + n),
|
||||
None => Console.print(" name: (not a string)")
|
||||
},
|
||||
None => Console.print(" name: (not found)")
|
||||
}
|
||||
|
||||
// Get int field
|
||||
None => Console.print(" name: (not a string)"),
|
||||
},
|
||||
None => Console.print(" name: (not found)"),
|
||||
}
|
||||
match Json.get(json, "age") {
|
||||
Some(ageJson) => match Json.asInt(ageJson) {
|
||||
Some(a) => Console.print(" age: " + toString(a)),
|
||||
None => Console.print(" age: (not an int)")
|
||||
},
|
||||
None => Console.print(" age: (not found)")
|
||||
}
|
||||
|
||||
// Get bool field
|
||||
None => Console.print(" age: (not an int)"),
|
||||
},
|
||||
None => Console.print(" age: (not found)"),
|
||||
}
|
||||
match Json.get(json, "active") {
|
||||
Some(activeJson) => match Json.asBool(activeJson) {
|
||||
Some(a) => Console.print(" active: " + toString(a)),
|
||||
None => Console.print(" active: (not a bool)")
|
||||
},
|
||||
None => Console.print(" active: (not found)")
|
||||
}
|
||||
|
||||
// Get array field
|
||||
None => Console.print(" active: (not a bool)"),
|
||||
},
|
||||
None => Console.print(" active: (not found)"),
|
||||
}
|
||||
match Json.get(json, "scores") {
|
||||
Some(scoresJson) => match Json.asArray(scoresJson) {
|
||||
Some(arr) => {
|
||||
Console.print(" scores: " + toString(List.length(arr)) + " items")
|
||||
// Get first score
|
||||
match Json.getIndex(scoresJson, 0) {
|
||||
Some(firstJson) => match Json.asInt(firstJson) {
|
||||
Some(first) => Console.print(" first score: " + toString(first)),
|
||||
None => Console.print(" first score: (not an int)")
|
||||
},
|
||||
None => Console.print(" (no first element)")
|
||||
}
|
||||
},
|
||||
None => Console.print(" scores: (not an array)")
|
||||
},
|
||||
None => Console.print(" scores: (not found)")
|
||||
}
|
||||
None => Console.print(" first score: (not an int)"),
|
||||
},
|
||||
None => Console.print(" (no first element)"),
|
||||
}
|
||||
},
|
||||
None => Console.print(" scores: (not an array)"),
|
||||
},
|
||||
None => Console.print(" scores: (not found)"),
|
||||
}
|
||||
Console.print("")
|
||||
|
||||
// Get the keys
|
||||
Console.print("Object keys:")
|
||||
match Json.keys(json) {
|
||||
Some(ks) => Console.print(" " + String.join(ks, ", ")),
|
||||
None => Console.print(" (not an object)")
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("Parse error: " + e)
|
||||
}
|
||||
|
||||
None => Console.print(" (not an object)"),
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("Parse error: " + e),
|
||||
}
|
||||
Console.print("")
|
||||
Console.print("=== JSON Null Check ===")
|
||||
let nullVal = Json.null()
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
// Main program that imports modules
|
||||
import examples/modules/math_utils
|
||||
import examples/modules/string_utils
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Testing Module Imports ===")
|
||||
|
||||
// Use math_utils
|
||||
Console.print("square(5) = " + toString(math_utils.square(5)))
|
||||
Console.print("cube(3) = " + toString(math_utils.cube(3)))
|
||||
Console.print("factorial(6) = " + toString(math_utils.factorial(6)))
|
||||
Console.print("sumRange(1, 10) = " + toString(math_utils.sumRange(1, 10)))
|
||||
|
||||
// Use string_utils
|
||||
Console.print(string_utils.greet("World"))
|
||||
Console.print(string_utils.exclaim("Modules work"))
|
||||
Console.print("repeat(\"ab\", 3) = " + string_utils.repeat("ab", 3))
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
// Test selective imports
|
||||
import examples/modules/math_utils.{square, factorial}
|
||||
import examples/modules/string_utils as str
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Selective & Aliased Imports ===")
|
||||
|
||||
// Direct imports (no module prefix)
|
||||
Console.print("square(7) = " + toString(square(7)))
|
||||
Console.print("factorial(5) = " + toString(factorial(5)))
|
||||
|
||||
// Aliased import
|
||||
Console.print(str.greet("Lux"))
|
||||
Console.print(str.exclaim("Aliased imports work"))
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
// Test wildcard imports
|
||||
import examples/modules/math_utils.*
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Wildcard Imports ===")
|
||||
|
||||
// All functions available directly
|
||||
Console.print("square(4) = " + toString(square(4)))
|
||||
Console.print("cube(4) = " + toString(cube(4)))
|
||||
Console.print("factorial(4) = " + toString(factorial(4)))
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
// Math utilities module
|
||||
// Exports: square, cube, factorial
|
||||
fn square(n: Int): Int = n * n
|
||||
|
||||
pub fn square(n: Int): Int = n * n
|
||||
fn cube(n: Int): Int = n * n * n
|
||||
|
||||
pub fn cube(n: Int): Int = n * n * n
|
||||
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
|
||||
|
||||
pub fn factorial(n: Int): Int =
|
||||
if n <= 1 then 1
|
||||
else n * factorial(n - 1)
|
||||
|
||||
pub fn sumRange(start: Int, end: Int): Int =
|
||||
if start > end then 0
|
||||
else start + sumRange(start + 1, end)
|
||||
fn sumRange(start: Int, end: Int): Int = if start > end then 0 else start + sumRange(start + 1, end)
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
// String utilities module
|
||||
// Exports: repeat, exclaim
|
||||
fn repeat(s: String, n: Int): String = if n <= 0 then "" else s + repeat(s, n - 1)
|
||||
|
||||
pub fn repeat(s: String, n: Int): String =
|
||||
if n <= 0 then ""
|
||||
else s + repeat(s, n - 1)
|
||||
fn exclaim(s: String): String = s + "!"
|
||||
|
||||
pub fn exclaim(s: String): String = s + "!"
|
||||
|
||||
pub fn greet(name: String): String =
|
||||
"Hello, " + name + "!"
|
||||
fn greet(name: String): String = "Hello, " + name + "!"
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
// Example using the standard library
|
||||
import std/prelude.*
|
||||
import std/option as opt
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Using Standard Library ===")
|
||||
|
||||
// Prelude functions
|
||||
Console.print("identity(42) = " + toString(identity(42)))
|
||||
Console.print("not(true) = " + toString(not(true)))
|
||||
Console.print("and(true, false) = " + toString(and(true, false)))
|
||||
Console.print("or(true, false) = " + toString(or(true, false)))
|
||||
|
||||
// Option utilities
|
||||
let x = opt.some(10)
|
||||
let y = opt.none()
|
||||
Console.print("isSome(Some(10)) = " + toString(opt.isSome(x)))
|
||||
|
||||
@@ -1,47 +1,31 @@
|
||||
// Demonstrating the pipe operator and functional data processing
|
||||
//
|
||||
// Expected output:
|
||||
// 5 |> double |> addTen |> square = 400
|
||||
// Pipeline result2 = 42
|
||||
// process(1) = 144
|
||||
// process(2) = 196
|
||||
// process(3) = 256
|
||||
// clamped = 0
|
||||
// composed = 121
|
||||
|
||||
// Basic transformations
|
||||
fn double(x: Int): Int = x * 2
|
||||
|
||||
fn addTen(x: Int): Int = x + 10
|
||||
|
||||
fn square(x: Int): Int = x * x
|
||||
|
||||
fn negate(x: Int): Int = -x
|
||||
|
||||
// Using the pipe operator for data transformation
|
||||
let result1 = 5 |> double |> addTen |> square
|
||||
let result1 = square(addTen(double(5)))
|
||||
|
||||
// Chaining multiple operations
|
||||
let result2 = 3 |> double |> addTen |> double |> addTen
|
||||
let result2 = addTen(double(addTen(double(3))))
|
||||
|
||||
// More complex pipelines
|
||||
fn process(n: Int): Int =
|
||||
n |> double |> addTen |> square
|
||||
fn process(n: Int): Int = square(addTen(double(n)))
|
||||
|
||||
// Multiple values through same pipeline
|
||||
let a = process(1)
|
||||
|
||||
let b = process(2)
|
||||
|
||||
let c = process(3)
|
||||
|
||||
// Conditional in pipeline
|
||||
fn clampPositive(x: Int): Int =
|
||||
if x < 0 then 0 else x
|
||||
fn clampPositive(x: Int): Int = if x < 0 then 0 else x
|
||||
|
||||
let clamped = -5 |> double |> clampPositive
|
||||
let clamped = clampPositive(double(-5))
|
||||
|
||||
// Function composition using pipe
|
||||
fn increment(x: Int): Int = x + 1
|
||||
|
||||
let composed = 5 |> double |> increment |> square
|
||||
let composed = square(increment(double(5)))
|
||||
|
||||
// Print results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("5 |> double |> addTen |> square = " + toString(result1))
|
||||
Console.print("Pipeline result2 = " + toString(result2))
|
||||
|
||||
@@ -1,36 +1,9 @@
|
||||
// PostgreSQL Database Example
|
||||
//
|
||||
// Demonstrates the Postgres effect for database operations.
|
||||
//
|
||||
// Prerequisites:
|
||||
// - PostgreSQL server running locally
|
||||
// - Database 'testdb' created
|
||||
// - User 'testuser' with password 'testpass'
|
||||
//
|
||||
// To set up:
|
||||
// createdb testdb
|
||||
// psql testdb -c "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT, email TEXT);"
|
||||
//
|
||||
// Run with: lux examples/postgres_demo.lux
|
||||
fn jsonStr(key: String, value: String): String = "\"" + key + "\":\"" + value + "\""
|
||||
|
||||
// ============================================================
|
||||
// Helper Functions
|
||||
// ============================================================
|
||||
fn jsonNum(key: String, value: Int): String = "\"" + key + "\":" + toString(value)
|
||||
|
||||
fn jsonStr(key: String, value: String): String =
|
||||
"\"" + key + "\":\"" + value + "\""
|
||||
fn jsonObj(content: String): String = toString(" + content + ")
|
||||
|
||||
fn jsonNum(key: String, value: Int): String =
|
||||
"\"" + key + "\":" + toString(value)
|
||||
|
||||
fn jsonObj(content: String): String =
|
||||
"{" + content + "}"
|
||||
|
||||
// ============================================================
|
||||
// Database Operations
|
||||
// ============================================================
|
||||
|
||||
// Insert a user
|
||||
fn insertUser(connId: Int, name: String, email: String): Int with {Console, Postgres} = {
|
||||
let sql = "INSERT INTO users (name, email) VALUES ('" + name + "', '" + email + "') RETURNING id"
|
||||
Console.print("Inserting user: " + name)
|
||||
@@ -38,35 +11,32 @@ fn insertUser(connId: Int, name: String, email: String): Int with {Console, Post
|
||||
Some(row) => {
|
||||
Console.print(" Inserted with ID: " + toString(row.id))
|
||||
row.id
|
||||
},
|
||||
},
|
||||
None => {
|
||||
Console.print(" Insert failed")
|
||||
-1
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Get all users
|
||||
fn getUsers(connId: Int): Unit with {Console, Postgres} = {
|
||||
Console.print("Fetching all users...")
|
||||
let rows = Postgres.query(connId, "SELECT id, name, email FROM users ORDER BY id")
|
||||
Console.print(" Found " + toString(List.length(rows)) + " users:")
|
||||
List.forEach(rows, fn(row: { id: Int, name: String, email: String }): Unit with {Console} => {
|
||||
List.forEach(rows, fn(row: { id: Int, name: String, email: String }): Unit => {
|
||||
Console.print(" - " + toString(row.id) + ": " + row.name + " <" + row.email + ">")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Get user by ID
|
||||
fn getUserById(connId: Int, id: Int): Unit with {Console, Postgres} = {
|
||||
let sql = "SELECT id, name, email FROM users WHERE id = " + toString(id)
|
||||
Console.print("Looking up user " + toString(id) + "...")
|
||||
match Postgres.queryOne(connId, sql) {
|
||||
Some(row) => Console.print(" Found: " + row.name + " <" + row.email + ">"),
|
||||
None => Console.print(" User not found")
|
||||
}
|
||||
None => Console.print(" User not found"),
|
||||
}
|
||||
}
|
||||
|
||||
// Update user email
|
||||
fn updateUserEmail(connId: Int, id: Int, newEmail: String): Unit with {Console, Postgres} = {
|
||||
let sql = "UPDATE users SET email = '" + newEmail + "' WHERE id = " + toString(id)
|
||||
Console.print("Updating user " + toString(id) + " email to " + newEmail)
|
||||
@@ -74,7 +44,6 @@ fn updateUserEmail(connId: Int, id: Int, newEmail: String): Unit with {Console,
|
||||
Console.print(" Rows affected: " + toString(affected))
|
||||
}
|
||||
|
||||
// Delete user
|
||||
fn deleteUser(connId: Int, id: Int): Unit with {Console, Postgres} = {
|
||||
let sql = "DELETE FROM users WHERE id = " + toString(id)
|
||||
Console.print("Deleting user " + toString(id))
|
||||
@@ -82,104 +51,63 @@ fn deleteUser(connId: Int, id: Int): Unit with {Console, Postgres} = {
|
||||
Console.print(" Rows affected: " + toString(affected))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Transaction Example
|
||||
// ============================================================
|
||||
|
||||
fn transactionDemo(connId: Int): Unit with {Console, Postgres} = {
|
||||
Console.print("")
|
||||
Console.print("=== Transaction Demo ===")
|
||||
|
||||
// Start transaction
|
||||
Console.print("Beginning transaction...")
|
||||
Postgres.beginTx(connId)
|
||||
|
||||
// Make some changes
|
||||
insertUser(connId, "TxUser1", "tx1@example.com")
|
||||
insertUser(connId, "TxUser2", "tx2@example.com")
|
||||
|
||||
// Show users before commit
|
||||
Console.print("Users before commit:")
|
||||
getUsers(connId)
|
||||
|
||||
// Commit the transaction
|
||||
Console.print("Committing transaction...")
|
||||
Postgres.commit(connId)
|
||||
|
||||
Console.print("Transaction committed!")
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Main
|
||||
// ============================================================
|
||||
|
||||
fn main(): Unit with {Console, Postgres} = {
|
||||
Console.print("========================================")
|
||||
Console.print(" PostgreSQL Demo")
|
||||
Console.print("========================================")
|
||||
Console.print("")
|
||||
|
||||
// Connect to database
|
||||
Console.print("Connecting to PostgreSQL...")
|
||||
let connStr = "host=localhost user=testuser password=testpass dbname=testdb"
|
||||
let connId = Postgres.connect(connStr)
|
||||
Console.print("Connected! Connection ID: " + toString(connId))
|
||||
Console.print("")
|
||||
|
||||
// Create table if not exists
|
||||
Console.print("Creating users table...")
|
||||
Postgres.execute(connId, "CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL)")
|
||||
Console.print("")
|
||||
|
||||
// Clear table for demo
|
||||
Console.print("Clearing existing data...")
|
||||
Postgres.execute(connId, "DELETE FROM users")
|
||||
Console.print("")
|
||||
|
||||
// Insert some users
|
||||
Console.print("=== Inserting Users ===")
|
||||
let id1 = insertUser(connId, "Alice", "alice@example.com")
|
||||
let id2 = insertUser(connId, "Bob", "bob@example.com")
|
||||
let id3 = insertUser(connId, "Charlie", "charlie@example.com")
|
||||
Console.print("")
|
||||
|
||||
// Query all users
|
||||
Console.print("=== All Users ===")
|
||||
getUsers(connId)
|
||||
Console.print("")
|
||||
|
||||
// Query single user
|
||||
Console.print("=== Single User Lookup ===")
|
||||
getUserById(connId, id2)
|
||||
Console.print("")
|
||||
|
||||
// Update user
|
||||
Console.print("=== Update User ===")
|
||||
updateUserEmail(connId, id2, "bob.new@example.com")
|
||||
getUserById(connId, id2)
|
||||
Console.print("")
|
||||
|
||||
// Delete user
|
||||
Console.print("=== Delete User ===")
|
||||
deleteUser(connId, id3)
|
||||
getUsers(connId)
|
||||
Console.print("")
|
||||
|
||||
// Transaction demo
|
||||
transactionDemo(connId)
|
||||
Console.print("")
|
||||
|
||||
// Final state
|
||||
Console.print("=== Final State ===")
|
||||
getUsers(connId)
|
||||
Console.print("")
|
||||
|
||||
// Close connection
|
||||
Console.print("Closing connection...")
|
||||
Postgres.close(connId)
|
||||
Console.print("Done!")
|
||||
}
|
||||
|
||||
// Note: This will fail if PostgreSQL is not running
|
||||
// To test the syntax only, you can comment out the last line
|
||||
let output = run main() with {}
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
// Property-Based Testing Example
|
||||
//
|
||||
// This example demonstrates property-based testing in Lux,
|
||||
// where we verify properties hold for randomly generated inputs.
|
||||
//
|
||||
// Run with: lux examples/property_testing.lux
|
||||
|
||||
// ============================================================
|
||||
// Generator Functions (using Random effect)
|
||||
// ============================================================
|
||||
|
||||
let CHARS = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
fn genInt(min: Int, max: Int): Int with {Random} =
|
||||
Random.int(min, max)
|
||||
fn genInt(min: Int, max: Int): Int with {Random} = Random.int(min, max)
|
||||
|
||||
fn genIntList(min: Int, max: Int, maxLen: Int): List<Int> with {Random} = {
|
||||
let len = Random.int(0, maxLen)
|
||||
@@ -20,10 +8,7 @@ fn genIntList(min: Int, max: Int, maxLen: Int): List<Int> with {Random} = {
|
||||
}
|
||||
|
||||
fn genIntListHelper(min: Int, max: Int, len: Int): List<Int> with {Random} = {
|
||||
if len <= 0 then
|
||||
[]
|
||||
else
|
||||
List.concat([Random.int(min, max)], genIntListHelper(min, max, len - 1))
|
||||
if len <= 0 then [] else List.concat([Random.int(min, max)], genIntListHelper(min, max, len - 1))
|
||||
}
|
||||
|
||||
fn genChar(): String with {Random} = {
|
||||
@@ -37,195 +22,147 @@ fn genString(maxLen: Int): String with {Random} = {
|
||||
}
|
||||
|
||||
fn genStringHelper(len: Int): String with {Random} = {
|
||||
if len <= 0 then
|
||||
""
|
||||
else
|
||||
genChar() + genStringHelper(len - 1)
|
||||
if len <= 0 then "" else genChar() + genStringHelper(len - 1)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Test Runner State
|
||||
// ============================================================
|
||||
|
||||
fn printResult(name: String, passed: Bool, count: Int): Unit with {Console} = {
|
||||
if passed then
|
||||
Console.print(" PASS " + name + " (" + toString(count) + " tests)")
|
||||
else
|
||||
Console.print(" FAIL " + name)
|
||||
if passed then Console.print(" PASS " + name + " (" + toString(count) + " tests)") else Console.print(" FAIL " + name)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Property Tests
|
||||
// ============================================================
|
||||
|
||||
// Test: List reverse is involutive
|
||||
fn testReverseInvolutive(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("reverse(reverse(xs)) == xs", true, count)
|
||||
true
|
||||
} else {
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.reverse(List.reverse(xs)) == xs then
|
||||
testReverseInvolutive(n - 1, count)
|
||||
else {
|
||||
if List.reverse(List.reverse(xs)) == xs then testReverseInvolutive(n - 1, count) else {
|
||||
printResult("reverse(reverse(xs)) == xs", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: List reverse preserves length
|
||||
fn testReverseLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("length(reverse(xs)) == length(xs)", true, count)
|
||||
true
|
||||
} else {
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.length(List.reverse(xs)) == List.length(xs) then
|
||||
testReverseLength(n - 1, count)
|
||||
else {
|
||||
if List.length(List.reverse(xs)) == List.length(xs) then testReverseLength(n - 1, count) else {
|
||||
printResult("length(reverse(xs)) == length(xs)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: List map preserves length
|
||||
fn testMapLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("length(map(xs, f)) == length(xs)", true, count)
|
||||
true
|
||||
} else {
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.length(List.map(xs, fn(x) => x * 2)) == List.length(xs) then
|
||||
testMapLength(n - 1, count)
|
||||
else {
|
||||
if List.length(List.map(xs, fn(x: _) => x * 2)) == List.length(xs) then testMapLength(n - 1, count) else {
|
||||
printResult("length(map(xs, f)) == length(xs)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: List concat length is sum
|
||||
fn testConcatLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("length(xs ++ ys) == length(xs) + length(ys)", true, count)
|
||||
true
|
||||
} else {
|
||||
} else {
|
||||
let xs = genIntList(0, 50, 10)
|
||||
let ys = genIntList(0, 50, 10)
|
||||
if List.length(List.concat(xs, ys)) == List.length(xs) + List.length(ys) then
|
||||
testConcatLength(n - 1, count)
|
||||
else {
|
||||
if List.length(List.concat(xs, ys)) == List.length(xs) + List.length(ys) then testConcatLength(n - 1, count) else {
|
||||
printResult("length(xs ++ ys) == length(xs) + length(ys)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Addition is commutative
|
||||
fn testAddCommutative(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("a + b == b + a", true, count)
|
||||
true
|
||||
} else {
|
||||
} else {
|
||||
let a = genInt(-1000, 1000)
|
||||
let b = genInt(-1000, 1000)
|
||||
if a + b == b + a then
|
||||
testAddCommutative(n - 1, count)
|
||||
else {
|
||||
if a + b == b + a then testAddCommutative(n - 1, count) else {
|
||||
printResult("a + b == b + a", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Multiplication is associative
|
||||
fn testMulAssociative(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("(a * b) * c == a * (b * c)", true, count)
|
||||
true
|
||||
} else {
|
||||
} else {
|
||||
let a = genInt(-100, 100)
|
||||
let b = genInt(-100, 100)
|
||||
let c = genInt(-100, 100)
|
||||
if (a * b) * c == a * (b * c) then
|
||||
testMulAssociative(n - 1, count)
|
||||
else {
|
||||
if a * b * c == a * b * c then testMulAssociative(n - 1, count) else {
|
||||
printResult("(a * b) * c == a * (b * c)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: String concat length is sum
|
||||
fn testStringConcatLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("length(s1 + s2) == length(s1) + length(s2)", true, count)
|
||||
true
|
||||
} else {
|
||||
} else {
|
||||
let s1 = genString(10)
|
||||
let s2 = genString(10)
|
||||
if String.length(s1 + s2) == String.length(s1) + String.length(s2) then
|
||||
testStringConcatLength(n - 1, count)
|
||||
else {
|
||||
if String.length(s1 + s2) == String.length(s1) + String.length(s2) then testStringConcatLength(n - 1, count) else {
|
||||
printResult("length(s1 + s2) == length(s1) + length(s2)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Zero is identity for addition
|
||||
fn testAddIdentity(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("x + 0 == x && 0 + x == x", true, count)
|
||||
true
|
||||
} else {
|
||||
} else {
|
||||
let x = genInt(-10000, 10000)
|
||||
if x + 0 == x && 0 + x == x then
|
||||
testAddIdentity(n - 1, count)
|
||||
else {
|
||||
if x + 0 == x && 0 + x == x then testAddIdentity(n - 1, count) else {
|
||||
printResult("x + 0 == x && 0 + x == x", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Filter reduces or maintains length
|
||||
fn testFilterLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("length(filter(xs, p)) <= length(xs)", true, count)
|
||||
true
|
||||
} else {
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.length(List.filter(xs, fn(x) => x > 50)) <= List.length(xs) then
|
||||
testFilterLength(n - 1, count)
|
||||
else {
|
||||
if List.length(List.filter(xs, fn(x: _) => x > 50)) <= List.length(xs) then testFilterLength(n - 1, count) else {
|
||||
printResult("length(filter(xs, p)) <= length(xs)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Empty list is identity for concat
|
||||
fn testConcatIdentity(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("concat(xs, []) == xs && concat([], xs) == xs", true, count)
|
||||
true
|
||||
} else {
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 10)
|
||||
if List.concat(xs, []) == xs && List.concat([], xs) == xs then
|
||||
testConcatIdentity(n - 1, count)
|
||||
else {
|
||||
if List.concat(xs, []) == xs && List.concat([], xs) == xs then testConcatIdentity(n - 1, count) else {
|
||||
printResult("concat(xs, []) == xs && concat([], xs) == xs", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Main
|
||||
// ============================================================
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console, Random} = {
|
||||
Console.print("========================================")
|
||||
@@ -234,7 +171,6 @@ fn main(): Unit with {Console, Random} = {
|
||||
Console.print("")
|
||||
Console.print("Running 100 iterations per property...")
|
||||
Console.print("")
|
||||
|
||||
testReverseInvolutive(100, 100)
|
||||
testReverseLength(100, 100)
|
||||
testMapLength(100, 100)
|
||||
@@ -245,7 +181,6 @@ fn main(): Unit with {Console, Random} = {
|
||||
testAddIdentity(100, 100)
|
||||
testFilterLength(100, 100)
|
||||
testConcatIdentity(100, 100)
|
||||
|
||||
Console.print("")
|
||||
Console.print("========================================")
|
||||
Console.print(" All property tests completed!")
|
||||
|
||||
@@ -1,39 +1,22 @@
|
||||
// Demonstrating Random and Time effects in Lux
|
||||
//
|
||||
// Expected output (values will vary):
|
||||
// Rolling dice...
|
||||
// Die 1: <random 1-6>
|
||||
// Die 2: <random 1-6>
|
||||
// Die 3: <random 1-6>
|
||||
// Coin flip: <true/false>
|
||||
// Random float: <0.0-1.0>
|
||||
// Current time: <timestamp>
|
||||
|
||||
// Roll a single die (1-6)
|
||||
fn rollDie(): Int with {Random} = Random.int(1, 6)
|
||||
|
||||
// Roll multiple dice and print results
|
||||
fn rollDice(count: Int): Unit with {Random, Console} = {
|
||||
if count > 0 then {
|
||||
let value = rollDie()
|
||||
Console.print("Die " + toString(4 - count) + ": " + toString(value))
|
||||
rollDice(count - 1)
|
||||
} else {
|
||||
} else {
|
||||
()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main function demonstrating random effects
|
||||
fn main(): Unit with {Random, Console, Time} = {
|
||||
Console.print("Rolling dice...")
|
||||
rollDice(3)
|
||||
|
||||
let coin = Random.bool()
|
||||
Console.print("Coin flip: " + toString(coin))
|
||||
|
||||
let f = Random.float()
|
||||
Console.print("Random float: " + toString(f))
|
||||
|
||||
let now = Time.now()
|
||||
Console.print("Current time: " + toString(now))
|
||||
}
|
||||
|
||||
@@ -1,67 +1,41 @@
|
||||
// Schema Evolution Demo
|
||||
// Demonstrates version tracking and automatic migrations
|
||||
|
||||
// ============================================================
|
||||
// PART 1: Type-Declared Migrations
|
||||
// ============================================================
|
||||
|
||||
// Define a versioned type with a migration from v1 to v2
|
||||
type User @v2 {
|
||||
type User = {
|
||||
name: String,
|
||||
email: String,
|
||||
|
||||
// Migration from v1: add default email
|
||||
from @v1 = { name: old.name, email: "unknown@example.com" }
|
||||
}
|
||||
|
||||
// Create a v1 user
|
||||
let v1_user = Schema.versioned("User", 1, { name: "Alice" })
|
||||
let v1_version = Schema.getVersion(v1_user) // 1
|
||||
|
||||
// Migrate to v2 - uses the declared migration automatically
|
||||
let v1_version = Schema.getVersion(v1_user)
|
||||
|
||||
let v2_user = Schema.migrate(v1_user, 2)
|
||||
let v2_version = Schema.getVersion(v2_user) // 2
|
||||
|
||||
// ============================================================
|
||||
// PART 2: Runtime Schema Operations (separate type)
|
||||
// ============================================================
|
||||
let v2_version = Schema.getVersion(v2_user)
|
||||
|
||||
// Create versioned values for a different type (no migration)
|
||||
let config1 = Schema.versioned("Config", 1, "debug")
|
||||
|
||||
let config2 = Schema.versioned("Config", 2, "release")
|
||||
|
||||
// Check versions
|
||||
let c1 = Schema.getVersion(config1) // 1
|
||||
let c2 = Schema.getVersion(config2) // 2
|
||||
let c1 = Schema.getVersion(config1)
|
||||
|
||||
let c2 = Schema.getVersion(config2)
|
||||
|
||||
// Migrate config (auto-migration since no explicit migration defined)
|
||||
let upgradedConfig = Schema.migrate(config1, 2)
|
||||
let upgradedConfigVersion = Schema.getVersion(upgradedConfig) // 2
|
||||
|
||||
// ============================================================
|
||||
// PART 2: Practical Example - API Versioning
|
||||
// ============================================================
|
||||
let upgradedConfigVersion = Schema.getVersion(upgradedConfig)
|
||||
|
||||
// Simulate different API response versions
|
||||
fn createResponseV1(data: String): { version: Int, payload: String } =
|
||||
{ version: 1, payload: data }
|
||||
fn createResponseV1(data: String): { version: Int, payload: String } = { version: 1, payload: data }
|
||||
|
||||
fn createResponseV2(data: String, timestamp: Int): { version: Int, payload: String, meta: { ts: Int } } =
|
||||
{ version: 2, payload: data, meta: { ts: timestamp } }
|
||||
fn createResponseV2(data: String, timestamp: Int): { version: Int, payload: String, meta: { ts: Int } } = { version: 2, payload: data, meta: { ts: timestamp } }
|
||||
|
||||
// Version-aware processing
|
||||
fn getPayload(response: { version: Int, payload: String }): String =
|
||||
response.payload
|
||||
fn getPayload(response: { version: Int, payload: String }): String = response.payload
|
||||
|
||||
let resp1 = createResponseV1("Hello")
|
||||
|
||||
let resp2 = createResponseV2("World", 1234567890)
|
||||
|
||||
let payload1 = getPayload(resp1)
|
||||
let payload2 = resp2.payload
|
||||
|
||||
// ============================================================
|
||||
// RESULTS
|
||||
// ============================================================
|
||||
let payload2 = resp2.payload
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Schema Evolution Demo ===")
|
||||
|
||||
@@ -1,58 +1,43 @@
|
||||
// Shell/Process example - demonstrates the Process effect
|
||||
//
|
||||
// This script runs shell commands and uses environment variables
|
||||
|
||||
fn main(): Unit with {Process, Console} = {
|
||||
Console.print("=== Lux Shell Example ===")
|
||||
Console.print("")
|
||||
|
||||
// Get current working directory
|
||||
let cwd = Process.cwd()
|
||||
Console.print("Current directory: " + cwd)
|
||||
Console.print("")
|
||||
|
||||
// Get environment variables
|
||||
Console.print("Environment variables:")
|
||||
match Process.env("USER") {
|
||||
Some(user) => Console.print(" USER: " + user),
|
||||
None => Console.print(" USER: (not set)")
|
||||
}
|
||||
None => Console.print(" USER: (not set)"),
|
||||
}
|
||||
match Process.env("HOME") {
|
||||
Some(home) => Console.print(" HOME: " + home),
|
||||
None => Console.print(" HOME: (not set)")
|
||||
}
|
||||
None => Console.print(" HOME: (not set)"),
|
||||
}
|
||||
match Process.env("SHELL") {
|
||||
Some(shell) => Console.print(" SHELL: " + shell),
|
||||
None => Console.print(" SHELL: (not set)")
|
||||
}
|
||||
None => Console.print(" SHELL: (not set)"),
|
||||
}
|
||||
Console.print("")
|
||||
|
||||
// Run shell commands
|
||||
Console.print("Running shell commands:")
|
||||
|
||||
let date = Process.exec("date")
|
||||
Console.print(" date: " + String.trim(date))
|
||||
|
||||
let kernel = Process.exec("uname -r")
|
||||
Console.print(" kernel: " + String.trim(kernel))
|
||||
|
||||
let files = Process.exec("ls examples/*.lux | wc -l")
|
||||
Console.print(" .lux files in examples/: " + String.trim(files))
|
||||
Console.print("")
|
||||
|
||||
// Command line arguments
|
||||
Console.print("Command line arguments:")
|
||||
let args = Process.args()
|
||||
let argCount = List.length(args)
|
||||
if argCount == 0 then {
|
||||
Console.print(" (no arguments)")
|
||||
} else {
|
||||
} else {
|
||||
Console.print(" Count: " + toString(argCount))
|
||||
match List.head(args) {
|
||||
Some(first) => Console.print(" First: " + first),
|
||||
None => Console.print(" First: (empty)")
|
||||
}
|
||||
}
|
||||
None => Console.print(" First: (empty)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = run main() with {}
|
||||
|
||||
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 {
|
||||
fn get(key: String): String
|
||||
}
|
||||
@@ -25,14 +13,13 @@ fn configure(): String with {Config, Console} = {
|
||||
}
|
||||
|
||||
handler envConfig: Config {
|
||||
fn get(key) =
|
||||
if key == "api_url" then resume("https://api.example.com")
|
||||
else if key == "timeout" then resume("30")
|
||||
else resume("unknown")
|
||||
fn get(key) = if key == "api_url" then resume("https://api.example.com") else if key == "timeout" then resume("30") else resume("unknown")
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
let result = run configure() with { Config = envConfig }
|
||||
let result = run configure() with {
|
||||
Config = envConfig,
|
||||
}
|
||||
Console.print(result)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
// Custom Logging with Effects
|
||||
//
|
||||
// This demonstrates how effects let you abstract side effects.
|
||||
// The same code can be run with different logging implementations.
|
||||
//
|
||||
// Expected output:
|
||||
// [INFO] Starting computation
|
||||
// [DEBUG] x = 10
|
||||
// [INFO] Processing
|
||||
// [DEBUG] result = 20
|
||||
// Final: 20
|
||||
|
||||
effect Log {
|
||||
fn info(msg: String): Unit
|
||||
fn debug(msg: String): Unit
|
||||
@@ -26,18 +14,22 @@ fn computation(): Int with {Log} = {
|
||||
}
|
||||
|
||||
handler consoleLogger: Log {
|
||||
fn info(msg) = {
|
||||
fn info(msg) =
|
||||
{
|
||||
Console.print("[INFO] " + msg)
|
||||
resume(())
|
||||
}
|
||||
fn debug(msg) = {
|
||||
}
|
||||
fn debug(msg) =
|
||||
{
|
||||
Console.print("[DEBUG] " + msg)
|
||||
resume(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
let result = run computation() with { Log = consoleLogger }
|
||||
let result = run computation() with {
|
||||
Log = consoleLogger,
|
||||
}
|
||||
Console.print("Final: " + toString(result))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,18 @@
|
||||
// Early Return with Fail Effect
|
||||
//
|
||||
// The Fail effect provides clean early termination.
|
||||
// Functions declare their failure modes in the type signature.
|
||||
//
|
||||
// Expected output:
|
||||
// Parsing "42"...
|
||||
// Result: 42
|
||||
// Parsing "100"...
|
||||
// Result: 100
|
||||
// Dividing 100 by 4...
|
||||
// Result: 25
|
||||
|
||||
fn parsePositive(s: String): Int with {Fail, Console} = {
|
||||
Console.print("Parsing \"" + s + "\"...")
|
||||
if s == "42" then 42
|
||||
else if s == "100" then 100
|
||||
else Fail.fail("Invalid number: " + s)
|
||||
if s == "42" then 42 else if s == "100" then 100 else Fail.fail("Invalid number: " + s)
|
||||
}
|
||||
|
||||
fn safeDivide(a: Int, b: Int): Int with {Fail, Console} = {
|
||||
Console.print("Dividing " + toString(a) + " by " + toString(b) + "...")
|
||||
if b == 0 then Fail.fail("Division by zero")
|
||||
else a / b
|
||||
if b == 0 then Fail.fail("Division by zero") else a / b
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
// These succeed
|
||||
let n1 = run parsePositive("42") with {}
|
||||
Console.print("Result: " + toString(n1))
|
||||
|
||||
let n2 = run parsePositive("100") with {}
|
||||
Console.print("Result: " + toString(n2))
|
||||
|
||||
let n3 = run safeDivide(100, 4) with {}
|
||||
Console.print("Result: " + toString(n3))
|
||||
}
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
// Effect Composition - Combine multiple effects cleanly
|
||||
//
|
||||
// Unlike monad transformers (which have ordering issues),
|
||||
// effects can be freely combined without boilerplate.
|
||||
// Each handler handles its own effect, ignoring others.
|
||||
//
|
||||
// Expected output:
|
||||
// [LOG] Starting computation
|
||||
// Generated: 7
|
||||
// [LOG] Processing value
|
||||
// [LOG] Done
|
||||
// Result: 14
|
||||
|
||||
effect Log {
|
||||
fn log(msg: String): Unit
|
||||
}
|
||||
@@ -30,8 +17,8 @@ handler consoleLog: Log {
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
let result = run computation() with {
|
||||
Log = consoleLog
|
||||
}
|
||||
Log = consoleLog,
|
||||
}
|
||||
Console.print("Generated: " + toString(result / 2))
|
||||
Console.print("Result: " + toString(result))
|
||||
}
|
||||
|
||||
@@ -1,38 +1,19 @@
|
||||
// Higher-Order Functions and Closures
|
||||
//
|
||||
// Functions are first-class values in Lux.
|
||||
// Closures capture their environment.
|
||||
//
|
||||
// Expected output:
|
||||
// Square of 5: 25
|
||||
// Cube of 3: 27
|
||||
// Add 10 to 5: 15
|
||||
// Add 10 to 20: 30
|
||||
// Composed: 15625 (cube(square(5)) = cube(25) = 15625)
|
||||
|
||||
fn apply(f: fn(Int): Int, x: Int): Int = f(x)
|
||||
|
||||
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int =
|
||||
fn(x: Int): Int => f(g(x))
|
||||
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int = fn(x: Int): Int => f(g(x))
|
||||
|
||||
fn square(n: Int): Int = n * n
|
||||
|
||||
fn cube(n: Int): Int = n * n * n
|
||||
|
||||
fn makeAdder(n: Int): fn(Int): Int =
|
||||
fn(x: Int): Int => x + n
|
||||
fn makeAdder(n: Int): fn(Int): Int = fn(x: Int): Int => x + n
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
// Apply functions
|
||||
Console.print("Square of 5: " + toString(apply(square, 5)))
|
||||
Console.print("Cube of 3: " + toString(apply(cube, 3)))
|
||||
|
||||
// Closures
|
||||
let add10 = makeAdder(10)
|
||||
Console.print("Add 10 to 5: " + toString(add10(5)))
|
||||
Console.print("Add 10 to 20: " + toString(add10(20)))
|
||||
|
||||
// Function composition
|
||||
let squareThenCube = compose(cube, square)
|
||||
Console.print("Composed: " + toString(squareThenCube(5)))
|
||||
}
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
// Algebraic Data Types and Pattern Matching
|
||||
//
|
||||
// Lux has powerful ADTs with exhaustive pattern matching.
|
||||
// The type system ensures all cases are handled.
|
||||
//
|
||||
// Expected output:
|
||||
// Evaluating: (2 + 3)
|
||||
// Result: 5
|
||||
// Evaluating: ((1 + 2) * (3 + 4))
|
||||
// Result: 21
|
||||
// Evaluating: (10 - (2 * 3))
|
||||
// Result: 4
|
||||
|
||||
type Expr =
|
||||
| Num(Int)
|
||||
| Add(Expr, Expr)
|
||||
@@ -22,16 +9,16 @@ fn eval(e: Expr): Int =
|
||||
Num(n) => n,
|
||||
Add(a, b) => eval(a) + eval(b),
|
||||
Sub(a, b) => eval(a) - eval(b),
|
||||
Mul(a, b) => eval(a) * eval(b)
|
||||
}
|
||||
Mul(a, b) => eval(a) * eval(b),
|
||||
}
|
||||
|
||||
fn showExpr(e: Expr): String =
|
||||
match e {
|
||||
Num(n) => toString(n),
|
||||
Add(a, b) => "(" + showExpr(a) + " + " + showExpr(b) + ")",
|
||||
Sub(a, b) => "(" + showExpr(a) + " - " + showExpr(b) + ")",
|
||||
Mul(a, b) => "(" + showExpr(a) + " * " + showExpr(b) + ")"
|
||||
}
|
||||
Mul(a, b) => "(" + showExpr(a) + " * " + showExpr(b) + ")",
|
||||
}
|
||||
|
||||
fn evalAndPrint(e: Expr): Unit with {Console} = {
|
||||
Console.print("Evaluating: " + showExpr(e))
|
||||
@@ -39,15 +26,10 @@ fn evalAndPrint(e: Expr): Unit with {Console} = {
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
// (2 + 3)
|
||||
let e1 = Add(Num(2), Num(3))
|
||||
evalAndPrint(e1)
|
||||
|
||||
// ((1 + 2) * (3 + 4))
|
||||
let e2 = Mul(Add(Num(1), Num(2)), Add(Num(3), Num(4)))
|
||||
evalAndPrint(e2)
|
||||
|
||||
// (10 - (2 * 3))
|
||||
let e3 = Sub(Num(10), Mul(Num(2), Num(3)))
|
||||
evalAndPrint(e3)
|
||||
}
|
||||
|
||||
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 factorial(n: Int): Int =
|
||||
if n <= 1 then 1
|
||||
else n * factorial(n - 1)
|
||||
|
||||
// Tail-recursive version (optimized)
|
||||
fn factorialTail(n: Int, acc: Int): Int =
|
||||
if n <= 1 then acc
|
||||
else factorialTail(n - 1, n * acc)
|
||||
fn factorialTail(n: Int, acc: Int): Int = if n <= 1 then acc else factorialTail(n - 1, n * acc)
|
||||
|
||||
fn factorial2(n: Int): Int = factorialTail(n, 1)
|
||||
|
||||
|
||||
@@ -1,22 +1,11 @@
|
||||
// FizzBuzz - print numbers 1-100, but:
|
||||
// - multiples of 3: print "Fizz"
|
||||
// - multiples of 5: print "Buzz"
|
||||
// - multiples of both: print "FizzBuzz"
|
||||
|
||||
fn fizzbuzz(n: Int): String =
|
||||
if n % 15 == 0 then "FizzBuzz"
|
||||
else if n % 3 == 0 then "Fizz"
|
||||
else if n % 5 == 0 then "Buzz"
|
||||
else toString(n)
|
||||
fn fizzbuzz(n: Int): String = if n % 15 == 0 then "FizzBuzz" else if n % 3 == 0 then "Fizz" else if n % 5 == 0 then "Buzz" else toString(n)
|
||||
|
||||
fn printFizzbuzz(i: Int, max: Int): Unit with {Console} =
|
||||
if i > max then ()
|
||||
else {
|
||||
if i > max then () else {
|
||||
Console.print(fizzbuzz(i))
|
||||
printFizzbuzz(i + 1, max)
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} =
|
||||
printFizzbuzz(1, 100)
|
||||
fn main(): Unit with {Console} = printFizzbuzz(1, 100)
|
||||
|
||||
let output = run main() with {}
|
||||
|
||||
@@ -1,42 +1,17 @@
|
||||
// Number guessing game - demonstrates Random and Console effects
|
||||
//
|
||||
// Expected output:
|
||||
// Welcome to the Guessing Game!
|
||||
// Target number: 42
|
||||
// Simulating guesses...
|
||||
// Guess 50: Too high!
|
||||
// Guess 25: Too low!
|
||||
// Guess 37: Too low!
|
||||
// Guess 43: Too high!
|
||||
// Guess 40: Too low!
|
||||
// Guess 41: Too low!
|
||||
// Guess 42: Correct!
|
||||
// Found in 7 attempts!
|
||||
fn checkGuess(guess: Int, secret: Int): String = if guess == secret then "Correct" else if guess < secret then "Too low" else "Too high"
|
||||
|
||||
// Game logic - check a guess against the secret
|
||||
fn checkGuess(guess: Int, secret: Int): String =
|
||||
if guess == secret then "Correct"
|
||||
else if guess < secret then "Too low"
|
||||
else "Too high"
|
||||
|
||||
// Binary search simulation to find the number
|
||||
fn binarySearch(low: Int, high: Int, secret: Int, attempts: Int): Int with {Console} = {
|
||||
let mid = (low + high) / 2
|
||||
let mid = low + high / 2
|
||||
let result = checkGuess(mid, secret)
|
||||
Console.print("Guess " + toString(mid) + ": " + result + "!")
|
||||
|
||||
if result == "Correct" then attempts
|
||||
else if result == "Too low" then binarySearch(mid + 1, high, secret, attempts + 1)
|
||||
else binarySearch(low, mid - 1, secret, attempts + 1)
|
||||
if result == "Correct" then attempts else if result == "Too low" then binarySearch(mid + 1, high, secret, attempts + 1) else binarySearch(low, mid - 1, secret, attempts + 1)
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("Welcome to the Guessing Game!")
|
||||
// Use a fixed "secret" for reproducible output
|
||||
let secret = 42
|
||||
Console.print("Target number: " + toString(secret))
|
||||
Console.print("Simulating guesses...")
|
||||
|
||||
let attempts = binarySearch(1, 100, secret, 1)
|
||||
Console.print("Found in " + toString(attempts) + " attempts!")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// The classic first program
|
||||
// Expected output: Hello, World!
|
||||
|
||||
fn main(): Unit with {Console} =
|
||||
Console.print("Hello, World!")
|
||||
fn main(): Unit with {Console} = Console.print("Hello, World!")
|
||||
|
||||
let output = run main() with {}
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
// Prime number utilities
|
||||
fn isPrime(n: Int): Bool = if n < 2 then false else isPrimeHelper(n, 2)
|
||||
|
||||
fn isPrime(n: Int): Bool =
|
||||
if n < 2 then false
|
||||
else isPrimeHelper(n, 2)
|
||||
fn isPrimeHelper(n: Int, i: Int): Bool = if i * i > n then true else if n % i == 0 then false else isPrimeHelper(n, i + 1)
|
||||
|
||||
fn isPrimeHelper(n: Int, i: Int): Bool =
|
||||
if i * i > n then true
|
||||
else if n % i == 0 then false
|
||||
else isPrimeHelper(n, i + 1)
|
||||
|
||||
// Find first n primes
|
||||
fn findPrimes(count: Int): Unit with {Console} =
|
||||
findPrimesHelper(2, count)
|
||||
fn findPrimes(count: Int): Unit with {Console} = findPrimesHelper(2, count)
|
||||
|
||||
fn findPrimesHelper(current: Int, remaining: Int): Unit with {Console} =
|
||||
if remaining <= 0 then ()
|
||||
else if isPrime(current) then {
|
||||
if remaining <= 0 then () else if isPrime(current) then {
|
||||
Console.print(toString(current))
|
||||
findPrimesHelper(current + 1, remaining - 1)
|
||||
}
|
||||
else findPrimesHelper(current + 1, remaining)
|
||||
} else findPrimesHelper(current + 1, remaining)
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("First 20 prime numbers:")
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// Standard Library Demo
|
||||
// Demonstrates the built-in modules: List, String, Option, Math
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== List Operations ===")
|
||||
let nums = [1, 2, 3, 4, 5]
|
||||
@@ -11,7 +8,6 @@ fn main(): Unit with {Console} = {
|
||||
Console.print("Length: " + toString(List.length(nums)))
|
||||
Console.print("Reversed: " + toString(List.reverse(nums)))
|
||||
Console.print("Range 1-5: " + toString(List.range(1, 6)))
|
||||
|
||||
Console.print("")
|
||||
Console.print("=== String Operations ===")
|
||||
let text = " Hello, World! "
|
||||
@@ -22,7 +18,6 @@ fn main(): Unit with {Console} = {
|
||||
Console.print("Contains 'World': " + toString(String.contains(text, "World")))
|
||||
Console.print("Split by comma: " + toString(String.split("a,b,c", ",")))
|
||||
Console.print("Join with dash: " + String.join(["x", "y", "z"], "-"))
|
||||
|
||||
Console.print("")
|
||||
Console.print("=== Option Operations ===")
|
||||
let some_val = Some(42)
|
||||
@@ -31,7 +26,6 @@ fn main(): Unit with {Console} = {
|
||||
Console.print("None mapped: " + toString(Option.map(none_val, fn(x: Int): Int => x * 2)))
|
||||
Console.print("Some(42) getOrElse(0): " + toString(Option.getOrElse(some_val, 0)))
|
||||
Console.print("None getOrElse(0): " + toString(Option.getOrElse(none_val, 0)))
|
||||
|
||||
Console.print("")
|
||||
Console.print("=== Math Operations ===")
|
||||
Console.print("abs(-5): " + toString(Math.abs(-5)))
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
// State machine example using algebraic data types
|
||||
// Demonstrates pattern matching for state transitions
|
||||
//
|
||||
// Expected output:
|
||||
// Initial light: red
|
||||
// After transition: green
|
||||
// After two transitions: yellow
|
||||
// Door: Closed -> Open -> Closed -> Locked
|
||||
|
||||
// Traffic light state machine
|
||||
type TrafficLight =
|
||||
| Red
|
||||
| Yellow
|
||||
@@ -17,24 +7,23 @@ fn nextLight(light: TrafficLight): TrafficLight =
|
||||
match light {
|
||||
Red => Green,
|
||||
Green => Yellow,
|
||||
Yellow => Red
|
||||
}
|
||||
Yellow => Red,
|
||||
}
|
||||
|
||||
fn canGo(light: TrafficLight): Bool =
|
||||
match light {
|
||||
Green => true,
|
||||
Yellow => false,
|
||||
Red => false
|
||||
}
|
||||
Red => false,
|
||||
}
|
||||
|
||||
fn lightColor(light: TrafficLight): String =
|
||||
match light {
|
||||
Red => "red",
|
||||
Yellow => "yellow",
|
||||
Green => "green"
|
||||
}
|
||||
Green => "green",
|
||||
}
|
||||
|
||||
// Door state machine
|
||||
type DoorState =
|
||||
| Open
|
||||
| Closed
|
||||
@@ -52,27 +41,30 @@ fn applyAction(state: DoorState, action: DoorAction): DoorState =
|
||||
(Open, CloseDoor) => Closed,
|
||||
(Closed, LockDoor) => Locked,
|
||||
(Locked, UnlockDoor) => Closed,
|
||||
_ => state
|
||||
}
|
||||
_ => state,
|
||||
}
|
||||
|
||||
fn doorStateName(state: DoorState): String =
|
||||
match state {
|
||||
Open => "Open",
|
||||
Closed => "Closed",
|
||||
Locked => "Locked"
|
||||
}
|
||||
Locked => "Locked",
|
||||
}
|
||||
|
||||
// Test the state machines
|
||||
let light1 = Red
|
||||
|
||||
let light2 = nextLight(light1)
|
||||
|
||||
let light3 = nextLight(light2)
|
||||
|
||||
let door1 = Closed
|
||||
|
||||
let door2 = applyAction(door1, OpenDoor)
|
||||
|
||||
let door3 = applyAction(door2, CloseDoor)
|
||||
|
||||
let door4 = applyAction(door3, LockDoor)
|
||||
|
||||
// Print results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("Initial light: " + lightColor(light1))
|
||||
Console.print("After transition: " + lightColor(light2))
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
// Stress test for RC system with large lists
|
||||
// Tests FBIP optimization with single-owner chains
|
||||
|
||||
fn processChain(n: Int): Int = {
|
||||
// Single owner chain - FBIP should reuse lists
|
||||
let nums = List.range(1, n)
|
||||
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
||||
let filtered = List.filter(doubled, fn(x: Int): Bool => x > n)
|
||||
@@ -12,13 +8,10 @@ fn processChain(n: Int): Int = {
|
||||
|
||||
fn main(): Unit = {
|
||||
Console.print("=== RC Stress Test ===")
|
||||
|
||||
// Run multiple iterations of list operations
|
||||
let result1 = processChain(100)
|
||||
let result2 = processChain(200)
|
||||
let result3 = processChain(500)
|
||||
let result4 = processChain(1000)
|
||||
|
||||
Console.print("Completed 4 chains")
|
||||
Console.print("Sizes: 100, 200, 500, 1000")
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
// Stress test for RC system WITH shared references
|
||||
// Forces rc>1 path by keeping aliases
|
||||
|
||||
fn processWithAlias(n: Int): Int = {
|
||||
let nums = List.range(1, n)
|
||||
let alias = nums // This increments rc, forcing copy path
|
||||
let _len = List.length(alias) // Use the alias
|
||||
|
||||
// Now nums has rc>1, so map must allocate new
|
||||
let alias = nums
|
||||
let _len = List.length(alias)
|
||||
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
||||
let filtered = List.filter(doubled, fn(x: Int): Bool => x > n)
|
||||
let reversed = List.reverse(filtered)
|
||||
@@ -15,12 +10,9 @@ fn processWithAlias(n: Int): Int = {
|
||||
|
||||
fn main(): Unit = {
|
||||
Console.print("=== RC Stress Test (Shared Refs) ===")
|
||||
|
||||
// Run multiple iterations with shared references
|
||||
let result1 = processWithAlias(100)
|
||||
let result2 = processWithAlias(200)
|
||||
let result3 = processWithAlias(500)
|
||||
let result4 = processWithAlias(1000)
|
||||
|
||||
Console.print("Completed 4 chains with shared refs")
|
||||
}
|
||||
|
||||
@@ -1,45 +1,25 @@
|
||||
// Demonstrating tail call optimization (TCO) in Lux
|
||||
// TCO allows recursive functions to run in constant stack space
|
||||
//
|
||||
// Expected output:
|
||||
// factorial(20) = 2432902008176640000
|
||||
// fib(30) = 832040
|
||||
// sumTo(1000) = 500500
|
||||
// countdown(10000) completed
|
||||
|
||||
// Factorial with accumulator - tail recursive
|
||||
fn factorialTCO(n: Int, acc: Int): Int =
|
||||
if n <= 1 then acc
|
||||
else factorialTCO(n - 1, n * acc)
|
||||
fn factorialTCO(n: Int, acc: Int): Int = if n <= 1 then acc else factorialTCO(n - 1, n * acc)
|
||||
|
||||
fn factorial(n: Int): Int = factorialTCO(n, 1)
|
||||
|
||||
// Fibonacci with accumulator - tail recursive
|
||||
fn fibTCO(n: Int, a: Int, b: Int): Int =
|
||||
if n <= 0 then a
|
||||
else fibTCO(n - 1, b, a + b)
|
||||
fn fibTCO(n: Int, a: Int, b: Int): Int = if n <= 0 then a else fibTCO(n - 1, b, a + b)
|
||||
|
||||
fn fib(n: Int): Int = fibTCO(n, 0, 1)
|
||||
|
||||
// Count down - simple tail recursion
|
||||
fn countdown(n: Int): Int =
|
||||
if n <= 0 then 0
|
||||
else countdown(n - 1)
|
||||
fn countdown(n: Int): Int = if n <= 0 then 0 else countdown(n - 1)
|
||||
|
||||
// Sum with accumulator - tail recursive
|
||||
fn sumToTCO(n: Int, acc: Int): Int =
|
||||
if n <= 0 then acc
|
||||
else sumToTCO(n - 1, acc + n)
|
||||
fn sumToTCO(n: Int, acc: Int): Int = if n <= 0 then acc else sumToTCO(n - 1, acc + n)
|
||||
|
||||
fn sumTo(n: Int): Int = sumToTCO(n, 0)
|
||||
|
||||
// Test the functions
|
||||
let fact20 = factorial(20)
|
||||
|
||||
let fib30 = fib(30)
|
||||
|
||||
let sum1000 = sumTo(1000)
|
||||
|
||||
let countResult = countdown(10000)
|
||||
|
||||
// Print results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("factorial(20) = " + toString(fact20))
|
||||
Console.print("fib(30) = " + toString(fib30))
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
// This test shows FBIP optimization by comparing allocation counts
|
||||
// With FBIP (rc=1): lists are reused in-place
|
||||
// Without FBIP (rc>1): new lists are allocated
|
||||
|
||||
fn main(): Unit = {
|
||||
Console.print("=== FBIP Allocation Test ===")
|
||||
|
||||
// Case 1: Single owner (FBIP active) - should reuse list
|
||||
let a = List.range(1, 100)
|
||||
let b = List.map(a, fn(x: Int): Int => x * 2)
|
||||
let c = List.filter(b, fn(x: Int): Bool => x > 50)
|
||||
let d = List.reverse(c)
|
||||
Console.print("Single owner chain done")
|
||||
|
||||
// The allocation count will show FBIP is working
|
||||
// if allocations are low relative to operations performed
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
fn main(): Unit = {
|
||||
// Test FBIP without string operations
|
||||
let nums = [1, 2, 3, 4, 5]
|
||||
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
||||
let filtered = List.filter(doubled, fn(x: Int): Bool => x > 4)
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// List Operations Test Suite
|
||||
// Run with: lux test examples/test_lists.lux
|
||||
|
||||
fn test_list_length(): Unit with {Test} = {
|
||||
Test.assertEqual(0, List.length([]))
|
||||
Test.assertEqual(1, List.length([1]))
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// Math Test Suite
|
||||
// Run with: lux test examples/test_math.lux
|
||||
|
||||
fn test_addition(): Unit with {Test} = {
|
||||
Test.assertEqual(4, 2 + 2)
|
||||
Test.assertEqual(0, 0 + 0)
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
// Test demonstrating ownership transfer with aliases
|
||||
// The ownership transfer optimization ensures FBIP still works
|
||||
// even when variables are aliased, because ownership is transferred
|
||||
// rather than reference count being incremented.
|
||||
|
||||
fn main(): Unit = {
|
||||
Console.print("=== Ownership Transfer Test ===")
|
||||
|
||||
let a = List.range(1, 100)
|
||||
// Ownership transfers from 'a' to 'alias', keeping rc=1
|
||||
let alias = a
|
||||
let len1 = List.length(alias)
|
||||
|
||||
// Since ownership transferred, 'a' still has rc=1
|
||||
// FBIP can still optimize map/filter/reverse
|
||||
let b = List.map(a, fn(x: Int): Int => x * 2)
|
||||
let c = List.filter(b, fn(x: Int): Bool => x > 50)
|
||||
let d = List.reverse(c)
|
||||
|
||||
Console.print("Ownership transfer chain done")
|
||||
}
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
fn main(): Unit = {
|
||||
Console.print("=== Allocation Comparison ===")
|
||||
|
||||
// FBIP path (rc=1): list is reused
|
||||
Console.print("Test 1: FBIP path")
|
||||
let a1 = List.range(1, 50)
|
||||
let b1 = List.map(a1, fn(x: Int): Int => x * 2)
|
||||
let c1 = List.reverse(b1)
|
||||
Console.print("FBIP done")
|
||||
|
||||
// To show non-FBIP, we need concat which doesn't have FBIP
|
||||
Console.print("Test 2: Non-FBIP path (concat)")
|
||||
let x = List.range(1, 25)
|
||||
let y = List.range(26, 50)
|
||||
let z = List.concat(x, y) // concat always allocates new
|
||||
let z = List.concat(x, y)
|
||||
Console.print("Concat done")
|
||||
}
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
// Demonstrating type classes (traits) in Lux
|
||||
//
|
||||
// Expected output:
|
||||
// RGB color: rgb(255,128,0)
|
||||
// Red color: red
|
||||
// Green color: green
|
||||
|
||||
// Define a simple Printable trait
|
||||
trait Printable {
|
||||
fn format(value: Int): String
|
||||
}
|
||||
|
||||
// Implement Printable
|
||||
impl Printable for Int {
|
||||
fn format(value: Int): String = "Number: " + toString(value)
|
||||
}
|
||||
|
||||
// A Color type with pattern matching
|
||||
type Color =
|
||||
| Red
|
||||
| Green
|
||||
@@ -27,15 +17,15 @@ fn colorName(c: Color): String =
|
||||
Red => "red",
|
||||
Green => "green",
|
||||
Blue => "blue",
|
||||
RGB(r, g, b) => "rgb(" + toString(r) + "," + toString(g) + "," + toString(b) + ")"
|
||||
}
|
||||
RGB(r, g, b) => "rgb(" + toString(r) + "," + toString(g) + "," + toString(b) + ")",
|
||||
}
|
||||
|
||||
// Test
|
||||
let myColor = RGB(255, 128, 0)
|
||||
|
||||
let redColor = Red
|
||||
|
||||
let greenColor = Green
|
||||
|
||||
// Print results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("RGB color: " + colorName(myColor))
|
||||
Console.print("Red color: " + colorName(redColor))
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
// Demonstrating Schema Evolution in Lux
|
||||
//
|
||||
// Lux provides versioned types to help manage data evolution over time.
|
||||
// The Schema module provides functions for creating and migrating versioned values.
|
||||
//
|
||||
// Expected output:
|
||||
// Created user v1: Alice (age unknown)
|
||||
// User version: 1
|
||||
// Migrated to v2: Alice (age unknown)
|
||||
// User version after migration: 2
|
||||
|
||||
// Create a versioned User value at v1
|
||||
fn createUserV1(name: String): Unit with {Console} = {
|
||||
let user = Schema.versioned("User", 1, { name: name })
|
||||
let version = Schema.getVersion(user)
|
||||
@@ -17,7 +5,6 @@ fn createUserV1(name: String): Unit with {Console} = {
|
||||
Console.print("User version: " + toString(version))
|
||||
}
|
||||
|
||||
// Migrate a user to v2
|
||||
fn migrateUserToV2(name: String): Unit with {Console} = {
|
||||
let userV1 = Schema.versioned("User", 1, { name: name })
|
||||
let userV2 = Schema.migrate(userV1, 2)
|
||||
@@ -26,7 +13,6 @@ fn migrateUserToV2(name: String): Unit with {Console} = {
|
||||
Console.print("User version after migration: " + toString(newVersion))
|
||||
}
|
||||
|
||||
// Main
|
||||
fn main(): Unit with {Console} = {
|
||||
createUserV1("Alice")
|
||||
migrateUserToV2("Alice")
|
||||
|
||||
@@ -1,54 +1,30 @@
|
||||
// Simple Counter for Browser
|
||||
// Compile with: lux compile examples/web/counter.lux --target js -o examples/web/counter.js
|
||||
type Model =
|
||||
| Counter(Int)
|
||||
|
||||
// ============================================================================
|
||||
// Model
|
||||
// ============================================================================
|
||||
|
||||
type Model = | Counter(Int)
|
||||
|
||||
fn getCount(m: Model): Int = match m { Counter(n) => n }
|
||||
fn getCount(m: Model): Int =
|
||||
match m {
|
||||
Counter(n) => n,
|
||||
}
|
||||
|
||||
fn init(): Model = Counter(0)
|
||||
|
||||
// ============================================================================
|
||||
// Messages
|
||||
// ============================================================================
|
||||
|
||||
type Msg = | Increment | Decrement | Reset
|
||||
|
||||
// ============================================================================
|
||||
// Update
|
||||
// ============================================================================
|
||||
type Msg =
|
||||
| Increment
|
||||
| Decrement
|
||||
| Reset
|
||||
|
||||
fn update(model: Model, msg: Msg): Model =
|
||||
match msg {
|
||||
Increment => Counter(getCount(model) + 1),
|
||||
Decrement => Counter(getCount(model) - 1),
|
||||
Reset => Counter(0)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// View - Returns HTML string for simplicity
|
||||
// ============================================================================
|
||||
Reset => Counter(0),
|
||||
}
|
||||
|
||||
fn view(model: Model): String = {
|
||||
let count = getCount(model)
|
||||
"<div class=\"counter\">" +
|
||||
"<h1>Lux Counter</h1>" +
|
||||
"<div class=\"display\">" + toString(count) + "</div>" +
|
||||
"<div class=\"buttons\">" +
|
||||
"<button onclick=\"dispatch('Decrement')\">-</button>" +
|
||||
"<button onclick=\"dispatch('Reset')\">Reset</button>" +
|
||||
"<button onclick=\"dispatch('Increment')\">+</button>" +
|
||||
"</div>" +
|
||||
"</div>"
|
||||
"<div class=\"counter\">" + "<h1>Lux Counter</h1>" + "<div class=\"display\">" + toString(count) + "</div>" + "<div class=\"buttons\">" + "<button onclick=\"dispatch('Decrement')\">-</button>" + "<button onclick=\"dispatch('Reset')\">Reset</button>" + "<button onclick=\"dispatch('Increment')\">+</button>" + "</div>" + "</div>"
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Export for browser runtime
|
||||
// ============================================================================
|
||||
|
||||
fn luxInit(): Model = init()
|
||||
|
||||
fn luxUpdate(model: Model, msgName: String): Model =
|
||||
@@ -56,7 +32,7 @@ fn luxUpdate(model: Model, msgName: String): Model =
|
||||
"Increment" => update(model, Increment),
|
||||
"Decrement" => update(model, Decrement),
|
||||
"Reset" => update(model, Reset),
|
||||
_ => model
|
||||
}
|
||||
_ => model,
|
||||
}
|
||||
|
||||
fn luxView(model: Model): String = view(model)
|
||||
|
||||
46
flake.nix
46
flake.nix
@@ -14,6 +14,7 @@
|
||||
pkgs = import nixpkgs { inherit system overlays; };
|
||||
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
||||
extensions = [ "rust-src" "rust-analyzer" ];
|
||||
targets = [ "x86_64-unknown-linux-musl" ];
|
||||
};
|
||||
in
|
||||
{
|
||||
@@ -22,8 +23,8 @@
|
||||
rustToolchain
|
||||
cargo-watch
|
||||
cargo-edit
|
||||
pkg-config
|
||||
openssl
|
||||
# Static builds
|
||||
pkgsStatic.stdenv.cc
|
||||
# Benchmark tools
|
||||
hyperfine
|
||||
poop
|
||||
@@ -43,7 +44,7 @@
|
||||
printf "\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.9\n"
|
||||
printf "\n"
|
||||
printf " Functional language with first-class effects\n"
|
||||
printf "\n"
|
||||
@@ -61,18 +62,47 @@
|
||||
|
||||
packages.default = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "lux";
|
||||
version = "0.1.0";
|
||||
version = "0.1.9";
|
||||
src = ./.;
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
|
||||
nativeBuildInputs = [ pkgs.pkg-config ];
|
||||
buildInputs = [ pkgs.openssl ];
|
||||
|
||||
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.9";
|
||||
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 = {
|
||||
# Release automation
|
||||
release = {
|
||||
type = "app";
|
||||
program = toString (pkgs.writeShellScript "lux-release" ''
|
||||
exec ${self}/scripts/release.sh "$@"
|
||||
'');
|
||||
};
|
||||
|
||||
# Benchmark scripts
|
||||
# Run hyperfine benchmark comparison
|
||||
bench = {
|
||||
type = "app";
|
||||
|
||||
225
projects/lux-compiler/ast.lux
Normal file
225
projects/lux-compiler/ast.lux
Normal file
@@ -0,0 +1,225 @@
|
||||
// Lux AST — Self-hosted Abstract Syntax Tree definitions
|
||||
//
|
||||
// Direct translation of src/ast.rs into Lux ADTs.
|
||||
// These types represent the parsed structure of a Lux program.
|
||||
//
|
||||
// Naming conventions to avoid collisions:
|
||||
// Ex = Expr variant, Pat = Pattern, Te = TypeExpr
|
||||
// Td = TypeDef, Vf = VariantFields, Op = Operator
|
||||
// Decl = Declaration, St = Statement
|
||||
|
||||
// === Source Location ===
|
||||
|
||||
type Span = | Span(Int, Int)
|
||||
|
||||
// === Identifiers ===
|
||||
|
||||
type Ident = | Ident(String, Span)
|
||||
|
||||
// === Visibility ===
|
||||
|
||||
type Visibility = | Public | Private
|
||||
|
||||
// === Schema Evolution ===
|
||||
|
||||
type Version = | Version(Int, Span)
|
||||
|
||||
type VersionConstraint =
|
||||
| VcExact(Version)
|
||||
| VcAtLeast(Version)
|
||||
| VcLatest(Span)
|
||||
|
||||
// === Behavioral Types ===
|
||||
|
||||
type BehavioralProperty =
|
||||
| BpPure
|
||||
| BpTotal
|
||||
| BpIdempotent
|
||||
| BpDeterministic
|
||||
| BpCommutative
|
||||
|
||||
// === Trait Bound (needed before WhereClause) ===
|
||||
|
||||
type TraitBound = | TraitBound(Ident, List<TypeExpr>, Span)
|
||||
|
||||
// === Trait Constraint (needed before WhereClause) ===
|
||||
|
||||
type TraitConstraint = | TraitConstraint(Ident, List<TraitBound>, Span)
|
||||
|
||||
// === Where Clauses ===
|
||||
|
||||
type WhereClause =
|
||||
| WcProperty(Ident, BehavioralProperty, Span)
|
||||
| WcResult(Expr, Span)
|
||||
| WcTrait(TraitConstraint)
|
||||
|
||||
// === Module Path ===
|
||||
|
||||
type ModulePath = | ModulePath(List<Ident>, Span)
|
||||
|
||||
// === Import ===
|
||||
|
||||
// path, alias, items, wildcard, span
|
||||
type ImportDecl = | ImportDecl(ModulePath, Option<Ident>, Option<List<Ident>>, Bool, Span)
|
||||
|
||||
// === Program ===
|
||||
|
||||
type Program = | Program(List<ImportDecl>, List<Declaration>)
|
||||
|
||||
// === Declarations ===
|
||||
|
||||
type Declaration =
|
||||
| DeclFunction(FunctionDecl)
|
||||
| DeclEffect(EffectDecl)
|
||||
| DeclType(TypeDecl)
|
||||
| DeclHandler(HandlerDecl)
|
||||
| DeclLet(LetDecl)
|
||||
| DeclTrait(TraitDecl)
|
||||
| DeclImpl(ImplDecl)
|
||||
|
||||
// === Parameter ===
|
||||
|
||||
type Parameter = | Parameter(Ident, TypeExpr, Span)
|
||||
|
||||
// === Effect Operation ===
|
||||
|
||||
type EffectOp = | EffectOp(Ident, List<Parameter>, TypeExpr, Span)
|
||||
|
||||
// === Record Field ===
|
||||
|
||||
type RecordField = | RecordField(Ident, TypeExpr, Span)
|
||||
|
||||
// === Variant Fields ===
|
||||
|
||||
type VariantFields =
|
||||
| VfUnit
|
||||
| VfTuple(List<TypeExpr>)
|
||||
| VfRecord(List<RecordField>)
|
||||
|
||||
// === Variant ===
|
||||
|
||||
type Variant = | Variant(Ident, VariantFields, Span)
|
||||
|
||||
// === Migration ===
|
||||
|
||||
type Migration = | Migration(Version, Expr, Span)
|
||||
|
||||
// === Handler Impl ===
|
||||
|
||||
// op_name, params, resume, body, span
|
||||
type HandlerImpl = | HandlerImpl(Ident, List<Ident>, Option<Ident>, Expr, Span)
|
||||
|
||||
// === Impl Method ===
|
||||
|
||||
// name, params, return_type, body, span
|
||||
type ImplMethod = | ImplMethod(Ident, List<Parameter>, Option<TypeExpr>, Expr, Span)
|
||||
|
||||
// === Trait Method ===
|
||||
|
||||
// name, type_params, params, return_type, default_impl, span
|
||||
type TraitMethod = | TraitMethod(Ident, List<Ident>, List<Parameter>, TypeExpr, Option<Expr>, Span)
|
||||
|
||||
// === Type Expressions ===
|
||||
|
||||
type TypeExpr =
|
||||
| TeNamed(Ident)
|
||||
| TeApp(TypeExpr, List<TypeExpr>)
|
||||
| TeFunction(List<TypeExpr>, TypeExpr, List<Ident>)
|
||||
| TeTuple(List<TypeExpr>)
|
||||
| TeRecord(List<RecordField>)
|
||||
| TeUnit
|
||||
| TeVersioned(TypeExpr, VersionConstraint)
|
||||
|
||||
// === Literal ===
|
||||
|
||||
type LiteralKind =
|
||||
| LitInt(Int)
|
||||
| LitFloat(String)
|
||||
| LitString(String)
|
||||
| LitChar(Char)
|
||||
| LitBool(Bool)
|
||||
| LitUnit
|
||||
|
||||
type Literal = | Literal(LiteralKind, Span)
|
||||
|
||||
// === Binary Operators ===
|
||||
|
||||
type BinaryOp =
|
||||
| OpAdd | OpSub | OpMul | OpDiv | OpMod
|
||||
| OpEq | OpNe | OpLt | OpLe | OpGt | OpGe
|
||||
| OpAnd | OpOr
|
||||
| OpPipe | OpConcat
|
||||
|
||||
// === Unary Operators ===
|
||||
|
||||
type UnaryOp = | OpNeg | OpNot
|
||||
|
||||
// === Statements ===
|
||||
|
||||
type Statement =
|
||||
| StExpr(Expr)
|
||||
| StLet(Ident, Option<TypeExpr>, Expr, Span)
|
||||
|
||||
// === Match Arms ===
|
||||
|
||||
type MatchArm = | MatchArm(Pattern, Option<Expr>, Expr, Span)
|
||||
|
||||
// === Patterns ===
|
||||
|
||||
type Pattern =
|
||||
| PatWildcard(Span)
|
||||
| PatVar(Ident)
|
||||
| PatLiteral(Literal)
|
||||
| PatConstructor(Ident, List<Pattern>, Span)
|
||||
| PatRecord(List<(Ident, Pattern)>, Span)
|
||||
| PatTuple(List<Pattern>, Span)
|
||||
|
||||
// === Function Declaration ===
|
||||
// visibility, doc, name, type_params, params, return_type, effects, properties, where_clauses, body, span
|
||||
type FunctionDecl = | FunctionDecl(Visibility, Option<String>, Ident, List<Ident>, List<Parameter>, TypeExpr, List<Ident>, List<BehavioralProperty>, List<WhereClause>, Expr, Span)
|
||||
|
||||
// === Effect Declaration ===
|
||||
// doc, name, type_params, operations, span
|
||||
type EffectDecl = | EffectDecl(Option<String>, Ident, List<Ident>, List<EffectOp>, Span)
|
||||
|
||||
// === Type Declaration ===
|
||||
// visibility, doc, name, type_params, version, definition, migrations, span
|
||||
type TypeDecl = | TypeDecl(Visibility, Option<String>, Ident, List<Ident>, Option<Version>, TypeDef, List<Migration>, Span)
|
||||
|
||||
// === Handler Declaration ===
|
||||
// name, params, effect, implementations, span
|
||||
type HandlerDecl = | HandlerDecl(Ident, List<Parameter>, Ident, List<HandlerImpl>, Span)
|
||||
|
||||
// === Let Declaration ===
|
||||
// visibility, doc, name, typ, value, span
|
||||
type LetDecl = | LetDecl(Visibility, Option<String>, Ident, Option<TypeExpr>, Expr, Span)
|
||||
|
||||
// === Trait Declaration ===
|
||||
// visibility, doc, name, type_params, super_traits, methods, span
|
||||
type TraitDecl = | TraitDecl(Visibility, Option<String>, Ident, List<Ident>, List<TraitBound>, List<TraitMethod>, Span)
|
||||
|
||||
// === Impl Declaration ===
|
||||
// type_params, constraints, trait_name, trait_args, target_type, methods, span
|
||||
type ImplDecl = | ImplDecl(List<Ident>, List<TraitConstraint>, Ident, List<TypeExpr>, TypeExpr, List<ImplMethod>, Span)
|
||||
|
||||
// === Expressions ===
|
||||
|
||||
type Expr =
|
||||
| ExLiteral(Literal)
|
||||
| ExVar(Ident)
|
||||
| ExBinaryOp(BinaryOp, Expr, Expr, Span)
|
||||
| ExUnaryOp(UnaryOp, Expr, Span)
|
||||
| ExCall(Expr, List<Expr>, Span)
|
||||
| ExEffectOp(Ident, Ident, List<Expr>, Span)
|
||||
| ExField(Expr, Ident, Span)
|
||||
| ExTupleIndex(Expr, Int, Span)
|
||||
| ExLambda(List<Parameter>, Option<TypeExpr>, List<Ident>, Expr, Span)
|
||||
| ExLet(Ident, Option<TypeExpr>, Expr, Expr, Span)
|
||||
| ExIf(Expr, Expr, Expr, Span)
|
||||
| ExMatch(Expr, List<MatchArm>, Span)
|
||||
| ExBlock(List<Statement>, Expr, Span)
|
||||
| ExRecord(Option<Expr>, List<(Ident, Expr)>, Span)
|
||||
| ExTuple(List<Expr>, Span)
|
||||
| ExList(List<Expr>, Span)
|
||||
| ExRun(Expr, List<(Ident, Expr)>, Span)
|
||||
| ExResume(Expr, Span)
|
||||
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"
|
||||
211
scripts/validate.sh
Executable file
211
scripts/validate.sh
Executable file
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Lux Full Validation Script
|
||||
# Runs all checks: Rust tests, package tests, type checking, example compilation.
|
||||
# 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"
|
||||
PROJECTS_DIR="$(pwd)/projects"
|
||||
EXAMPLES_DIR="$(pwd)/examples"
|
||||
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>/dev/null; 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>/dev/null; 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
|
||||
|
||||
# --- Project checks ---
|
||||
for proj_dir in "$PROJECTS_DIR"/*/; do
|
||||
proj=$(basename "$proj_dir")
|
||||
if [ -f "$proj_dir/main.lux" ]; then
|
||||
step "lux check (project: $proj)"
|
||||
OUTPUT=$("$LUX" check "$proj_dir/main.lux" 2>&1 || true)
|
||||
if echo "$OUTPUT" | grep -qi "error"; then fail; else ok; fi
|
||||
fi
|
||||
# Check any standalone .lux files in the project
|
||||
for lux_file in "$proj_dir"/*.lux; do
|
||||
[ -f "$lux_file" ] || continue
|
||||
fname=$(basename "$lux_file")
|
||||
[ "$fname" = "main.lux" ] && continue
|
||||
step "lux check (project: $proj/$fname)"
|
||||
OUTPUT=$("$LUX" check "$lux_file" 2>&1 || true)
|
||||
if echo "$OUTPUT" | grep -qi "error"; then fail; else ok; fi
|
||||
done
|
||||
done
|
||||
|
||||
# === Compilation & Interpreter Checks ===
|
||||
|
||||
# --- Interpreter: examples ---
|
||||
# Skip: http_api, http, http_router, http_server (network), postgres_demo (db),
|
||||
# random, property_testing (Random effect), shell (Process), json (File I/O),
|
||||
# file_io (File I/O), test_math, test_lists (Test effect), stress_shared_rc,
|
||||
# test_rc_comparison (internal tests), modules/* (need cwd)
|
||||
INTERP_SKIP="http_api http http_router http_server postgres_demo random property_testing shell json file_io test_math test_lists stress_shared_rc test_rc_comparison"
|
||||
for f in "$EXAMPLES_DIR"/*.lux; do
|
||||
name=$(basename "$f" .lux)
|
||||
skip=false
|
||||
for s in $INTERP_SKIP; do [ "$name" = "$s" ] && skip=true; done
|
||||
$skip && continue
|
||||
step "interpreter (examples/$name)"
|
||||
if timeout 10 "$LUX" "$f" >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- Interpreter: examples/standard ---
|
||||
# Skip: guessing_game (reads stdin)
|
||||
for f in "$EXAMPLES_DIR"/standard/*.lux; do
|
||||
[ -f "$f" ] || continue
|
||||
name=$(basename "$f" .lux)
|
||||
[ "$name" = "guessing_game" ] && continue
|
||||
step "interpreter (standard/$name)"
|
||||
if timeout 10 "$LUX" "$f" >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- Interpreter: examples/showcase ---
|
||||
# Skip: task_manager (parse error in current version)
|
||||
for f in "$EXAMPLES_DIR"/showcase/*.lux; do
|
||||
[ -f "$f" ] || continue
|
||||
name=$(basename "$f" .lux)
|
||||
[ "$name" = "task_manager" ] && continue
|
||||
step "interpreter (showcase/$name)"
|
||||
if timeout 10 "$LUX" "$f" >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- Interpreter: projects ---
|
||||
# Skip: guessing-game (Random), rest-api (HttpServer)
|
||||
PROJ_INTERP_SKIP="guessing-game rest-api"
|
||||
for proj_dir in "$PROJECTS_DIR"/*/; do
|
||||
proj=$(basename "$proj_dir")
|
||||
[ -f "$proj_dir/main.lux" ] || continue
|
||||
skip=false
|
||||
for s in $PROJ_INTERP_SKIP; do [ "$proj" = "$s" ] && skip=true; done
|
||||
$skip && continue
|
||||
step "interpreter (project: $proj)"
|
||||
if timeout 10 "$LUX" "$proj_dir/main.lux" >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- JS compilation: examples ---
|
||||
# Skip files that fail JS compilation (unsupported features)
|
||||
JS_SKIP="http_api http http_router postgres_demo property_testing json test_lists test_rc_comparison"
|
||||
for f in "$EXAMPLES_DIR"/*.lux; do
|
||||
name=$(basename "$f" .lux)
|
||||
skip=false
|
||||
for s in $JS_SKIP; do [ "$name" = "$s" ] && skip=true; done
|
||||
$skip && continue
|
||||
step "compile JS (examples/$name)"
|
||||
if "$LUX" compile "$f" --target js -o /tmp/lux_validate.js >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- JS compilation: examples/standard ---
|
||||
# Skip: stdlib_demo (uses String.toUpper not in JS backend)
|
||||
for f in "$EXAMPLES_DIR"/standard/*.lux; do
|
||||
[ -f "$f" ] || continue
|
||||
name=$(basename "$f" .lux)
|
||||
[ "$name" = "stdlib_demo" ] && continue
|
||||
step "compile JS (standard/$name)"
|
||||
if "$LUX" compile "$f" --target js -o /tmp/lux_validate.js >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- JS compilation: examples/showcase ---
|
||||
# Skip: task_manager (unsupported features)
|
||||
for f in "$EXAMPLES_DIR"/showcase/*.lux; do
|
||||
[ -f "$f" ] || continue
|
||||
name=$(basename "$f" .lux)
|
||||
[ "$name" = "task_manager" ] && continue
|
||||
step "compile JS (showcase/$name)"
|
||||
if "$LUX" compile "$f" --target js -o /tmp/lux_validate.js >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- JS compilation: projects ---
|
||||
# Skip: json-parser, rest-api (unsupported features)
|
||||
JS_PROJ_SKIP="json-parser rest-api"
|
||||
for proj_dir in "$PROJECTS_DIR"/*/; do
|
||||
proj=$(basename "$proj_dir")
|
||||
[ -f "$proj_dir/main.lux" ] || continue
|
||||
skip=false
|
||||
for s in $JS_PROJ_SKIP; do [ "$proj" = "$s" ] && skip=true; done
|
||||
$skip && continue
|
||||
step "compile JS (project: $proj)"
|
||||
if "$LUX" compile "$proj_dir/main.lux" --target js -o /tmp/lux_validate.js >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- C compilation: examples ---
|
||||
# Only compile examples known to work with C backend
|
||||
C_EXAMPLES="hello factorial pipelines tailcall jit_test"
|
||||
for name in $C_EXAMPLES; do
|
||||
f="$EXAMPLES_DIR/$name.lux"
|
||||
[ -f "$f" ] || continue
|
||||
step "compile C (examples/$name)"
|
||||
if "$LUX" compile "$f" -o /tmp/lux_validate_bin >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- C compilation: examples/standard ---
|
||||
C_STD_EXAMPLES="hello_world factorial fizzbuzz primes guessing_game"
|
||||
for name in $C_STD_EXAMPLES; do
|
||||
f="$EXAMPLES_DIR/standard/$name.lux"
|
||||
[ -f "$f" ] || continue
|
||||
step "compile C (standard/$name)"
|
||||
if "$LUX" compile "$f" -o /tmp/lux_validate_bin >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- Cleanup ---
|
||||
rm -f /tmp/lux_validate.js /tmp/lux_validate_bin
|
||||
|
||||
# --- 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
|
||||
31
src/ast.rs
31
src/ast.rs
@@ -221,6 +221,8 @@ pub enum Declaration {
|
||||
Trait(TraitDecl),
|
||||
/// Trait implementation: impl Trait for Type { ... }
|
||||
Impl(ImplDecl),
|
||||
/// Extern function declaration (FFI): extern fn name(params): ReturnType
|
||||
ExternFn(ExternFnDecl),
|
||||
}
|
||||
|
||||
/// Function declaration
|
||||
@@ -428,6 +430,21 @@ pub struct ImplMethod {
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
/// Extern function declaration (FFI)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExternFnDecl {
|
||||
pub visibility: Visibility,
|
||||
/// Documentation comment
|
||||
pub doc: Option<String>,
|
||||
pub name: Ident,
|
||||
pub type_params: Vec<Ident>,
|
||||
pub params: Vec<Parameter>,
|
||||
pub return_type: TypeExpr,
|
||||
/// Optional JS name override: extern fn foo(...): T = "jsFoo"
|
||||
pub js_name: Option<String>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
/// Type expressions
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TypeExpr {
|
||||
@@ -499,6 +516,12 @@ pub enum Expr {
|
||||
field: Ident,
|
||||
span: Span,
|
||||
},
|
||||
/// Tuple index access: tuple.0, tuple.1
|
||||
TupleIndex {
|
||||
object: Box<Expr>,
|
||||
index: usize,
|
||||
span: Span,
|
||||
},
|
||||
/// Lambda: fn(x, y) => x + y or fn(x: Int): Int => x + 1
|
||||
Lambda {
|
||||
params: Vec<Parameter>,
|
||||
@@ -535,7 +558,9 @@ pub enum Expr {
|
||||
span: Span,
|
||||
},
|
||||
/// Record literal: { name: "Alice", age: 30 }
|
||||
/// With optional spread: { ...base, name: "Bob" }
|
||||
Record {
|
||||
spread: Option<Box<Expr>>,
|
||||
fields: Vec<(Ident, Expr)>,
|
||||
span: Span,
|
||||
},
|
||||
@@ -563,6 +588,7 @@ impl Expr {
|
||||
Expr::Call { span, .. } => *span,
|
||||
Expr::EffectOp { span, .. } => *span,
|
||||
Expr::Field { span, .. } => *span,
|
||||
Expr::TupleIndex { span, .. } => *span,
|
||||
Expr::Lambda { span, .. } => *span,
|
||||
Expr::Let { span, .. } => *span,
|
||||
Expr::If { span, .. } => *span,
|
||||
@@ -615,6 +641,7 @@ pub enum BinaryOp {
|
||||
Or,
|
||||
// Other
|
||||
Pipe, // |>
|
||||
Concat, // ++
|
||||
}
|
||||
|
||||
impl fmt::Display for BinaryOp {
|
||||
@@ -634,6 +661,7 @@ impl fmt::Display for BinaryOp {
|
||||
BinaryOp::And => write!(f, "&&"),
|
||||
BinaryOp::Or => write!(f, "||"),
|
||||
BinaryOp::Pipe => write!(f, "|>"),
|
||||
BinaryOp::Concat => write!(f, "++"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -686,8 +714,9 @@ pub enum Pattern {
|
||||
Var(Ident),
|
||||
/// Literal: 42, "hello", true
|
||||
Literal(Literal),
|
||||
/// Constructor: Some(x), None, Ok(v)
|
||||
/// Constructor: Some(x), None, Ok(v), module.Constructor(x)
|
||||
Constructor {
|
||||
module: Option<Ident>,
|
||||
name: Ident,
|
||||
fields: Vec<Pattern>,
|
||||
span: Span,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -69,6 +69,10 @@ pub struct JsBackend {
|
||||
has_handlers: bool,
|
||||
/// Variable substitutions for let binding
|
||||
var_substitutions: HashMap<String, String>,
|
||||
/// Effects actually used in the program (for tree-shaking runtime)
|
||||
used_effects: HashSet<String>,
|
||||
/// Extern function names mapped to their JS names
|
||||
extern_fns: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl JsBackend {
|
||||
@@ -90,6 +94,8 @@ impl JsBackend {
|
||||
effectful_functions: HashSet::new(),
|
||||
has_handlers: false,
|
||||
var_substitutions: HashMap::new(),
|
||||
used_effects: HashSet::new(),
|
||||
extern_fns: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,9 +103,6 @@ impl JsBackend {
|
||||
pub fn generate(&mut self, program: &Program) -> Result<String, JsGenError> {
|
||||
self.output.clear();
|
||||
|
||||
// Emit runtime helpers
|
||||
self.emit_runtime();
|
||||
|
||||
// First pass: collect all function names, types, and effects
|
||||
for decl in &program.declarations {
|
||||
match decl {
|
||||
@@ -112,10 +115,24 @@ impl JsBackend {
|
||||
Declaration::Type(t) => {
|
||||
self.collect_type(t)?;
|
||||
}
|
||||
Declaration::ExternFn(ext) => {
|
||||
let js_name = ext
|
||||
.js_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| ext.name.name.clone());
|
||||
self.extern_fns.insert(ext.name.name.clone(), js_name);
|
||||
self.functions.insert(ext.name.name.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect used effects for tree-shaking
|
||||
self.collect_used_effects(program);
|
||||
|
||||
// Emit runtime helpers (tree-shaken based on used effects)
|
||||
self.emit_runtime();
|
||||
|
||||
// Emit type constructors
|
||||
for decl in &program.declarations {
|
||||
if let Declaration::Type(t) = decl {
|
||||
@@ -163,32 +180,181 @@ impl JsBackend {
|
||||
Ok(self.output.clone())
|
||||
}
|
||||
|
||||
/// Emit the minimal Lux runtime
|
||||
/// Collect all effects used in the program for runtime tree-shaking
|
||||
fn collect_used_effects(&mut self, program: &Program) {
|
||||
for decl in &program.declarations {
|
||||
match decl {
|
||||
Declaration::Function(f) => {
|
||||
for effect in &f.effects {
|
||||
self.used_effects.insert(effect.name.clone());
|
||||
}
|
||||
self.collect_effects_from_expr(&f.body);
|
||||
}
|
||||
Declaration::Let(l) => {
|
||||
self.collect_effects_from_expr(&l.value);
|
||||
}
|
||||
Declaration::Handler(h) => {
|
||||
self.used_effects.insert(h.effect.name.clone());
|
||||
for imp in &h.implementations {
|
||||
self.collect_effects_from_expr(&imp.body);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively collect effect names from an expression
|
||||
fn collect_effects_from_expr(&mut self, expr: &Expr) {
|
||||
match expr {
|
||||
Expr::EffectOp { effect, args, .. } => {
|
||||
self.used_effects.insert(effect.name.clone());
|
||||
for arg in args {
|
||||
self.collect_effects_from_expr(arg);
|
||||
}
|
||||
}
|
||||
Expr::Run { expr, handlers, .. } => {
|
||||
self.collect_effects_from_expr(expr);
|
||||
for (effect, handler) in handlers {
|
||||
self.used_effects.insert(effect.name.clone());
|
||||
self.collect_effects_from_expr(handler);
|
||||
}
|
||||
}
|
||||
Expr::Call { func, args, .. } => {
|
||||
self.collect_effects_from_expr(func);
|
||||
for arg in args {
|
||||
self.collect_effects_from_expr(arg);
|
||||
}
|
||||
}
|
||||
Expr::Lambda { body, effects, .. } => {
|
||||
for effect in effects {
|
||||
self.used_effects.insert(effect.name.clone());
|
||||
}
|
||||
self.collect_effects_from_expr(body);
|
||||
}
|
||||
Expr::Let { value, body, .. } => {
|
||||
self.collect_effects_from_expr(value);
|
||||
self.collect_effects_from_expr(body);
|
||||
}
|
||||
Expr::If { condition, then_branch, else_branch, .. } => {
|
||||
self.collect_effects_from_expr(condition);
|
||||
self.collect_effects_from_expr(then_branch);
|
||||
self.collect_effects_from_expr(else_branch);
|
||||
}
|
||||
Expr::Match { scrutinee, arms, .. } => {
|
||||
self.collect_effects_from_expr(scrutinee);
|
||||
for arm in arms {
|
||||
self.collect_effects_from_expr(&arm.body);
|
||||
if let Some(guard) = &arm.guard {
|
||||
self.collect_effects_from_expr(guard);
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::Block { statements, result, .. } => {
|
||||
for stmt in statements {
|
||||
match stmt {
|
||||
Statement::Expr(e) => self.collect_effects_from_expr(e),
|
||||
Statement::Let { value, .. } => self.collect_effects_from_expr(value),
|
||||
}
|
||||
}
|
||||
self.collect_effects_from_expr(result);
|
||||
}
|
||||
Expr::BinaryOp { left, right, .. } => {
|
||||
self.collect_effects_from_expr(left);
|
||||
self.collect_effects_from_expr(right);
|
||||
}
|
||||
Expr::UnaryOp { operand, .. } => {
|
||||
self.collect_effects_from_expr(operand);
|
||||
}
|
||||
Expr::Field { object, .. } => {
|
||||
self.collect_effects_from_expr(object);
|
||||
}
|
||||
Expr::TupleIndex { object, .. } => {
|
||||
self.collect_effects_from_expr(object);
|
||||
}
|
||||
Expr::Record { spread, fields, .. } => {
|
||||
if let Some(s) = spread {
|
||||
self.collect_effects_from_expr(s);
|
||||
}
|
||||
for (_, expr) in fields {
|
||||
self.collect_effects_from_expr(expr);
|
||||
}
|
||||
}
|
||||
Expr::Tuple { elements, .. } | Expr::List { elements, .. } => {
|
||||
for el in elements {
|
||||
self.collect_effects_from_expr(el);
|
||||
}
|
||||
}
|
||||
Expr::Resume { value, .. } => {
|
||||
self.collect_effects_from_expr(value);
|
||||
}
|
||||
Expr::Literal(_) | Expr::Var(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit the Lux runtime, tree-shaken based on used effects
|
||||
fn emit_runtime(&mut self) {
|
||||
let uses_console = self.used_effects.contains("Console");
|
||||
let uses_random = self.used_effects.contains("Random");
|
||||
let uses_time = self.used_effects.contains("Time");
|
||||
let uses_http = self.used_effects.contains("Http");
|
||||
let uses_dom = self.used_effects.contains("Dom");
|
||||
let uses_html = self.used_effects.contains("Html") || uses_dom;
|
||||
|
||||
self.writeln("// Lux Runtime");
|
||||
self.writeln("const Lux = {");
|
||||
self.indent += 1;
|
||||
|
||||
// Option helpers
|
||||
// Core helpers — always emitted
|
||||
self.writeln("Some: (value) => ({ tag: \"Some\", value }),");
|
||||
self.writeln("None: () => ({ tag: \"None\" }),");
|
||||
self.writeln("");
|
||||
|
||||
// Result helpers
|
||||
self.writeln("Ok: (value) => ({ tag: \"Ok\", value }),");
|
||||
self.writeln("Err: (error) => ({ tag: \"Err\", error }),");
|
||||
self.writeln("");
|
||||
|
||||
// List helpers
|
||||
self.writeln("Cons: (head, tail) => [head, ...tail],");
|
||||
self.writeln("Nil: () => [],");
|
||||
self.writeln("");
|
||||
|
||||
// Default handlers for effects
|
||||
// Default handlers — only include effects that are used
|
||||
self.writeln("defaultHandlers: {");
|
||||
self.indent += 1;
|
||||
|
||||
// Console effect
|
||||
if uses_console {
|
||||
self.emit_console_handler();
|
||||
}
|
||||
if uses_random {
|
||||
self.emit_random_handler();
|
||||
}
|
||||
if uses_time {
|
||||
self.emit_time_handler();
|
||||
}
|
||||
if uses_http {
|
||||
self.emit_http_handler();
|
||||
}
|
||||
if uses_dom {
|
||||
self.emit_dom_handler();
|
||||
}
|
||||
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
|
||||
// HTML rendering — only if Html or Dom effects are used
|
||||
if uses_html {
|
||||
self.emit_html_helpers();
|
||||
}
|
||||
|
||||
// TEA runtime — only if Dom is used
|
||||
if uses_dom {
|
||||
self.emit_tea_runtime();
|
||||
}
|
||||
|
||||
self.indent -= 1;
|
||||
self.writeln("};");
|
||||
self.writeln("");
|
||||
}
|
||||
|
||||
fn emit_console_handler(&mut self) {
|
||||
self.writeln("Console: {");
|
||||
self.indent += 1;
|
||||
self.writeln("print: (msg) => console.log(msg),");
|
||||
@@ -207,8 +373,9 @@ impl JsBackend {
|
||||
self.writeln("readInt: () => parseInt(Lux.defaultHandlers.Console.readLine(), 10)");
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
}
|
||||
|
||||
// Random effect
|
||||
fn emit_random_handler(&mut self) {
|
||||
self.writeln("Random: {");
|
||||
self.indent += 1;
|
||||
self.writeln("int: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min,");
|
||||
@@ -216,16 +383,18 @@ impl JsBackend {
|
||||
self.writeln("float: () => Math.random()");
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
}
|
||||
|
||||
// Time effect
|
||||
fn emit_time_handler(&mut self) {
|
||||
self.writeln("Time: {");
|
||||
self.indent += 1;
|
||||
self.writeln("now: () => Date.now(),");
|
||||
self.writeln("sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms))");
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
}
|
||||
|
||||
// Http effect (browser/Node compatible)
|
||||
fn emit_http_handler(&mut self) {
|
||||
self.writeln("Http: {");
|
||||
self.indent += 1;
|
||||
self.writeln("get: async (url) => {");
|
||||
@@ -287,8 +456,9 @@ impl JsBackend {
|
||||
self.writeln("}");
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
}
|
||||
|
||||
// Dom effect (browser only - stubs for Node.js)
|
||||
fn emit_dom_handler(&mut self) {
|
||||
self.writeln("Dom: {");
|
||||
self.indent += 1;
|
||||
|
||||
@@ -316,7 +486,6 @@ impl JsBackend {
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
|
||||
// Element creation
|
||||
self.writeln("createElement: (tag) => {");
|
||||
self.indent += 1;
|
||||
self.writeln("if (typeof document === 'undefined') return null;");
|
||||
@@ -331,7 +500,6 @@ impl JsBackend {
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
|
||||
// DOM manipulation
|
||||
self.writeln("appendChild: (parent, child) => {");
|
||||
self.indent += 1;
|
||||
self.writeln("if (parent && child) parent.appendChild(child);");
|
||||
@@ -356,7 +524,6 @@ impl JsBackend {
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
|
||||
// Content
|
||||
self.writeln("setTextContent: (el, text) => {");
|
||||
self.indent += 1;
|
||||
self.writeln("if (el) el.textContent = text;");
|
||||
@@ -381,7 +548,6 @@ impl JsBackend {
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
|
||||
// Attributes
|
||||
self.writeln("setAttribute: (el, name, value) => {");
|
||||
self.indent += 1;
|
||||
self.writeln("if (el) el.setAttribute(name, value);");
|
||||
@@ -408,7 +574,6 @@ impl JsBackend {
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
|
||||
// Classes
|
||||
self.writeln("addClass: (el, className) => {");
|
||||
self.indent += 1;
|
||||
self.writeln("if (el) el.classList.add(className);");
|
||||
@@ -433,7 +598,6 @@ impl JsBackend {
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
|
||||
// Styles
|
||||
self.writeln("setStyle: (el, property, value) => {");
|
||||
self.indent += 1;
|
||||
self.writeln("if (el) el.style[property] = value;");
|
||||
@@ -446,7 +610,6 @@ impl JsBackend {
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
|
||||
// Form elements
|
||||
self.writeln("getValue: (el) => {");
|
||||
self.indent += 1;
|
||||
self.writeln("return el ? el.value : '';");
|
||||
@@ -471,7 +634,6 @@ impl JsBackend {
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
|
||||
// Events
|
||||
self.writeln("addEventListener: (el, event, handler) => {");
|
||||
self.indent += 1;
|
||||
self.writeln("if (el) el.addEventListener(event, handler);");
|
||||
@@ -484,7 +646,6 @@ impl JsBackend {
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
|
||||
// Focus
|
||||
self.writeln("focus: (el) => {");
|
||||
self.indent += 1;
|
||||
self.writeln("if (el && el.focus) el.focus();");
|
||||
@@ -497,7 +658,6 @@ impl JsBackend {
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
|
||||
// Document
|
||||
self.writeln("getBody: () => {");
|
||||
self.indent += 1;
|
||||
self.writeln("if (typeof document === 'undefined') return null;");
|
||||
@@ -512,7 +672,6 @@ impl JsBackend {
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
|
||||
// Window
|
||||
self.writeln("getWindow: () => {");
|
||||
self.indent += 1;
|
||||
self.writeln("if (typeof window === 'undefined') return null;");
|
||||
@@ -545,7 +704,6 @@ impl JsBackend {
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
|
||||
// Scroll
|
||||
self.writeln("scrollTo: (x, y) => {");
|
||||
self.indent += 1;
|
||||
self.writeln("if (typeof window !== 'undefined') window.scrollTo(x, y);");
|
||||
@@ -558,7 +716,6 @@ impl JsBackend {
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
|
||||
// Dimensions
|
||||
self.writeln("getBoundingClientRect: (el) => {");
|
||||
self.indent += 1;
|
||||
self.writeln("if (!el) return { top: 0, left: 0, width: 0, height: 0, right: 0, bottom: 0 };");
|
||||
@@ -574,13 +731,11 @@ impl JsBackend {
|
||||
self.indent -= 1;
|
||||
self.writeln("}");
|
||||
|
||||
self.indent -= 1;
|
||||
self.writeln("}");
|
||||
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
}
|
||||
|
||||
// HTML rendering helpers
|
||||
fn emit_html_helpers(&mut self) {
|
||||
self.writeln("");
|
||||
self.writeln("// HTML rendering");
|
||||
self.writeln("renderHtml: (node) => {");
|
||||
@@ -682,8 +837,9 @@ impl JsBackend {
|
||||
self.writeln("return el;");
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
}
|
||||
|
||||
// TEA (The Elm Architecture) runtime
|
||||
fn emit_tea_runtime(&mut self) {
|
||||
self.writeln("");
|
||||
self.writeln("// The Elm Architecture (TEA) runtime");
|
||||
self.writeln("app: (config) => {");
|
||||
@@ -727,7 +883,6 @@ impl JsBackend {
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
|
||||
// Simple app (for string-based views like the counter example)
|
||||
self.writeln("");
|
||||
self.writeln("// Simple TEA app (string-based view)");
|
||||
self.writeln("simpleApp: (config) => {");
|
||||
@@ -757,7 +912,6 @@ impl JsBackend {
|
||||
self.indent -= 1;
|
||||
self.writeln("},");
|
||||
|
||||
// Diff and patch (basic implementation for view_deps optimization)
|
||||
self.writeln("");
|
||||
self.writeln("// Basic diff - checks if model fields changed");
|
||||
self.writeln("hasChanged: (oldModel, newModel, ...paths) => {");
|
||||
@@ -777,11 +931,7 @@ impl JsBackend {
|
||||
self.writeln("}");
|
||||
self.writeln("return false;");
|
||||
self.indent -= 1;
|
||||
self.writeln("}");
|
||||
|
||||
self.indent -= 1;
|
||||
self.writeln("};");
|
||||
self.writeln("");
|
||||
self.writeln("},");
|
||||
}
|
||||
|
||||
/// Collect type information from a type declaration
|
||||
@@ -888,7 +1038,8 @@ impl JsBackend {
|
||||
let prev_has_handlers = self.has_handlers;
|
||||
self.has_handlers = is_effectful;
|
||||
|
||||
// Clear var substitutions for this function
|
||||
// Save and clear var substitutions for this function scope
|
||||
let saved_substitutions = self.var_substitutions.clone();
|
||||
self.var_substitutions.clear();
|
||||
|
||||
// Emit function body
|
||||
@@ -896,6 +1047,7 @@ impl JsBackend {
|
||||
self.writeln(&format!("return {};", body_code));
|
||||
|
||||
self.has_handlers = prev_has_handlers;
|
||||
self.var_substitutions = saved_substitutions;
|
||||
|
||||
self.indent -= 1;
|
||||
self.writeln("}");
|
||||
@@ -909,13 +1061,16 @@ impl JsBackend {
|
||||
let val = self.emit_expr(&let_decl.value)?;
|
||||
let var_name = &let_decl.name.name;
|
||||
|
||||
// Check if this is a run expression (often results in undefined)
|
||||
// We still want to execute it for its side effects
|
||||
if var_name == "_" {
|
||||
// Wildcard binding: just execute for side effects
|
||||
self.writeln(&format!("{};", val));
|
||||
} else {
|
||||
self.writeln(&format!("const {} = {};", var_name, val));
|
||||
|
||||
// Register the variable for future use
|
||||
self.var_substitutions
|
||||
.insert(var_name.clone(), var_name.clone());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -954,12 +1109,17 @@ impl JsBackend {
|
||||
let r = self.emit_expr(right)?;
|
||||
|
||||
// Check for string concatenation
|
||||
if matches!(op, BinaryOp::Add) {
|
||||
if matches!(op, BinaryOp::Add | BinaryOp::Concat) {
|
||||
if self.is_string_expr(left) || self.is_string_expr(right) {
|
||||
return Ok(format!("({} + {})", l, r));
|
||||
}
|
||||
}
|
||||
|
||||
// ++ on lists: use .concat()
|
||||
if matches!(op, BinaryOp::Concat) {
|
||||
return Ok(format!("{}.concat({})", l, r));
|
||||
}
|
||||
|
||||
let op_str = match op {
|
||||
BinaryOp::Add => "+",
|
||||
BinaryOp::Sub => "-",
|
||||
@@ -974,6 +1134,7 @@ impl JsBackend {
|
||||
BinaryOp::Ge => ">=",
|
||||
BinaryOp::And => "&&",
|
||||
BinaryOp::Or => "||",
|
||||
BinaryOp::Concat => unreachable!("handled above"),
|
||||
BinaryOp::Pipe => {
|
||||
// Pipe operator: x |> f becomes f(x)
|
||||
return Ok(format!("{}({})", r, l));
|
||||
@@ -1034,6 +1195,11 @@ impl JsBackend {
|
||||
name, value, body, ..
|
||||
} => {
|
||||
let val = self.emit_expr(value)?;
|
||||
|
||||
if name.name == "_" {
|
||||
// Wildcard binding: just execute for side effects
|
||||
self.writeln(&format!("{};", val));
|
||||
} else {
|
||||
let var_name = format!("{}_{}", name.name, self.fresh_name());
|
||||
|
||||
self.writeln(&format!("const {} = {};", var_name, val));
|
||||
@@ -1041,11 +1207,14 @@ impl JsBackend {
|
||||
// Add substitution
|
||||
self.var_substitutions
|
||||
.insert(name.name.clone(), var_name.clone());
|
||||
}
|
||||
|
||||
let body_result = self.emit_expr(body)?;
|
||||
|
||||
// Remove substitution
|
||||
if name.name != "_" {
|
||||
self.var_substitutions.remove(&name.name);
|
||||
}
|
||||
|
||||
Ok(body_result)
|
||||
}
|
||||
@@ -1057,6 +1226,31 @@ impl JsBackend {
|
||||
if module_name.name == "List" {
|
||||
return self.emit_list_operation(&field.name, args);
|
||||
}
|
||||
if module_name.name == "Map" {
|
||||
return self.emit_map_operation(&field.name, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Int/Float module operations
|
||||
if let Expr::Field { object, field, .. } = func.as_ref() {
|
||||
if let Expr::Var(module_name) = object.as_ref() {
|
||||
if module_name.name == "Int" {
|
||||
let arg = self.emit_expr(&args[0])?;
|
||||
match field.name.as_str() {
|
||||
"toFloat" => return Ok(arg),
|
||||
"toString" => return Ok(format!("String({})", arg)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if module_name.name == "Float" {
|
||||
let arg = self.emit_expr(&args[0])?;
|
||||
match field.name.as_str() {
|
||||
"toInt" => return Ok(format!("Math.trunc({})", arg)),
|
||||
"toString" => return Ok(format!("String({})", arg)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1066,6 +1260,10 @@ impl JsBackend {
|
||||
let arg = self.emit_expr(&args[0])?;
|
||||
return Ok(format!("String({})", arg));
|
||||
}
|
||||
if ident.name == "print" {
|
||||
let arg = self.emit_expr(&args[0])?;
|
||||
return Ok(format!("console.log({})", arg));
|
||||
}
|
||||
}
|
||||
|
||||
let arg_strs: Result<Vec<_>, _> = args.iter().map(|a| self.emit_expr(a)).collect();
|
||||
@@ -1142,6 +1340,26 @@ impl JsBackend {
|
||||
return self.emit_math_operation(&operation.name, args);
|
||||
}
|
||||
|
||||
// Special case: Int module operations
|
||||
if effect.name == "Int" {
|
||||
let arg = self.emit_expr(&args[0])?;
|
||||
match operation.name.as_str() {
|
||||
"toFloat" => return Ok(arg), // JS numbers are already floats
|
||||
"toString" => return Ok(format!("String({})", arg)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Special case: Float module operations
|
||||
if effect.name == "Float" {
|
||||
let arg = self.emit_expr(&args[0])?;
|
||||
match operation.name.as_str() {
|
||||
"toInt" => return Ok(format!("Math.trunc({})", arg)),
|
||||
"toString" => return Ok(format!("String({})", arg)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Special case: Result module operations (not an effect)
|
||||
if effect.name == "Result" {
|
||||
return self.emit_result_operation(&operation.name, args);
|
||||
@@ -1152,6 +1370,11 @@ impl JsBackend {
|
||||
return self.emit_json_operation(&operation.name, args);
|
||||
}
|
||||
|
||||
// Special case: Map module operations (not an effect)
|
||||
if effect.name == "Map" {
|
||||
return self.emit_map_operation(&operation.name, args);
|
||||
}
|
||||
|
||||
// Special case: Html module operations (not an effect)
|
||||
if effect.name == "Html" {
|
||||
return self.emit_html_operation(&operation.name, args);
|
||||
@@ -1197,18 +1420,39 @@ impl JsBackend {
|
||||
param_names
|
||||
};
|
||||
|
||||
// Save handler state
|
||||
// Save state
|
||||
let prev_has_handlers = self.has_handlers;
|
||||
let saved_substitutions = self.var_substitutions.clone();
|
||||
self.has_handlers = !effects.is_empty();
|
||||
|
||||
// Register lambda params as themselves (override any outer substitutions)
|
||||
for p in &all_params {
|
||||
self.var_substitutions.insert(p.clone(), p.clone());
|
||||
}
|
||||
|
||||
// Capture any statements emitted during body evaluation
|
||||
let output_start = self.output.len();
|
||||
let prev_indent = self.indent;
|
||||
self.indent += 1;
|
||||
|
||||
let body_code = self.emit_expr(body)?;
|
||||
self.writeln(&format!("return {};", body_code));
|
||||
|
||||
// Extract body statements and restore output
|
||||
let body_statements = self.output[output_start..].to_string();
|
||||
self.output.truncate(output_start);
|
||||
self.indent = prev_indent;
|
||||
|
||||
// Restore state
|
||||
self.has_handlers = prev_has_handlers;
|
||||
self.var_substitutions = saved_substitutions;
|
||||
|
||||
let indent_str = " ".repeat(self.indent);
|
||||
Ok(format!(
|
||||
"(function({}) {{ return {}; }})",
|
||||
"(function({}) {{\n{}{}}})",
|
||||
all_params.join(", "),
|
||||
body_code
|
||||
body_statements,
|
||||
indent_str,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1228,27 +1472,36 @@ impl JsBackend {
|
||||
}
|
||||
Statement::Let { name, value, .. } => {
|
||||
let val = self.emit_expr(value)?;
|
||||
let var_name = format!("{}_{}", name.name, self.fresh_name());
|
||||
if name.name == "_" {
|
||||
self.writeln(&format!("{};", val));
|
||||
} else {
|
||||
let var_name =
|
||||
format!("{}_{}", name.name, self.fresh_name());
|
||||
self.writeln(&format!("const {} = {};", var_name, val));
|
||||
self.var_substitutions
|
||||
.insert(name.name.clone(), var_name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emit result
|
||||
self.emit_expr(result)
|
||||
}
|
||||
|
||||
Expr::Record { fields, .. } => {
|
||||
let field_strs: Result<Vec<_>, _> = fields
|
||||
.iter()
|
||||
.map(|(name, expr)| {
|
||||
Expr::Record {
|
||||
spread, fields, ..
|
||||
} => {
|
||||
let mut parts = Vec::new();
|
||||
if let Some(spread_expr) = spread {
|
||||
let spread_code = self.emit_expr(spread_expr)?;
|
||||
parts.push(format!("...{}", spread_code));
|
||||
}
|
||||
for (name, expr) in fields {
|
||||
let val = self.emit_expr(expr)?;
|
||||
Ok(format!("{}: {}", name.name, val))
|
||||
})
|
||||
.collect();
|
||||
Ok(format!("{{ {} }}", field_strs?.join(", ")))
|
||||
parts.push(format!("{}: {}", name.name, val));
|
||||
}
|
||||
Ok(format!("{{ {} }}", parts.join(", ")))
|
||||
}
|
||||
|
||||
Expr::Tuple { elements, .. } => {
|
||||
@@ -1268,6 +1521,11 @@ impl JsBackend {
|
||||
Ok(format!("{}.{}", obj, field.name))
|
||||
}
|
||||
|
||||
Expr::TupleIndex { object, index, .. } => {
|
||||
let obj = self.emit_expr(object)?;
|
||||
Ok(format!("{}[{}]", obj, index))
|
||||
}
|
||||
|
||||
Expr::Run {
|
||||
expr, handlers, ..
|
||||
} => {
|
||||
@@ -1565,6 +1823,18 @@ impl JsBackend {
|
||||
end, start, start
|
||||
))
|
||||
}
|
||||
"sort" => {
|
||||
let list = self.emit_expr(&args[0])?;
|
||||
Ok(format!(
|
||||
"[...{}].sort((a, b) => a < b ? -1 : a > b ? 1 : 0)",
|
||||
list
|
||||
))
|
||||
}
|
||||
"sortBy" => {
|
||||
let list = self.emit_expr(&args[0])?;
|
||||
let func = self.emit_expr(&args[1])?;
|
||||
Ok(format!("[...{}].sort({})", list, func))
|
||||
}
|
||||
_ => Err(JsGenError {
|
||||
message: format!("Unknown List operation: {}", operation),
|
||||
span: None,
|
||||
@@ -2062,6 +2332,86 @@ impl JsBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit Map module operations using JS Map
|
||||
fn emit_map_operation(
|
||||
&mut self,
|
||||
operation: &str,
|
||||
args: &[Expr],
|
||||
) -> Result<String, JsGenError> {
|
||||
match operation {
|
||||
"new" => Ok("new Map()".to_string()),
|
||||
"set" => {
|
||||
let map = self.emit_expr(&args[0])?;
|
||||
let key = self.emit_expr(&args[1])?;
|
||||
let val = self.emit_expr(&args[2])?;
|
||||
Ok(format!(
|
||||
"(function() {{ var m = new Map({}); m.set({}, {}); return m; }})()",
|
||||
map, key, val
|
||||
))
|
||||
}
|
||||
"get" => {
|
||||
let map = self.emit_expr(&args[0])?;
|
||||
let key = self.emit_expr(&args[1])?;
|
||||
Ok(format!(
|
||||
"({0}.has({1}) ? Lux.Some({0}.get({1})) : Lux.None())",
|
||||
map, key
|
||||
))
|
||||
}
|
||||
"contains" => {
|
||||
let map = self.emit_expr(&args[0])?;
|
||||
let key = self.emit_expr(&args[1])?;
|
||||
Ok(format!("{}.has({})", map, key))
|
||||
}
|
||||
"remove" => {
|
||||
let map = self.emit_expr(&args[0])?;
|
||||
let key = self.emit_expr(&args[1])?;
|
||||
Ok(format!(
|
||||
"(function() {{ var m = new Map({}); m.delete({}); return m; }})()",
|
||||
map, key
|
||||
))
|
||||
}
|
||||
"keys" => {
|
||||
let map = self.emit_expr(&args[0])?;
|
||||
Ok(format!("Array.from({}.keys()).sort()", map))
|
||||
}
|
||||
"values" => {
|
||||
let map = self.emit_expr(&args[0])?;
|
||||
Ok(format!(
|
||||
"Array.from({0}.entries()).sort(function(a,b) {{ return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0; }}).map(function(e) {{ return e[1]; }})",
|
||||
map
|
||||
))
|
||||
}
|
||||
"size" => {
|
||||
let map = self.emit_expr(&args[0])?;
|
||||
Ok(format!("{}.size", map))
|
||||
}
|
||||
"isEmpty" => {
|
||||
let map = self.emit_expr(&args[0])?;
|
||||
Ok(format!("({}.size === 0)", map))
|
||||
}
|
||||
"fromList" => {
|
||||
let list = self.emit_expr(&args[0])?;
|
||||
Ok(format!("new Map({}.map(function(t) {{ return [t[0], t[1]]; }}))", list))
|
||||
}
|
||||
"toList" => {
|
||||
let map = self.emit_expr(&args[0])?;
|
||||
Ok(format!(
|
||||
"Array.from({}.entries()).sort(function(a,b) {{ return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0; }})",
|
||||
map
|
||||
))
|
||||
}
|
||||
"merge" => {
|
||||
let m1 = self.emit_expr(&args[0])?;
|
||||
let m2 = self.emit_expr(&args[1])?;
|
||||
Ok(format!("new Map([...{}, ...{}])", m1, m2))
|
||||
}
|
||||
_ => Err(JsGenError {
|
||||
message: format!("Unknown Map operation: {}", operation),
|
||||
span: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit Html module operations for type-safe HTML construction
|
||||
fn emit_html_operation(
|
||||
&mut self,
|
||||
@@ -2333,7 +2683,7 @@ impl JsBackend {
|
||||
}
|
||||
}
|
||||
Expr::BinaryOp { op, left, right, .. } => {
|
||||
matches!(op, BinaryOp::Add)
|
||||
matches!(op, BinaryOp::Add | BinaryOp::Concat)
|
||||
&& (self.is_string_expr(left) || self.is_string_expr(right))
|
||||
}
|
||||
_ => false,
|
||||
@@ -2384,6 +2734,10 @@ impl JsBackend {
|
||||
|
||||
/// Mangle a Lux name to a valid JavaScript name
|
||||
fn mangle_name(&self, name: &str) -> String {
|
||||
// Extern functions use their JS name directly (no mangling)
|
||||
if let Some(js_name) = self.extern_fns.get(name) {
|
||||
return js_name.clone();
|
||||
}
|
||||
format!("{}_lux", name)
|
||||
}
|
||||
|
||||
@@ -3732,7 +4086,7 @@ line3"
|
||||
|
||||
#[test]
|
||||
fn test_js_runtime_generated() {
|
||||
// Test that the Lux runtime is properly generated
|
||||
// Test that the Lux runtime core is always generated
|
||||
use crate::parser::Parser;
|
||||
|
||||
let source = r#"
|
||||
@@ -3743,21 +4097,51 @@ line3"
|
||||
let mut backend = JsBackend::new();
|
||||
let js_code = backend.generate(&program).expect("Should generate");
|
||||
|
||||
// Check that Lux runtime includes key functions
|
||||
// Core runtime is always present
|
||||
assert!(js_code.contains("const Lux = {"), "Lux object should be defined");
|
||||
assert!(js_code.contains("Some:"), "Option Some should be defined");
|
||||
assert!(js_code.contains("None:"), "Option None should be defined");
|
||||
assert!(js_code.contains("renderHtml:"), "renderHtml should be defined");
|
||||
assert!(js_code.contains("renderToDom:"), "renderToDom should be defined");
|
||||
assert!(js_code.contains("escapeHtml:"), "escapeHtml should be defined");
|
||||
assert!(js_code.contains("app:"), "TEA app should be defined");
|
||||
assert!(js_code.contains("simpleApp:"), "simpleApp should be defined");
|
||||
assert!(js_code.contains("hasChanged:"), "hasChanged should be defined");
|
||||
|
||||
// Console-only program should NOT include Dom, Html, or TEA sections
|
||||
assert!(!js_code.contains("Dom:"), "Dom handler should not be in Console-only program");
|
||||
assert!(!js_code.contains("renderHtml:"), "renderHtml should not be in Console-only program");
|
||||
assert!(!js_code.contains("app:"), "TEA app should not be in Console-only program");
|
||||
assert!(!js_code.contains("Http:"), "Http should not be in Console-only program");
|
||||
|
||||
// Console should be present
|
||||
assert!(js_code.contains("Console:"), "Console handler should exist");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_js_runtime_tree_shaking_all_effects() {
|
||||
// Test that all effects are included when all are used
|
||||
use crate::parser::Parser;
|
||||
|
||||
let source = r#"
|
||||
fn main(): Unit with {Console, Dom} = {
|
||||
Console.print("Hello")
|
||||
let _ = Dom.getElementById("app")
|
||||
()
|
||||
}
|
||||
"#;
|
||||
|
||||
let program = Parser::parse_source(source).expect("Should parse");
|
||||
let mut backend = JsBackend::new();
|
||||
let js_code = backend.generate(&program).expect("Should generate");
|
||||
|
||||
assert!(js_code.contains("Console:"), "Console handler should exist");
|
||||
assert!(js_code.contains("Dom:"), "Dom handler should exist");
|
||||
assert!(js_code.contains("renderHtml:"), "renderHtml should be defined when Dom is used");
|
||||
assert!(js_code.contains("renderToDom:"), "renderToDom should be defined when Dom is used");
|
||||
assert!(js_code.contains("escapeHtml:"), "escapeHtml should be defined when Dom is used");
|
||||
assert!(js_code.contains("app:"), "TEA app should be defined when Dom is used");
|
||||
assert!(js_code.contains("simpleApp:"), "simpleApp should be defined when Dom is used");
|
||||
assert!(js_code.contains("hasChanged:"), "hasChanged should be defined when Dom is used");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_js_runtime_default_handlers() {
|
||||
// Test that default handlers are properly generated
|
||||
// Test that only used effect handlers are generated
|
||||
use crate::parser::Parser;
|
||||
|
||||
let source = r#"
|
||||
@@ -3768,12 +4152,12 @@ line3"
|
||||
let mut backend = JsBackend::new();
|
||||
let js_code = backend.generate(&program).expect("Should generate");
|
||||
|
||||
// Check that default handlers include all effects
|
||||
// Only Console should be present
|
||||
assert!(js_code.contains("Console:"), "Console handler should exist");
|
||||
assert!(js_code.contains("Random:"), "Random handler should exist");
|
||||
assert!(js_code.contains("Time:"), "Time handler should exist");
|
||||
assert!(js_code.contains("Http:"), "Http handler should exist");
|
||||
assert!(js_code.contains("Dom:"), "Dom handler should exist");
|
||||
assert!(!js_code.contains("Random:"), "Random handler should not exist in Console-only program");
|
||||
assert!(!js_code.contains("Time:"), "Time handler should not exist in Console-only program");
|
||||
assert!(!js_code.contains("Http:"), "Http handler should not exist in Console-only program");
|
||||
assert!(!js_code.contains("Dom:"), "Dom handler should not exist in Console-only program");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -224,10 +224,31 @@ pub mod colors {
|
||||
pub const BOLD: &str = "\x1b[1m";
|
||||
pub const DIM: &str = "\x1b[2m";
|
||||
pub const RED: &str = "\x1b[31m";
|
||||
pub const GREEN: &str = "\x1b[32m";
|
||||
pub const YELLOW: &str = "\x1b[33m";
|
||||
pub const BLUE: &str = "\x1b[34m";
|
||||
pub const MAGENTA: &str = "\x1b[35m";
|
||||
pub const CYAN: &str = "\x1b[36m";
|
||||
pub const WHITE: &str = "\x1b[37m";
|
||||
pub const GRAY: &str = "\x1b[90m";
|
||||
}
|
||||
|
||||
/// Apply color to text, respecting NO_COLOR / TERM=dumb
|
||||
pub fn c(color: &str, text: &str) -> String {
|
||||
if supports_color() {
|
||||
format!("{}{}{}", color, text, colors::RESET)
|
||||
} else {
|
||||
text.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply bold + color to text
|
||||
pub fn bc(color: &str, text: &str) -> String {
|
||||
if supports_color() {
|
||||
format!("{}{}{}{}", colors::BOLD, color, text, colors::RESET)
|
||||
} else {
|
||||
text.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Severity level for diagnostics
|
||||
|
||||
@@ -333,11 +333,13 @@ mod tests {
|
||||
fn test_option_exhaustive() {
|
||||
let patterns = vec![
|
||||
Pattern::Constructor {
|
||||
module: None,
|
||||
name: make_ident("None"),
|
||||
fields: vec![],
|
||||
span: span(),
|
||||
},
|
||||
Pattern::Constructor {
|
||||
module: None,
|
||||
name: make_ident("Some"),
|
||||
fields: vec![Pattern::Wildcard(span())],
|
||||
span: span(),
|
||||
@@ -352,6 +354,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_option_missing_none() {
|
||||
let patterns = vec![Pattern::Constructor {
|
||||
module: None,
|
||||
name: make_ident("Some"),
|
||||
fields: vec![Pattern::Wildcard(span())],
|
||||
span: span(),
|
||||
@@ -391,11 +394,13 @@ mod tests {
|
||||
fn test_result_exhaustive() {
|
||||
let patterns = vec![
|
||||
Pattern::Constructor {
|
||||
module: None,
|
||||
name: make_ident("Ok"),
|
||||
fields: vec![Pattern::Wildcard(span())],
|
||||
span: span(),
|
||||
},
|
||||
Pattern::Constructor {
|
||||
module: None,
|
||||
name: make_ident("Err"),
|
||||
fields: vec![Pattern::Wildcard(span())],
|
||||
span: span(),
|
||||
|
||||
117
src/formatter.rs
117
src/formatter.rs
@@ -3,9 +3,9 @@
|
||||
//! Formats Lux source code according to standard style guidelines.
|
||||
|
||||
use crate::ast::{
|
||||
BehavioralProperty, BinaryOp, Declaration, EffectDecl, Expr, FunctionDecl, HandlerDecl,
|
||||
ImplDecl, ImplMethod, LetDecl, Literal, LiteralKind, Pattern, Program, Statement, TraitDecl,
|
||||
TypeDecl, TypeDef, TypeExpr, UnaryOp, VariantFields,
|
||||
BehavioralProperty, BinaryOp, Declaration, EffectDecl, ExternFnDecl, Expr, FunctionDecl,
|
||||
HandlerDecl, ImplDecl, ImplMethod, LetDecl, Literal, LiteralKind, Pattern, Program, Statement,
|
||||
TraitDecl, TypeDecl, TypeDef, TypeExpr, UnaryOp, VariantFields, Visibility,
|
||||
};
|
||||
use crate::lexer::Lexer;
|
||||
use crate::parser::Parser;
|
||||
@@ -103,9 +103,55 @@ impl Formatter {
|
||||
Declaration::Handler(h) => self.format_handler(h),
|
||||
Declaration::Trait(t) => self.format_trait(t),
|
||||
Declaration::Impl(i) => self.format_impl(i),
|
||||
Declaration::ExternFn(e) => self.format_extern_fn(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_extern_fn(&mut self, ext: &ExternFnDecl) {
|
||||
let indent = self.indent();
|
||||
self.write(&indent);
|
||||
|
||||
if ext.visibility == Visibility::Public {
|
||||
self.write("pub ");
|
||||
}
|
||||
|
||||
self.write("extern fn ");
|
||||
self.write(&ext.name.name);
|
||||
|
||||
// Type parameters
|
||||
if !ext.type_params.is_empty() {
|
||||
self.write("<");
|
||||
self.write(
|
||||
&ext.type_params
|
||||
.iter()
|
||||
.map(|p| p.name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
);
|
||||
self.write(">");
|
||||
}
|
||||
|
||||
// Parameters
|
||||
self.write("(");
|
||||
let params: Vec<String> = ext
|
||||
.params
|
||||
.iter()
|
||||
.map(|p| format!("{}: {}", p.name.name, self.format_type_expr(&p.typ)))
|
||||
.collect();
|
||||
self.write(¶ms.join(", "));
|
||||
self.write("): ");
|
||||
|
||||
// Return type
|
||||
self.write(&self.format_type_expr(&ext.return_type));
|
||||
|
||||
// Optional JS name
|
||||
if let Some(js_name) = &ext.js_name {
|
||||
self.write(&format!(" = \"{}\"", js_name));
|
||||
}
|
||||
|
||||
self.newline();
|
||||
}
|
||||
|
||||
fn format_function(&mut self, func: &FunctionDecl) {
|
||||
let indent = self.indent();
|
||||
self.write(&indent);
|
||||
@@ -598,6 +644,9 @@ impl Formatter {
|
||||
Expr::Field { object, field, .. } => {
|
||||
format!("{}.{}", self.format_expr(object), field.name)
|
||||
}
|
||||
Expr::TupleIndex { object, index, .. } => {
|
||||
format!("{}.{}", self.format_expr(object), index)
|
||||
}
|
||||
Expr::If { condition, then_branch, else_branch, .. } => {
|
||||
format!(
|
||||
"if {} then {} else {}",
|
||||
@@ -685,15 +734,17 @@ impl Formatter {
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
Expr::Record { fields, .. } => {
|
||||
format!(
|
||||
"{{ {} }}",
|
||||
fields
|
||||
.iter()
|
||||
.map(|(name, val)| format!("{}: {}", name.name, self.format_expr(val)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
Expr::Record {
|
||||
spread, fields, ..
|
||||
} => {
|
||||
let mut parts = Vec::new();
|
||||
if let Some(spread_expr) = spread {
|
||||
parts.push(format!("...{}", self.format_expr(spread_expr)));
|
||||
}
|
||||
for (name, val) in fields {
|
||||
parts.push(format!("{}: {}", name.name, self.format_expr(val)));
|
||||
}
|
||||
format!("{{ {} }}", parts.join(", "))
|
||||
}
|
||||
Expr::EffectOp { effect, operation, args, .. } => {
|
||||
format!(
|
||||
@@ -728,7 +779,30 @@ impl Formatter {
|
||||
match &lit.kind {
|
||||
LiteralKind::Int(n) => n.to_string(),
|
||||
LiteralKind::Float(f) => format!("{}", f),
|
||||
LiteralKind::String(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
|
||||
LiteralKind::String(s) => {
|
||||
if s.contains('\n') {
|
||||
// Use triple-quoted multiline string
|
||||
let tab = " ".repeat(self.config.indent_size);
|
||||
let base_indent = tab.repeat(self.indent_level);
|
||||
let content_indent = tab.repeat(self.indent_level + 1);
|
||||
let lines: Vec<&str> = s.split('\n').collect();
|
||||
let mut result = String::from("\"\"\"\n");
|
||||
for line in &lines {
|
||||
if line.is_empty() {
|
||||
result.push('\n');
|
||||
} else {
|
||||
result.push_str(&content_indent);
|
||||
result.push_str(&line.replace('{', "\\{").replace('}', "\\}"));
|
||||
result.push('\n');
|
||||
}
|
||||
}
|
||||
result.push_str(&base_indent);
|
||||
result.push_str("\"\"\"");
|
||||
result
|
||||
} else {
|
||||
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"").replace('{', "\\{").replace('}', "\\}"))
|
||||
}
|
||||
},
|
||||
LiteralKind::Char(c) => format!("'{}'", c),
|
||||
LiteralKind::Bool(b) => b.to_string(),
|
||||
LiteralKind::Unit => "()".to_string(),
|
||||
@@ -750,6 +824,7 @@ impl Formatter {
|
||||
BinaryOp::Ge => ">=",
|
||||
BinaryOp::And => "&&",
|
||||
BinaryOp::Or => "||",
|
||||
BinaryOp::Concat => "++",
|
||||
BinaryOp::Pipe => "|>",
|
||||
}
|
||||
}
|
||||
@@ -766,12 +841,22 @@ impl Formatter {
|
||||
Pattern::Wildcard(_) => "_".to_string(),
|
||||
Pattern::Var(ident) => ident.name.clone(),
|
||||
Pattern::Literal(lit) => self.format_literal(lit),
|
||||
Pattern::Constructor { name, fields, .. } => {
|
||||
Pattern::Constructor {
|
||||
module,
|
||||
name,
|
||||
fields,
|
||||
..
|
||||
} => {
|
||||
let prefix = match module {
|
||||
Some(m) => format!("{}.", m.name),
|
||||
None => String::new(),
|
||||
};
|
||||
if fields.is_empty() {
|
||||
name.name.clone()
|
||||
format!("{}{}", prefix, name.name)
|
||||
} else {
|
||||
format!(
|
||||
"{}({})",
|
||||
"{}{}({})",
|
||||
prefix,
|
||||
name.name,
|
||||
fields
|
||||
.iter()
|
||||
|
||||
1047
src/interpreter.rs
1047
src/interpreter.rs
File diff suppressed because it is too large
Load Diff
266
src/lexer.rs
266
src/lexer.rs
@@ -42,6 +42,7 @@ pub enum TokenKind {
|
||||
Effect,
|
||||
Handler,
|
||||
Run,
|
||||
Handle,
|
||||
Resume,
|
||||
Type,
|
||||
True,
|
||||
@@ -54,6 +55,7 @@ pub enum TokenKind {
|
||||
Trait, // trait (for type classes)
|
||||
Impl, // impl (for trait implementations)
|
||||
For, // for (in impl Trait for Type)
|
||||
Extern, // extern (for FFI declarations)
|
||||
|
||||
// Documentation
|
||||
DocComment(String), // /// doc comment
|
||||
@@ -70,6 +72,7 @@ pub enum TokenKind {
|
||||
|
||||
// Operators
|
||||
Plus, // +
|
||||
PlusPlus, // ++
|
||||
Minus, // -
|
||||
Star, // *
|
||||
Slash, // /
|
||||
@@ -89,6 +92,7 @@ pub enum TokenKind {
|
||||
Arrow, // =>
|
||||
ThinArrow, // ->
|
||||
Dot, // .
|
||||
DotDotDot, // ...
|
||||
Colon, // :
|
||||
ColonColon, // ::
|
||||
Comma, // ,
|
||||
@@ -138,6 +142,7 @@ impl fmt::Display for TokenKind {
|
||||
TokenKind::Effect => write!(f, "effect"),
|
||||
TokenKind::Handler => write!(f, "handler"),
|
||||
TokenKind::Run => write!(f, "run"),
|
||||
TokenKind::Handle => write!(f, "handle"),
|
||||
TokenKind::Resume => write!(f, "resume"),
|
||||
TokenKind::Type => write!(f, "type"),
|
||||
TokenKind::Import => write!(f, "import"),
|
||||
@@ -148,6 +153,7 @@ impl fmt::Display for TokenKind {
|
||||
TokenKind::Trait => write!(f, "trait"),
|
||||
TokenKind::Impl => write!(f, "impl"),
|
||||
TokenKind::For => write!(f, "for"),
|
||||
TokenKind::Extern => write!(f, "extern"),
|
||||
TokenKind::DocComment(s) => write!(f, "/// {}", s),
|
||||
TokenKind::Is => write!(f, "is"),
|
||||
TokenKind::Pure => write!(f, "pure"),
|
||||
@@ -160,6 +166,7 @@ impl fmt::Display for TokenKind {
|
||||
TokenKind::True => write!(f, "true"),
|
||||
TokenKind::False => write!(f, "false"),
|
||||
TokenKind::Plus => write!(f, "+"),
|
||||
TokenKind::PlusPlus => write!(f, "++"),
|
||||
TokenKind::Minus => write!(f, "-"),
|
||||
TokenKind::Star => write!(f, "*"),
|
||||
TokenKind::Slash => write!(f, "/"),
|
||||
@@ -179,6 +186,7 @@ impl fmt::Display for TokenKind {
|
||||
TokenKind::Arrow => write!(f, "=>"),
|
||||
TokenKind::ThinArrow => write!(f, "->"),
|
||||
TokenKind::Dot => write!(f, "."),
|
||||
TokenKind::DotDotDot => write!(f, "..."),
|
||||
TokenKind::Colon => write!(f, ":"),
|
||||
TokenKind::ColonColon => write!(f, "::"),
|
||||
TokenKind::Comma => write!(f, ","),
|
||||
@@ -268,7 +276,14 @@ impl<'a> Lexer<'a> {
|
||||
|
||||
let kind = match c {
|
||||
// Single-character tokens
|
||||
'+' => TokenKind::Plus,
|
||||
'+' => {
|
||||
if self.peek() == Some('+') {
|
||||
self.advance();
|
||||
TokenKind::PlusPlus
|
||||
} else {
|
||||
TokenKind::Plus
|
||||
}
|
||||
}
|
||||
'*' => TokenKind::Star,
|
||||
'%' => TokenKind::Percent,
|
||||
'(' => TokenKind::LParen,
|
||||
@@ -364,7 +379,22 @@ impl<'a> Lexer<'a> {
|
||||
TokenKind::Pipe
|
||||
}
|
||||
}
|
||||
'.' => TokenKind::Dot,
|
||||
'.' => {
|
||||
if self.peek() == Some('.') {
|
||||
// Check for ... (need to peek past second dot)
|
||||
// We look at source directly since we can only peek one ahead
|
||||
let next_next = self.source[self.pos..].chars().nth(1);
|
||||
if next_next == Some('.') {
|
||||
self.advance(); // consume second '.'
|
||||
self.advance(); // consume third '.'
|
||||
TokenKind::DotDotDot
|
||||
} else {
|
||||
TokenKind::Dot
|
||||
}
|
||||
} else {
|
||||
TokenKind::Dot
|
||||
}
|
||||
}
|
||||
':' => {
|
||||
if self.peek() == Some(':') {
|
||||
self.advance();
|
||||
@@ -383,7 +413,26 @@ impl<'a> Lexer<'a> {
|
||||
}
|
||||
|
||||
// String literals
|
||||
'"' => self.scan_string(start)?,
|
||||
'"' => {
|
||||
// Check for triple-quote multiline string """
|
||||
if self.peek() == Some('"') {
|
||||
// Clone to peek at the second char
|
||||
let mut lookahead = self.chars.clone();
|
||||
lookahead.next(); // consume first peeked "
|
||||
if lookahead.peek() == Some(&'"') {
|
||||
// It's a triple-quote: consume both remaining quotes
|
||||
self.advance(); // second "
|
||||
self.advance(); // third "
|
||||
self.scan_multiline_string(start)?
|
||||
} else {
|
||||
// It's an empty string ""
|
||||
self.advance(); // consume closing "
|
||||
TokenKind::String(String::new())
|
||||
}
|
||||
} else {
|
||||
self.scan_string(start)?
|
||||
}
|
||||
}
|
||||
|
||||
// Char literals
|
||||
'\'' => self.scan_char(start)?,
|
||||
@@ -493,6 +542,8 @@ impl<'a> Lexer<'a> {
|
||||
Some('"') => '"',
|
||||
Some('0') => '\0',
|
||||
Some('\'') => '\'',
|
||||
Some('{') => '{',
|
||||
Some('}') => '}',
|
||||
Some('x') => {
|
||||
// Hex escape \xNN
|
||||
let h1 = self.advance().and_then(|c| c.to_digit(16));
|
||||
@@ -639,6 +690,211 @@ impl<'a> Lexer<'a> {
|
||||
Ok(TokenKind::InterpolatedString(parts))
|
||||
}
|
||||
|
||||
fn scan_multiline_string(&mut self, _start: usize) -> Result<TokenKind, LexError> {
|
||||
let mut parts: Vec<StringPart> = Vec::new();
|
||||
let mut current_literal = String::new();
|
||||
|
||||
// Skip the first newline after opening """ if present
|
||||
if self.peek() == Some('\n') {
|
||||
self.advance();
|
||||
} else if self.peek() == Some('\r') {
|
||||
self.advance();
|
||||
if self.peek() == Some('\n') {
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
match self.advance() {
|
||||
Some('"') => {
|
||||
// Check for closing """
|
||||
if self.peek() == Some('"') {
|
||||
let mut lookahead = self.chars.clone();
|
||||
lookahead.next(); // consume first peeked "
|
||||
if lookahead.peek() == Some(&'"') {
|
||||
// Closing """ found
|
||||
self.advance(); // second "
|
||||
self.advance(); // third "
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Not closing triple-quote, just a regular " in the string
|
||||
current_literal.push('"');
|
||||
}
|
||||
Some('\\') => {
|
||||
// Handle escape sequences (same as regular strings)
|
||||
match self.peek() {
|
||||
Some('{') => {
|
||||
self.advance();
|
||||
current_literal.push('{');
|
||||
}
|
||||
Some('}') => {
|
||||
self.advance();
|
||||
current_literal.push('}');
|
||||
}
|
||||
_ => {
|
||||
let escape_start = self.pos;
|
||||
let escaped = match self.advance() {
|
||||
Some('n') => '\n',
|
||||
Some('r') => '\r',
|
||||
Some('t') => '\t',
|
||||
Some('\\') => '\\',
|
||||
Some('"') => '"',
|
||||
Some('0') => '\0',
|
||||
Some('\'') => '\'',
|
||||
Some(c) => {
|
||||
return Err(LexError {
|
||||
message: format!("Invalid escape sequence: \\{}", c),
|
||||
span: Span::new(escape_start - 1, self.pos),
|
||||
});
|
||||
}
|
||||
None => {
|
||||
return Err(LexError {
|
||||
message: "Unterminated multiline string".into(),
|
||||
span: Span::new(_start, self.pos),
|
||||
});
|
||||
}
|
||||
};
|
||||
current_literal.push(escaped);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some('{') => {
|
||||
// Interpolation (same as regular strings)
|
||||
if !current_literal.is_empty() {
|
||||
parts.push(StringPart::Literal(std::mem::take(&mut current_literal)));
|
||||
}
|
||||
|
||||
let mut expr_text = String::new();
|
||||
let mut brace_depth = 1;
|
||||
|
||||
loop {
|
||||
match self.advance() {
|
||||
Some('{') => {
|
||||
brace_depth += 1;
|
||||
expr_text.push('{');
|
||||
}
|
||||
Some('}') => {
|
||||
brace_depth -= 1;
|
||||
if brace_depth == 0 {
|
||||
break;
|
||||
}
|
||||
expr_text.push('}');
|
||||
}
|
||||
Some(c) => expr_text.push(c),
|
||||
None => {
|
||||
return Err(LexError {
|
||||
message: "Unterminated interpolation in multiline string"
|
||||
.into(),
|
||||
span: Span::new(_start, self.pos),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parts.push(StringPart::Expr(expr_text));
|
||||
}
|
||||
Some(c) => current_literal.push(c),
|
||||
None => {
|
||||
return Err(LexError {
|
||||
message: "Unterminated multiline string".into(),
|
||||
span: Span::new(_start, self.pos),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strip common leading whitespace from all lines
|
||||
let strip_indent = |s: &str| -> String {
|
||||
if s.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
let lines: Vec<&str> = s.split('\n').collect();
|
||||
// Find minimum indentation of non-empty lines
|
||||
let min_indent = lines
|
||||
.iter()
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.map(|line| line.len() - line.trim_start().len())
|
||||
.min()
|
||||
.unwrap_or(0);
|
||||
// Strip that indentation from each line
|
||||
lines
|
||||
.iter()
|
||||
.map(|line| {
|
||||
if line.len() >= min_indent {
|
||||
&line[min_indent..]
|
||||
} else {
|
||||
line.trim_start()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
};
|
||||
|
||||
// Strip trailing whitespace-only line before closing """
|
||||
let trim_trailing = |s: &mut String| {
|
||||
// Remove trailing spaces/tabs (indent before closing """)
|
||||
while s.ends_with(' ') || s.ends_with('\t') {
|
||||
s.pop();
|
||||
}
|
||||
// Remove the trailing newline
|
||||
if s.ends_with('\n') {
|
||||
s.pop();
|
||||
if s.ends_with('\r') {
|
||||
s.pop();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if parts.is_empty() {
|
||||
trim_trailing(&mut current_literal);
|
||||
let result = strip_indent(¤t_literal);
|
||||
return Ok(TokenKind::String(result));
|
||||
}
|
||||
|
||||
// Add remaining literal
|
||||
if !current_literal.is_empty() {
|
||||
trim_trailing(&mut current_literal);
|
||||
parts.push(StringPart::Literal(current_literal));
|
||||
}
|
||||
|
||||
// For interpolated multiline strings, strip indent from literal parts
|
||||
// First, collect all literal content to find min indent
|
||||
let mut all_text = String::new();
|
||||
for part in &parts {
|
||||
if let StringPart::Literal(lit) = part {
|
||||
all_text.push_str(lit);
|
||||
}
|
||||
}
|
||||
let lines: Vec<&str> = all_text.split('\n').collect();
|
||||
let min_indent = lines
|
||||
.iter()
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.map(|line| line.len() - line.trim_start().len())
|
||||
.min()
|
||||
.unwrap_or(0);
|
||||
|
||||
if min_indent > 0 {
|
||||
for part in &mut parts {
|
||||
if let StringPart::Literal(lit) = part {
|
||||
let stripped_lines: Vec<&str> = lit
|
||||
.split('\n')
|
||||
.map(|line| {
|
||||
if line.len() >= min_indent {
|
||||
&line[min_indent..]
|
||||
} else {
|
||||
line.trim_start()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
*lit = stripped_lines.join("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(TokenKind::InterpolatedString(parts))
|
||||
}
|
||||
|
||||
fn scan_char(&mut self, start: usize) -> Result<TokenKind, LexError> {
|
||||
let c = match self.advance() {
|
||||
Some('\\') => match self.advance() {
|
||||
@@ -743,6 +999,7 @@ impl<'a> Lexer<'a> {
|
||||
"effect" => TokenKind::Effect,
|
||||
"handler" => TokenKind::Handler,
|
||||
"run" => TokenKind::Run,
|
||||
"handle" => TokenKind::Handle,
|
||||
"resume" => TokenKind::Resume,
|
||||
"type" => TokenKind::Type,
|
||||
"import" => TokenKind::Import,
|
||||
@@ -753,6 +1010,7 @@ impl<'a> Lexer<'a> {
|
||||
"trait" => TokenKind::Trait,
|
||||
"impl" => TokenKind::Impl,
|
||||
"for" => TokenKind::For,
|
||||
"extern" => TokenKind::Extern,
|
||||
"is" => TokenKind::Is,
|
||||
"pure" => TokenKind::Pure,
|
||||
"total" => TokenKind::Total,
|
||||
@@ -761,6 +1019,8 @@ impl<'a> Lexer<'a> {
|
||||
"commutative" => TokenKind::Commutative,
|
||||
"where" => TokenKind::Where,
|
||||
"assume" => TokenKind::Assume,
|
||||
"and" => TokenKind::And,
|
||||
"or" => TokenKind::Or,
|
||||
"true" => TokenKind::Bool(true),
|
||||
"false" => TokenKind::Bool(false),
|
||||
_ => TokenKind::Ident(ident.to_string()),
|
||||
|
||||
1149
src/linter.rs
Normal file
1149
src/linter.rs
Normal file
File diff suppressed because it is too large
Load Diff
1198
src/lsp.rs
1198
src/lsp.rs
File diff suppressed because it is too large
Load Diff
2020
src/main.rs
2020
src/main.rs
File diff suppressed because it is too large
Load Diff
@@ -52,6 +52,7 @@ impl Module {
|
||||
Declaration::Let(l) => l.visibility == Visibility::Public,
|
||||
Declaration::Type(t) => t.visibility == Visibility::Public,
|
||||
Declaration::Trait(t) => t.visibility == Visibility::Public,
|
||||
Declaration::ExternFn(e) => e.visibility == Visibility::Public,
|
||||
// Effects, handlers, and impls are always public for now
|
||||
Declaration::Effect(_) | Declaration::Handler(_) | Declaration::Impl(_) => true,
|
||||
}
|
||||
@@ -279,6 +280,12 @@ impl ModuleLoader {
|
||||
}
|
||||
Declaration::Type(t) if t.visibility == Visibility::Public => {
|
||||
exports.insert(t.name.name.clone());
|
||||
// Also export constructors for ADT types
|
||||
if let crate::ast::TypeDef::Enum(variants) = &t.definition {
|
||||
for variant in variants {
|
||||
exports.insert(variant.name.name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
Declaration::Effect(e) => {
|
||||
// Effects are always exported
|
||||
@@ -288,6 +295,9 @@ impl ModuleLoader {
|
||||
// Handlers are always exported
|
||||
exports.insert(h.name.name.clone());
|
||||
}
|
||||
Declaration::ExternFn(e) if e.visibility == Visibility::Public => {
|
||||
exports.insert(e.name.name.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -305,6 +315,11 @@ impl ModuleLoader {
|
||||
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
|
||||
pub fn clear_cache(&mut self) {
|
||||
self.cache.clear();
|
||||
|
||||
777
src/package.rs
777
src/package.rs
@@ -6,6 +6,618 @@ use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
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)
|
||||
#[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
|
||||
pub fn find_project_root() -> Option<PathBuf> {
|
||||
let mut current = std::env::current_dir().ok()?;
|
||||
@@ -154,19 +803,139 @@ impl PackageManager {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Resolve dependencies and generate/update lock file
|
||||
let lock = self.resolve()?;
|
||||
|
||||
// Create packages directory
|
||||
fs::create_dir_all(&self.packages_dir)
|
||||
.map_err(|e| format!("Failed to create packages directory: {}", e))?;
|
||||
|
||||
println!("Installing {} dependencies...", manifest.dependencies.len());
|
||||
println!("Installing {} dependencies...", lock.packages.len());
|
||||
println!();
|
||||
|
||||
for (_name, dep) in &manifest.dependencies {
|
||||
self.install_dependency(dep)?;
|
||||
// Install from lock file for reproducibility
|
||||
for locked_pkg in &lock.packages {
|
||||
self.install_locked_package(locked_pkg, &manifest)?;
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
|
||||
395
src/parser.rs
395
src/parser.rs
@@ -238,13 +238,16 @@ impl Parser {
|
||||
|
||||
match self.peek_kind() {
|
||||
TokenKind::Fn => Ok(Declaration::Function(self.parse_function_decl(visibility, doc)?)),
|
||||
TokenKind::Extern => Ok(Declaration::ExternFn(self.parse_extern_fn_decl(visibility, doc)?)),
|
||||
TokenKind::Effect => Ok(Declaration::Effect(self.parse_effect_decl(doc)?)),
|
||||
TokenKind::Handler => Ok(Declaration::Handler(self.parse_handler_decl()?)),
|
||||
TokenKind::Type => Ok(Declaration::Type(self.parse_type_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::Impl => Ok(Declaration::Impl(self.parse_impl_decl()?)),
|
||||
_ => Err(self.error("Expected declaration (fn, effect, handler, type, trait, impl, or let)")),
|
||||
TokenKind::Run => Err(self.error("Bare 'run' expressions are not allowed at top level. Use 'let _ = run ...' or 'let result = run ...'")),
|
||||
TokenKind::Handle => Err(self.error("Bare 'handle' expressions are not allowed at top level. Use 'let _ = handle ...' or 'let result = handle ...'")),
|
||||
_ => Err(self.error("Expected declaration (fn, extern, effect, handler, type, trait, impl, or let)")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,6 +324,57 @@ impl Parser {
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse extern function declaration: extern fn name<T>(params): ReturnType = "jsName"
|
||||
fn parse_extern_fn_decl(&mut self, visibility: Visibility, doc: Option<String>) -> Result<ExternFnDecl, ParseError> {
|
||||
let start = self.current_span();
|
||||
self.expect(TokenKind::Extern)?;
|
||||
self.expect(TokenKind::Fn)?;
|
||||
|
||||
let name = self.parse_ident()?;
|
||||
|
||||
// Optional type parameters
|
||||
let type_params = if self.check(TokenKind::Lt) {
|
||||
self.parse_type_params()?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
self.expect(TokenKind::LParen)?;
|
||||
let params = self.parse_params()?;
|
||||
self.expect(TokenKind::RParen)?;
|
||||
|
||||
// Return type
|
||||
self.expect(TokenKind::Colon)?;
|
||||
let return_type = self.parse_type()?;
|
||||
|
||||
// Optional JS name override: = "jsName"
|
||||
let js_name = if self.check(TokenKind::Eq) {
|
||||
self.advance();
|
||||
match self.peek_kind() {
|
||||
TokenKind::String(s) => {
|
||||
let name = s.clone();
|
||||
self.advance();
|
||||
Some(name)
|
||||
}
|
||||
_ => return Err(self.error("Expected string literal for JS name in extern fn")),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let span = start.merge(self.previous_span());
|
||||
Ok(ExternFnDecl {
|
||||
visibility,
|
||||
doc,
|
||||
name,
|
||||
type_params,
|
||||
params,
|
||||
return_type,
|
||||
js_name,
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse effect declaration
|
||||
fn parse_effect_decl(&mut self, doc: Option<String>) -> Result<EffectDecl, ParseError> {
|
||||
let start = self.current_span();
|
||||
@@ -532,7 +586,14 @@ impl Parser {
|
||||
let start = self.current_span();
|
||||
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) {
|
||||
self.advance();
|
||||
@@ -837,6 +898,7 @@ impl Parser {
|
||||
/// Parse function parameters
|
||||
fn parse_params(&mut self) -> Result<Vec<Parameter>, ParseError> {
|
||||
let mut params = Vec::new();
|
||||
self.skip_newlines();
|
||||
|
||||
while !self.check(TokenKind::RParen) {
|
||||
let start = self.current_span();
|
||||
@@ -846,9 +908,11 @@ impl Parser {
|
||||
let span = start.merge(self.previous_span());
|
||||
|
||||
params.push(Parameter { name, typ, span });
|
||||
self.skip_newlines();
|
||||
|
||||
if !self.check(TokenKind::RParen) {
|
||||
self.expect(TokenKind::Comma)?;
|
||||
self.skip_newlines();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -871,7 +935,8 @@ impl Parser {
|
||||
Ok(effects)
|
||||
}
|
||||
|
||||
/// Parse behavioral properties: is pure, is total, is idempotent, etc.
|
||||
/// Parse behavioral properties: is pure, total, idempotent, etc.
|
||||
/// Supports: `is pure`, `is pure is total`, `is pure, total`, `is pure, is total`
|
||||
fn parse_behavioral_properties(&mut self) -> Result<Vec<BehavioralProperty>, ParseError> {
|
||||
let mut properties = Vec::new();
|
||||
|
||||
@@ -893,10 +958,16 @@ impl Parser {
|
||||
let property = self.parse_single_property()?;
|
||||
properties.push(property);
|
||||
|
||||
// Optional comma for multiple properties: is pure, is total
|
||||
if self.check(TokenKind::Comma) {
|
||||
// After first property, allow comma-separated list without repeating 'is'
|
||||
while self.check(TokenKind::Comma) {
|
||||
self.advance(); // consume comma
|
||||
// Allow optional 'is' after comma: `is pure, is total` or `is pure, total`
|
||||
if self.check(TokenKind::Is) {
|
||||
self.advance();
|
||||
}
|
||||
let property = self.parse_single_property()?;
|
||||
properties.push(property);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(properties)
|
||||
@@ -1543,6 +1614,7 @@ impl Parser {
|
||||
loop {
|
||||
let op = match self.peek_kind() {
|
||||
TokenKind::Plus => BinaryOp::Add,
|
||||
TokenKind::PlusPlus => BinaryOp::Concat,
|
||||
TokenKind::Minus => BinaryOp::Sub,
|
||||
_ => break,
|
||||
};
|
||||
@@ -1631,6 +1703,20 @@ impl Parser {
|
||||
} else if self.check(TokenKind::Dot) {
|
||||
let start = expr.span();
|
||||
self.advance();
|
||||
|
||||
// Check for tuple index access: expr.0, expr.1, etc.
|
||||
if let TokenKind::Int(n) = self.peek_kind() {
|
||||
let index = n as usize;
|
||||
self.advance();
|
||||
let span = start.merge(self.previous_span());
|
||||
expr = Expr::TupleIndex {
|
||||
object: Box::new(expr),
|
||||
index,
|
||||
span,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
let field = self.parse_ident()?;
|
||||
|
||||
// Check if this is an effect operation: Effect.operation(args)
|
||||
@@ -1666,11 +1752,14 @@ impl Parser {
|
||||
|
||||
fn parse_args(&mut self) -> Result<Vec<Expr>, ParseError> {
|
||||
let mut args = Vec::new();
|
||||
self.skip_newlines();
|
||||
|
||||
while !self.check(TokenKind::RParen) {
|
||||
args.push(self.parse_expr()?);
|
||||
self.skip_newlines();
|
||||
if !self.check(TokenKind::RParen) {
|
||||
self.expect(TokenKind::Comma)?;
|
||||
self.skip_newlines();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1742,6 +1831,7 @@ impl Parser {
|
||||
TokenKind::Let => self.parse_let_expr(),
|
||||
TokenKind::Fn => self.parse_lambda_expr(),
|
||||
TokenKind::Run => self.parse_run_expr(),
|
||||
TokenKind::Handle => self.parse_handle_expr(),
|
||||
TokenKind::Resume => self.parse_resume_expr(),
|
||||
|
||||
// Delimiters
|
||||
@@ -1759,6 +1849,7 @@ impl Parser {
|
||||
|
||||
let condition = Box::new(self.parse_expr()?);
|
||||
|
||||
self.skip_newlines();
|
||||
self.expect(TokenKind::Then)?;
|
||||
self.skip_newlines();
|
||||
let then_branch = Box::new(self.parse_expr()?);
|
||||
@@ -1872,12 +1963,38 @@ impl Parser {
|
||||
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) => {
|
||||
// Check if it starts with uppercase (constructor) or lowercase (variable)
|
||||
if name.chars().next().map_or(false, |c| c.is_uppercase()) {
|
||||
self.parse_constructor_pattern()
|
||||
self.parse_constructor_pattern_with_module(None)
|
||||
} else {
|
||||
let ident = self.parse_ident()?;
|
||||
// Check for module-qualified constructor: module.Constructor
|
||||
if self.check(TokenKind::Dot) {
|
||||
// Peek ahead to see if next is an uppercase identifier
|
||||
let dot_pos = self.pos;
|
||||
self.advance(); // skip dot
|
||||
if let TokenKind::Ident(next_name) = self.peek_kind() {
|
||||
if next_name
|
||||
.chars()
|
||||
.next()
|
||||
.map_or(false, |c| c.is_uppercase())
|
||||
{
|
||||
return self
|
||||
.parse_constructor_pattern_with_module(Some(ident));
|
||||
}
|
||||
}
|
||||
// Not a module-qualified constructor, backtrack
|
||||
self.pos = dot_pos;
|
||||
}
|
||||
Ok(Pattern::Var(ident))
|
||||
}
|
||||
}
|
||||
@@ -1887,25 +2004,40 @@ impl Parser {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_constructor_pattern(&mut self) -> Result<Pattern, ParseError> {
|
||||
let start = self.current_span();
|
||||
fn parse_constructor_pattern_with_module(
|
||||
&mut self,
|
||||
module: Option<Ident>,
|
||||
) -> Result<Pattern, ParseError> {
|
||||
let start = module
|
||||
.as_ref()
|
||||
.map(|m| m.span)
|
||||
.unwrap_or_else(|| self.current_span());
|
||||
let name = self.parse_ident()?;
|
||||
|
||||
if self.check(TokenKind::LParen) {
|
||||
self.advance();
|
||||
self.skip_newlines();
|
||||
let mut fields = Vec::new();
|
||||
while !self.check(TokenKind::RParen) {
|
||||
fields.push(self.parse_pattern()?);
|
||||
self.skip_newlines();
|
||||
if !self.check(TokenKind::RParen) {
|
||||
self.expect(TokenKind::Comma)?;
|
||||
self.skip_newlines();
|
||||
}
|
||||
}
|
||||
self.expect(TokenKind::RParen)?;
|
||||
let span = start.merge(self.previous_span());
|
||||
Ok(Pattern::Constructor { name, fields, span })
|
||||
} else {
|
||||
let span = name.span;
|
||||
Ok(Pattern::Constructor {
|
||||
module,
|
||||
name,
|
||||
fields,
|
||||
span,
|
||||
})
|
||||
} else {
|
||||
let span = start.merge(name.span);
|
||||
Ok(Pattern::Constructor {
|
||||
module,
|
||||
name,
|
||||
fields: Vec::new(),
|
||||
span,
|
||||
@@ -1916,12 +2048,15 @@ impl Parser {
|
||||
fn parse_tuple_pattern(&mut self) -> Result<Pattern, ParseError> {
|
||||
let start = self.current_span();
|
||||
self.expect(TokenKind::LParen)?;
|
||||
self.skip_newlines();
|
||||
|
||||
let mut elements = Vec::new();
|
||||
while !self.check(TokenKind::RParen) {
|
||||
elements.push(self.parse_pattern()?);
|
||||
self.skip_newlines();
|
||||
if !self.check(TokenKind::RParen) {
|
||||
self.expect(TokenKind::Comma)?;
|
||||
self.skip_newlines();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1962,7 +2097,14 @@ impl Parser {
|
||||
let start = self.current_span();
|
||||
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) {
|
||||
self.advance();
|
||||
@@ -2044,6 +2186,7 @@ impl Parser {
|
||||
|
||||
fn parse_lambda_params(&mut self) -> Result<Vec<Parameter>, ParseError> {
|
||||
let mut params = Vec::new();
|
||||
self.skip_newlines();
|
||||
|
||||
while !self.check(TokenKind::RParen) {
|
||||
let start = self.current_span();
|
||||
@@ -2059,9 +2202,11 @@ impl Parser {
|
||||
|
||||
let span = start.merge(self.previous_span());
|
||||
params.push(Parameter { name, typ, span });
|
||||
self.skip_newlines();
|
||||
|
||||
if !self.check(TokenKind::RParen) {
|
||||
self.expect(TokenKind::Comma)?;
|
||||
self.skip_newlines();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2102,6 +2247,40 @@ impl Parser {
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_handle_expr(&mut self) -> Result<Expr, ParseError> {
|
||||
let start = self.current_span();
|
||||
self.expect(TokenKind::Handle)?;
|
||||
|
||||
let expr = Box::new(self.parse_call_expr()?);
|
||||
|
||||
self.expect(TokenKind::With)?;
|
||||
self.expect(TokenKind::LBrace)?;
|
||||
self.skip_newlines();
|
||||
|
||||
let mut handlers = Vec::new();
|
||||
while !self.check(TokenKind::RBrace) {
|
||||
let effect = self.parse_ident()?;
|
||||
self.expect(TokenKind::Eq)?;
|
||||
let handler = self.parse_expr()?;
|
||||
handlers.push((effect, handler));
|
||||
|
||||
self.skip_newlines();
|
||||
if self.check(TokenKind::Comma) {
|
||||
self.advance();
|
||||
}
|
||||
self.skip_newlines();
|
||||
}
|
||||
|
||||
let end = self.current_span();
|
||||
self.expect(TokenKind::RBrace)?;
|
||||
|
||||
Ok(Expr::Run {
|
||||
expr,
|
||||
handlers,
|
||||
span: start.merge(end),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_resume_expr(&mut self) -> Result<Expr, ParseError> {
|
||||
let start = self.current_span();
|
||||
self.expect(TokenKind::Resume)?;
|
||||
@@ -2115,6 +2294,7 @@ impl Parser {
|
||||
fn parse_tuple_or_paren_expr(&mut self) -> Result<Expr, ParseError> {
|
||||
let start = self.current_span();
|
||||
self.expect(TokenKind::LParen)?;
|
||||
self.skip_newlines();
|
||||
|
||||
if self.check(TokenKind::RParen) {
|
||||
self.advance();
|
||||
@@ -2125,16 +2305,19 @@ impl Parser {
|
||||
}
|
||||
|
||||
let first = self.parse_expr()?;
|
||||
self.skip_newlines();
|
||||
|
||||
if self.check(TokenKind::Comma) {
|
||||
// Tuple
|
||||
let mut elements = vec![first];
|
||||
while self.check(TokenKind::Comma) {
|
||||
self.advance();
|
||||
self.skip_newlines();
|
||||
if self.check(TokenKind::RParen) {
|
||||
break;
|
||||
}
|
||||
elements.push(self.parse_expr()?);
|
||||
self.skip_newlines();
|
||||
}
|
||||
self.expect(TokenKind::RParen)?;
|
||||
let span = start.merge(self.previous_span());
|
||||
@@ -2160,12 +2343,39 @@ impl Parser {
|
||||
}));
|
||||
}
|
||||
|
||||
// Check if it's a record (ident: expr) or block
|
||||
// Check for record spread: { ...expr, field: val }
|
||||
if matches!(self.peek_kind(), TokenKind::DotDotDot) {
|
||||
return self.parse_record_expr_rest(start);
|
||||
}
|
||||
|
||||
// Check if it's a record (ident: expr or ident.path: expr) or block
|
||||
if matches!(self.peek_kind(), TokenKind::Ident(_)) {
|
||||
let lookahead = self.tokens.get(self.pos + 1).map(|t| &t.kind);
|
||||
if matches!(lookahead, Some(TokenKind::Colon)) {
|
||||
return self.parse_record_expr_rest(start);
|
||||
}
|
||||
// Check for deep path record: { ident.ident...: expr }
|
||||
if matches!(lookahead, Some(TokenKind::Dot)) {
|
||||
let mut look = self.pos + 2;
|
||||
loop {
|
||||
match self.tokens.get(look).map(|t| &t.kind) {
|
||||
Some(TokenKind::Ident(_)) => {
|
||||
look += 1;
|
||||
match self.tokens.get(look).map(|t| &t.kind) {
|
||||
Some(TokenKind::Colon) => {
|
||||
return self.parse_record_expr_rest(start);
|
||||
}
|
||||
Some(TokenKind::Dot) => {
|
||||
look += 1;
|
||||
continue;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It's a block
|
||||
@@ -2173,13 +2383,40 @@ impl Parser {
|
||||
}
|
||||
|
||||
fn parse_record_expr_rest(&mut self, start: Span) -> Result<Expr, ParseError> {
|
||||
let mut fields = Vec::new();
|
||||
let mut raw_fields: Vec<(Vec<Ident>, Expr)> = Vec::new();
|
||||
let mut spread = None;
|
||||
let mut has_deep_paths = false;
|
||||
|
||||
// Check for spread: { ...expr, ... }
|
||||
if self.check(TokenKind::DotDotDot) {
|
||||
self.advance(); // consume ...
|
||||
let spread_expr = self.parse_expr()?;
|
||||
spread = Some(Box::new(spread_expr));
|
||||
|
||||
self.skip_newlines();
|
||||
if self.check(TokenKind::Comma) {
|
||||
self.advance();
|
||||
}
|
||||
self.skip_newlines();
|
||||
}
|
||||
|
||||
while !self.check(TokenKind::RBrace) {
|
||||
let name = self.parse_ident()?;
|
||||
|
||||
// Check for dotted path: pos.x, pos.x.y, etc.
|
||||
let mut path = vec![name];
|
||||
while self.check(TokenKind::Dot) {
|
||||
self.advance(); // consume .
|
||||
let segment = self.parse_ident()?;
|
||||
path.push(segment);
|
||||
}
|
||||
if path.len() > 1 {
|
||||
has_deep_paths = true;
|
||||
}
|
||||
|
||||
self.expect(TokenKind::Colon)?;
|
||||
let value = self.parse_expr()?;
|
||||
fields.push((name, value));
|
||||
raw_fields.push((path, value));
|
||||
|
||||
self.skip_newlines();
|
||||
if self.check(TokenKind::Comma) {
|
||||
@@ -2190,7 +2427,120 @@ impl Parser {
|
||||
|
||||
self.expect(TokenKind::RBrace)?;
|
||||
let span = start.merge(self.previous_span());
|
||||
Ok(Expr::Record { fields, span })
|
||||
|
||||
if has_deep_paths {
|
||||
Self::desugar_deep_fields(spread, raw_fields, span)
|
||||
} else {
|
||||
// No deep paths — use flat fields directly (common case, no allocation overhead)
|
||||
let fields = raw_fields
|
||||
.into_iter()
|
||||
.map(|(mut path, value)| (path.remove(0), value))
|
||||
.collect();
|
||||
Ok(Expr::Record {
|
||||
spread,
|
||||
fields,
|
||||
span,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Desugar deep path record fields into nested record spread expressions.
|
||||
/// `{ ...base, pos.x: vx, pos.y: vy }` becomes `{ ...base, pos: { ...base.pos, x: vx, y: vy } }`
|
||||
fn desugar_deep_fields(
|
||||
spread: Option<Box<Expr>>,
|
||||
raw_fields: Vec<(Vec<Ident>, Expr)>,
|
||||
outer_span: Span,
|
||||
) -> Result<Expr, ParseError> {
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Group fields by first path segment, preserving order
|
||||
let mut groups: Vec<(String, Vec<(Vec<Ident>, Expr)>)> = Vec::new();
|
||||
let mut group_map: HashMap<String, usize> = HashMap::new();
|
||||
|
||||
for (path, value) in raw_fields {
|
||||
let key = path[0].name.clone();
|
||||
if let Some(&idx) = group_map.get(&key) {
|
||||
groups[idx].1.push((path, value));
|
||||
} else {
|
||||
group_map.insert(key.clone(), groups.len());
|
||||
groups.push((key, vec![(path, value)]));
|
||||
}
|
||||
}
|
||||
|
||||
let mut fields = Vec::new();
|
||||
for (_, group) in groups {
|
||||
let first_ident = group[0].0[0].clone();
|
||||
|
||||
let has_flat = group.iter().any(|(p, _)| p.len() == 1);
|
||||
let has_deep = group.iter().any(|(p, _)| p.len() > 1);
|
||||
|
||||
if has_flat && has_deep {
|
||||
return Err(ParseError {
|
||||
message: format!(
|
||||
"Field '{}' appears as both a direct field and a deep path prefix",
|
||||
first_ident.name
|
||||
),
|
||||
span: first_ident.span,
|
||||
});
|
||||
}
|
||||
|
||||
if has_flat {
|
||||
if group.len() > 1 {
|
||||
return Err(ParseError {
|
||||
message: format!("Duplicate field '{}'", first_ident.name),
|
||||
span: group[1].0[0].span,
|
||||
});
|
||||
}
|
||||
let (_, value) = group.into_iter().next().unwrap();
|
||||
fields.push((first_ident, value));
|
||||
} else {
|
||||
// Deep paths — create nested record with spread from parent
|
||||
let sub_spread = spread.as_ref().map(|s| {
|
||||
Box::new(Expr::Field {
|
||||
object: s.clone(),
|
||||
field: first_ident.clone(),
|
||||
span: first_ident.span,
|
||||
})
|
||||
});
|
||||
|
||||
// Strip first segment from all paths
|
||||
let sub_fields: Vec<(Vec<Ident>, Expr)> = group
|
||||
.into_iter()
|
||||
.map(|(mut path, value)| {
|
||||
path.remove(0);
|
||||
(path, value)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let has_nested_deep = sub_fields.iter().any(|(p, _)| p.len() > 1);
|
||||
if has_nested_deep {
|
||||
// Recursively desugar deeper paths
|
||||
let nested =
|
||||
Self::desugar_deep_fields(sub_spread, sub_fields, first_ident.span)?;
|
||||
fields.push((first_ident, nested));
|
||||
} else {
|
||||
// All sub-paths are single-segment — build Record directly
|
||||
let flat_fields: Vec<(Ident, Expr)> = sub_fields
|
||||
.into_iter()
|
||||
.map(|(mut path, value)| (path.remove(0), value))
|
||||
.collect();
|
||||
fields.push((
|
||||
first_ident.clone(),
|
||||
Expr::Record {
|
||||
spread: sub_spread,
|
||||
fields: flat_fields,
|
||||
span: first_ident.span,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Expr::Record {
|
||||
spread,
|
||||
fields,
|
||||
span: outer_span,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_block_rest(&mut self, start: Span) -> Result<Expr, ParseError> {
|
||||
@@ -2207,7 +2557,15 @@ impl Parser {
|
||||
// Let statement
|
||||
let let_start = self.current_span();
|
||||
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) {
|
||||
self.advance();
|
||||
@@ -2266,12 +2624,15 @@ impl Parser {
|
||||
fn parse_list_expr(&mut self) -> Result<Expr, ParseError> {
|
||||
let start = self.current_span();
|
||||
self.expect(TokenKind::LBracket)?;
|
||||
self.skip_newlines();
|
||||
|
||||
let mut elements = Vec::new();
|
||||
while !self.check(TokenKind::RBracket) {
|
||||
elements.push(self.parse_expr()?);
|
||||
self.skip_newlines();
|
||||
if !self.check(TokenKind::RBracket) {
|
||||
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()
|
||||
}
|
||||
706
src/symbol_table.rs
Normal file
706
src/symbol_table.rs
Normal file
@@ -0,0 +1,706 @@
|
||||
//! 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),
|
||||
Declaration::ExternFn(ext) => {
|
||||
let is_public = matches!(ext.visibility, Visibility::Public);
|
||||
let params: Vec<String> = ext
|
||||
.params
|
||||
.iter()
|
||||
.map(|p| format!("{}: {}", p.name.name, self.type_expr_to_string(&p.typ)))
|
||||
.collect();
|
||||
let sig = format!(
|
||||
"extern fn {}({}): {}",
|
||||
ext.name.name,
|
||||
params.join(", "),
|
||||
self.type_expr_to_string(&ext.return_type)
|
||||
);
|
||||
let mut symbol = self.new_symbol(
|
||||
ext.name.name.clone(),
|
||||
SymbolKind::Function,
|
||||
ext.span,
|
||||
Some(sig),
|
||||
is_public,
|
||||
);
|
||||
symbol.documentation = ext.doc.clone();
|
||||
let id = self.add_symbol(scope_idx, symbol);
|
||||
self.add_reference(id, ext.name.span, true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 { spread, fields, .. } => {
|
||||
if let Some(spread_expr) = spread {
|
||||
self.visit_expr(spread_expr, scope_idx);
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,9 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::ast::{
|
||||
self, BinaryOp, Declaration, EffectDecl, Expr, FunctionDecl, HandlerDecl, Ident, ImplDecl,
|
||||
ImportDecl, LetDecl, Literal, LiteralKind, MatchArm, Parameter, Pattern, Program, Span,
|
||||
Statement, TraitDecl, TypeDecl, TypeExpr, UnaryOp, VariantFields,
|
||||
self, BinaryOp, Declaration, EffectDecl, ExternFnDecl, Expr, FunctionDecl, HandlerDecl, Ident,
|
||||
ImplDecl, ImportDecl, LetDecl, Literal, LiteralKind, MatchArm, Parameter, Pattern, Program,
|
||||
Span, Statement, TraitDecl, TypeDecl, TypeExpr, UnaryOp, VariantFields,
|
||||
};
|
||||
use crate::diagnostics::{find_similar_names, format_did_you_mean, Diagnostic, ErrorCode, Severity};
|
||||
use crate::exhaustiveness::{check_exhaustiveness, missing_patterns_hint};
|
||||
@@ -335,11 +335,14 @@ fn references_params(expr: &Expr, params: &[&str]) -> bool {
|
||||
Statement::Expr(e) => references_params(e, params),
|
||||
}) || references_params(result, params)
|
||||
}
|
||||
Expr::Field { object, .. } => references_params(object, params),
|
||||
Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => references_params(object, params),
|
||||
Expr::Lambda { body, .. } => references_params(body, params),
|
||||
Expr::Tuple { elements, .. } => elements.iter().any(|e| references_params(e, params)),
|
||||
Expr::List { elements, .. } => elements.iter().any(|e| references_params(e, params)),
|
||||
Expr::Record { fields, .. } => fields.iter().any(|(_, e)| references_params(e, params)),
|
||||
Expr::Record { spread, fields, .. } => {
|
||||
spread.as_ref().is_some_and(|s| references_params(s, params))
|
||||
|| fields.iter().any(|(_, e)| references_params(e, params))
|
||||
}
|
||||
Expr::Match { scrutinee, arms, .. } => {
|
||||
references_params(scrutinee, params)
|
||||
|| arms.iter().any(|a| references_params(&a.body, params))
|
||||
@@ -516,10 +519,11 @@ fn has_recursive_calls(func_name: &str, body: &Expr) -> bool {
|
||||
Expr::Tuple { elements, .. } | Expr::List { elements, .. } => {
|
||||
elements.iter().any(|e| has_recursive_calls(func_name, e))
|
||||
}
|
||||
Expr::Record { fields, .. } => {
|
||||
fields.iter().any(|(_, e)| has_recursive_calls(func_name, e))
|
||||
Expr::Record { spread, fields, .. } => {
|
||||
spread.as_ref().is_some_and(|s| has_recursive_calls(func_name, s))
|
||||
|| fields.iter().any(|(_, e)| has_recursive_calls(func_name, e))
|
||||
}
|
||||
Expr::Field { object, .. } => has_recursive_calls(func_name, object),
|
||||
Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => has_recursive_calls(func_name, object),
|
||||
Expr::Let { value, body, .. } => {
|
||||
has_recursive_calls(func_name, value) || has_recursive_calls(func_name, body)
|
||||
}
|
||||
@@ -672,6 +676,7 @@ fn generate_auto_migration_expr(
|
||||
|
||||
// Build the record expression
|
||||
Some(Expr::Record {
|
||||
spread: None,
|
||||
fields: field_exprs,
|
||||
span,
|
||||
})
|
||||
@@ -759,6 +764,17 @@ impl TypeChecker {
|
||||
self.env.bindings.get(name)
|
||||
}
|
||||
|
||||
/// Get the inferred type of a binding as a display string (for LSP inlay hints)
|
||||
pub fn get_inferred_type(&self, name: &str) -> Option<String> {
|
||||
let scheme = self.env.bindings.get(name)?;
|
||||
let type_str = scheme.typ.to_string();
|
||||
// Skip unhelpful types
|
||||
if type_str == "<error>" || type_str.contains('?') {
|
||||
return None;
|
||||
}
|
||||
Some(type_str)
|
||||
}
|
||||
|
||||
/// Get auto-generated migrations from type checking
|
||||
/// Returns: type_name -> from_version -> migration_body
|
||||
pub fn get_auto_migrations(&self) -> &HashMap<String, HashMap<u32, Expr>> {
|
||||
@@ -965,6 +981,13 @@ impl TypeChecker {
|
||||
if !fields.is_empty() {
|
||||
self.env.bind(&name, TypeScheme::mono(Type::Record(fields)));
|
||||
}
|
||||
|
||||
// Also copy type definitions so imported types are usable
|
||||
for (type_name, type_def) in &module_checker.env.types {
|
||||
if !self.env.types.contains_key(type_name) {
|
||||
self.env.types.insert(type_name.clone(), type_def.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
ImportKind::Direct => {
|
||||
// Import a specific name directly
|
||||
@@ -1204,6 +1227,17 @@ impl TypeChecker {
|
||||
let trait_impl = self.collect_impl(impl_decl);
|
||||
self.env.trait_impls.push(trait_impl);
|
||||
}
|
||||
Declaration::ExternFn(ext) => {
|
||||
// Register extern fn type signature (like a regular function but no body)
|
||||
let param_types: Vec<Type> = ext
|
||||
.params
|
||||
.iter()
|
||||
.map(|p| self.resolve_type(&p.typ))
|
||||
.collect();
|
||||
let return_type = self.resolve_type(&ext.return_type);
|
||||
let fn_type = Type::function(param_types, return_type);
|
||||
self.env.bind(&ext.name.name, TypeScheme::mono(fn_type));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1525,7 +1559,7 @@ impl TypeChecker {
|
||||
// Use the declared type if present, otherwise use inferred
|
||||
let final_type = if let Some(ref type_expr) = let_decl.typ {
|
||||
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 {
|
||||
message: format!(
|
||||
"Variable '{}' has type {}, but declared type is {}: {}",
|
||||
@@ -1662,6 +1696,42 @@ impl TypeChecker {
|
||||
span,
|
||||
} => self.infer_field(object, field, *span),
|
||||
|
||||
Expr::TupleIndex {
|
||||
object,
|
||||
index,
|
||||
span,
|
||||
} => {
|
||||
let object_type = self.infer_expr(object);
|
||||
match &object_type {
|
||||
Type::Tuple(types) => {
|
||||
if *index < types.len() {
|
||||
types[*index].clone()
|
||||
} else {
|
||||
self.errors.push(TypeError {
|
||||
message: format!(
|
||||
"Tuple index {} out of bounds for tuple with {} elements",
|
||||
index,
|
||||
types.len()
|
||||
),
|
||||
span: *span,
|
||||
});
|
||||
Type::Error
|
||||
}
|
||||
}
|
||||
Type::Var(_) => Type::var(),
|
||||
_ => {
|
||||
self.errors.push(TypeError {
|
||||
message: format!(
|
||||
"Cannot use tuple index on non-tuple type {}",
|
||||
object_type
|
||||
),
|
||||
span: *span,
|
||||
});
|
||||
Type::Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Expr::Lambda {
|
||||
params,
|
||||
return_type,
|
||||
@@ -1697,7 +1767,11 @@ impl TypeChecker {
|
||||
span,
|
||||
} => self.infer_block(statements, result, *span),
|
||||
|
||||
Expr::Record { fields, span } => self.infer_record(fields, *span),
|
||||
Expr::Record {
|
||||
spread,
|
||||
fields,
|
||||
span,
|
||||
} => self.infer_record(spread.as_deref(), fields, *span),
|
||||
|
||||
Expr::Tuple { elements, span } => self.infer_tuple(elements, *span),
|
||||
|
||||
@@ -1736,7 +1810,7 @@ impl TypeChecker {
|
||||
match op {
|
||||
BinaryOp::Add => {
|
||||
// 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 {
|
||||
message: format!("Operands of '{}' must have same type: {}", op, e),
|
||||
span,
|
||||
@@ -1757,9 +1831,32 @@ impl TypeChecker {
|
||||
}
|
||||
}
|
||||
|
||||
BinaryOp::Concat => {
|
||||
// Concat (++) supports strings and lists
|
||||
if let Err(e) = unify_with_env(&left_type, &right_type, &self.env) {
|
||||
self.errors.push(TypeError {
|
||||
message: format!("Operands of '++' must have same type: {}", e),
|
||||
span,
|
||||
});
|
||||
}
|
||||
match &left_type {
|
||||
Type::String | Type::List(_) | Type::Var(_) => left_type,
|
||||
_ => {
|
||||
self.errors.push(TypeError {
|
||||
message: format!(
|
||||
"Operator '++' requires String or List operands, got {}",
|
||||
left_type
|
||||
),
|
||||
span,
|
||||
});
|
||||
Type::Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => {
|
||||
// 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 {
|
||||
message: format!("Operands of '{}' must have same type: {}", op, e),
|
||||
span,
|
||||
@@ -1783,7 +1880,7 @@ impl TypeChecker {
|
||||
|
||||
BinaryOp::Eq | BinaryOp::Ne => {
|
||||
// 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 {
|
||||
message: format!("Operands of '{}' must have same type: {}", op, e),
|
||||
span,
|
||||
@@ -1794,7 +1891,7 @@ impl TypeChecker {
|
||||
|
||||
BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge => {
|
||||
// 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 {
|
||||
message: format!("Operands of '{}' must have same type: {}", op, e),
|
||||
span,
|
||||
@@ -1805,13 +1902,13 @@ impl TypeChecker {
|
||||
|
||||
BinaryOp::And | BinaryOp::Or => {
|
||||
// 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 {
|
||||
message: format!("Left operand of '{}' must be Bool: {}", op, e),
|
||||
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 {
|
||||
message: format!("Right operand of '{}' must be Bool: {}", op, e),
|
||||
span: right.span(),
|
||||
@@ -1825,7 +1922,7 @@ impl TypeChecker {
|
||||
// right must be a function that accepts left's type
|
||||
let result_type = Type::var();
|
||||
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 {
|
||||
message: format!(
|
||||
"Pipe target must be a function accepting {}: {}",
|
||||
@@ -1857,7 +1954,7 @@ impl TypeChecker {
|
||||
}
|
||||
},
|
||||
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 {
|
||||
message: format!("Operator '!' requires Bool operand: {}", e),
|
||||
span,
|
||||
@@ -1872,6 +1969,17 @@ impl TypeChecker {
|
||||
let func_type = self.infer_expr(func);
|
||||
let arg_types: Vec<Type> = args.iter().map(|a| self.infer_expr(a)).collect();
|
||||
|
||||
// Propagate effects from callback arguments to enclosing scope
|
||||
for arg_type in &arg_types {
|
||||
if let Type::Function { effects, .. } = arg_type {
|
||||
for effect in &effects.effects {
|
||||
if self.inferring_effects {
|
||||
self.inferred_effects.insert(effect.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check property constraints from where clauses
|
||||
if let Expr::Var(func_id) = func {
|
||||
if let Some(constraints) = self.property_constraints.get(&func_id.name).cloned() {
|
||||
@@ -1908,7 +2016,7 @@ impl TypeChecker {
|
||||
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),
|
||||
Err(e) => {
|
||||
// Provide more detailed error message based on the type of mismatch
|
||||
@@ -1982,10 +2090,22 @@ impl TypeChecker {
|
||||
if let Some((_, field_type)) = fields.iter().find(|(n, _)| n == &operation.name) {
|
||||
// It's a function call on a module field
|
||||
let arg_types: Vec<Type> = args.iter().map(|a| self.infer_expr(a)).collect();
|
||||
|
||||
// Propagate effects from callback arguments to enclosing scope
|
||||
for arg_type in &arg_types {
|
||||
if let Type::Function { effects, .. } = arg_type {
|
||||
for effect in &effects.effects {
|
||||
if self.inferring_effects {
|
||||
self.inferred_effects.insert(effect.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result_type = Type::var();
|
||||
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 {
|
||||
message: format!(
|
||||
"Type mismatch in {}.{} call: {}",
|
||||
@@ -2041,6 +2161,17 @@ impl TypeChecker {
|
||||
// Check argument types
|
||||
let arg_types: Vec<Type> = args.iter().map(|a| self.infer_expr(a)).collect();
|
||||
|
||||
// Propagate effects from callback arguments to enclosing scope
|
||||
for arg_type in &arg_types {
|
||||
if let Type::Function { effects, .. } = arg_type {
|
||||
for effect in &effects.effects {
|
||||
if self.inferring_effects {
|
||||
self.inferred_effects.insert(effect.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if arg_types.len() != op.params.len() {
|
||||
self.errors.push(TypeError {
|
||||
message: format!(
|
||||
@@ -2057,7 +2188,7 @@ impl TypeChecker {
|
||||
for (i, (arg_type, (_, param_type))) in
|
||||
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 {
|
||||
message: format!(
|
||||
"Argument {} of '{}.{}' has type {}, expected {}: {}",
|
||||
@@ -2090,6 +2221,7 @@ impl TypeChecker {
|
||||
|
||||
fn infer_field(&mut self, object: &Expr, field: &Ident, span: Span) -> Type {
|
||||
let object_type = self.infer_expr(object);
|
||||
let object_type = self.env.expand_type_alias(&object_type);
|
||||
|
||||
match &object_type {
|
||||
Type::Record(fields) => match fields.iter().find(|(n, _)| n == &field.name) {
|
||||
@@ -2170,7 +2302,7 @@ impl TypeChecker {
|
||||
// Check return type if specified
|
||||
let ret_type = if let Some(rt) = return_type {
|
||||
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 {
|
||||
message: format!(
|
||||
"Lambda body type {} doesn't match declared {}: {}",
|
||||
@@ -2236,7 +2368,7 @@ impl TypeChecker {
|
||||
span: Span,
|
||||
) -> Type {
|
||||
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 {
|
||||
message: format!("If condition must be Bool, got {}: {}", cond_type, e),
|
||||
span: condition.span(),
|
||||
@@ -2246,7 +2378,7 @@ impl TypeChecker {
|
||||
let then_type = self.infer_expr(then_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),
|
||||
Err(e) => {
|
||||
self.errors.push(TypeError {
|
||||
@@ -2287,7 +2419,7 @@ impl TypeChecker {
|
||||
// Check guard if present
|
||||
if let Some(ref guard) = arm.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 {
|
||||
message: format!("Match guard must be Bool: {}", e),
|
||||
span: guard.span(),
|
||||
@@ -2303,7 +2435,7 @@ impl TypeChecker {
|
||||
match &result_type {
|
||||
None => result_type = Some(body_type),
|
||||
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 {
|
||||
message: format!(
|
||||
"Match arm has incompatible type: expected {}, got {}: {}",
|
||||
@@ -2353,7 +2485,7 @@ impl TypeChecker {
|
||||
|
||||
Pattern::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 {
|
||||
message: format!("Pattern literal type mismatch: {}", e),
|
||||
span: lit.span,
|
||||
@@ -2362,12 +2494,12 @@ impl TypeChecker {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
Pattern::Constructor { name, fields, span } => {
|
||||
Pattern::Constructor { name, fields, span, .. } => {
|
||||
// Look up constructor
|
||||
// For now, handle Option specially
|
||||
match name.name.as_str() {
|
||||
"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 {
|
||||
message: format!(
|
||||
"None pattern doesn't match type {}: {}",
|
||||
@@ -2380,7 +2512,7 @@ impl TypeChecker {
|
||||
}
|
||||
"Some" => {
|
||||
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 {
|
||||
message: format!(
|
||||
@@ -2409,7 +2541,7 @@ impl TypeChecker {
|
||||
|
||||
Pattern::Tuple { elements, span } => {
|
||||
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 {
|
||||
message: format!("Tuple pattern doesn't match type {}: {}", expected, e),
|
||||
span: *span,
|
||||
@@ -2459,7 +2591,7 @@ impl TypeChecker {
|
||||
|
||||
if let Some(type_expr) = typ {
|
||||
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 {
|
||||
message: format!(
|
||||
"Variable '{}' has type {}, but declared type is {}: {}",
|
||||
@@ -2480,12 +2612,47 @@ impl TypeChecker {
|
||||
self.infer_expr(result)
|
||||
}
|
||||
|
||||
fn infer_record(&mut self, fields: &[(Ident, Expr)], _span: Span) -> Type {
|
||||
let field_types: Vec<(String, Type)> = fields
|
||||
fn infer_record(
|
||||
&mut self,
|
||||
spread: Option<&Expr>,
|
||||
fields: &[(Ident, Expr)],
|
||||
span: Span,
|
||||
) -> Type {
|
||||
// Start with spread fields if present
|
||||
let mut field_types: Vec<(String, Type)> = if let Some(spread_expr) = spread {
|
||||
let spread_type = self.infer_expr(spread_expr);
|
||||
let spread_type = self.env.expand_type_alias(&spread_type);
|
||||
match spread_type {
|
||||
Type::Record(spread_fields) => spread_fields,
|
||||
_ => {
|
||||
self.errors.push(TypeError {
|
||||
message: format!(
|
||||
"Spread expression must be a record type, got {}",
|
||||
spread_type
|
||||
),
|
||||
span,
|
||||
});
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Apply explicit field overrides
|
||||
let explicit_types: Vec<(String, Type)> = fields
|
||||
.iter()
|
||||
.map(|(name, expr)| (name.name.clone(), self.infer_expr(expr)))
|
||||
.collect();
|
||||
|
||||
for (name, typ) in explicit_types {
|
||||
if let Some(existing) = field_types.iter_mut().find(|(n, _)| n == &name) {
|
||||
existing.1 = typ;
|
||||
} else {
|
||||
field_types.push((name, typ));
|
||||
}
|
||||
}
|
||||
|
||||
Type::Record(field_types)
|
||||
}
|
||||
|
||||
@@ -2502,7 +2669,7 @@ impl TypeChecker {
|
||||
let first_type = self.infer_expr(&elements[0]);
|
||||
for elem in &elements[1..] {
|
||||
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 {
|
||||
message: format!("List elements must have same type: {}", e),
|
||||
span,
|
||||
@@ -2808,7 +2975,7 @@ impl TypeChecker {
|
||||
// Check return type matches if specified
|
||||
if let Some(ref return_type_expr) = impl_method.return_type {
|
||||
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 {
|
||||
message: format!(
|
||||
"Method '{}' body has type {}, but declared return type is {}: {}",
|
||||
@@ -2851,6 +3018,9 @@ impl TypeChecker {
|
||||
"Option" if resolved_args.len() == 1 => {
|
||||
return Type::Option(Box::new(resolved_args[0].clone()));
|
||||
}
|
||||
"Map" if resolved_args.len() == 2 => {
|
||||
return Type::Map(Box::new(resolved_args[0].clone()), Box::new(resolved_args[1].clone()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
344
src/types.rs
344
src/types.rs
@@ -47,6 +47,8 @@ pub enum Type {
|
||||
List(Box<Type>),
|
||||
/// Option type (sugar for App(Option, [T]))
|
||||
Option(Box<Type>),
|
||||
/// Map type (sugar for App(Map, [K, V]))
|
||||
Map(Box<Type>, Box<Type>),
|
||||
/// Versioned type (e.g., User @v2)
|
||||
Versioned {
|
||||
base: Box<Type>,
|
||||
@@ -119,6 +121,7 @@ impl Type {
|
||||
Type::Tuple(elements) => elements.iter().any(|e| e.contains_var(var)),
|
||||
Type::Record(fields) => fields.iter().any(|(_, t)| t.contains_var(var)),
|
||||
Type::List(inner) | Type::Option(inner) => inner.contains_var(var),
|
||||
Type::Map(k, v) => k.contains_var(var) || v.contains_var(var),
|
||||
Type::Versioned { base, .. } => base.contains_var(var),
|
||||
_ => false,
|
||||
}
|
||||
@@ -158,6 +161,7 @@ impl Type {
|
||||
),
|
||||
Type::List(inner) => Type::List(Box::new(inner.apply(subst))),
|
||||
Type::Option(inner) => Type::Option(Box::new(inner.apply(subst))),
|
||||
Type::Map(k, v) => Type::Map(Box::new(k.apply(subst)), Box::new(v.apply(subst))),
|
||||
Type::Versioned { base, version } => Type::Versioned {
|
||||
base: Box::new(base.apply(subst)),
|
||||
version: version.clone(),
|
||||
@@ -208,6 +212,11 @@ impl Type {
|
||||
vars
|
||||
}
|
||||
Type::List(inner) | Type::Option(inner) => inner.free_vars(),
|
||||
Type::Map(k, v) => {
|
||||
let mut vars = k.free_vars();
|
||||
vars.extend(v.free_vars());
|
||||
vars
|
||||
}
|
||||
Type::Versioned { base, .. } => base.free_vars(),
|
||||
_ => HashSet::new(),
|
||||
}
|
||||
@@ -279,6 +288,7 @@ impl fmt::Display for Type {
|
||||
}
|
||||
Type::List(inner) => write!(f, "List<{}>", inner),
|
||||
Type::Option(inner) => write!(f, "Option<{}>", inner),
|
||||
Type::Map(k, v) => write!(f, "Map<{}, {}>", k, v),
|
||||
Type::Versioned { base, version } => {
|
||||
write!(f, "{} {}", base, version)
|
||||
}
|
||||
@@ -946,6 +956,46 @@ impl TypeEnv {
|
||||
params: vec![("path".to_string(), Type::String)],
|
||||
return_type: Type::Unit,
|
||||
},
|
||||
EffectOpDef {
|
||||
name: "copy".to_string(),
|
||||
params: vec![
|
||||
("source".to_string(), Type::String),
|
||||
("dest".to_string(), Type::String),
|
||||
],
|
||||
return_type: Type::Unit,
|
||||
},
|
||||
EffectOpDef {
|
||||
name: "glob".to_string(),
|
||||
params: vec![("pattern".to_string(), Type::String)],
|
||||
return_type: Type::List(Box::new(Type::String)),
|
||||
},
|
||||
EffectOpDef {
|
||||
name: "tryRead".to_string(),
|
||||
params: vec![("path".to_string(), Type::String)],
|
||||
return_type: Type::App {
|
||||
constructor: Box::new(Type::Named("Result".to_string())),
|
||||
args: vec![Type::String, Type::String],
|
||||
},
|
||||
},
|
||||
EffectOpDef {
|
||||
name: "tryWrite".to_string(),
|
||||
params: vec![
|
||||
("path".to_string(), Type::String),
|
||||
("content".to_string(), Type::String),
|
||||
],
|
||||
return_type: Type::App {
|
||||
constructor: Box::new(Type::Named("Result".to_string())),
|
||||
args: vec![Type::Unit, Type::String],
|
||||
},
|
||||
},
|
||||
EffectOpDef {
|
||||
name: "tryDelete".to_string(),
|
||||
params: vec![("path".to_string(), Type::String)],
|
||||
return_type: Type::App {
|
||||
constructor: Box::new(Type::Named("Result".to_string())),
|
||||
args: vec![Type::Unit, Type::String],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -1146,6 +1196,15 @@ impl TypeEnv {
|
||||
],
|
||||
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 {
|
||||
name: "assertNotEqual".to_string(),
|
||||
params: vec![
|
||||
@@ -1173,6 +1232,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
|
||||
// Connection is represented as Int (connection ID)
|
||||
let row_type = Type::Record(vec![]); // Dynamic record type
|
||||
@@ -1376,6 +1539,16 @@ impl TypeEnv {
|
||||
Type::Option(Box::new(Type::var())),
|
||||
),
|
||||
),
|
||||
(
|
||||
"findIndex".to_string(),
|
||||
Type::function(
|
||||
vec![
|
||||
Type::List(Box::new(Type::var())),
|
||||
Type::function(vec![Type::var()], Type::Bool),
|
||||
],
|
||||
Type::Option(Box::new(Type::Int)),
|
||||
),
|
||||
),
|
||||
(
|
||||
"any".to_string(),
|
||||
Type::function(
|
||||
@@ -1420,6 +1593,50 @@ impl TypeEnv {
|
||||
Type::Unit,
|
||||
),
|
||||
),
|
||||
(
|
||||
"sort".to_string(),
|
||||
Type::function(
|
||||
vec![Type::List(Box::new(Type::var()))],
|
||||
Type::List(Box::new(Type::var())),
|
||||
),
|
||||
),
|
||||
(
|
||||
"sortBy".to_string(),
|
||||
{
|
||||
let elem = Type::var();
|
||||
Type::function(
|
||||
vec![
|
||||
Type::List(Box::new(elem.clone())),
|
||||
Type::function(vec![elem.clone(), elem], Type::Int),
|
||||
],
|
||||
Type::List(Box::new(Type::var())),
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
"zip".to_string(),
|
||||
Type::function(
|
||||
vec![
|
||||
Type::List(Box::new(Type::var())),
|
||||
Type::List(Box::new(Type::var())),
|
||||
],
|
||||
Type::List(Box::new(Type::Tuple(vec![Type::var(), Type::var()]))),
|
||||
),
|
||||
),
|
||||
(
|
||||
"flatten".to_string(),
|
||||
Type::function(
|
||||
vec![Type::List(Box::new(Type::List(Box::new(Type::var()))))],
|
||||
Type::List(Box::new(Type::var())),
|
||||
),
|
||||
),
|
||||
(
|
||||
"contains".to_string(),
|
||||
Type::function(
|
||||
vec![Type::List(Box::new(Type::var())), Type::var()],
|
||||
Type::Bool,
|
||||
),
|
||||
),
|
||||
]);
|
||||
env.bind("List", TypeScheme::mono(list_module_type));
|
||||
|
||||
@@ -1495,6 +1712,14 @@ impl TypeEnv {
|
||||
"parseFloat".to_string(),
|
||||
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));
|
||||
|
||||
@@ -1654,6 +1879,73 @@ impl TypeEnv {
|
||||
]);
|
||||
env.bind("Option", TypeScheme::mono(option_module_type));
|
||||
|
||||
// Map module
|
||||
let map_v = || Type::var();
|
||||
let map_type = || Type::Map(Box::new(Type::String), Box::new(Type::var()));
|
||||
let map_module_type = Type::Record(vec![
|
||||
(
|
||||
"new".to_string(),
|
||||
Type::function(vec![], map_type()),
|
||||
),
|
||||
(
|
||||
"set".to_string(),
|
||||
Type::function(
|
||||
vec![map_type(), Type::String, map_v()],
|
||||
map_type(),
|
||||
),
|
||||
),
|
||||
(
|
||||
"get".to_string(),
|
||||
Type::function(
|
||||
vec![map_type(), Type::String],
|
||||
Type::Option(Box::new(map_v())),
|
||||
),
|
||||
),
|
||||
(
|
||||
"contains".to_string(),
|
||||
Type::function(vec![map_type(), Type::String], Type::Bool),
|
||||
),
|
||||
(
|
||||
"remove".to_string(),
|
||||
Type::function(vec![map_type(), Type::String], map_type()),
|
||||
),
|
||||
(
|
||||
"keys".to_string(),
|
||||
Type::function(vec![map_type()], Type::List(Box::new(Type::String))),
|
||||
),
|
||||
(
|
||||
"values".to_string(),
|
||||
Type::function(vec![map_type()], Type::List(Box::new(map_v()))),
|
||||
),
|
||||
(
|
||||
"size".to_string(),
|
||||
Type::function(vec![map_type()], Type::Int),
|
||||
),
|
||||
(
|
||||
"isEmpty".to_string(),
|
||||
Type::function(vec![map_type()], Type::Bool),
|
||||
),
|
||||
(
|
||||
"fromList".to_string(),
|
||||
Type::function(
|
||||
vec![Type::List(Box::new(Type::Tuple(vec![Type::String, map_v()])))],
|
||||
map_type(),
|
||||
),
|
||||
),
|
||||
(
|
||||
"toList".to_string(),
|
||||
Type::function(
|
||||
vec![map_type()],
|
||||
Type::List(Box::new(Type::Tuple(vec![Type::String, map_v()]))),
|
||||
),
|
||||
),
|
||||
(
|
||||
"merge".to_string(),
|
||||
Type::function(vec![map_type(), map_type()], map_type()),
|
||||
),
|
||||
]);
|
||||
env.bind("Map", TypeScheme::mono(map_module_type));
|
||||
|
||||
// Result module
|
||||
let result_type = Type::App {
|
||||
constructor: Box::new(Type::Named("Result".to_string())),
|
||||
@@ -1766,9 +2058,47 @@ impl TypeEnv {
|
||||
"round".to_string(),
|
||||
Type::function(vec![Type::var()], Type::Int),
|
||||
),
|
||||
(
|
||||
"sin".to_string(),
|
||||
Type::function(vec![Type::Float], Type::Float),
|
||||
),
|
||||
(
|
||||
"cos".to_string(),
|
||||
Type::function(vec![Type::Float], Type::Float),
|
||||
),
|
||||
(
|
||||
"atan2".to_string(),
|
||||
Type::function(vec![Type::Float, Type::Float], Type::Float),
|
||||
),
|
||||
]);
|
||||
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),
|
||||
),
|
||||
(
|
||||
"toFloat".to_string(),
|
||||
Type::function(vec![Type::Int], Type::Float),
|
||||
),
|
||||
]);
|
||||
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),
|
||||
),
|
||||
(
|
||||
"toInt".to_string(),
|
||||
Type::function(vec![Type::Float], Type::Int),
|
||||
),
|
||||
]);
|
||||
env.bind("Float", TypeScheme::mono(float_module_type));
|
||||
|
||||
env
|
||||
}
|
||||
|
||||
@@ -1852,6 +2182,9 @@ impl TypeEnv {
|
||||
Type::Option(inner) => {
|
||||
Type::Option(Box::new(self.expand_type_alias(inner)))
|
||||
}
|
||||
Type::Map(k, v) => {
|
||||
Type::Map(Box::new(self.expand_type_alias(k)), Box::new(self.expand_type_alias(v)))
|
||||
}
|
||||
Type::Versioned { base, version } => {
|
||||
Type::Versioned {
|
||||
base: Box::new(self.expand_type_alias(base)),
|
||||
@@ -1928,7 +2261,9 @@ pub fn unify(t1: &Type, t2: &Type) -> Result<Substitution, String> {
|
||||
// Function's required effects (e1) must be a subset of available effects (e2)
|
||||
// A pure function (empty effects) can be called anywhere
|
||||
// A function requiring {Logger} can be called in context with {Logger} or {Logger, Console}
|
||||
if !e1.is_subset(&e2) {
|
||||
// When expected effects (e2) are empty, it means "no constraint" (e.g., callback parameter)
|
||||
// so we allow any actual effects through
|
||||
if !e2.is_empty() && !e1.is_subset(&e2) {
|
||||
return Err(format!(
|
||||
"Effect mismatch: expected {{{}}}, got {{{}}}",
|
||||
e1, e2
|
||||
@@ -2010,6 +2345,13 @@ pub fn unify(t1: &Type, t2: &Type) -> Result<Substitution, String> {
|
||||
// Option
|
||||
(Type::Option(a), Type::Option(b)) => unify(a, b),
|
||||
|
||||
// Map
|
||||
(Type::Map(k1, v1), Type::Map(k2, v2)) => {
|
||||
let s1 = unify(k1, k2)?;
|
||||
let s2 = unify(&v1.apply(&s1), &v2.apply(&s1))?;
|
||||
Ok(s1.compose(&s2))
|
||||
}
|
||||
|
||||
// Versioned types
|
||||
(
|
||||
Type::Versioned {
|
||||
|
||||
234
stdlib/html.lux
234
stdlib/html.lux
@@ -11,13 +11,14 @@
|
||||
|
||||
// Html type represents a DOM structure
|
||||
// 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>>)
|
||||
| Text(String)
|
||||
| RawHtml(String)
|
||||
| Empty
|
||||
|
||||
// Attributes that can be applied to elements
|
||||
type Attr<M> =
|
||||
pub type Attr<M> =
|
||||
| Class(String)
|
||||
| Id(String)
|
||||
| Style(String, String)
|
||||
@@ -41,260 +42,289 @@ type Attr<M> =
|
||||
| OnKeyDown(fn(String): M)
|
||||
| OnKeyUp(fn(String): M)
|
||||
| DataAttr(String, String)
|
||||
| Attribute(String, String)
|
||||
|
||||
// ============================================================================
|
||||
// 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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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 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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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 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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
fn br<M>(): Html<M> =
|
||||
pub fn br<M>(): Html<M> =
|
||||
Element("br", [], [])
|
||||
|
||||
fn hr<M>(): Html<M> =
|
||||
pub fn hr<M>(): Html<M> =
|
||||
Element("hr", [], [])
|
||||
|
||||
// ============================================================================
|
||||
// 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)
|
||||
|
||||
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)
|
||||
|
||||
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 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)
|
||||
|
||||
fn input<M>(attrs: List<Attr<M>>): Html<M> =
|
||||
pub fn input<M>(attrs: List<Attr<M>>): Html<M> =
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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 builders - Media
|
||||
// ============================================================================
|
||||
|
||||
fn img<M>(attrs: List<Attr<M>>): Html<M> =
|
||||
pub fn img<M>(attrs: List<Attr<M>>): Html<M> =
|
||||
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)
|
||||
|
||||
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 builders - Document / Head elements
|
||||
// ============================================================================
|
||||
|
||||
pub fn meta<M>(attrs: List<Attr<M>>): Html<M> =
|
||||
Element("meta", attrs, [])
|
||||
|
||||
pub fn link<M>(attrs: List<Attr<M>>): Html<M> =
|
||||
Element("link", attrs, [])
|
||||
|
||||
pub fn script<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("script", attrs, children)
|
||||
|
||||
pub fn iframe<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("iframe", attrs, children)
|
||||
|
||||
pub fn figure<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("figure", attrs, children)
|
||||
|
||||
pub fn figcaption<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("figcaption", attrs, children)
|
||||
|
||||
// ============================================================================
|
||||
// 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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
// ============================================================================
|
||||
// Text and empty nodes
|
||||
// ============================================================================
|
||||
|
||||
fn text<M>(content: String): Html<M> =
|
||||
pub fn text<M>(content: String): Html<M> =
|
||||
Text(content)
|
||||
|
||||
fn empty<M>(): Html<M> =
|
||||
pub fn empty<M>(): Html<M> =
|
||||
Empty
|
||||
|
||||
// ============================================================================
|
||||
// Attribute helpers
|
||||
// ============================================================================
|
||||
|
||||
fn class<M>(name: String): Attr<M> =
|
||||
pub fn class<M>(name: String): Attr<M> =
|
||||
Class(name)
|
||||
|
||||
fn id<M>(name: String): Attr<M> =
|
||||
pub fn id<M>(name: String): Attr<M> =
|
||||
Id(name)
|
||||
|
||||
fn style<M>(property: String, value: String): Attr<M> =
|
||||
pub fn style<M>(property: String, value: String): Attr<M> =
|
||||
Style(property, value)
|
||||
|
||||
fn href<M>(url: String): Attr<M> =
|
||||
pub fn href<M>(url: String): Attr<M> =
|
||||
Href(url)
|
||||
|
||||
fn src<M>(url: String): Attr<M> =
|
||||
pub fn src<M>(url: String): Attr<M> =
|
||||
Src(url)
|
||||
|
||||
fn alt<M>(description: String): Attr<M> =
|
||||
pub fn alt<M>(description: String): Attr<M> =
|
||||
Alt(description)
|
||||
|
||||
fn inputType<M>(t: String): Attr<M> =
|
||||
pub fn inputType<M>(t: String): Attr<M> =
|
||||
Type(t)
|
||||
|
||||
fn value<M>(v: String): Attr<M> =
|
||||
pub fn value<M>(v: String): Attr<M> =
|
||||
Value(v)
|
||||
|
||||
fn placeholder<M>(p: String): Attr<M> =
|
||||
pub fn placeholder<M>(p: String): Attr<M> =
|
||||
Placeholder(p)
|
||||
|
||||
fn disabled<M>(d: Bool): Attr<M> =
|
||||
pub fn disabled<M>(d: Bool): Attr<M> =
|
||||
Disabled(d)
|
||||
|
||||
fn checked<M>(c: Bool): Attr<M> =
|
||||
pub fn checked<M>(c: Bool): Attr<M> =
|
||||
Checked(c)
|
||||
|
||||
fn name<M>(n: String): Attr<M> =
|
||||
pub fn name<M>(n: String): Attr<M> =
|
||||
Name(n)
|
||||
|
||||
fn onClick<M>(msg: M): Attr<M> =
|
||||
pub fn onClick<M>(msg: M): Attr<M> =
|
||||
OnClick(msg)
|
||||
|
||||
fn onInput<M>(h: fn(String): M): Attr<M> =
|
||||
pub fn onInput<M>(h: fn(String): M): Attr<M> =
|
||||
OnInput(h)
|
||||
|
||||
fn onSubmit<M>(msg: M): Attr<M> =
|
||||
pub fn onSubmit<M>(msg: M): Attr<M> =
|
||||
OnSubmit(msg)
|
||||
|
||||
fn onChange<M>(h: fn(String): M): Attr<M> =
|
||||
pub fn onChange<M>(h: fn(String): M): Attr<M> =
|
||||
OnChange(h)
|
||||
|
||||
fn onMouseEnter<M>(msg: M): Attr<M> =
|
||||
pub fn onMouseEnter<M>(msg: M): Attr<M> =
|
||||
OnMouseEnter(msg)
|
||||
|
||||
fn onMouseLeave<M>(msg: M): Attr<M> =
|
||||
pub fn onMouseLeave<M>(msg: M): Attr<M> =
|
||||
OnMouseLeave(msg)
|
||||
|
||||
fn onFocus<M>(msg: M): Attr<M> =
|
||||
pub fn onFocus<M>(msg: M): Attr<M> =
|
||||
OnFocus(msg)
|
||||
|
||||
fn onBlur<M>(msg: M): Attr<M> =
|
||||
pub fn onBlur<M>(msg: M): Attr<M> =
|
||||
OnBlur(msg)
|
||||
|
||||
fn onKeyDown<M>(h: fn(String): M): Attr<M> =
|
||||
pub fn onKeyDown<M>(h: fn(String): M): Attr<M> =
|
||||
OnKeyDown(h)
|
||||
|
||||
fn onKeyUp<M>(h: fn(String): M): Attr<M> =
|
||||
pub fn onKeyUp<M>(h: fn(String): M): Attr<M> =
|
||||
OnKeyUp(h)
|
||||
|
||||
fn data<M>(name: String, value: String): Attr<M> =
|
||||
pub fn data<M>(name: String, value: String): Attr<M> =
|
||||
DataAttr(name, value)
|
||||
|
||||
pub fn attr<M>(name: String, value: String): Attr<M> =
|
||||
Attribute(name, value)
|
||||
|
||||
pub fn rawHtml<M>(content: String): Html<M> =
|
||||
RawHtml(content)
|
||||
|
||||
// ============================================================================
|
||||
// Utility functions
|
||||
// ============================================================================
|
||||
|
||||
// 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
|
||||
|
||||
// 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 []
|
||||
|
||||
// ============================================================================
|
||||
@@ -302,7 +332,7 @@ fn attrIf<M>(condition: Bool, attr: Attr<M>): List<Attr<M>> =
|
||||
// ============================================================================
|
||||
|
||||
// Render an attribute to a string
|
||||
fn renderAttr<M>(attr: Attr<M>): String =
|
||||
pub fn renderAttr<M>(attr: Attr<M>): String =
|
||||
match attr {
|
||||
Class(name) => " class=\"" + name + "\"",
|
||||
Id(name) => " id=\"" + name + "\"",
|
||||
@@ -319,6 +349,7 @@ fn renderAttr<M>(attr: Attr<M>): String =
|
||||
Checked(false) => "",
|
||||
Name(n) => " name=\"" + n + "\"",
|
||||
DataAttr(name, value) => " data-" + name + "=\"" + value + "\"",
|
||||
Attribute(name, value) => " " + name + "=\"" + value + "\"",
|
||||
// Event handlers are ignored in static rendering
|
||||
OnClick(_) => "",
|
||||
OnInput(_) => "",
|
||||
@@ -333,33 +364,34 @@ fn renderAttr<M>(attr: Attr<M>): String =
|
||||
}
|
||||
|
||||
// Render attributes list to string
|
||||
fn renderAttrs<M>(attrs: List<Attr<M>>): String =
|
||||
List.foldl(attrs, "", fn(acc, attr) => acc + renderAttr(attr))
|
||||
pub fn renderAttrs<M>(attrs: List<Attr<M>>): String =
|
||||
List.fold(attrs, "", fn(acc, attr) => acc + renderAttr(attr))
|
||||
|
||||
// Self-closing tags
|
||||
fn isSelfClosing(tag: String): Bool =
|
||||
pub fn isSelfClosing(tag: String): Bool =
|
||||
tag == "br" || tag == "hr" || tag == "img" || tag == "input" ||
|
||||
tag == "meta" || tag == "link" || tag == "area" || tag == "base" ||
|
||||
tag == "col" || tag == "embed" || tag == "source" || tag == "track" || tag == "wbr"
|
||||
|
||||
// Render Html to string
|
||||
fn render<M>(html: Html<M>): String =
|
||||
pub fn render<M>(html: Html<M>): String =
|
||||
match html {
|
||||
Element(tag, attrs, children) => {
|
||||
let attrStr = renderAttrs(attrs)
|
||||
if isSelfClosing(tag) then
|
||||
"<" + tag + attrStr + " />"
|
||||
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 + ">"
|
||||
}
|
||||
},
|
||||
Text(content) => escapeHtml(content),
|
||||
RawHtml(content) => content,
|
||||
Empty => ""
|
||||
}
|
||||
|
||||
// Escape HTML special characters
|
||||
fn escapeHtml(s: String): String = {
|
||||
pub fn escapeHtml(s: String): String = {
|
||||
// Simple replacement - a full implementation would handle all entities
|
||||
let s1 = String.replace(s, "&", "&")
|
||||
let s2 = String.replace(s1, "<", "<")
|
||||
@@ -368,15 +400,47 @@ fn escapeHtml(s: String): String = {
|
||||
s4
|
||||
}
|
||||
|
||||
// Render a full HTML document
|
||||
fn document(title: String, headExtra: List<Html<M>>, bodyContent: List<Html<M>>): String = {
|
||||
// Render a full HTML document (basic)
|
||||
pub fn document(title: String, headExtra: List<Html<M>>, bodyContent: List<Html<M>>): String = {
|
||||
let headElements = List.concat([
|
||||
[Element("meta", [DataAttr("charset", "UTF-8")], [])],
|
||||
[Element("meta", [Name("viewport"), Value("width=device-width, initial-scale=1.0")], [])],
|
||||
[Element("meta", [Attribute("charset", "UTF-8")], [])],
|
||||
[Element("meta", [Name("viewport"), Attribute("content", "width=device-width, initial-scale=1.0")], [])],
|
||||
[Element("title", [], [Text(title)])],
|
||||
headExtra
|
||||
])
|
||||
let doc = Element("html", [DataAttr("lang", "en")], [
|
||||
let doc = Element("html", [Attribute("lang", "en")], [
|
||||
Element("head", [], headElements),
|
||||
Element("body", [], bodyContent)
|
||||
])
|
||||
"<!DOCTYPE html>\n" + render(doc)
|
||||
}
|
||||
|
||||
// Render a full HTML document with SEO meta tags
|
||||
pub fn seoDocument(
|
||||
title: String,
|
||||
description: String,
|
||||
url: String,
|
||||
ogImage: String,
|
||||
headExtra: List<Html<M>>,
|
||||
bodyContent: List<Html<M>>
|
||||
): String = {
|
||||
let headElements = List.concat([
|
||||
[Element("meta", [Attribute("charset", "UTF-8")], [])],
|
||||
[Element("meta", [Name("viewport"), Attribute("content", "width=device-width, initial-scale=1.0")], [])],
|
||||
[Element("title", [], [Text(title)])],
|
||||
[Element("meta", [Name("description"), Attribute("content", description)], [])],
|
||||
[Element("meta", [Attribute("property", "og:title"), Attribute("content", title)], [])],
|
||||
[Element("meta", [Attribute("property", "og:description"), Attribute("content", description)], [])],
|
||||
[Element("meta", [Attribute("property", "og:type"), Attribute("content", "website")], [])],
|
||||
[Element("meta", [Attribute("property", "og:url"), Attribute("content", url)], [])],
|
||||
[Element("meta", [Attribute("property", "og:image"), Attribute("content", ogImage)], [])],
|
||||
[Element("meta", [Name("twitter:card"), Attribute("content", "summary_large_image")], [])],
|
||||
[Element("meta", [Name("twitter:title"), Attribute("content", title)], [])],
|
||||
[Element("meta", [Name("twitter:description"), Attribute("content", description)], [])],
|
||||
[Element("link", [Attribute("rel", "canonical"), Href(url)], [])],
|
||||
headExtra
|
||||
])
|
||||
let doc = Element("html", [Attribute("lang", "en")], [
|
||||
Element("head", [], headElements),
|
||||
Element("body", [], bodyContent)
|
||||
])
|
||||
|
||||
562
stdlib/http.lux
562
stdlib/http.lux
@@ -42,6 +42,10 @@ fn httpNotFound(body: String): { status: Int, body: String } =
|
||||
fn httpServerError(body: String): { status: Int, body: String } =
|
||||
{ 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
|
||||
// ============================================================
|
||||
@@ -84,6 +88,54 @@ fn getPathSegment(path: String, index: Int): Option<String> = {
|
||||
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
|
||||
// ============================================================
|
||||
@@ -130,32 +182,518 @@ fn jsonMessage(text: String): String =
|
||||
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 } = {
|
||||
// if method == "GET" && path == "/" then httpOk("Welcome!")
|
||||
// if method == "GET" && path == "/" then httpOk("Home")
|
||||
// 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))),
|
||||
// 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} = {
|
||||
// HttpServer.listen(8080)
|
||||
// Console.print("Server running on port 8080")
|
||||
// serveLoop(5) // Handle 5 requests
|
||||
// }
|
||||
// For testing with a fixed number of requests:
|
||||
//
|
||||
// fn serveLoop(remaining: Int): Unit with {Console, HttpServer} = {
|
||||
// fn serverLoopN(remaining: Int): Unit with {HttpServer} = {
|
||||
// if remaining <= 0 then HttpServer.stop()
|
||||
// else {
|
||||
// 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)
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Static File Serving
|
||||
// ============================================================
|
||||
|
||||
// Serve a static file from disk
|
||||
pub fn serveStaticFile(basePath: String, requestPath: String): Response with {File} = {
|
||||
let filePath = basePath + requestPath
|
||||
if File.exists(filePath) then {
|
||||
let content = File.read(filePath)
|
||||
let mime = getMimeType(filePath)
|
||||
{ status: 200, headers: [("Content-Type", mime)], body: content }
|
||||
} else
|
||||
{ status: 404, headers: textHeaders(), body: "Not Found" }
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Form Body Parsing
|
||||
// ============================================================
|
||||
|
||||
// Parse URL-encoded form body (same format as query strings)
|
||||
pub fn parseFormBody(body: String): List<(String, String)> =
|
||||
parseQueryParams(body)
|
||||
|
||||
// Get a form field value by name
|
||||
pub fn getFormField(fields: List<(String, String)>, name: String): Option<String> =
|
||||
getParam(fields, name)
|
||||
|
||||
// ============================================================
|
||||
// Response Helpers
|
||||
// ============================================================
|
||||
|
||||
// Send a Response using HttpServer effect (convenience wrapper)
|
||||
pub fn sendResponse(resp: Response): Unit with {HttpServer} =
|
||||
HttpServer.respondWithHeaders(resp.status, resp.body, resp.headers)
|
||||
|
||||
// ============================================================
|
||||
// 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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user