Compare commits
28 Commits
7e76acab18
...
v0.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ee3050704 | |||
| 80b1276f9f | |||
| bd843d2219 | |||
| d76aa17b38 | |||
| c23d9c7078 | |||
| fffacd2467 | |||
| 2ae2c132e5 | |||
| 4909ff9fff | |||
| 8e788c8a9f | |||
| dbdd3cca57 | |||
| 3ac022c04a | |||
| 6bedd37ac7 | |||
| 2909bf14b6 | |||
| d8871acf7e | |||
| 73b5eee664 | |||
| 542255780d | |||
| bac63bab2a | |||
| db82ca1a1c | |||
| 98605d2b70 | |||
| e3b6f4322a | |||
| d26fd975d1 | |||
| 1fa599f856 | |||
| c2404a5ec1 | |||
| 19068ead96 | |||
| 44ea1eebb0 | |||
| 8c90d5a8dc | |||
| bc60f1c8f1 | |||
| 52e3876b81 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -4,6 +4,11 @@
|
||||
# Claude Code project instructions
|
||||
CLAUDE.md
|
||||
|
||||
# Build output
|
||||
_site/
|
||||
docs/*.html
|
||||
docs/*.css
|
||||
|
||||
# Test binaries
|
||||
hello
|
||||
test_rc
|
||||
|
||||
168
CLAUDE.md
Normal file
168
CLAUDE.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Lux Project Notes
|
||||
|
||||
## Development Environment
|
||||
|
||||
This is a **Nix environment**. Tools like `cargo`, `rustc`, `clippy`, etc. are not available in the base shell.
|
||||
|
||||
To run Rust/Cargo commands, use one of:
|
||||
```bash
|
||||
nix develop --command cargo test
|
||||
nix develop --command cargo build
|
||||
nix develop --command cargo clippy
|
||||
nix develop --command cargo fmt
|
||||
```
|
||||
|
||||
Or enter the development shell first:
|
||||
```bash
|
||||
nix develop
|
||||
# then run commands normally
|
||||
cargo test
|
||||
```
|
||||
|
||||
The `lux` binary can be run directly if already built:
|
||||
```bash
|
||||
./target/debug/lux test
|
||||
./target/release/lux <file.lux>
|
||||
```
|
||||
|
||||
For additional tools not in the dev shell:
|
||||
```bash
|
||||
nix-shell -p <program>
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
When making changes:
|
||||
1. **Always run tests**: `cargo check && cargo test` - fix all errors and warnings
|
||||
2. **Lint the Lux code**: `./target/release/lux lint` - fix warnings
|
||||
3. **Check Lux code**: `./target/release/lux check` - type check + lint in one pass
|
||||
4. **Format Lux code**: `./target/release/lux fmt` - auto-format all .lux files
|
||||
5. **Write tests**: Add tests to cover new code
|
||||
6. **Document features**: Provide documentation and tutorials for new features/frameworks
|
||||
7. **Fix language limitations**: If you encounter parser/type system limitations, fix them (without regressions on guarantees or speed)
|
||||
8. **Git commits**: Always use `--no-gpg-sign` flag
|
||||
|
||||
### Post-work checklist (run after each committable change)
|
||||
|
||||
**MANDATORY: Run the full validation script after every committable change:**
|
||||
```bash
|
||||
./scripts/validate.sh
|
||||
```
|
||||
|
||||
This script runs ALL of the following checks and will fail if any regress:
|
||||
1. `cargo check` — no Rust compilation errors
|
||||
2. `cargo test` — all Rust tests pass (currently 387)
|
||||
3. `cargo build --release` — release binary builds
|
||||
4. `lux test` on every package (path, frontmatter, xml, rss, markdown) — all 286 package tests pass
|
||||
5. `lux check` on every package — type checking + lint passes
|
||||
|
||||
If `validate.sh` is not available or you need to run manually:
|
||||
```bash
|
||||
nix develop --command cargo check # No Rust errors
|
||||
nix develop --command cargo test # All Rust tests pass
|
||||
nix develop --command cargo build --release # Build release binary
|
||||
cd ../packages/path && ../../lang/target/release/lux test # Package tests
|
||||
cd ../packages/frontmatter && ../../lang/target/release/lux test
|
||||
cd ../packages/xml && ../../lang/target/release/lux test
|
||||
cd ../packages/rss && ../../lang/target/release/lux test
|
||||
cd ../packages/markdown && ../../lang/target/release/lux test
|
||||
```
|
||||
|
||||
**Do NOT commit if any check fails.** Fix the issue first.
|
||||
|
||||
### Commit after every piece of work
|
||||
**After completing each logical unit of work, commit immediately.** Do not let changes accumulate uncommitted across multiple features. Each commit should be a single logical change (one feature, one bugfix, etc.). Use `--no-gpg-sign` flag for all commits.
|
||||
|
||||
**IMPORTANT: Always verify Lux code you write:**
|
||||
- Run with interpreter: `./target/release/lux file.lux`
|
||||
- Compile to binary: `./target/release/lux compile file.lux`
|
||||
- Both must work before claiming code is functional
|
||||
- The C backend has limited effect support (Console, File only - no HttpServer, Http, etc.)
|
||||
|
||||
## CLI Commands & Aliases
|
||||
|
||||
| Command | Alias | Description |
|
||||
|---------|-------|-------------|
|
||||
| `lux fmt` | `lux f` | Format .lux files |
|
||||
| `lux test` | `lux t` | Run test suite |
|
||||
| `lux check` | `lux k` | Type check + lint |
|
||||
| `lux lint` | `lux l` | Lint only (with `--explain` for detailed help) |
|
||||
| `lux serve` | `lux s` | Static file server |
|
||||
| `lux compile` | `lux c` | Compile to binary |
|
||||
|
||||
## Documenting Lux Language Errors
|
||||
|
||||
When working on any major task that involves writing Lux code, **document every language error, limitation, or surprising behavior** you encounter. This log is optimized for LLM consumption so future sessions can avoid repeating mistakes.
|
||||
|
||||
**File:** Maintain an `ISSUES.md` in the relevant project directory (e.g., `~/src/blu-site/ISSUES.md`).
|
||||
|
||||
**Format for each entry:**
|
||||
```markdown
|
||||
## Issue N: <Short descriptive title>
|
||||
|
||||
**Category**: Parser limitation | Type checker gap | Missing feature | Runtime error | Documentation gap
|
||||
**Severity**: High | Medium | Low
|
||||
**Status**: Open | **Fixed** (commit hash or version)
|
||||
|
||||
<1-2 sentence description of the problem>
|
||||
|
||||
**Reproduction:**
|
||||
```lux
|
||||
// Minimal code that triggers the issue
|
||||
```
|
||||
|
||||
**Error message:** `<exact error text>`
|
||||
|
||||
**Workaround:** <how to accomplish the goal despite the limitation>
|
||||
|
||||
**Fix:** <if fixed, what was changed and where>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Add new issues as you encounter them during any task
|
||||
- When a previously documented issue gets fixed, update its status to **Fixed** and note the commit/version
|
||||
- Remove entries that are no longer relevant (e.g., the feature was redesigned entirely)
|
||||
- Keep the summary table at the bottom of ISSUES.md in sync with the entries
|
||||
- Do NOT duplicate issues already documented -- check existing entries first
|
||||
|
||||
## Code Quality
|
||||
|
||||
- Fix all compiler warnings before committing
|
||||
- Ensure all tests pass (currently 387 tests)
|
||||
- Add new tests when adding features
|
||||
- Keep examples and documentation in sync
|
||||
|
||||
## Lux Language Notes
|
||||
|
||||
### Top-level expressions
|
||||
Bare `run` expressions are not allowed at top-level. You must wrap them in a `let` binding:
|
||||
```lux
|
||||
// WRONG: parse error
|
||||
run main() with {}
|
||||
|
||||
// CORRECT
|
||||
let output = run main() with {}
|
||||
```
|
||||
|
||||
### String methods
|
||||
Lux uses module-qualified function calls, not method syntax on primitives:
|
||||
```lux
|
||||
// WRONG: not valid syntax
|
||||
path.endsWith(".html")
|
||||
|
||||
// CORRECT
|
||||
String.endsWith(path, ".html")
|
||||
```
|
||||
|
||||
### Available String functions
|
||||
Key string functions (all in `String.` namespace):
|
||||
- `String.length(s)` - get length
|
||||
- `String.startsWith(s, prefix)` - check prefix
|
||||
- `String.endsWith(s, suffix)` - check suffix
|
||||
- `String.split(s, delimiter)` - split into list
|
||||
- `String.join(list, delimiter)` - join list
|
||||
- `String.substring(s, start, end)` - extract substring
|
||||
- `String.indexOf(s, needle)` - find position (returns Option)
|
||||
- `String.replace(s, old, new)` - replace occurrences
|
||||
- `String.trim(s)` - trim whitespace
|
||||
- `String.toLower(s)` / `String.toUpper(s)` - case conversion
|
||||
214
Cargo.lock
generated
214
Cargo.lock
generated
@@ -135,16 +135,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -235,7 +225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -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"
|
||||
@@ -552,16 +527,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]]
|
||||
@@ -843,23 +819,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 +864,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 +1118,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 +1134,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"
|
||||
@@ -1252,7 +1182,19 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.21.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
"rustls-webpki",
|
||||
"sct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1264,6 +1206,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 +1250,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 +1257,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 +1451,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",
|
||||
]
|
||||
|
||||
@@ -1545,7 +1475,7 @@ dependencies = [
|
||||
"getrandom 0.4.1",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1619,16 +1549,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 +1575,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 +1680,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 +1877,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.1"
|
||||
edition = "2021"
|
||||
description = "A functional programming language with first-class effects, schema evolution, and behavioral types"
|
||||
license = "MIT"
|
||||
@@ -13,7 +13,7 @@ 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"
|
||||
|
||||
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.
|
||||
19
README.md
19
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
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
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.
|
||||
@@ -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)
|
||||
Reset => Counter(0),
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// View - Render the UI
|
||||
// ============================================================================
|
||||
|
||||
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,8 +65,8 @@ 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 =
|
||||
@@ -115,8 +74,8 @@ fn showAttrs(attrs: List<Attr<Msg>>): String =
|
||||
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 =
|
||||
@@ -124,8 +83,8 @@ fn showChildren(children: List<Html<Msg>>, indent: Int): String =
|
||||
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 =
|
||||
@@ -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)
|
||||
1 + if leftDepth > rightDepth then leftDepth else rightDepth
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Example tree:
|
||||
// Node
|
||||
// / \
|
||||
// Node Leaf(5)
|
||||
// / \
|
||||
// Leaf(1) Leaf(2)
|
||||
|
||||
let myTree = Node(Node(Leaf(1), Leaf(2)), Leaf(5))
|
||||
|
||||
let 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,16 +10,14 @@ 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,13 +11,11 @@ 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))
|
||||
@@ -32,17 +27,12 @@ fn analyzeFile(path: String): Unit with {File, Console} = {
|
||||
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,7 +36,7 @@ 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} = {
|
||||
|
||||
@@ -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,19 +12,18 @@ 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,90 +1,71 @@
|
||||
// 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 a string)"),
|
||||
},
|
||||
None => Console.print(" origin: (not found)")
|
||||
None => Console.print(" origin: (not found)"),
|
||||
}
|
||||
match Json.get(json, "url") {
|
||||
Some(url) => match Json.asString(url) {
|
||||
Some(u) => Console.print(" URL: " + u),
|
||||
None => Console.print(" url: (not a string)")
|
||||
None => Console.print(" url: (not a string)"),
|
||||
},
|
||||
None => Console.print(" url: (not found)")
|
||||
None => Console.print(" url: (not found)"),
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("JSON parse error: " + e)
|
||||
Err(e) => Console.print("JSON parse error: " + e),
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("GET request failed: " + e)
|
||||
Err(e) => Console.print("GET request failed: " + e),
|
||||
}
|
||||
|
||||
Console.print("")
|
||||
Console.print("--- 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)")
|
||||
None => Console.print(" (no json field in response)"),
|
||||
},
|
||||
Err(e) => Console.print("JSON parse error: " + e)
|
||||
Err(e) => Console.print("JSON parse error: " + e),
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("POST request failed: " + e)
|
||||
Err(e) => Console.print("POST request failed: " + e),
|
||||
}
|
||||
|
||||
Console.print("")
|
||||
Console.print("--- 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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
@@ -75,9 +38,9 @@ fn matchParts(pathParts: List<String>, patternParts: List<String>): Bool = {
|
||||
let restPattern = Option.getOrElse(List.tail(patternParts), [])
|
||||
matchParts(restPath, restPattern)
|
||||
} else false
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
@@ -109,7 +66,7 @@ fn getUserHandler(path: String): { status: Int, body: String } = {
|
||||
let body = jsonObj(jsonStr("id", id) + "," + jsonStr("name", "User " + id))
|
||||
httpOk(body)
|
||||
},
|
||||
None => httpNotFound(jsonError("User not found"))
|
||||
None => httpNotFound(jsonError("User not found")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,23 +75,10 @@ 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.")
|
||||
|
||||
@@ -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")]))
|
||||
@@ -33,29 +13,16 @@ fn getUserHandler(path: String): { status: Int, body: String } = {
|
||||
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.")
|
||||
|
||||
@@ -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 a string)"),
|
||||
},
|
||||
None => Console.print(" name: (not found)")
|
||||
None => Console.print(" name: (not found)"),
|
||||
}
|
||||
|
||||
// Get int field
|
||||
match Json.get(json, "age") {
|
||||
Some(ageJson) => match Json.asInt(ageJson) {
|
||||
Some(a) => Console.print(" age: " + toString(a)),
|
||||
None => Console.print(" age: (not an int)")
|
||||
None => Console.print(" age: (not an int)"),
|
||||
},
|
||||
None => Console.print(" age: (not found)")
|
||||
None => Console.print(" age: (not found)"),
|
||||
}
|
||||
|
||||
// Get bool field
|
||||
match Json.get(json, "active") {
|
||||
Some(activeJson) => match Json.asBool(activeJson) {
|
||||
Some(a) => Console.print(" active: " + toString(a)),
|
||||
None => Console.print(" active: (not a bool)")
|
||||
None => Console.print(" active: (not a bool)"),
|
||||
},
|
||||
None => Console.print(" active: (not found)")
|
||||
None => Console.print(" active: (not found)"),
|
||||
}
|
||||
|
||||
// Get array field
|
||||
match Json.get(json, "scores") {
|
||||
Some(scoresJson) => match Json.asArray(scoresJson) {
|
||||
Some(arr) => {
|
||||
Console.print(" scores: " + toString(List.length(arr)) + " items")
|
||||
// Get first score
|
||||
match Json.getIndex(scoresJson, 0) {
|
||||
Some(firstJson) => match Json.asInt(firstJson) {
|
||||
Some(first) => Console.print(" first score: " + toString(first)),
|
||||
None => Console.print(" first score: (not an int)")
|
||||
None => Console.print(" first score: (not an int)"),
|
||||
},
|
||||
None => Console.print(" (no first element)")
|
||||
None => Console.print(" (no first element)"),
|
||||
}
|
||||
},
|
||||
None => Console.print(" scores: (not an array)")
|
||||
None => Console.print(" scores: (not an array)"),
|
||||
},
|
||||
None => Console.print(" scores: (not found)")
|
||||
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)")
|
||||
None => Console.print(" (not an object)"),
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("Parse error: " + e)
|
||||
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)
|
||||
@@ -42,31 +15,28 @@ fn insertUser(connId: Int, name: String, email: String): Int with {Console, Post
|
||||
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,76 +22,52 @@ fn genString(maxLen: Int): String with {Random} = {
|
||||
}
|
||||
|
||||
fn genStringHelper(len: Int): String with {Random} = {
|
||||
if len <= 0 then
|
||||
""
|
||||
else
|
||||
genChar() + genStringHelper(len - 1)
|
||||
if len <= 0 then "" else genChar() + genStringHelper(len - 1)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Test Runner State
|
||||
// ============================================================
|
||||
|
||||
fn printResult(name: String, passed: Bool, count: Int): Unit with {Console} = {
|
||||
if passed then
|
||||
Console.print(" PASS " + name + " (" + toString(count) + " tests)")
|
||||
else
|
||||
Console.print(" FAIL " + name)
|
||||
if passed then Console.print(" PASS " + name + " (" + toString(count) + " tests)") else Console.print(" FAIL " + name)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Property Tests
|
||||
// ============================================================
|
||||
|
||||
// Test: List reverse is involutive
|
||||
fn testReverseInvolutive(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("reverse(reverse(xs)) == xs", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.reverse(List.reverse(xs)) == xs then
|
||||
testReverseInvolutive(n - 1, count)
|
||||
else {
|
||||
if List.reverse(List.reverse(xs)) == xs then testReverseInvolutive(n - 1, count) else {
|
||||
printResult("reverse(reverse(xs)) == xs", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: List reverse preserves length
|
||||
fn testReverseLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("length(reverse(xs)) == length(xs)", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.length(List.reverse(xs)) == List.length(xs) then
|
||||
testReverseLength(n - 1, count)
|
||||
else {
|
||||
if List.length(List.reverse(xs)) == List.length(xs) then testReverseLength(n - 1, count) else {
|
||||
printResult("length(reverse(xs)) == length(xs)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: List map preserves length
|
||||
fn testMapLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("length(map(xs, f)) == length(xs)", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.length(List.map(xs, fn(x) => x * 2)) == List.length(xs) then
|
||||
testMapLength(n - 1, count)
|
||||
else {
|
||||
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)
|
||||
@@ -114,16 +75,13 @@ fn testConcatLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
} 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)
|
||||
@@ -131,16 +89,13 @@ fn testAddCommutative(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
} 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)
|
||||
@@ -149,16 +104,13 @@ fn testMulAssociative(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
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)
|
||||
@@ -166,67 +118,52 @@ fn testStringConcatLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
} 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 {
|
||||
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 {
|
||||
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 {
|
||||
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("========================================")
|
||||
Console.print(" Property-Based Testing Demo")
|
||||
@@ -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,18 +1,5 @@
|
||||
// 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()
|
||||
@@ -23,17 +10,13 @@ fn rollDice(count: Int): Unit with {Random, Console} = {
|
||||
}
|
||||
}
|
||||
|
||||
// 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,46 +1,31 @@
|
||||
// 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)
|
||||
@@ -50,7 +35,7 @@ fn main(): Unit with {Process, Console} = {
|
||||
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)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +17,7 @@ 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,7 +9,7 @@ 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 =
|
||||
@@ -30,7 +17,7 @@ fn showExpr(e: Expr): String =
|
||||
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} = {
|
||||
@@ -39,15 +26,10 @@ fn evalAndPrint(e: Expr): Unit with {Console} = {
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
// (2 + 3)
|
||||
let e1 = Add(Num(2), Num(3))
|
||||
evalAndPrint(e1)
|
||||
|
||||
// ((1 + 2) * (3 + 4))
|
||||
let e2 = Mul(Add(Num(1), Num(2)), Add(Num(3), Num(4)))
|
||||
evalAndPrint(e2)
|
||||
|
||||
// (10 - (2 * 3))
|
||||
let e3 = Sub(Num(10), Mul(Num(2), Num(3)))
|
||||
evalAndPrint(e3)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
// Factorial - compute n!
|
||||
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
|
||||
|
||||
// Recursive version
|
||||
fn factorial(n: Int): Int =
|
||||
if n <= 1 then 1
|
||||
else n * factorial(n - 1)
|
||||
|
||||
// Tail-recursive version (optimized)
|
||||
fn factorialTail(n: Int, acc: Int): Int =
|
||||
if n <= 1 then acc
|
||||
else factorialTail(n - 1, n * acc)
|
||||
fn factorialTail(n: Int, acc: Int): Int = if n <= 1 then acc else factorialTail(n - 1, n * acc)
|
||||
|
||||
fn factorial2(n: Int): Int = factorialTail(n, 1)
|
||||
|
||||
|
||||
@@ -1,22 +1,11 @@
|
||||
// FizzBuzz - print numbers 1-100, but:
|
||||
// - multiples of 3: print "Fizz"
|
||||
// - multiples of 5: print "Buzz"
|
||||
// - multiples of both: print "FizzBuzz"
|
||||
|
||||
fn fizzbuzz(n: Int): String =
|
||||
if n % 15 == 0 then "FizzBuzz"
|
||||
else if n % 3 == 0 then "Fizz"
|
||||
else if n % 5 == 0 then "Buzz"
|
||||
else toString(n)
|
||||
fn fizzbuzz(n: Int): String = if n % 15 == 0 then "FizzBuzz" else if n % 3 == 0 then "Fizz" else if n % 5 == 0 then "Buzz" else toString(n)
|
||||
|
||||
fn printFizzbuzz(i: Int, max: Int): Unit with {Console} =
|
||||
if i > max then ()
|
||||
else {
|
||||
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)
|
||||
Reset => Counter(0),
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// View - Returns HTML string for simplicity
|
||||
// ============================================================================
|
||||
|
||||
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.1\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.1";
|
||||
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.1";
|
||||
src = ./.;
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
|
||||
CARGO_BUILD_TARGET = "x86_64-unknown-linux-musl";
|
||||
CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static";
|
||||
|
||||
doCheck = false;
|
||||
|
||||
postInstall = ''
|
||||
$STRIP $out/bin/lux 2>/dev/null || true
|
||||
'';
|
||||
};
|
||||
|
||||
apps = {
|
||||
# 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";
|
||||
|
||||
512
projects/lux-compiler/lexer.lux
Normal file
512
projects/lux-compiler/lexer.lux
Normal file
@@ -0,0 +1,512 @@
|
||||
// Lux Lexer — Self-hosted lexer for the Lux language
|
||||
//
|
||||
// This is the first component of the Lux-in-Lux compiler.
|
||||
// It tokenizes Lux source code into a list of tokens.
|
||||
//
|
||||
// Design:
|
||||
// - Recursive descent character scanning
|
||||
// - Immutable state (ParseState tracks chars + position)
|
||||
// - Pattern matching for all token types
|
||||
|
||||
// === Token types ===
|
||||
|
||||
type TokenKind =
|
||||
// Literals
|
||||
| TkInt(Int)
|
||||
| TkFloat(String)
|
||||
| TkString(String)
|
||||
| TkChar(Char)
|
||||
| TkBool(Bool)
|
||||
// Identifiers
|
||||
| TkIdent(String)
|
||||
// Keywords
|
||||
| TkFn | TkLet | TkIf | TkThen | TkElse | TkMatch
|
||||
| TkWith | TkEffect | TkHandler | TkRun | TkResume
|
||||
| TkType | TkImport | TkPub | TkAs | TkFrom
|
||||
| TkTrait | TkImpl | TkFor
|
||||
// Behavioral
|
||||
| TkIs | TkPure | TkTotal | TkIdempotent
|
||||
| TkDeterministic | TkCommutative
|
||||
| TkWhere | TkAssume
|
||||
// Operators
|
||||
| TkPlus | TkMinus | TkStar | TkSlash | TkPercent
|
||||
| TkEq | TkEqEq | TkNe | TkLt | TkLe | TkGt | TkGe
|
||||
| TkAnd | TkOr | TkNot
|
||||
| TkPipe | TkPipeGt | TkArrow | TkThinArrow
|
||||
| TkDot | TkColon | TkColonColon | TkComma | TkSemi | TkAt
|
||||
// Delimiters
|
||||
| TkLParen | TkRParen | TkLBrace | TkRBrace
|
||||
| TkLBracket | TkRBracket
|
||||
// Special
|
||||
| TkUnderscore | TkNewline | TkEof
|
||||
// Doc comment
|
||||
| TkDocComment(String)
|
||||
|
||||
type Token =
|
||||
| Token(TokenKind, Int, Int) // kind, start, end
|
||||
|
||||
type LexState =
|
||||
| LexState(List<Char>, Int) // chars, position
|
||||
|
||||
type LexResult =
|
||||
| LexOk(Token, LexState)
|
||||
| LexErr(String, Int)
|
||||
|
||||
// === Character utilities ===
|
||||
|
||||
fn peek(state: LexState): Option<Char> =
|
||||
match state {
|
||||
LexState(chars, pos) => List.get(chars, pos)
|
||||
}
|
||||
|
||||
fn peekAt(state: LexState, offset: Int): Option<Char> =
|
||||
match state {
|
||||
LexState(chars, pos) => List.get(chars, pos + offset)
|
||||
}
|
||||
|
||||
fn advance(state: LexState): LexState =
|
||||
match state {
|
||||
LexState(chars, pos) => LexState(chars, pos + 1)
|
||||
}
|
||||
|
||||
fn position(state: LexState): Int =
|
||||
match state { LexState(_, pos) => pos }
|
||||
|
||||
fn isDigit(c: Char): Bool =
|
||||
c == '0' || c == '1' || c == '2' || c == '3' || c == '4' ||
|
||||
c == '5' || c == '6' || c == '7' || c == '8' || c == '9'
|
||||
|
||||
fn isAlpha(c: Char): Bool =
|
||||
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'
|
||||
|
||||
fn isAlphaNumeric(c: Char): Bool =
|
||||
isAlpha(c) || isDigit(c)
|
||||
|
||||
fn isWhitespace(c: Char): Bool =
|
||||
c == ' ' || c == '\t' || c == '\r'
|
||||
|
||||
// === Core lexing ===
|
||||
|
||||
fn skipLineComment(state: LexState): LexState =
|
||||
match peek(state) {
|
||||
None => state,
|
||||
Some(c) =>
|
||||
if c == '\n' then state
|
||||
else skipLineComment(advance(state))
|
||||
}
|
||||
|
||||
fn skipWhitespaceAndComments(state: LexState): LexState =
|
||||
match peek(state) {
|
||||
None => state,
|
||||
Some(c) =>
|
||||
if isWhitespace(c) then
|
||||
skipWhitespaceAndComments(advance(state))
|
||||
else if c == '/' then
|
||||
match peekAt(state, 1) {
|
||||
Some('/') =>
|
||||
// Check for doc comment (///)
|
||||
match peekAt(state, 2) {
|
||||
Some('/') => state, // Don't skip doc comments
|
||||
_ => skipWhitespaceAndComments(skipLineComment(advance(advance(state))))
|
||||
},
|
||||
_ => state
|
||||
}
|
||||
else state
|
||||
}
|
||||
|
||||
// Collect identifier characters
|
||||
fn collectIdent(state: LexState, acc: List<Char>): (List<Char>, LexState) =
|
||||
match peek(state) {
|
||||
None => (acc, state),
|
||||
Some(c) =>
|
||||
if isAlphaNumeric(c) then
|
||||
collectIdent(advance(state), List.concat(acc, [c]))
|
||||
else (acc, state)
|
||||
}
|
||||
|
||||
// Collect number characters (digits only)
|
||||
fn collectDigits(state: LexState, acc: List<Char>): (List<Char>, LexState) =
|
||||
match peek(state) {
|
||||
None => (acc, state),
|
||||
Some(c) =>
|
||||
if isDigit(c) then
|
||||
collectDigits(advance(state), List.concat(acc, [c]))
|
||||
else (acc, state)
|
||||
}
|
||||
|
||||
// Convert list of digit chars to int
|
||||
fn charsToInt(chars: List<Char>): Int =
|
||||
List.fold(chars, 0, fn(acc, c) => acc * 10 + charToDigit(c))
|
||||
|
||||
fn charToDigit(c: Char): Int =
|
||||
if c == '0' then 0
|
||||
else if c == '1' then 1
|
||||
else if c == '2' then 2
|
||||
else if c == '3' then 3
|
||||
else if c == '4' then 4
|
||||
else if c == '5' then 5
|
||||
else if c == '6' then 6
|
||||
else if c == '7' then 7
|
||||
else if c == '8' then 8
|
||||
else 9
|
||||
|
||||
// Map identifier string to keyword token or ident
|
||||
fn identToToken(name: String): TokenKind =
|
||||
if name == "fn" then TkFn
|
||||
else if name == "let" then TkLet
|
||||
else if name == "if" then TkIf
|
||||
else if name == "then" then TkThen
|
||||
else if name == "else" then TkElse
|
||||
else if name == "match" then TkMatch
|
||||
else if name == "with" then TkWith
|
||||
else if name == "effect" then TkEffect
|
||||
else if name == "handler" then TkHandler
|
||||
else if name == "run" then TkRun
|
||||
else if name == "resume" then TkResume
|
||||
else if name == "type" then TkType
|
||||
else if name == "true" then TkBool(true)
|
||||
else if name == "false" then TkBool(false)
|
||||
else if name == "import" then TkImport
|
||||
else if name == "pub" then TkPub
|
||||
else if name == "as" then TkAs
|
||||
else if name == "from" then TkFrom
|
||||
else if name == "trait" then TkTrait
|
||||
else if name == "impl" then TkImpl
|
||||
else if name == "for" then TkFor
|
||||
else if name == "is" then TkIs
|
||||
else if name == "pure" then TkPure
|
||||
else if name == "total" then TkTotal
|
||||
else if name == "idempotent" then TkIdempotent
|
||||
else if name == "deterministic" then TkDeterministic
|
||||
else if name == "commutative" then TkCommutative
|
||||
else if name == "where" then TkWhere
|
||||
else if name == "assume" then TkAssume
|
||||
else TkIdent(name)
|
||||
|
||||
// Lex a string literal (after opening quote consumed)
|
||||
fn lexStringBody(state: LexState, acc: List<Char>): (List<Char>, LexState) =
|
||||
match peek(state) {
|
||||
None => (acc, state),
|
||||
Some(c) =>
|
||||
if c == '"' then (acc, advance(state))
|
||||
else if c == '\\' then
|
||||
match peekAt(state, 1) {
|
||||
Some('n') => lexStringBody(advance(advance(state)), List.concat(acc, ['\n'])),
|
||||
Some('t') => lexStringBody(advance(advance(state)), List.concat(acc, ['\t'])),
|
||||
Some('\\') => lexStringBody(advance(advance(state)), List.concat(acc, ['\\'])),
|
||||
Some('"') => lexStringBody(advance(advance(state)), List.concat(acc, ['"'])),
|
||||
_ => lexStringBody(advance(state), List.concat(acc, [c]))
|
||||
}
|
||||
else lexStringBody(advance(state), List.concat(acc, [c]))
|
||||
}
|
||||
|
||||
// Lex a char literal (after opening quote consumed)
|
||||
fn lexCharLiteral(state: LexState): LexResult =
|
||||
let start = position(state) - 1;
|
||||
match peek(state) {
|
||||
None => LexErr("Unexpected end of input in char literal", start),
|
||||
Some(c) =>
|
||||
if c == '\\' then
|
||||
match peekAt(state, 1) {
|
||||
Some('n') =>
|
||||
match peekAt(state, 2) {
|
||||
Some('\'') => LexOk(Token(TkChar('\n'), start, position(state) + 3), advance(advance(advance(state)))),
|
||||
_ => LexErr("Expected closing quote", position(state))
|
||||
},
|
||||
Some('t') =>
|
||||
match peekAt(state, 2) {
|
||||
Some('\'') => LexOk(Token(TkChar('\t'), start, position(state) + 3), advance(advance(advance(state)))),
|
||||
_ => LexErr("Expected closing quote", position(state))
|
||||
},
|
||||
Some('\\') =>
|
||||
match peekAt(state, 2) {
|
||||
Some('\'') => LexOk(Token(TkChar('\\'), start, position(state) + 3), advance(advance(advance(state)))),
|
||||
_ => LexErr("Expected closing quote", position(state))
|
||||
},
|
||||
_ => LexErr("Unknown escape sequence", position(state))
|
||||
}
|
||||
else
|
||||
match peekAt(state, 1) {
|
||||
Some('\'') => LexOk(Token(TkChar(c), start, position(state) + 2), advance(advance(state))),
|
||||
_ => LexErr("Expected closing quote", position(state))
|
||||
}
|
||||
}
|
||||
|
||||
// Collect doc comment text (after /// consumed)
|
||||
fn collectDocComment(state: LexState, acc: List<Char>): (List<Char>, LexState) =
|
||||
match peek(state) {
|
||||
None => (acc, state),
|
||||
Some(c) =>
|
||||
if c == '\n' then (acc, state)
|
||||
else collectDocComment(advance(state), List.concat(acc, [c]))
|
||||
}
|
||||
|
||||
// Lex a single token
|
||||
fn lexToken(state: LexState): LexResult =
|
||||
let state = skipWhitespaceAndComments(state);
|
||||
let start = position(state);
|
||||
match peek(state) {
|
||||
None => LexOk(Token(TkEof, start, start), state),
|
||||
Some(c) =>
|
||||
if c == '\n' then
|
||||
LexOk(Token(TkNewline, start, start + 1), advance(state))
|
||||
// Numbers
|
||||
else if isDigit(c) then
|
||||
let result = collectDigits(state, []);
|
||||
match result {
|
||||
(digits, nextState) =>
|
||||
// Check for float
|
||||
match peek(nextState) {
|
||||
Some('.') =>
|
||||
match peekAt(nextState, 1) {
|
||||
Some(d) =>
|
||||
if isDigit(d) then
|
||||
let fracResult = collectDigits(advance(nextState), []);
|
||||
match fracResult {
|
||||
(fracDigits, finalState) =>
|
||||
let intPart = String.join(List.map(digits, fn(ch) => String.fromChar(ch)), "");
|
||||
let fracPart = String.join(List.map(fracDigits, fn(ch) => String.fromChar(ch)), "");
|
||||
LexOk(Token(TkFloat(intPart + "." + fracPart), start, position(finalState)), finalState)
|
||||
}
|
||||
else
|
||||
LexOk(Token(TkInt(charsToInt(digits)), start, position(nextState)), nextState),
|
||||
None =>
|
||||
LexOk(Token(TkInt(charsToInt(digits)), start, position(nextState)), nextState)
|
||||
},
|
||||
_ => LexOk(Token(TkInt(charsToInt(digits)), start, position(nextState)), nextState)
|
||||
}
|
||||
}
|
||||
// Identifiers and keywords
|
||||
else if isAlpha(c) then
|
||||
let result = collectIdent(state, []);
|
||||
match result {
|
||||
(chars, nextState) =>
|
||||
let name = String.join(List.map(chars, fn(ch) => String.fromChar(ch)), "");
|
||||
LexOk(Token(identToToken(name), start, position(nextState)), nextState)
|
||||
}
|
||||
// String literals
|
||||
else if c == '"' then
|
||||
let result = lexStringBody(advance(state), []);
|
||||
match result {
|
||||
(chars, nextState) =>
|
||||
let str = String.join(List.map(chars, fn(ch) => String.fromChar(ch)), "");
|
||||
LexOk(Token(TkString(str), start, position(nextState)), nextState)
|
||||
}
|
||||
// Char literals
|
||||
else if c == '\'' then
|
||||
lexCharLiteral(advance(state))
|
||||
// Doc comments (///)
|
||||
else if c == '/' then
|
||||
match peekAt(state, 1) {
|
||||
Some('/') =>
|
||||
match peekAt(state, 2) {
|
||||
Some('/') =>
|
||||
// Skip the "/// " prefix
|
||||
let docState = advance(advance(advance(state)));
|
||||
let docState = match peek(docState) {
|
||||
Some(' ') => advance(docState),
|
||||
_ => docState
|
||||
};
|
||||
let result = collectDocComment(docState, []);
|
||||
match result {
|
||||
(chars, nextState) =>
|
||||
let text = String.join(List.map(chars, fn(ch) => String.fromChar(ch)), "");
|
||||
LexOk(Token(TkDocComment(text), start, position(nextState)), nextState)
|
||||
},
|
||||
_ => LexOk(Token(TkSlash, start, start + 1), advance(state))
|
||||
},
|
||||
_ => LexOk(Token(TkSlash, start, start + 1), advance(state))
|
||||
}
|
||||
// Two-character operators
|
||||
else if c == '=' then
|
||||
match peekAt(state, 1) {
|
||||
Some('=') => LexOk(Token(TkEqEq, start, start + 2), advance(advance(state))),
|
||||
Some('>') => LexOk(Token(TkArrow, start, start + 2), advance(advance(state))),
|
||||
_ => LexOk(Token(TkEq, start, start + 1), advance(state))
|
||||
}
|
||||
else if c == '!' then
|
||||
match peekAt(state, 1) {
|
||||
Some('=') => LexOk(Token(TkNe, start, start + 2), advance(advance(state))),
|
||||
_ => LexOk(Token(TkNot, start, start + 1), advance(state))
|
||||
}
|
||||
else if c == '<' then
|
||||
match peekAt(state, 1) {
|
||||
Some('=') => LexOk(Token(TkLe, start, start + 2), advance(advance(state))),
|
||||
_ => LexOk(Token(TkLt, start, start + 1), advance(state))
|
||||
}
|
||||
else if c == '>' then
|
||||
match peekAt(state, 1) {
|
||||
Some('=') => LexOk(Token(TkGe, start, start + 2), advance(advance(state))),
|
||||
_ => LexOk(Token(TkGt, start, start + 1), advance(state))
|
||||
}
|
||||
else if c == '&' then
|
||||
match peekAt(state, 1) {
|
||||
Some('&') => LexOk(Token(TkAnd, start, start + 2), advance(advance(state))),
|
||||
_ => LexErr("Expected '&&'", start)
|
||||
}
|
||||
else if c == '|' then
|
||||
match peekAt(state, 1) {
|
||||
Some('|') => LexOk(Token(TkOr, start, start + 2), advance(advance(state))),
|
||||
Some('>') => LexOk(Token(TkPipeGt, start, start + 2), advance(advance(state))),
|
||||
_ => LexOk(Token(TkPipe, start, start + 1), advance(state))
|
||||
}
|
||||
else if c == '-' then
|
||||
match peekAt(state, 1) {
|
||||
Some('>') => LexOk(Token(TkThinArrow, start, start + 2), advance(advance(state))),
|
||||
_ => LexOk(Token(TkMinus, start, start + 1), advance(state))
|
||||
}
|
||||
else if c == ':' then
|
||||
match peekAt(state, 1) {
|
||||
Some(':') => LexOk(Token(TkColonColon, start, start + 2), advance(advance(state))),
|
||||
_ => LexOk(Token(TkColon, start, start + 1), advance(state))
|
||||
}
|
||||
// Single-character tokens
|
||||
else if c == '+' then LexOk(Token(TkPlus, start, start + 1), advance(state))
|
||||
else if c == '*' then LexOk(Token(TkStar, start, start + 1), advance(state))
|
||||
else if c == '%' then LexOk(Token(TkPercent, start, start + 1), advance(state))
|
||||
else if c == '.' then LexOk(Token(TkDot, start, start + 1), advance(state))
|
||||
else if c == ',' then LexOk(Token(TkComma, start, start + 1), advance(state))
|
||||
else if c == ';' then LexOk(Token(TkSemi, start, start + 1), advance(state))
|
||||
else if c == '@' then LexOk(Token(TkAt, start, start + 1), advance(state))
|
||||
else if c == '(' then LexOk(Token(TkLParen, start, start + 1), advance(state))
|
||||
else if c == ')' then LexOk(Token(TkRParen, start, start + 1), advance(state))
|
||||
else if c == '{' then LexOk(Token(TkLBrace, start, start + 1), advance(state))
|
||||
else if c == '}' then LexOk(Token(TkRBrace, start, start + 1), advance(state))
|
||||
else if c == '[' then LexOk(Token(TkLBracket, start, start + 1), advance(state))
|
||||
else if c == ']' then LexOk(Token(TkRBracket, start, start + 1), advance(state))
|
||||
else if c == '_' then
|
||||
// Check if it's just underscore or start of ident
|
||||
match peekAt(state, 1) {
|
||||
Some(next) =>
|
||||
if isAlphaNumeric(next) then
|
||||
let result = collectIdent(state, []);
|
||||
match result {
|
||||
(chars, nextState) =>
|
||||
let name = String.join(List.map(chars, fn(ch) => String.fromChar(ch)), "");
|
||||
LexOk(Token(TkIdent(name), start, position(nextState)), nextState)
|
||||
}
|
||||
else LexOk(Token(TkUnderscore, start, start + 1), advance(state)),
|
||||
None => LexOk(Token(TkUnderscore, start, start + 1), advance(state))
|
||||
}
|
||||
else LexErr("Unexpected character: " + String.fromChar(c), start)
|
||||
}
|
||||
|
||||
// Lex all tokens from source
|
||||
fn lexAll(state: LexState, acc: List<Token>): List<Token> =
|
||||
match lexToken(state) {
|
||||
LexErr(msg, pos) =>
|
||||
// On error, skip the character and continue
|
||||
List.concat(acc, [Token(TkEof, pos, pos)]),
|
||||
LexOk(token, nextState) =>
|
||||
match token {
|
||||
Token(TkEof, _, _) => List.concat(acc, [token]),
|
||||
Token(TkNewline, _, _) =>
|
||||
// Skip consecutive newlines
|
||||
lexAll(nextState, List.concat(acc, [token])),
|
||||
_ => lexAll(nextState, List.concat(acc, [token]))
|
||||
}
|
||||
}
|
||||
|
||||
// Public API: tokenize a source string
|
||||
fn tokenize(source: String): List<Token> =
|
||||
let chars = String.chars(source);
|
||||
let state = LexState(chars, 0);
|
||||
lexAll(state, [])
|
||||
|
||||
// === Token display ===
|
||||
|
||||
fn tokenKindToString(kind: TokenKind): String =
|
||||
match kind {
|
||||
TkInt(n) => "Int(" + toString(n) + ")",
|
||||
TkFloat(s) => "Float(" + s + ")",
|
||||
TkString(s) => "String(\"" + s + "\")",
|
||||
TkChar(c) => "Char('" + String.fromChar(c) + "')",
|
||||
TkBool(b) => if b then "true" else "false",
|
||||
TkIdent(name) => "Ident(" + name + ")",
|
||||
TkFn => "fn", TkLet => "let", TkIf => "if",
|
||||
TkThen => "then", TkElse => "else", TkMatch => "match",
|
||||
TkWith => "with", TkEffect => "effect", TkHandler => "handler",
|
||||
TkRun => "run", TkResume => "resume", TkType => "type",
|
||||
TkImport => "import", TkPub => "pub", TkAs => "as",
|
||||
TkFrom => "from", TkTrait => "trait", TkImpl => "impl", TkFor => "for",
|
||||
TkIs => "is", TkPure => "pure", TkTotal => "total",
|
||||
TkIdempotent => "idempotent", TkDeterministic => "deterministic",
|
||||
TkCommutative => "commutative", TkWhere => "where", TkAssume => "assume",
|
||||
TkPlus => "+", TkMinus => "-", TkStar => "*", TkSlash => "/",
|
||||
TkPercent => "%", TkEq => "=", TkEqEq => "==", TkNe => "!=",
|
||||
TkLt => "<", TkLe => "<=", TkGt => ">", TkGe => ">=",
|
||||
TkAnd => "&&", TkOr => "||", TkNot => "!",
|
||||
TkPipe => "|", TkPipeGt => "|>",
|
||||
TkArrow => "=>", TkThinArrow => "->",
|
||||
TkDot => ".", TkColon => ":", TkColonColon => "::",
|
||||
TkComma => ",", TkSemi => ";", TkAt => "@",
|
||||
TkLParen => "(", TkRParen => ")", TkLBrace => "{", TkRBrace => "}",
|
||||
TkLBracket => "[", TkRBracket => "]",
|
||||
TkUnderscore => "_", TkNewline => "\\n", TkEof => "EOF",
|
||||
TkDocComment(text) => "DocComment(\"" + text + "\")",
|
||||
_ => "?"
|
||||
}
|
||||
|
||||
fn tokenToString(token: Token): String =
|
||||
match token {
|
||||
Token(kind, start, end) =>
|
||||
tokenKindToString(kind) + " [" + toString(start) + ".." + toString(end) + "]"
|
||||
}
|
||||
|
||||
// === Tests ===
|
||||
|
||||
fn printTokens(tokens: List<Token>): Unit with {Console} =
|
||||
match List.head(tokens) {
|
||||
None => Console.print(""),
|
||||
Some(t) => {
|
||||
Console.print(" " + tokenToString(t));
|
||||
match List.tail(tokens) {
|
||||
Some(rest) => printTokens(rest),
|
||||
None => Console.print("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn testLexer(label: String, source: String): Unit with {Console} = {
|
||||
Console.print("--- " + label + " ---");
|
||||
Console.print(" Input: \"" + source + "\"");
|
||||
let tokens = tokenize(source);
|
||||
printTokens(tokens)
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Lux Self-Hosted Lexer ===");
|
||||
Console.print("");
|
||||
|
||||
// Basic tokens
|
||||
testLexer("numbers", "42 3");
|
||||
Console.print("");
|
||||
|
||||
// Identifiers and keywords
|
||||
testLexer("keywords", "fn main let x");
|
||||
Console.print("");
|
||||
|
||||
// Operators
|
||||
testLexer("operators", "a + b == c");
|
||||
Console.print("");
|
||||
|
||||
// String literal
|
||||
testLexer("string", "\"hello world\"");
|
||||
Console.print("");
|
||||
|
||||
// Function declaration
|
||||
testLexer("function", "fn add(a: Int, b: Int): Int = a + b");
|
||||
Console.print("");
|
||||
|
||||
// Behavioral properties
|
||||
testLexer("behavioral", "fn add(a: Int): Int is pure = a");
|
||||
Console.print("");
|
||||
|
||||
// Complex expression
|
||||
testLexer("complex", "let result = if x > 0 then x else 0 - x");
|
||||
Console.print("");
|
||||
|
||||
Console.print("=== Lexer test complete ===")
|
||||
}
|
||||
|
||||
let _ = run main() with {}
|
||||
213
scripts/release.sh
Executable file
213
scripts/release.sh
Executable file
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Lux Release Script
|
||||
# Builds a static binary, generates changelog, and creates a Gitea release.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/release.sh # auto-bump patch (0.2.0 → 0.2.1)
|
||||
# ./scripts/release.sh patch # same as above
|
||||
# ./scripts/release.sh minor # bump minor (0.2.0 → 0.3.0)
|
||||
# ./scripts/release.sh major # bump major (0.2.0 → 1.0.0)
|
||||
# ./scripts/release.sh v1.2.3 # explicit version
|
||||
#
|
||||
# Environment:
|
||||
# GITEA_TOKEN - API token for git.qrty.ink (prompted if not set)
|
||||
# GITEA_URL - Gitea instance URL (default: https://git.qrty.ink)
|
||||
|
||||
# cd to repo root (directory containing this script's parent)
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$SCRIPT_DIR/.."
|
||||
|
||||
GITEA_URL="${GITEA_URL:-https://git.qrty.ink}"
|
||||
REPO_OWNER="blu"
|
||||
REPO_NAME="lux"
|
||||
API_BASE="$GITEA_URL/api/v1"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { printf "${CYAN}::${NC} %s\n" "$1"; }
|
||||
ok() { printf "${GREEN}ok${NC} %s\n" "$1"; }
|
||||
warn() { printf "${YELLOW}!!${NC} %s\n" "$1"; }
|
||||
err() { printf "${RED}error:${NC} %s\n" "$1" >&2; exit 1; }
|
||||
|
||||
# --- Determine version ---
|
||||
CURRENT=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||
BUMP="${1:-patch}"
|
||||
|
||||
bump_version() {
|
||||
local ver="$1" part="$2"
|
||||
IFS='.' read -r major minor patch <<< "$ver"
|
||||
case "$part" in
|
||||
major) echo "$((major + 1)).0.0" ;;
|
||||
minor) echo "$major.$((minor + 1)).0" ;;
|
||||
patch) echo "$major.$minor.$((patch + 1))" ;;
|
||||
*) echo "$part" ;; # treat as explicit version
|
||||
esac
|
||||
}
|
||||
|
||||
case "$BUMP" in
|
||||
major|minor|patch)
|
||||
VERSION=$(bump_version "$CURRENT" "$BUMP")
|
||||
info "Bumping $BUMP: $CURRENT → $VERSION"
|
||||
;;
|
||||
*)
|
||||
# Explicit version — strip v prefix if present
|
||||
VERSION="${BUMP#v}"
|
||||
info "Explicit version: $VERSION"
|
||||
;;
|
||||
esac
|
||||
|
||||
TAG="v$VERSION"
|
||||
|
||||
# --- Check for clean working tree ---
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
warn "Working tree has uncommitted changes:"
|
||||
git status --short
|
||||
printf "\n"
|
||||
read -rp "Continue anyway? [y/N] " confirm
|
||||
[[ "$confirm" =~ ^[Yy]$ ]] || exit 1
|
||||
fi
|
||||
|
||||
# --- Check if tag already exists ---
|
||||
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||
err "Tag $TAG already exists. Choose a different version."
|
||||
fi
|
||||
|
||||
# --- Update version in source files ---
|
||||
if [ "$VERSION" != "$CURRENT" ]; then
|
||||
info "Updating version in Cargo.toml and flake.nix..."
|
||||
sed -i "0,/^version = \"$CURRENT\"/s//version = \"$VERSION\"/" Cargo.toml
|
||||
sed -i "s/version = \"$CURRENT\";/version = \"$VERSION\";/g" flake.nix
|
||||
sed -i "s/v$CURRENT/v$VERSION/g" flake.nix
|
||||
git add Cargo.toml flake.nix
|
||||
git commit --no-gpg-sign -m "chore: bump version to $VERSION"
|
||||
ok "Version updated and committed"
|
||||
fi
|
||||
|
||||
# --- Generate changelog ---
|
||||
info "Generating changelog..."
|
||||
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
||||
if [ -n "$LAST_TAG" ]; then
|
||||
RANGE="$LAST_TAG..HEAD"
|
||||
info "Changes since $LAST_TAG:"
|
||||
else
|
||||
RANGE="HEAD"
|
||||
info "First release — summarizing recent commits:"
|
||||
fi
|
||||
|
||||
CHANGELOG=$(git log "$RANGE" --pretty=format:"- %s" --no-merges 2>/dev/null | head -50 || true)
|
||||
if [ -z "$CHANGELOG" ]; then
|
||||
CHANGELOG="- Initial release"
|
||||
fi
|
||||
|
||||
# --- Build static binary ---
|
||||
info "Building static binary (nix build .#static)..."
|
||||
nix build .#static
|
||||
BINARY="result/bin/lux"
|
||||
if [ ! -f "$BINARY" ]; then
|
||||
err "Static binary not found at $BINARY"
|
||||
fi
|
||||
|
||||
BINARY_SIZE=$(ls -lh "$BINARY" | awk '{print $5}')
|
||||
BINARY_TYPE=$(file "$BINARY" | sed 's/.*: //')
|
||||
ok "Binary: $BINARY_SIZE, $BINARY_TYPE"
|
||||
|
||||
# --- Prepare release artifact ---
|
||||
ARTIFACT="/tmp/lux-${TAG}-linux-x86_64"
|
||||
cp "$BINARY" "$ARTIFACT"
|
||||
chmod +x "$ARTIFACT"
|
||||
|
||||
# --- Show release summary ---
|
||||
printf "\n"
|
||||
printf "${BOLD}═══ Release Summary ═══${NC}\n"
|
||||
printf "\n"
|
||||
printf " ${BOLD}Tag:${NC} %s\n" "$TAG"
|
||||
printf " ${BOLD}Binary:${NC} %s (%s)\n" "lux-${TAG}-linux-x86_64" "$BINARY_SIZE"
|
||||
printf " ${BOLD}Commit:${NC} %s\n" "$(git rev-parse --short HEAD)"
|
||||
printf "\n"
|
||||
printf "${BOLD}Changelog:${NC}\n"
|
||||
printf "%s\n" "$CHANGELOG"
|
||||
printf "\n"
|
||||
|
||||
# --- Confirm ---
|
||||
read -rp "Create release $TAG? [y/N] " confirm
|
||||
[[ "$confirm" =~ ^[Yy]$ ]] || { info "Aborted."; exit 0; }
|
||||
|
||||
# --- Get Gitea token ---
|
||||
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||
printf "\n"
|
||||
info "Gitea API token required (create at $GITEA_URL/user/settings/applications)"
|
||||
read -rsp "Token: " GITEA_TOKEN
|
||||
printf "\n"
|
||||
fi
|
||||
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
err "No token provided"
|
||||
fi
|
||||
|
||||
# --- Create and push tag ---
|
||||
info "Creating tag $TAG..."
|
||||
git tag -a "$TAG" -m "Release $TAG" --no-sign
|
||||
ok "Tag created"
|
||||
|
||||
info "Pushing tag to origin..."
|
||||
git push origin "$TAG"
|
||||
ok "Tag pushed"
|
||||
|
||||
# --- Create Gitea release ---
|
||||
info "Creating release on Gitea..."
|
||||
|
||||
RELEASE_BODY=$(printf "## Lux %s\n\n### Changes\n\n%s\n\n### Installation\n\n\`\`\`bash\ncurl -Lo lux %s/%s/%s/releases/download/%s/lux-linux-x86_64\nchmod +x lux\n./lux --version\n\`\`\`" \
|
||||
"$TAG" "$CHANGELOG" "$GITEA_URL" "$REPO_OWNER" "$REPO_NAME" "$TAG")
|
||||
|
||||
RELEASE_JSON=$(jq -n \
|
||||
--arg tag "$TAG" \
|
||||
--arg name "Lux $TAG" \
|
||||
--arg body "$RELEASE_BODY" \
|
||||
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')
|
||||
|
||||
RELEASE_RESPONSE=$(curl -s -X POST \
|
||||
"$API_BASE/repos/$REPO_OWNER/$REPO_NAME/releases" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$RELEASE_JSON")
|
||||
|
||||
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id // empty')
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "$RELEASE_RESPONSE" | jq . 2>/dev/null || echo "$RELEASE_RESPONSE"
|
||||
err "Failed to create release"
|
||||
fi
|
||||
ok "Release created (id: $RELEASE_ID)"
|
||||
|
||||
# --- Upload binary ---
|
||||
info "Uploading binary..."
|
||||
UPLOAD_RESPONSE=$(curl -s -X POST \
|
||||
"$API_BASE/repos/$REPO_OWNER/$REPO_NAME/releases/$RELEASE_ID/assets?name=lux-linux-x86_64" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@$ARTIFACT")
|
||||
|
||||
ASSET_NAME=$(echo "$UPLOAD_RESPONSE" | jq -r '.name // empty')
|
||||
if [ -z "$ASSET_NAME" ]; then
|
||||
echo "$UPLOAD_RESPONSE" | jq . 2>/dev/null || echo "$UPLOAD_RESPONSE"
|
||||
err "Failed to upload binary"
|
||||
fi
|
||||
ok "Binary uploaded: $ASSET_NAME"
|
||||
|
||||
# --- Done ---
|
||||
printf "\n"
|
||||
printf "${GREEN}${BOLD}Release $TAG published!${NC}\n"
|
||||
printf "\n"
|
||||
printf " ${BOLD}URL:${NC} %s/%s/%s/releases/tag/%s\n" "$GITEA_URL" "$REPO_OWNER" "$REPO_NAME" "$TAG"
|
||||
printf " ${BOLD}Download:${NC} %s/%s/%s/releases/download/%s/lux-linux-x86_64\n" "$GITEA_URL" "$REPO_OWNER" "$REPO_NAME" "$TAG"
|
||||
printf "\n"
|
||||
|
||||
# Cleanup
|
||||
rm -f "$ARTIFACT"
|
||||
73
scripts/validate.sh
Executable file
73
scripts/validate.sh
Executable file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Lux Full Validation Script
|
||||
# Runs all checks: Rust tests, package tests, type checking, formatting, linting.
|
||||
# Run after every committable change to ensure no regressions.
|
||||
|
||||
# cd to repo root (directory containing this script's parent)
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$SCRIPT_DIR/.."
|
||||
|
||||
LUX="$(pwd)/target/release/lux"
|
||||
PACKAGES_DIR="$(pwd)/../packages"
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
FAILED=0
|
||||
TOTAL=0
|
||||
|
||||
step() {
|
||||
TOTAL=$((TOTAL + 1))
|
||||
printf "${CYAN}[%d]${NC} %s... " "$TOTAL" "$1"
|
||||
}
|
||||
|
||||
ok() { printf "${GREEN}ok${NC} %s\n" "${1:-}"; }
|
||||
fail() { printf "${RED}FAIL${NC} %s\n" "${1:-}"; FAILED=$((FAILED + 1)); }
|
||||
|
||||
# --- Rust checks ---
|
||||
step "cargo check"
|
||||
if nix develop --command cargo check 2>&1 | grep -q "Finished"; then ok; else fail; fi
|
||||
|
||||
step "cargo test"
|
||||
OUTPUT=$(nix develop --command cargo test 2>&1 || true)
|
||||
RESULT=$(echo "$OUTPUT" | grep "test result:" || echo "no result")
|
||||
if echo "$RESULT" | grep -q "0 failed"; then ok "$RESULT"; else fail "$RESULT"; fi
|
||||
|
||||
# --- Build release binary ---
|
||||
step "cargo build --release"
|
||||
if nix develop --command cargo build --release 2>&1 | grep -q "Finished"; then ok; else fail; fi
|
||||
|
||||
# --- Package tests ---
|
||||
for pkg in path frontmatter xml rss markdown; do
|
||||
PKG_DIR="$PACKAGES_DIR/$pkg"
|
||||
if [ -d "$PKG_DIR" ]; then
|
||||
step "lux test ($pkg)"
|
||||
OUTPUT=$(cd "$PKG_DIR" && "$LUX" test 2>&1 || true)
|
||||
RESULT=$(echo "$OUTPUT" | grep "passed" | tail -1 || echo "no result")
|
||||
if echo "$RESULT" | grep -q "passed"; then ok "$RESULT"; else fail "$RESULT"; fi
|
||||
fi
|
||||
done
|
||||
|
||||
# --- Lux check on packages ---
|
||||
for pkg in path frontmatter xml rss markdown; do
|
||||
PKG_DIR="$PACKAGES_DIR/$pkg"
|
||||
if [ -d "$PKG_DIR" ]; then
|
||||
step "lux check ($pkg)"
|
||||
OUTPUT=$(cd "$PKG_DIR" && "$LUX" check 2>&1 || true)
|
||||
RESULT=$(echo "$OUTPUT" | grep "passed" | tail -1 || echo "no result")
|
||||
if echo "$RESULT" | grep -q "passed"; then ok; else fail "$RESULT"; fi
|
||||
fi
|
||||
done
|
||||
|
||||
# --- Summary ---
|
||||
printf "\n${BOLD}═══ Validation Summary ═══${NC}\n"
|
||||
if [ $FAILED -eq 0 ]; then
|
||||
printf "${GREEN}All %d checks passed.${NC}\n" "$TOTAL"
|
||||
else
|
||||
printf "${RED}%d/%d checks failed.${NC}\n" "$FAILED" "$TOTAL"
|
||||
exit 1
|
||||
fi
|
||||
@@ -499,6 +499,12 @@ pub enum Expr {
|
||||
field: Ident,
|
||||
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>,
|
||||
@@ -563,6 +569,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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1268,6 +1268,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, ..
|
||||
} => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -598,6 +598,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 {}",
|
||||
@@ -728,7 +731,7 @@ 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) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"").replace('{', "\\{").replace('}', "\\}")),
|
||||
LiteralKind::Char(c) => format!("'{}'", c),
|
||||
LiteralKind::Bool(b) => b.to_string(),
|
||||
LiteralKind::Unit => "()".to_string(),
|
||||
|
||||
@@ -95,6 +95,10 @@ pub enum BuiltinFn {
|
||||
StringLastIndexOf,
|
||||
StringRepeat,
|
||||
|
||||
// Int/Float operations
|
||||
IntToString,
|
||||
FloatToString,
|
||||
|
||||
// JSON operations
|
||||
JsonParse,
|
||||
JsonStringify,
|
||||
@@ -1071,6 +1075,18 @@ impl Interpreter {
|
||||
]));
|
||||
env.define("Math", math_module);
|
||||
|
||||
// Int module
|
||||
let int_module = Value::Record(HashMap::from([
|
||||
("toString".to_string(), Value::Builtin(BuiltinFn::IntToString)),
|
||||
]));
|
||||
env.define("Int", int_module);
|
||||
|
||||
// Float module
|
||||
let float_module = Value::Record(HashMap::from([
|
||||
("toString".to_string(), Value::Builtin(BuiltinFn::FloatToString)),
|
||||
]));
|
||||
env.define("Float", float_module);
|
||||
|
||||
// JSON module
|
||||
let json_module = Value::Record(HashMap::from([
|
||||
("parse".to_string(), Value::Builtin(BuiltinFn::JsonParse)),
|
||||
@@ -1415,6 +1431,34 @@ impl Interpreter {
|
||||
}
|
||||
}
|
||||
|
||||
Expr::TupleIndex {
|
||||
object,
|
||||
index,
|
||||
span,
|
||||
} => {
|
||||
let obj_val = self.eval_expr(object, env)?;
|
||||
match obj_val {
|
||||
Value::Tuple(elements) => {
|
||||
if *index < elements.len() {
|
||||
Ok(EvalResult::Value(elements[*index].clone()))
|
||||
} else {
|
||||
Err(RuntimeError {
|
||||
message: format!(
|
||||
"Tuple index {} out of bounds for tuple with {} elements",
|
||||
index,
|
||||
elements.len()
|
||||
),
|
||||
span: Some(*span),
|
||||
})
|
||||
}
|
||||
}
|
||||
_ => Err(RuntimeError {
|
||||
message: format!("Cannot use tuple index on {}", obj_val.type_name()),
|
||||
span: Some(*span),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
Expr::Lambda { params, body, .. } => {
|
||||
let closure = Closure {
|
||||
params: params.iter().map(|p| p.name.name.clone()).collect(),
|
||||
@@ -1610,6 +1654,7 @@ impl Interpreter {
|
||||
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a < b)),
|
||||
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a < b)),
|
||||
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a < b)),
|
||||
(Value::Char(a), Value::Char(b)) => Ok(Value::Bool(a < b)),
|
||||
(l, r) => Err(RuntimeError {
|
||||
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
|
||||
span: Some(span),
|
||||
@@ -1619,6 +1664,7 @@ impl Interpreter {
|
||||
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a <= b)),
|
||||
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a <= b)),
|
||||
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a <= b)),
|
||||
(Value::Char(a), Value::Char(b)) => Ok(Value::Bool(a <= b)),
|
||||
(l, r) => Err(RuntimeError {
|
||||
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
|
||||
span: Some(span),
|
||||
@@ -1628,6 +1674,7 @@ impl Interpreter {
|
||||
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a > b)),
|
||||
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a > b)),
|
||||
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a > b)),
|
||||
(Value::Char(a), Value::Char(b)) => Ok(Value::Bool(a > b)),
|
||||
(l, r) => Err(RuntimeError {
|
||||
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
|
||||
span: Some(span),
|
||||
@@ -1637,6 +1684,7 @@ impl Interpreter {
|
||||
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a >= b)),
|
||||
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a >= b)),
|
||||
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a >= b)),
|
||||
(Value::Char(a), Value::Char(b)) => Ok(Value::Bool(a >= b)),
|
||||
(l, r) => Err(RuntimeError {
|
||||
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
|
||||
span: Some(span),
|
||||
@@ -2219,6 +2267,26 @@ impl Interpreter {
|
||||
Ok(EvalResult::Value(Value::String(result)))
|
||||
}
|
||||
|
||||
BuiltinFn::IntToString => {
|
||||
if args.len() != 1 {
|
||||
return Err(err("Int.toString requires 1 argument"));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::Int(n) => Ok(EvalResult::Value(Value::String(format!("{}", n)))),
|
||||
v => Ok(EvalResult::Value(Value::String(format!("{}", v)))),
|
||||
}
|
||||
}
|
||||
|
||||
BuiltinFn::FloatToString => {
|
||||
if args.len() != 1 {
|
||||
return Err(err("Float.toString requires 1 argument"));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::Float(f) => Ok(EvalResult::Value(Value::String(format!("{}", f)))),
|
||||
v => Ok(EvalResult::Value(Value::String(format!("{}", v)))),
|
||||
}
|
||||
}
|
||||
|
||||
BuiltinFn::TypeOf => {
|
||||
if args.len() != 1 {
|
||||
return Err(err("typeOf requires 1 argument"));
|
||||
@@ -3824,6 +3892,26 @@ impl Interpreter {
|
||||
}
|
||||
Ok(Value::Unit)
|
||||
}
|
||||
("Test", "assertEqualMsg") => {
|
||||
let expected = request.args.first().cloned().unwrap_or(Value::Unit);
|
||||
let actual = request.args.get(1).cloned().unwrap_or(Value::Unit);
|
||||
let label = match request.args.get(2) {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
_ => "Values not equal".to_string(),
|
||||
};
|
||||
|
||||
if Value::values_equal(&expected, &actual) {
|
||||
self.test_results.borrow_mut().passed += 1;
|
||||
} else {
|
||||
self.test_results.borrow_mut().failed += 1;
|
||||
self.test_results.borrow_mut().failures.push(TestFailure {
|
||||
message: label,
|
||||
expected: Some(format!("{}", expected)),
|
||||
actual: Some(format!("{}", actual)),
|
||||
});
|
||||
}
|
||||
Ok(Value::Unit)
|
||||
}
|
||||
("Test", "assertNotEqual") => {
|
||||
let a = request.args.first().cloned().unwrap_or(Value::Unit);
|
||||
let b = request.args.get(1).cloned().unwrap_or(Value::Unit);
|
||||
|
||||
@@ -493,6 +493,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));
|
||||
|
||||
1143
src/linter.rs
Normal file
1143
src/linter.rs
Normal file
File diff suppressed because it is too large
Load Diff
596
src/lsp.rs
596
src/lsp.rs
@@ -19,7 +19,7 @@ use crate::formatter::{format as format_source, FormatConfig};
|
||||
use lsp_server::{Connection, ExtractError, Message, Request, RequestId, Response};
|
||||
use lsp_types::{
|
||||
notification::{DidChangeTextDocument, DidOpenTextDocument, Notification},
|
||||
request::{Completion, GotoDefinition, HoverRequest, References, DocumentSymbolRequest, Rename, SignatureHelpRequest, Formatting},
|
||||
request::{Completion, GotoDefinition, HoverRequest, References, DocumentSymbolRequest, Rename, SignatureHelpRequest, Formatting, InlayHintRequest},
|
||||
CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse,
|
||||
Diagnostic, DiagnosticSeverity, DidChangeTextDocumentParams, DidOpenTextDocumentParams,
|
||||
GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams,
|
||||
@@ -28,7 +28,8 @@ use lsp_types::{
|
||||
TextDocumentSyncKind, Url, ReferenceParams, Location, DocumentSymbolParams,
|
||||
DocumentSymbolResponse, SymbolInformation, RenameParams, WorkspaceEdit, TextEdit,
|
||||
SignatureHelpParams, SignatureHelp, SignatureInformation, ParameterInformation,
|
||||
SignatureHelpOptions, DocumentFormattingParams, TextDocumentIdentifier,
|
||||
SignatureHelpOptions, DocumentFormattingParams,
|
||||
InlayHint, InlayHintKind, InlayHintLabel, InlayHintParams,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
@@ -88,6 +89,7 @@ impl LspServer {
|
||||
work_done_progress_options: Default::default(),
|
||||
}),
|
||||
document_formatting_provider: Some(lsp_types::OneOf::Left(true)),
|
||||
inlay_hint_provider: Some(lsp_types::OneOf::Left(true)),
|
||||
..Default::default()
|
||||
})?;
|
||||
|
||||
@@ -191,7 +193,7 @@ impl LspServer {
|
||||
Err(req) => req,
|
||||
};
|
||||
|
||||
let _req = match cast_request::<Formatting>(req) {
|
||||
let req = match cast_request::<Formatting>(req) {
|
||||
Ok((id, params)) => {
|
||||
let result = self.handle_formatting(params);
|
||||
let resp = Response::new_ok(id, result);
|
||||
@@ -201,6 +203,16 @@ impl LspServer {
|
||||
Err(req) => req,
|
||||
};
|
||||
|
||||
let _req = match cast_request::<InlayHintRequest>(req) {
|
||||
Ok((id, params)) => {
|
||||
let result = self.handle_inlay_hints(params);
|
||||
let resp = Response::new_ok(id, result);
|
||||
self.connection.sender.send(Message::Response(resp))?;
|
||||
return Ok(());
|
||||
}
|
||||
Err(req) => req,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -305,10 +317,71 @@ impl LspServer {
|
||||
let doc = self.documents.get(&uri)?;
|
||||
let source = &doc.text;
|
||||
|
||||
// Try to get info from symbol table first
|
||||
// Try to get info from symbol table first (position-based lookup)
|
||||
if let Some(ref table) = doc.symbol_table {
|
||||
let offset = self.position_to_offset(source, position);
|
||||
if let Some(symbol) = table.definition_at_position(offset) {
|
||||
return Some(self.format_symbol_hover(symbol));
|
||||
}
|
||||
}
|
||||
|
||||
// Get the word under cursor
|
||||
let word = self.get_word_at_position(source, position)?;
|
||||
|
||||
// When hovering on a keyword like 'fn', 'type', 'effect', 'let', 'trait',
|
||||
// look ahead to find the declaration name and show that symbol's info
|
||||
if let Some(ref table) = doc.symbol_table {
|
||||
if matches!(word.as_str(), "fn" | "type" | "effect" | "let" | "trait" | "handler" | "impl") {
|
||||
let offset = self.position_to_offset(source, position);
|
||||
if let Some(name) = self.find_next_ident(source, offset + word.len()) {
|
||||
for sym in table.global_symbols() {
|
||||
if sym.name == name {
|
||||
return Some(self.format_symbol_hover(sym));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try name-based lookup in symbol table (for usage sites)
|
||||
for sym in table.global_symbols() {
|
||||
if sym.name == word {
|
||||
return Some(self.format_symbol_hover(sym));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for module names (Console, List, String, etc.)
|
||||
if let Some(hover) = self.get_module_hover(&word) {
|
||||
return Some(hover);
|
||||
}
|
||||
|
||||
// Rich documentation for behavioral property keywords
|
||||
if let Some((signature, doc_text)) = self.get_rich_symbol_info(&word) {
|
||||
return Some(Hover {
|
||||
contents: HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: format!("```lux\n{}\n```\n\n{}", signature, doc_text),
|
||||
}),
|
||||
range: None,
|
||||
});
|
||||
}
|
||||
|
||||
// Builtin keyword/function info
|
||||
if let Some((signature, doc_text)) = self.get_symbol_info(&word) {
|
||||
return Some(Hover {
|
||||
contents: HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: format!("```lux\n{}\n```\n\n{}", signature, doc_text),
|
||||
}),
|
||||
range: None,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Format a symbol into a hover response
|
||||
fn format_symbol_hover(&self, symbol: &crate::symbol_table::Symbol) -> Hover {
|
||||
let signature = symbol.type_signature.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(&symbol.name);
|
||||
@@ -327,37 +400,144 @@ impl LspServer {
|
||||
let doc_str = symbol.documentation.as_ref()
|
||||
.map(|d| format!("\n\n{}", d))
|
||||
.unwrap_or_default();
|
||||
let formatted_sig = format_signature_for_hover(signature);
|
||||
let property_docs = extract_property_docs(signature);
|
||||
|
||||
return Some(Hover {
|
||||
Hover {
|
||||
contents: HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: format!("```lux\n{}\n```\n\n*{}*{}", signature, kind_str, doc_str),
|
||||
value: format!(
|
||||
"```lux\n{}\n```\n*{}*{}{}",
|
||||
formatted_sig, kind_str, property_docs, doc_str
|
||||
),
|
||||
}),
|
||||
range: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to hardcoded info
|
||||
/// Get hover info for built-in module names
|
||||
fn get_module_hover(&self, name: &str) -> Option<Hover> {
|
||||
let (sig, doc) = match name {
|
||||
"Console" => (
|
||||
"effect Console",
|
||||
"**Console I/O**\n\n\
|
||||
- `Console.print(msg: String): Unit` — print to stdout\n\
|
||||
- `Console.readLine(): String` — read a line from stdin\n\
|
||||
- `Console.readInt(): Int` — read an integer from stdin",
|
||||
),
|
||||
"File" => (
|
||||
"effect File",
|
||||
"**File System**\n\n\
|
||||
- `File.read(path: String): String` — read file contents\n\
|
||||
- `File.write(path: String, content: String): Unit` — write to file\n\
|
||||
- `File.append(path: String, content: String): Unit` — append to file\n\
|
||||
- `File.exists(path: String): Bool` — check if file exists\n\
|
||||
- `File.delete(path: String): Unit` — delete a file\n\
|
||||
- `File.list(path: String): List<String>` — list directory",
|
||||
),
|
||||
"Http" => (
|
||||
"effect Http",
|
||||
"**HTTP Client**\n\n\
|
||||
- `Http.get(url: String): String` — GET request\n\
|
||||
- `Http.post(url: String, body: String): String` — POST request\n\
|
||||
- `Http.put(url: String, body: String): String` — PUT request\n\
|
||||
- `Http.delete(url: String): String` — DELETE request",
|
||||
),
|
||||
"Sql" => (
|
||||
"effect Sql",
|
||||
"**SQL Database**\n\n\
|
||||
- `Sql.open(path: String): Connection` — open database\n\
|
||||
- `Sql.execute(conn: Connection, sql: String): Unit` — execute SQL\n\
|
||||
- `Sql.query(conn: Connection, sql: String): List<Row>` — query rows\n\
|
||||
- `Sql.close(conn: Connection): Unit` — close connection",
|
||||
),
|
||||
"Random" => (
|
||||
"effect Random",
|
||||
"**Random Number Generation**\n\n\
|
||||
- `Random.int(min: Int, max: Int): Int` — random integer\n\
|
||||
- `Random.float(): Float` — random float 0.0–1.0\n\
|
||||
- `Random.bool(): Bool` — random boolean",
|
||||
),
|
||||
"Time" => (
|
||||
"effect Time",
|
||||
"**Time**\n\n\
|
||||
- `Time.now(): Int` — current Unix timestamp (ms)\n\
|
||||
- `Time.sleep(ms: Int): Unit` — sleep for milliseconds",
|
||||
),
|
||||
"Process" => (
|
||||
"effect Process",
|
||||
"**Process / System**\n\n\
|
||||
- `Process.exec(cmd: String): String` — run shell command\n\
|
||||
- `Process.env(name: String): String` — get env variable\n\
|
||||
- `Process.args(): List<String>` — command-line arguments\n\
|
||||
- `Process.exit(code: Int): Unit` — exit with code",
|
||||
),
|
||||
"Math" => (
|
||||
"module Math",
|
||||
"**Math Functions**\n\n\
|
||||
- `Math.abs(n: Int): Int` — absolute value\n\
|
||||
- `Math.min(a: Int, b: Int): Int` — minimum\n\
|
||||
- `Math.max(a: Int, b: Int): Int` — maximum\n\
|
||||
- `Math.sqrt(n: Float): Float` — square root\n\
|
||||
- `Math.pow(base: Float, exp: Float): Float` — power\n\
|
||||
- `Math.floor(n: Float): Int` — round down\n\
|
||||
- `Math.ceil(n: Float): Int` — round up",
|
||||
),
|
||||
"List" => (
|
||||
"module List",
|
||||
"**List Operations**\n\n\
|
||||
- `List.map(list, f)` — transform each element\n\
|
||||
- `List.filter(list, p)` — keep matching elements\n\
|
||||
- `List.fold(list, init, f)` — reduce to single value\n\
|
||||
- `List.head(list)` — first element (Option)\n\
|
||||
- `List.tail(list)` — all except first (Option)\n\
|
||||
- `List.length(list)` — number of elements\n\
|
||||
- `List.concat(a, b)` — concatenate lists\n\
|
||||
- `List.range(start, end)` — integer range\n\
|
||||
- `List.reverse(list)` — reverse order\n\
|
||||
- `List.get(list, i)` — element at index (Option)",
|
||||
),
|
||||
"String" => (
|
||||
"module String",
|
||||
"**String Operations**\n\n\
|
||||
- `String.length(s)` — string length\n\
|
||||
- `String.split(s, delim)` — split by delimiter\n\
|
||||
- `String.join(list, delim)` — join with delimiter\n\
|
||||
- `String.trim(s)` — trim whitespace\n\
|
||||
- `String.contains(s, sub)` — check substring\n\
|
||||
- `String.replace(s, from, to)` — replace occurrences\n\
|
||||
- `String.startsWith(s, prefix)` — check prefix\n\
|
||||
- `String.endsWith(s, suffix)` — check suffix\n\
|
||||
- `String.substring(s, start, end)` — extract range\n\
|
||||
- `String.chars(s)` — list of characters",
|
||||
),
|
||||
"Option" => (
|
||||
"type Option<A> = Some(A) | None",
|
||||
"**Optional Value**\n\n\
|
||||
- `Option.isSome(opt)` — has a value?\n\
|
||||
- `Option.isNone(opt)` — is empty?\n\
|
||||
- `Option.getOrElse(opt, default)` — unwrap or default\n\
|
||||
- `Option.map(opt, f)` — transform if present\n\
|
||||
- `Option.flatMap(opt, f)` — chain operations",
|
||||
),
|
||||
"Result" => (
|
||||
"type Result<A, E> = Ok(A) | Err(E)",
|
||||
"**Result of Fallible Operation**\n\n\
|
||||
- `Result.isOk(r)` — succeeded?\n\
|
||||
- `Result.isErr(r)` — failed?\n\
|
||||
- `Result.map(r, f)` — transform success value\n\
|
||||
- `Result.mapErr(r, f)` — transform error value",
|
||||
),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// Extract the word at the cursor position
|
||||
let word = self.get_word_at_position(source, position)?;
|
||||
|
||||
// Look up documentation for known symbols
|
||||
let info = self.get_symbol_info(&word);
|
||||
|
||||
if let Some((signature, doc)) = info {
|
||||
Some(Hover {
|
||||
contents: HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: format!("```lux\n{}\n```\n\n{}", signature, doc),
|
||||
value: format!("```lux\n{}\n```\n{}", sig, doc),
|
||||
}),
|
||||
range: None,
|
||||
})
|
||||
} else {
|
||||
// Return generic info for unknown symbols
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_word_at_position(&self, source: &str, position: Position) -> Option<String> {
|
||||
@@ -383,6 +563,26 @@ impl LspServer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the next identifier in source after the given offset (skipping whitespace)
|
||||
fn find_next_ident(&self, source: &str, start: usize) -> Option<String> {
|
||||
let chars: Vec<char> = source.chars().collect();
|
||||
let mut pos = start;
|
||||
// Skip whitespace
|
||||
while pos < chars.len() && (chars[pos] == ' ' || chars[pos] == '\t' || chars[pos] == '\n' || chars[pos] == '\r') {
|
||||
pos += 1;
|
||||
}
|
||||
// Collect identifier
|
||||
let ident_start = pos;
|
||||
while pos < chars.len() && (chars[pos].is_alphanumeric() || chars[pos] == '_') {
|
||||
pos += 1;
|
||||
}
|
||||
if pos > ident_start {
|
||||
Some(chars[ident_start..pos].iter().collect())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_symbol_info(&self, word: &str) -> Option<(&'static str, &'static str)> {
|
||||
match word {
|
||||
// Keywords
|
||||
@@ -439,6 +639,84 @@ impl LspServer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rich documentation for behavioral properties and keywords
|
||||
fn get_rich_symbol_info(&self, word: &str) -> Option<(String, String)> {
|
||||
match word {
|
||||
"pure" => Some((
|
||||
"is pure".to_string(),
|
||||
"**Behavioral Property: Pure**\n\n\
|
||||
A pure function has no side effects and always produces the same output for the same inputs. \
|
||||
The compiler can safely memoize calls, reorder them, or eliminate duplicates.\n\n\
|
||||
```lux\nfn add(a: Int, b: Int): Int is pure = a + b\n```\n\n\
|
||||
**Guarantees:**\n\
|
||||
- No effect operations (Console, File, Http, etc.)\n\
|
||||
- Referential transparency: `f(x)` can be replaced with its result\n\
|
||||
- Enables memoization and common subexpression elimination".to_string(),
|
||||
)),
|
||||
"total" => Some((
|
||||
"is total".to_string(),
|
||||
"**Behavioral Property: Total**\n\n\
|
||||
A total function always terminates and never throws exceptions. \
|
||||
The compiler verifies termination through structural recursion analysis.\n\n\
|
||||
```lux\nfn factorial(n: Int): Int is total =\n if n <= 0 then 1\n else n * factorial(n - 1)\n```\n\n\
|
||||
**Guarantees:**\n\
|
||||
- Always produces a result (no infinite loops)\n\
|
||||
- Cannot use the `Fail` effect\n\
|
||||
- Recursive calls must be structurally decreasing".to_string(),
|
||||
)),
|
||||
"idempotent" => Some((
|
||||
"is idempotent".to_string(),
|
||||
"**Behavioral Property: Idempotent**\n\n\
|
||||
An idempotent function satisfies `f(f(x)) == f(x)` for all inputs. \
|
||||
Applying it multiple times has the same effect as applying it once.\n\n\
|
||||
```lux\nfn abs(x: Int): Int is idempotent =\n if x < 0 then 0 - x else x\n\n\
|
||||
fn clamp(x: Int): Int is idempotent =\n if x < 0 then 0\n else if x > 100 then 100\n else x\n```\n\n\
|
||||
**Guarantees:**\n\
|
||||
- `f(f(x)) == f(x)` for all valid inputs\n\
|
||||
- Safe to retry without changing outcome\n\
|
||||
- Compiler can deduplicate consecutive calls".to_string(),
|
||||
)),
|
||||
"deterministic" => Some((
|
||||
"is deterministic".to_string(),
|
||||
"**Behavioral Property: Deterministic**\n\n\
|
||||
A deterministic function always produces the same output for the same inputs, \
|
||||
with no dependence on randomness, time, or external state.\n\n\
|
||||
```lux\nfn multiply(a: Int, b: Int): Int is deterministic = a * b\n```\n\n\
|
||||
**Guarantees:**\n\
|
||||
- Cannot use `Random` or `Time` effects\n\
|
||||
- Same inputs always produce same outputs\n\
|
||||
- Results can be cached across runs".to_string(),
|
||||
)),
|
||||
"commutative" => Some((
|
||||
"is commutative".to_string(),
|
||||
"**Behavioral Property: Commutative**\n\n\
|
||||
A commutative function satisfies `f(a, b) == f(b, a)`. \
|
||||
The order of arguments doesn't affect the result.\n\n\
|
||||
```lux\nfn add(a: Int, b: Int): Int is commutative = a + b\nfn max(a: Int, b: Int): Int is commutative =\n if a > b then a else b\n```\n\n\
|
||||
**Guarantees:**\n\
|
||||
- Must have exactly 2 parameters\n\
|
||||
- `f(a, b) == f(b, a)` for all inputs\n\
|
||||
- Compiler can normalize argument order for optimization".to_string(),
|
||||
)),
|
||||
"run" => Some((
|
||||
"run expr with { handlers }".to_string(),
|
||||
"**Effect Handler**\n\n\
|
||||
Execute an effectful expression with explicit effect handlers. \
|
||||
Must be bound to a variable at top level.\n\n\
|
||||
```lux\nlet result = run myFunction() with {\n Console = { /* handler */ }\n}\n```\n\n\
|
||||
Handlers intercept effect operations and provide implementations.".to_string(),
|
||||
)),
|
||||
"with" => Some((
|
||||
"with {Effect1, Effect2}".to_string(),
|
||||
"**Effect Declaration / Handler Block**\n\n\
|
||||
Declares which effects a function may perform, or provides handlers in a `run` expression.\n\n\
|
||||
```lux\n// In function signature:\nfn greet(name: String): Unit with {Console} =\n Console.print(\"Hello, \" + name)\n\n\
|
||||
// In run expression:\nlet _ = run greet(\"world\") with {}\n```".to_string(),
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_completion(&self, params: CompletionParams) -> Option<CompletionResponse> {
|
||||
let uri = params.text_document_position.text_document.uri;
|
||||
let position = params.text_document_position.position;
|
||||
@@ -510,17 +788,11 @@ impl LspServer {
|
||||
|
||||
fn position_to_offset(&self, source: &str, position: Position) -> usize {
|
||||
let mut offset = 0;
|
||||
let mut line = 0u32;
|
||||
|
||||
for (i, c) in source.char_indices() {
|
||||
if line == position.line {
|
||||
let col = i - offset;
|
||||
return offset + (position.character as usize).min(col + 1);
|
||||
}
|
||||
if c == '\n' {
|
||||
line += 1;
|
||||
offset = i + 1;
|
||||
for (line_idx, line) in source.lines().enumerate() {
|
||||
if line_idx == position.line as usize {
|
||||
return offset + (position.character as usize).min(line.len());
|
||||
}
|
||||
offset += line.len() + 1; // +1 for newline
|
||||
}
|
||||
source.len()
|
||||
}
|
||||
@@ -1022,6 +1294,90 @@ impl LspServer {
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_inlay_hints(&self, params: InlayHintParams) -> Option<Vec<InlayHint>> {
|
||||
let uri = params.text_document.uri;
|
||||
let doc = self.documents.get(&uri)?;
|
||||
let source = &doc.text;
|
||||
|
||||
// Parse the document to get AST
|
||||
let program = Parser::parse_source(source).ok()?;
|
||||
|
||||
// Type-check to get inferred types
|
||||
let mut checker = TypeChecker::new();
|
||||
let _ = checker.check_program(&program);
|
||||
|
||||
let mut hints = Vec::new();
|
||||
|
||||
// Collect parameter names for known functions (from symbol table)
|
||||
let param_names = self.collect_function_params(&program);
|
||||
|
||||
for decl in &program.declarations {
|
||||
match decl {
|
||||
crate::ast::Declaration::Let(l) => {
|
||||
// Show inferred type for let bindings without explicit type annotations
|
||||
if l.typ.is_none() {
|
||||
if let Some(inferred_type) = checker.get_inferred_type(&l.name.name) {
|
||||
let type_str = format!(": {}", inferred_type);
|
||||
let pos = offset_to_position(source, l.name.span.end);
|
||||
hints.push(InlayHint {
|
||||
position: pos,
|
||||
label: InlayHintLabel::String(type_str),
|
||||
kind: Some(InlayHintKind::TYPE),
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: Some(false),
|
||||
padding_right: Some(true),
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Walk into the value expression for call-site parameter hints
|
||||
collect_call_site_hints(source, &l.value, ¶m_names, &mut hints);
|
||||
}
|
||||
crate::ast::Declaration::Function(f) => {
|
||||
// Walk into the function body for call-site parameter hints
|
||||
collect_call_site_hints(source, &f.body, ¶m_names, &mut hints);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if hints.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(hints)
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect parameter names for all functions defined in the program
|
||||
fn collect_function_params(&self, program: &crate::ast::Program) -> HashMap<String, Vec<String>> {
|
||||
let mut params = HashMap::new();
|
||||
for decl in &program.declarations {
|
||||
if let crate::ast::Declaration::Function(f) = decl {
|
||||
let names: Vec<String> = f.params.iter()
|
||||
.map(|p| p.name.name.clone())
|
||||
.collect();
|
||||
params.insert(f.name.name.clone(), names);
|
||||
}
|
||||
}
|
||||
// Add builtin function parameter names
|
||||
params.insert("map".into(), vec!["list".into(), "f".into()]);
|
||||
params.insert("filter".into(), vec!["list".into(), "predicate".into()]);
|
||||
params.insert("fold".into(), vec!["list".into(), "init".into(), "f".into()]);
|
||||
params.insert("concat".into(), vec!["a".into(), "b".into()]);
|
||||
params.insert("range".into(), vec!["start".into(), "end".into()]);
|
||||
params.insert("get".into(), vec!["list".into(), "index".into()]);
|
||||
params.insert("take".into(), vec!["list".into(), "n".into()]);
|
||||
params.insert("drop".into(), vec!["list".into(), "n".into()]);
|
||||
params.insert("split".into(), vec!["s".into(), "delimiter".into()]);
|
||||
params.insert("join".into(), vec!["list".into(), "delimiter".into()]);
|
||||
params.insert("replace".into(), vec!["s".into(), "old".into(), "new".into()]);
|
||||
params.insert("substring".into(), vec!["s".into(), "start".into(), "end".into()]);
|
||||
params.insert("contains".into(), vec!["s".into(), "substr".into()]);
|
||||
params.insert("getOrElse".into(), vec!["opt".into(), "default".into()]);
|
||||
params
|
||||
}
|
||||
|
||||
fn handle_formatting(&self, params: DocumentFormattingParams) -> Option<Vec<TextEdit>> {
|
||||
let uri = params.text_document.uri;
|
||||
let doc = self.documents.get(&uri)?;
|
||||
@@ -1061,6 +1417,186 @@ impl LspServer {
|
||||
}
|
||||
|
||||
/// Convert byte offsets to LSP Position
|
||||
/// Format a function signature for hover display, wrapping long lines
|
||||
fn format_signature_for_hover(sig: &str) -> String {
|
||||
// If it fits in ~60 chars, keep it on one line
|
||||
if sig.len() <= 60 {
|
||||
return sig.to_string();
|
||||
}
|
||||
|
||||
// Try to break at parameter list for function signatures
|
||||
if let Some(paren_start) = sig.find('(') {
|
||||
if let Some(paren_end) = sig.rfind(')') {
|
||||
let prefix = &sig[..paren_start + 1];
|
||||
let params = &sig[paren_start + 1..paren_end];
|
||||
let suffix = &sig[paren_end..];
|
||||
|
||||
// Split parameters and format each on its own line
|
||||
let param_parts: Vec<&str> = params.split(", ").collect();
|
||||
if param_parts.len() > 1 {
|
||||
let indent = " ";
|
||||
let formatted_params = param_parts.join(&format!(",\n{}", indent));
|
||||
return format!("{}\n{}{}\n{}", prefix, indent, formatted_params, suffix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sig.to_string()
|
||||
}
|
||||
|
||||
/// Extract behavioral property documentation from a signature string
|
||||
fn extract_property_docs(sig: &str) -> String {
|
||||
let properties = [
|
||||
("is pure", "**pure** — no side effects, same output for same inputs"),
|
||||
("is total", "**total** — always terminates, no exceptions"),
|
||||
("is idempotent", "**idempotent** — `f(f(x)) == f(x)`"),
|
||||
("is deterministic", "**deterministic** — no randomness or time dependence"),
|
||||
("is commutative", "**commutative** — `f(a, b) == f(b, a)`"),
|
||||
];
|
||||
|
||||
let mut found = Vec::new();
|
||||
for (keyword, description) in &properties {
|
||||
if sig.contains(keyword) {
|
||||
found.push(*description);
|
||||
}
|
||||
}
|
||||
|
||||
if found.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("\n\n{}", found.join(" \n"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively collect parameter name hints at call sites
|
||||
fn collect_call_site_hints(
|
||||
source: &str,
|
||||
expr: &crate::ast::Expr,
|
||||
param_names: &HashMap<String, Vec<String>>,
|
||||
hints: &mut Vec<InlayHint>,
|
||||
) {
|
||||
use crate::ast::Expr;
|
||||
match expr {
|
||||
Expr::Call { func, args, .. } => {
|
||||
// Get the function name for parameter lookup
|
||||
let func_name = match func.as_ref() {
|
||||
Expr::Var(ident) => Some(ident.name.clone()),
|
||||
// Module.method calls like List.map
|
||||
Expr::Field { object, field, .. } => {
|
||||
if let Expr::Var(_) = object.as_ref() {
|
||||
Some(field.name.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(name) = func_name {
|
||||
if let Some(names) = param_names.get(&name) {
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
if let Some(param_name) = names.get(i) {
|
||||
// Skip hint if the argument is already a variable with the same name
|
||||
if let Expr::Var(ident) = arg {
|
||||
if &ident.name == param_name {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Skip hints for single-arg functions (obvious)
|
||||
if args.len() <= 1 {
|
||||
continue;
|
||||
}
|
||||
let pos = offset_to_position(source, arg.span().start);
|
||||
hints.push(InlayHint {
|
||||
position: pos,
|
||||
label: InlayHintLabel::String(format!("{}:", param_name)),
|
||||
kind: Some(InlayHintKind::PARAMETER),
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: Some(false),
|
||||
padding_right: Some(true),
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse into function expression and arguments
|
||||
collect_call_site_hints(source, func, param_names, hints);
|
||||
for arg in args {
|
||||
collect_call_site_hints(source, arg, param_names, hints);
|
||||
}
|
||||
}
|
||||
Expr::BinaryOp { left, right, .. } => {
|
||||
collect_call_site_hints(source, left, param_names, hints);
|
||||
collect_call_site_hints(source, right, param_names, hints);
|
||||
}
|
||||
Expr::UnaryOp { operand, .. } => {
|
||||
collect_call_site_hints(source, operand, param_names, hints);
|
||||
}
|
||||
Expr::If { condition, then_branch, else_branch, .. } => {
|
||||
collect_call_site_hints(source, condition, param_names, hints);
|
||||
collect_call_site_hints(source, then_branch, param_names, hints);
|
||||
collect_call_site_hints(source, else_branch, param_names, hints);
|
||||
}
|
||||
Expr::Let { value, body, .. } => {
|
||||
collect_call_site_hints(source, value, param_names, hints);
|
||||
collect_call_site_hints(source, body, param_names, hints);
|
||||
}
|
||||
Expr::Block { statements, result, .. } => {
|
||||
for stmt in statements {
|
||||
match stmt {
|
||||
crate::ast::Statement::Expr(e) => {
|
||||
collect_call_site_hints(source, e, param_names, hints);
|
||||
}
|
||||
crate::ast::Statement::Let { value, .. } => {
|
||||
collect_call_site_hints(source, value, param_names, hints);
|
||||
}
|
||||
}
|
||||
}
|
||||
collect_call_site_hints(source, result, param_names, hints);
|
||||
}
|
||||
Expr::Match { scrutinee, arms, .. } => {
|
||||
collect_call_site_hints(source, scrutinee, param_names, hints);
|
||||
for arm in arms {
|
||||
collect_call_site_hints(source, &arm.body, param_names, hints);
|
||||
}
|
||||
}
|
||||
Expr::Lambda { body, .. } => {
|
||||
collect_call_site_hints(source, body, param_names, hints);
|
||||
}
|
||||
Expr::Tuple { elements, .. } | Expr::List { elements, .. } => {
|
||||
for e in elements {
|
||||
collect_call_site_hints(source, e, param_names, hints);
|
||||
}
|
||||
}
|
||||
Expr::Record { fields, .. } => {
|
||||
for (_, e) in fields {
|
||||
collect_call_site_hints(source, e, param_names, hints);
|
||||
}
|
||||
}
|
||||
Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => {
|
||||
collect_call_site_hints(source, object, param_names, hints);
|
||||
}
|
||||
Expr::Run { expr, handlers, .. } => {
|
||||
collect_call_site_hints(source, expr, param_names, hints);
|
||||
for (_, handler_expr) in handlers {
|
||||
collect_call_site_hints(source, handler_expr, param_names, hints);
|
||||
}
|
||||
}
|
||||
Expr::Resume { value, .. } => {
|
||||
collect_call_site_hints(source, value, param_names, hints);
|
||||
}
|
||||
Expr::EffectOp { args, .. } => {
|
||||
for arg in args {
|
||||
collect_call_site_hints(source, arg, param_names, hints);
|
||||
}
|
||||
}
|
||||
Expr::Literal { .. } | Expr::Var(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn span_to_range(source: &str, start: usize, end: usize) -> Range {
|
||||
let start_pos = offset_to_position(source, start);
|
||||
let end_pos = offset_to_position(source, end);
|
||||
|
||||
843
src/main.rs
843
src/main.rs
File diff suppressed because it is too large
Load Diff
@@ -305,6 +305,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();
|
||||
|
||||
@@ -244,6 +244,7 @@ impl Parser {
|
||||
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()?)),
|
||||
TokenKind::Run => Err(self.error("Bare 'run' expressions are not allowed at top level. Use 'let _ = run ...' or 'let result = run ...'")),
|
||||
_ => Err(self.error("Expected declaration (fn, effect, handler, type, trait, impl, or let)")),
|
||||
}
|
||||
}
|
||||
@@ -878,7 +879,8 @@ impl Parser {
|
||||
Ok(effects)
|
||||
}
|
||||
|
||||
/// Parse behavioral properties: is pure, is total, is idempotent, etc.
|
||||
/// Parse behavioral properties: is pure, total, idempotent, etc.
|
||||
/// Supports: `is pure`, `is pure is total`, `is pure, total`, `is pure, is total`
|
||||
fn parse_behavioral_properties(&mut self) -> Result<Vec<BehavioralProperty>, ParseError> {
|
||||
let mut properties = Vec::new();
|
||||
|
||||
@@ -900,10 +902,16 @@ impl Parser {
|
||||
let property = self.parse_single_property()?;
|
||||
properties.push(property);
|
||||
|
||||
// Optional comma for multiple properties: is pure, is total
|
||||
if self.check(TokenKind::Comma) {
|
||||
// After first property, allow comma-separated list without repeating 'is'
|
||||
while self.check(TokenKind::Comma) {
|
||||
self.advance(); // consume comma
|
||||
// Allow optional 'is' after comma: `is pure, is total` or `is pure, total`
|
||||
if self.check(TokenKind::Is) {
|
||||
self.advance();
|
||||
}
|
||||
let property = self.parse_single_property()?;
|
||||
properties.push(property);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(properties)
|
||||
@@ -1638,6 +1646,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)
|
||||
@@ -1673,11 +1695,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1879,6 +1904,14 @@ 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()) {
|
||||
|
||||
@@ -228,13 +228,14 @@ impl SymbolTable {
|
||||
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 symbol = self.new_symbol(
|
||||
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);
|
||||
|
||||
@@ -263,15 +264,30 @@ impl SymbolTable {
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "))
|
||||
};
|
||||
let type_sig = format!("fn {}({}): {}{}", f.name.name, param_types.join(", "), return_type, effects);
|
||||
let properties = if f.properties.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" is {}", f.properties.iter()
|
||||
.map(|p| match p {
|
||||
crate::ast::BehavioralProperty::Pure => "pure",
|
||||
crate::ast::BehavioralProperty::Total => "total",
|
||||
crate::ast::BehavioralProperty::Idempotent => "idempotent",
|
||||
crate::ast::BehavioralProperty::Deterministic => "deterministic",
|
||||
crate::ast::BehavioralProperty::Commutative => "commutative",
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "))
|
||||
};
|
||||
let type_sig = format!("fn {}({}): {}{}{}", f.name.name, param_types.join(", "), return_type, properties, effects);
|
||||
|
||||
let symbol = self.new_symbol(
|
||||
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);
|
||||
|
||||
@@ -312,13 +328,14 @@ impl SymbolTable {
|
||||
let is_public = matches!(t.visibility, Visibility::Public);
|
||||
let type_sig = format!("type {}", t.name.name);
|
||||
|
||||
let symbol = self.new_symbol(
|
||||
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);
|
||||
|
||||
@@ -358,13 +375,14 @@ impl SymbolTable {
|
||||
let is_public = true; // Effects are typically public
|
||||
let type_sig = format!("effect {}", e.name.name);
|
||||
|
||||
let symbol = self.new_symbol(
|
||||
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
|
||||
@@ -395,13 +413,14 @@ impl SymbolTable {
|
||||
let is_public = matches!(t.visibility, Visibility::Public);
|
||||
let type_sig = format!("trait {}", t.name.name);
|
||||
|
||||
let symbol = self.new_symbol(
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -465,7 +484,7 @@ impl SymbolTable {
|
||||
self.visit_expr(arg, scope_idx);
|
||||
}
|
||||
}
|
||||
Expr::Field { object, .. } => {
|
||||
Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => {
|
||||
self.visit_expr(object, scope_idx);
|
||||
}
|
||||
Expr::If { condition, then_branch, else_branch, .. } => {
|
||||
|
||||
@@ -335,7 +335,7 @@ 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)),
|
||||
@@ -519,7 +519,7 @@ fn has_recursive_calls(func_name: &str, body: &Expr) -> bool {
|
||||
Expr::Record { fields, .. } => {
|
||||
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)
|
||||
}
|
||||
@@ -759,6 +759,17 @@ impl TypeChecker {
|
||||
self.env.bindings.get(name)
|
||||
}
|
||||
|
||||
/// Get the inferred type of a binding as a display string (for LSP inlay hints)
|
||||
pub fn get_inferred_type(&self, name: &str) -> Option<String> {
|
||||
let scheme = self.env.bindings.get(name)?;
|
||||
let type_str = scheme.typ.to_string();
|
||||
// Skip unhelpful types
|
||||
if type_str == "<error>" || type_str.contains('?') {
|
||||
return None;
|
||||
}
|
||||
Some(type_str)
|
||||
}
|
||||
|
||||
/// Get auto-generated migrations from type checking
|
||||
/// Returns: type_name -> from_version -> migration_body
|
||||
pub fn get_auto_migrations(&self) -> &HashMap<String, HashMap<u32, Expr>> {
|
||||
@@ -1525,7 +1536,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 +1673,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,
|
||||
@@ -1736,7 +1783,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,
|
||||
@@ -1759,7 +1806,7 @@ impl TypeChecker {
|
||||
|
||||
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 +1830,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 +1841,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 +1852,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 +1872,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 +1904,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,
|
||||
@@ -1908,7 +1955,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
|
||||
@@ -1985,7 +2032,7 @@ impl TypeChecker {
|
||||
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: {}",
|
||||
@@ -2057,7 +2104,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 +2137,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 +2218,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 +2284,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 +2294,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 +2335,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 +2351,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 +2401,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,
|
||||
@@ -2367,7 +2415,7 @@ impl TypeChecker {
|
||||
// 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 +2428,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 +2457,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 +2507,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 {}: {}",
|
||||
@@ -2502,7 +2550,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 +2856,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 {}: {}",
|
||||
|
||||
39
src/types.rs
39
src/types.rs
@@ -1146,6 +1146,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![
|
||||
@@ -1599,6 +1608,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));
|
||||
|
||||
@@ -1873,6 +1890,24 @@ impl TypeEnv {
|
||||
]);
|
||||
env.bind("Math", TypeScheme::mono(math_module_type));
|
||||
|
||||
// Int module
|
||||
let int_module_type = Type::Record(vec![
|
||||
(
|
||||
"toString".to_string(),
|
||||
Type::function(vec![Type::Int], Type::String),
|
||||
),
|
||||
]);
|
||||
env.bind("Int", TypeScheme::mono(int_module_type));
|
||||
|
||||
// Float module
|
||||
let float_module_type = Type::Record(vec![
|
||||
(
|
||||
"toString".to_string(),
|
||||
Type::function(vec![Type::Float], Type::String),
|
||||
),
|
||||
]);
|
||||
env.bind("Float", TypeScheme::mono(float_module_type));
|
||||
|
||||
env
|
||||
}
|
||||
|
||||
@@ -2032,7 +2067,9 @@ pub fn unify(t1: &Type, t2: &Type) -> Result<Substitution, String> {
|
||||
// Function's required effects (e1) must be a subset of available effects (e2)
|
||||
// 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
|
||||
|
||||
@@ -194,6 +194,14 @@
|
||||
<li><a href="middleware.html">Middleware</a></li>
|
||||
<li><a href="routing.html">Routing</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>Projects</h2>
|
||||
<ul>
|
||||
<li><a href="/projects/">All Projects</a></li>
|
||||
<li><a href="/projects/#rest-api">REST API</a></li>
|
||||
<li><a href="/projects/#todo-app">Todo App</a></li>
|
||||
<li><a href="/projects/#json-parser">JSON Parser</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<div class="examples-content">
|
||||
|
||||
456
website/projects/index.html
Normal file
456
website/projects/index.html
Normal file
@@ -0,0 +1,456 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Projects - Lux</title>
|
||||
<meta name="description" content="Real-world example projects demonstrating Lux's capabilities.">
|
||||
<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>
|
||||
.projects-container {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: var(--space-xl) var(--space-lg);
|
||||
}
|
||||
|
||||
.projects-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--space-2xl);
|
||||
}
|
||||
|
||||
.projects-header p {
|
||||
font-size: 1.1rem;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.projects-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.project-card {
|
||||
background: var(--bg-glass);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 12px;
|
||||
padding: var(--space-lg);
|
||||
transition: border-color 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.project-card:hover {
|
||||
border-color: var(--border-gold);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.project-card h2 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: var(--space-sm);
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.project-card h2 .icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.project-card .description {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--space-md);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.project-card .features {
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.project-card .features h3 {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.project-card .features ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.project-card .features li {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
padding: 0.2rem 0;
|
||||
}
|
||||
|
||||
.project-card .features li::before {
|
||||
content: "\2022";
|
||||
color: var(--gold);
|
||||
margin-right: var(--space-xs);
|
||||
}
|
||||
|
||||
.project-card .code-preview {
|
||||
background: var(--code-bg);
|
||||
border: 1px solid var(--code-border);
|
||||
border-radius: 6px;
|
||||
padding: var(--space-md);
|
||||
margin-bottom: var(--space-md);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.project-card .code-preview pre {
|
||||
font-family: var(--font-code);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.project-card .actions {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.project-card .actions a {
|
||||
padding: var(--space-xs) var(--space-md);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.showcase-section {
|
||||
margin-top: var(--space-3xl);
|
||||
padding-top: var(--space-2xl);
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.showcase-section h2 {
|
||||
text-align: center;
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.showcase-card {
|
||||
background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%);
|
||||
border: 1px solid var(--border-gold);
|
||||
border-radius: 16px;
|
||||
padding: var(--space-xl);
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.showcase-card h3 {
|
||||
font-size: 1.5rem;
|
||||
color: var(--gold);
|
||||
margin-bottom: var(--space-md);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.showcase-card p {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--space-lg);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.showcase-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: var(--space-lg);
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.showcase-feature {
|
||||
background: var(--bg-glass);
|
||||
border-radius: 8px;
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.showcase-feature h4 {
|
||||
color: var(--gold);
|
||||
font-size: 1rem;
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.showcase-feature p {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.showcase-code {
|
||||
background: var(--code-bg);
|
||||
border: 1px solid var(--code-border);
|
||||
border-radius: 8px;
|
||||
padding: var(--space-lg);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.showcase-code pre {
|
||||
font-family: var(--font-code);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="/" class="logo">Lux</a>
|
||||
<ul class="nav-links" id="nav-links">
|
||||
<li><a href="/install">Install</a></li>
|
||||
<li><a href="/tour/">Tour</a></li>
|
||||
<li><a href="/examples/">Examples</a></li>
|
||||
<li><a href="/docs/">Docs</a></li>
|
||||
<li><a href="/play">Play</a></li>
|
||||
<li><a href="https://git.qrty.ink/blu/lux" class="nav-source">Source</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<main class="projects-container">
|
||||
<header class="projects-header">
|
||||
<h1>Example Projects</h1>
|
||||
<p>Real-world applications demonstrating Lux's unique capabilities. Clone these to learn by doing.</p>
|
||||
</header>
|
||||
|
||||
<div class="projects-grid">
|
||||
<div class="project-card">
|
||||
<h2><span class="icon">API</span> REST API</h2>
|
||||
<p class="description">
|
||||
A full REST API for task management with JSON responses, routing, and CRUD operations.
|
||||
</p>
|
||||
<div class="features">
|
||||
<h3>Demonstrates</h3>
|
||||
<ul>
|
||||
<li>HttpServer effect</li>
|
||||
<li>Pattern matching for routing</li>
|
||||
<li>JSON serialization</li>
|
||||
<li>Effect tracking</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="code-preview">
|
||||
<pre><code><span class="kw">fn</span> <span class="fn">router</span>(req): Response <span class="kw">with</span> {<span class="ef">Http</span>} =
|
||||
<span class="kw">match</span> (req.method, req.path) {
|
||||
(<span class="st">"GET"</span>, <span class="st">"/"</span>) => httpOk(welcome),
|
||||
(<span class="st">"GET"</span>, <span class="st">"/tasks"</span>) => httpOk(tasks),
|
||||
_ => httpNotFound(<span class="st">"404"</span>)
|
||||
}</code></pre>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="https://git.qrty.ink/blu/lux/src/branch/master/projects/rest-api" class="btn btn-primary">View Code</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-card">
|
||||
<h2><span class="icon">TODO</span> Todo App</h2>
|
||||
<p class="description">
|
||||
A command-line todo application showcasing algebraic data types and pattern matching.
|
||||
</p>
|
||||
<div class="features">
|
||||
<h3>Demonstrates</h3>
|
||||
<ul>
|
||||
<li>ADTs for data modeling</li>
|
||||
<li>Pattern matching</li>
|
||||
<li>Recursive list operations</li>
|
||||
<li>Console effect</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="code-preview">
|
||||
<pre><code><span class="kw">type</span> <span class="ty">Priority</span> =
|
||||
| <span class="ty">Low</span>
|
||||
| <span class="ty">Medium</span>
|
||||
| <span class="ty">High</span>
|
||||
|
||||
<span class="kw">type</span> <span class="ty">TodoItem</span> =
|
||||
| <span class="ty">TodoItem</span>(Int, String, Bool, Priority)</code></pre>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="https://git.qrty.ink/blu/lux/src/branch/master/projects/todo-app" class="btn btn-primary">View Code</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-card">
|
||||
<h2><span class="icon">JSON</span> JSON Parser</h2>
|
||||
<p class="description">
|
||||
A recursive descent JSON parser demonstrating ADTs for AST representation.
|
||||
</p>
|
||||
<div class="features">
|
||||
<h3>Demonstrates</h3>
|
||||
<ul>
|
||||
<li>ADTs for AST</li>
|
||||
<li>Recursive parsing</li>
|
||||
<li>String manipulation</li>
|
||||
<li>Pure functions</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="code-preview">
|
||||
<pre><code><span class="kw">type</span> <span class="ty">JsonValue</span> =
|
||||
| <span class="ty">JsonNull</span>
|
||||
| <span class="ty">JsonBool</span>(Bool)
|
||||
| <span class="ty">JsonNumber</span>(Int)
|
||||
| <span class="ty">JsonString</span>(String)
|
||||
| <span class="ty">JsonArray</span>(List<JsonValue>)
|
||||
| <span class="ty">JsonObject</span>(List<(String, JsonValue)>)</code></pre>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="https://git.qrty.ink/blu/lux/src/branch/master/projects/json-parser" class="btn btn-primary">View Code</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-card">
|
||||
<h2><span class="icon">GAME</span> Guessing Game</h2>
|
||||
<p class="description">
|
||||
A simple number guessing game demonstrating effects for randomness and I/O.
|
||||
</p>
|
||||
<div class="features">
|
||||
<h3>Demonstrates</h3>
|
||||
<ul>
|
||||
<li>Random effect</li>
|
||||
<li>Console I/O</li>
|
||||
<li>Recursive game loop</li>
|
||||
<li>User input handling</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="code-preview">
|
||||
<pre><code><span class="kw">fn</span> <span class="fn">playGame</span>(): Unit
|
||||
<span class="kw">with</span> {<span class="ef">Console</span>, <span class="ef">Random</span>} = {
|
||||
<span class="kw">let</span> secret = Random.int(<span class="num">1</span>, <span class="num">100</span>)
|
||||
Console.print(<span class="st">"Guess a number!"</span>)
|
||||
guessLoop(secret, <span class="num">0</span>)
|
||||
}</code></pre>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="https://git.qrty.ink/blu/lux/src/branch/master/projects/guessing-game" class="btn btn-primary">View Code</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-card">
|
||||
<h2><span class="icon">MD</span> Markdown Converter</h2>
|
||||
<p class="description">
|
||||
Convert Markdown to HTML using pattern matching and string processing.
|
||||
</p>
|
||||
<div class="features">
|
||||
<h3>Demonstrates</h3>
|
||||
<ul>
|
||||
<li>String manipulation</li>
|
||||
<li>Pattern matching</li>
|
||||
<li>Pure transformation</li>
|
||||
<li>List processing</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="code-preview">
|
||||
<pre><code><span class="kw">fn</span> <span class="fn">convertLine</span>(line: String): String =
|
||||
<span class="kw">if</span> startsWith(line, <span class="st">"# "</span>) <span class="kw">then</span>
|
||||
<span class="st">"<h1>"</span> + rest(line, <span class="num">2</span>) + <span class="st">"</h1>"</span>
|
||||
<span class="kw">else if</span> startsWith(line, <span class="st">"- "</span>) <span class="kw">then</span>
|
||||
<span class="st">"<li>"</span> + rest(line, <span class="num">2</span>) + <span class="st">"</li>"</span>
|
||||
<span class="kw">else</span> <span class="st">"<p>"</span> + line + <span class="st">"</p>"</span></code></pre>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="https://git.qrty.ink/blu/lux/src/branch/master/projects/markdown-converter" class="btn btn-primary">View Code</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-card">
|
||||
<h2><span class="icon">CALC</span> Mini Interpreter</h2>
|
||||
<p class="description">
|
||||
A tiny expression interpreter demonstrating language implementation patterns.
|
||||
</p>
|
||||
<div class="features">
|
||||
<h3>Demonstrates</h3>
|
||||
<ul>
|
||||
<li>ADTs for AST</li>
|
||||
<li>Recursive evaluation</li>
|
||||
<li>Environment handling</li>
|
||||
<li>Interpreter patterns</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="code-preview">
|
||||
<pre><code><span class="kw">type</span> <span class="ty">Expr</span> =
|
||||
| <span class="ty">Num</span>(Int)
|
||||
| <span class="ty">Add</span>(Expr, Expr)
|
||||
| <span class="ty">Mul</span>(Expr, Expr)
|
||||
| <span class="ty">Var</span>(String)
|
||||
| <span class="ty">Let</span>(String, Expr, Expr)</code></pre>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="https://git.qrty.ink/blu/lux/src/branch/master/projects/mini-interpreter" class="btn btn-primary">View Code</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Featured Showcase -->
|
||||
<section class="showcase-section">
|
||||
<h2>Featured: Task Manager API</h2>
|
||||
<div class="showcase-card">
|
||||
<h3>A Complete Showcase of Lux's Unique Features</h3>
|
||||
<p>This comprehensive example demonstrates all three of Lux's killer features working together.</p>
|
||||
|
||||
<div class="showcase-features">
|
||||
<div class="showcase-feature">
|
||||
<h4>1. Algebraic Effects</h4>
|
||||
<p>Every side effect is explicit in function signatures. No hidden I/O.</p>
|
||||
</div>
|
||||
<div class="showcase-feature">
|
||||
<h4>2. Behavioral Types</h4>
|
||||
<p>Compile-time guarantees: <code>is pure</code>, <code>is total</code>, <code>is idempotent</code>.</p>
|
||||
</div>
|
||||
<div class="showcase-feature">
|
||||
<h4>3. Schema Evolution</h4>
|
||||
<p>Versioned types with automatic migration. Data structures evolve safely.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="showcase-code">
|
||||
<pre><code><span class="cm">// Task v1: Original data model</span>
|
||||
<span class="kw">type</span> <span class="ty">Task</span> <span class="cm">@v1</span> {
|
||||
id: String,
|
||||
title: String,
|
||||
done: Bool
|
||||
}
|
||||
|
||||
<span class="cm">// Task v2: Added priority field with migration</span>
|
||||
<span class="kw">type</span> <span class="ty">Task</span> <span class="cm">@v2</span> {
|
||||
id: String,
|
||||
title: String,
|
||||
done: Bool,
|
||||
priority: String,
|
||||
|
||||
<span class="cm">// Old tasks get "medium" priority by default</span>
|
||||
<span class="kw">from</span> <span class="cm">@v1</span> = {
|
||||
id: old.id,
|
||||
title: old.title,
|
||||
done: old.done,
|
||||
priority: <span class="st">"medium"</span>
|
||||
}
|
||||
}
|
||||
|
||||
<span class="cm">// Pure, total, idempotent business logic</span>
|
||||
<span class="kw">fn</span> <span class="fn">validateTitle</span>(title: String): <span class="ty">Result</span><String, String>
|
||||
<span class="kw">is</span> <span class="ty">pure</span>, <span class="ty">total</span> =
|
||||
<span class="kw">if</span> String.length(title) == <span class="num">0</span> <span class="kw">then</span>
|
||||
Err(<span class="st">"Title cannot be empty"</span>)
|
||||
<span class="kw">else</span>
|
||||
Ok(title)</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="actions" style="margin-top: var(--space-lg);">
|
||||
<a href="https://git.qrty.ink/blu/lux/src/branch/master/examples/showcase/task_manager.lux" class="btn btn-primary">View Full Code</a>
|
||||
<a href="/tour/06-effects-intro.html" class="btn btn-secondary">Learn About Effects</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Get Started -->
|
||||
<section class="showcase-section" style="text-align: center;">
|
||||
<h2>Build Your Own</h2>
|
||||
<p style="margin-bottom: var(--space-lg); color: var(--text-secondary);">Ready to start building with Lux?</p>
|
||||
<div style="display: flex; gap: var(--space-md); justify-content: center; flex-wrap: wrap;">
|
||||
<a href="/install" class="btn btn-primary">Install Lux</a>
|
||||
<a href="/tour/" class="btn btn-secondary">Take the Tour</a>
|
||||
<a href="/examples/" class="btn btn-tertiary">Browse Examples</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
66
website/serve.lux
Normal file
66
website/serve.lux
Normal file
@@ -0,0 +1,66 @@
|
||||
// Static File Server for Lux Website
|
||||
//
|
||||
// Usage: lux website/serve.lux
|
||||
// Then open http://localhost:8080
|
||||
//
|
||||
// This demonstrates serving the Lux website using Lux's own HTTP server!
|
||||
|
||||
fn main(): Unit with {HttpServer, Console, File} = {
|
||||
let port = 8080
|
||||
let root = "website"
|
||||
|
||||
HttpServer.listen(port)
|
||||
Console.print("Lux website server running at http://localhost:" + toString(port))
|
||||
Console.print("Serving files from: " + root)
|
||||
Console.print("Press Ctrl+C to stop")
|
||||
Console.print("")
|
||||
|
||||
serverLoop(root)
|
||||
}
|
||||
|
||||
fn serverLoop(root: String): Unit with {HttpServer, Console, File} = {
|
||||
let req = HttpServer.accept()
|
||||
|
||||
// Log request
|
||||
Console.print(req.method + " " + req.path)
|
||||
|
||||
// Only handle GET requests
|
||||
if req.method != "GET" then {
|
||||
HttpServer.respond(405, "Method Not Allowed")
|
||||
serverLoop(root)
|
||||
} else {
|
||||
serveFile(root, req.path)
|
||||
serverLoop(root)
|
||||
}
|
||||
}
|
||||
|
||||
fn serveFile(root: String, reqPath: String): Unit with {HttpServer, Console, File} = {
|
||||
// Determine file path
|
||||
let path = if reqPath == "/" then "/index.html" else reqPath
|
||||
let filePath = root + path
|
||||
|
||||
// Try to serve the file
|
||||
if File.exists(filePath) then {
|
||||
let content = File.read(filePath)
|
||||
HttpServer.respond(200, content)
|
||||
} else {
|
||||
// Try with .html extension for clean URLs
|
||||
let htmlPath = filePath + ".html"
|
||||
if File.exists(htmlPath) then {
|
||||
let content = File.read(htmlPath)
|
||||
HttpServer.respond(200, content)
|
||||
} else {
|
||||
// Try index.html for directory paths
|
||||
let indexPath = filePath + "/index.html"
|
||||
if File.exists(indexPath) then {
|
||||
let content = File.read(indexPath)
|
||||
HttpServer.respond(200, content)
|
||||
} else {
|
||||
Console.print(" -> 404 Not Found: " + filePath)
|
||||
HttpServer.respond(404, "Not Found: " + reqPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output = run main() with {}
|
||||
Reference in New Issue
Block a user