Compare commits
63 Commits
26d340b5a3
...
v0.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
| b0ccde749c | |||
| 4ba7a23ae3 | |||
| 89741b4a32 | |||
| 3a2376cd49 | |||
| 4dfb04a1b6 | |||
| 3cdde02eb2 | |||
| a5762d0397 | |||
| 1132c621c6 | |||
| a0fff1814e | |||
| 4e9e823246 | |||
| 6a2e4a7ac1 | |||
| 3d706cb32b | |||
| 7c3bfa9301 | |||
| b56c5461f1 | |||
| 61e1469845 | |||
| bb0a288210 | |||
| 5d7f4633e1 | |||
| d05b13d840 | |||
| 0ee3050704 | |||
| 80b1276f9f | |||
| bd843d2219 | |||
| d76aa17b38 | |||
| c23d9c7078 | |||
| fffacd2467 | |||
| 2ae2c132e5 | |||
| 4909ff9fff | |||
| 8e788c8a9f | |||
| dbdd3cca57 | |||
| 3ac022c04a | |||
| 6bedd37ac7 | |||
| 2909bf14b6 | |||
| d8871acf7e | |||
| 73b5eee664 | |||
| 542255780d | |||
| bac63bab2a | |||
| db82ca1a1c | |||
| 98605d2b70 | |||
| e3b6f4322a | |||
| d26fd975d1 | |||
| 1fa599f856 | |||
| c2404a5ec1 | |||
| 19068ead96 | |||
| 44ea1eebb0 | |||
| 8c90d5a8dc | |||
| bc60f1c8f1 | |||
| 52e3876b81 | |||
| 7e76acab18 | |||
| 5a853702d1 | |||
| fe985c96f5 | |||
| 4b553031fd | |||
| 552e7a4972 | |||
| 49ab70829a | |||
| 8a001a8f26 | |||
| 0cf8f2a4a2 | |||
| dfcfda1f48 | |||
| 3ee3529ef6 | |||
| b02807ebf4 | |||
| 87c1fb1bbd | |||
| 204950357f | |||
| 3a46299404 | |||
| bc1e5aa8a1 | |||
| 33b4f57faf | |||
| ba3b713f8c |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,6 +1,14 @@
|
||||
/target
|
||||
/result
|
||||
|
||||
# Claude Code project instructions
|
||||
CLAUDE.md
|
||||
|
||||
# Build output
|
||||
_site/
|
||||
docs/*.html
|
||||
docs/*.css
|
||||
|
||||
# Test binaries
|
||||
hello
|
||||
test_rc
|
||||
|
||||
177
CLAUDE.md
Normal file
177
CLAUDE.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Lux Project Notes
|
||||
|
||||
## Development Environment
|
||||
|
||||
This is a **Nix environment**. Tools like `cargo`, `rustc`, `clippy`, etc. are not available in the base shell.
|
||||
|
||||
To run Rust/Cargo commands, use one of:
|
||||
```bash
|
||||
nix develop --command cargo test
|
||||
nix develop --command cargo build
|
||||
nix develop --command cargo clippy
|
||||
nix develop --command cargo fmt
|
||||
```
|
||||
|
||||
Or enter the development shell first:
|
||||
```bash
|
||||
nix develop
|
||||
# then run commands normally
|
||||
cargo test
|
||||
```
|
||||
|
||||
The `lux` binary can be run directly if already built:
|
||||
```bash
|
||||
./target/debug/lux test
|
||||
./target/release/lux <file.lux>
|
||||
```
|
||||
|
||||
For additional tools not in the dev shell:
|
||||
```bash
|
||||
nix-shell -p <program>
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
When making changes:
|
||||
1. **Always run tests**: `cargo check && cargo test` - fix all errors and warnings
|
||||
2. **Lint the Lux code**: `./target/release/lux lint` - fix warnings
|
||||
3. **Check Lux code**: `./target/release/lux check` - type check + lint in one pass
|
||||
4. **Format Lux code**: `./target/release/lux fmt` - auto-format all .lux files
|
||||
5. **Write tests**: Add tests to cover new code
|
||||
6. **Document features**: Provide documentation and tutorials for new features/frameworks
|
||||
7. **Fix language limitations**: If you encounter parser/type system limitations, fix them (without regressions on guarantees or speed)
|
||||
8. **Git commits**: Always use `--no-gpg-sign` flag
|
||||
|
||||
### Post-work checklist (run after each committable change)
|
||||
|
||||
**MANDATORY: Run the full validation script after every committable change:**
|
||||
```bash
|
||||
./scripts/validate.sh
|
||||
```
|
||||
|
||||
This script runs ALL of the following checks and will fail if any regress:
|
||||
1. `cargo check` — no Rust compilation errors
|
||||
2. `cargo test` — all Rust tests pass (currently 387)
|
||||
3. `cargo build --release` — release binary builds
|
||||
4. `lux test` on every package (path, frontmatter, xml, rss, markdown) — all 286 package tests pass
|
||||
5. `lux check` on every package — type checking + lint passes
|
||||
|
||||
If `validate.sh` is not available or you need to run manually:
|
||||
```bash
|
||||
nix develop --command cargo check # No Rust errors
|
||||
nix develop --command cargo test # All Rust tests pass
|
||||
nix develop --command cargo build --release # Build release binary
|
||||
cd ../packages/path && ../../lang/target/release/lux test # Package tests
|
||||
cd ../packages/frontmatter && ../../lang/target/release/lux test
|
||||
cd ../packages/xml && ../../lang/target/release/lux test
|
||||
cd ../packages/rss && ../../lang/target/release/lux test
|
||||
cd ../packages/markdown && ../../lang/target/release/lux test
|
||||
```
|
||||
|
||||
**Do NOT commit if any check fails.** Fix the issue first.
|
||||
|
||||
### Commit after every piece of work
|
||||
**After completing each logical unit of work, commit immediately.** This is NOT optional — every fix, feature, or change MUST be committed right away. Do not let changes accumulate uncommitted across multiple features. Each commit should be a single logical change (one feature, one bugfix, etc.). Use `--no-gpg-sign` flag for all commits.
|
||||
|
||||
**Commit workflow:**
|
||||
1. Make the change
|
||||
2. Run `./scripts/validate.sh` (all 13 checks must pass)
|
||||
3. `git add` the relevant files
|
||||
4. `git commit --no-gpg-sign -m "type: description"` (use conventional commits: fix/feat/chore/docs)
|
||||
5. Move on to the next task
|
||||
|
||||
**Never skip committing.** If you fixed a bug, commit it. If you added a feature, commit it. If you updated docs, commit it. Do not batch unrelated changes into one commit.
|
||||
|
||||
**IMPORTANT: Always verify Lux code you write:**
|
||||
- Run with interpreter: `./target/release/lux file.lux`
|
||||
- Compile to binary: `./target/release/lux compile file.lux`
|
||||
- Both must work before claiming code is functional
|
||||
- The C backend has limited effect support (Console, File only - no HttpServer, Http, etc.)
|
||||
|
||||
## CLI Commands & Aliases
|
||||
|
||||
| Command | Alias | Description |
|
||||
|---------|-------|-------------|
|
||||
| `lux fmt` | `lux f` | Format .lux files |
|
||||
| `lux test` | `lux t` | Run test suite |
|
||||
| `lux check` | `lux k` | Type check + lint |
|
||||
| `lux lint` | `lux l` | Lint only (with `--explain` for detailed help) |
|
||||
| `lux serve` | `lux s` | Static file server |
|
||||
| `lux compile` | `lux c` | Compile to binary |
|
||||
|
||||
## Documenting Lux Language Errors
|
||||
|
||||
When working on any major task that involves writing Lux code, **document every language error, limitation, or surprising behavior** you encounter. This log is optimized for LLM consumption so future sessions can avoid repeating mistakes.
|
||||
|
||||
**File:** Maintain an `ISSUES.md` in the relevant project directory (e.g., `~/src/blu-site/ISSUES.md`).
|
||||
|
||||
**Format for each entry:**
|
||||
```markdown
|
||||
## Issue N: <Short descriptive title>
|
||||
|
||||
**Category**: Parser limitation | Type checker gap | Missing feature | Runtime error | Documentation gap
|
||||
**Severity**: High | Medium | Low
|
||||
**Status**: Open | **Fixed** (commit hash or version)
|
||||
|
||||
<1-2 sentence description of the problem>
|
||||
|
||||
**Reproduction:**
|
||||
```lux
|
||||
// Minimal code that triggers the issue
|
||||
```
|
||||
|
||||
**Error message:** `<exact error text>`
|
||||
|
||||
**Workaround:** <how to accomplish the goal despite the limitation>
|
||||
|
||||
**Fix:** <if fixed, what was changed and where>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Add new issues as you encounter them during any task
|
||||
- When a previously documented issue gets fixed, update its status to **Fixed** and note the commit/version
|
||||
- Remove entries that are no longer relevant (e.g., the feature was redesigned entirely)
|
||||
- Keep the summary table at the bottom of ISSUES.md in sync with the entries
|
||||
- Do NOT duplicate issues already documented -- check existing entries first
|
||||
|
||||
## Code Quality
|
||||
|
||||
- Fix all compiler warnings before committing
|
||||
- Ensure all tests pass (currently 387 tests)
|
||||
- Add new tests when adding features
|
||||
- Keep examples and documentation in sync
|
||||
|
||||
## Lux Language Notes
|
||||
|
||||
### Top-level expressions
|
||||
Bare `run` expressions are not allowed at top-level. You must wrap them in a `let` binding:
|
||||
```lux
|
||||
// WRONG: parse error
|
||||
run main() with {}
|
||||
|
||||
// CORRECT
|
||||
let output = run main() with {}
|
||||
```
|
||||
|
||||
### String methods
|
||||
Lux uses module-qualified function calls, not method syntax on primitives:
|
||||
```lux
|
||||
// WRONG: not valid syntax
|
||||
path.endsWith(".html")
|
||||
|
||||
// CORRECT
|
||||
String.endsWith(path, ".html")
|
||||
```
|
||||
|
||||
### Available String functions
|
||||
Key string functions (all in `String.` namespace):
|
||||
- `String.length(s)` - get length
|
||||
- `String.startsWith(s, prefix)` - check prefix
|
||||
- `String.endsWith(s, suffix)` - check suffix
|
||||
- `String.split(s, delimiter)` - split into list
|
||||
- `String.join(list, delimiter)` - join list
|
||||
- `String.substring(s, start, end)` - extract substring
|
||||
- `String.indexOf(s, needle)` - find position (returns Option)
|
||||
- `String.replace(s, old, new)` - replace occurrences
|
||||
- `String.trim(s)` - trim whitespace
|
||||
- `String.toLower(s)` / `String.toUpper(s)` - case conversion
|
||||
685
Cargo.lock
generated
685
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lux"
|
||||
version = "0.1.0"
|
||||
version = "0.1.4"
|
||||
edition = "2021"
|
||||
description = "A functional programming language with first-class effects, schema evolution, and behavioral types"
|
||||
license = "MIT"
|
||||
@@ -13,8 +13,10 @@ 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"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
367
PACKAGES.md
Normal file
367
PACKAGES.md
Normal file
@@ -0,0 +1,367 @@
|
||||
# Lux Package Ecosystem Plan
|
||||
|
||||
## Current State
|
||||
|
||||
### Stdlib (built-in)
|
||||
| Module | Coverage |
|
||||
|--------|----------|
|
||||
| String | Comprehensive (split, join, trim, indexOf, replace, etc.) |
|
||||
| List | Good (map, filter, fold, head, tail, concat, range, find, any, all, take, drop) |
|
||||
| Option | Basic (map, flatMap, getOrElse, isSome, isNone) |
|
||||
| Result | Basic (map, flatMap, getOrElse, isOk, isErr) |
|
||||
| Math | Basic (abs, min, max, sqrt, pow, floor, ceil, round) |
|
||||
| Json | Comprehensive (parse, stringify, get, typed extractors, constructors) |
|
||||
| File | Good (read, write, append, exists, delete, readDir, isDir, mkdir) |
|
||||
| Console | Good (print, read, readLine, readInt) |
|
||||
| Process | Good (exec, execStatus, env, args, exit, cwd) |
|
||||
| Http | Basic (get, post, put, delete, setHeader) |
|
||||
| HttpServer | Basic (listen, accept, respond) |
|
||||
| Time | Minimal (now, sleep) |
|
||||
| Random | Basic (int, float, bool) |
|
||||
| Sql | Good (SQLite: open, query, execute, transactions) |
|
||||
| Postgres | Good (connect, query, execute, transactions) |
|
||||
| Schema | Niche (versioned data migration) |
|
||||
| Test | Good (assert, assertEqual, assertTrue) |
|
||||
| Concurrent | Experimental (spawn, await, yield, cancel) |
|
||||
| Channel | Experimental (create, send, receive) |
|
||||
|
||||
### Registry (pkgs.lux) - 3 packages
|
||||
| Package | Version | Notes |
|
||||
|---------|---------|-------|
|
||||
| json | 1.0.0 | Wraps stdlib Json with convenience functions (getPath, getString, etc.) |
|
||||
| http-client | 0.1.0 | Wraps stdlib Http with JSON helpers, URL encoding |
|
||||
| testing | 0.1.0 | Wraps stdlib Test with describe/it structure |
|
||||
|
||||
---
|
||||
|
||||
## Gap Analysis
|
||||
|
||||
### What's Missing vs Other Languages
|
||||
|
||||
Compared to ecosystems like Rust/cargo, Go, Python, Elm, Gleam:
|
||||
|
||||
| Category | Gap | Impact | Notes |
|
||||
|----------|-----|--------|-------|
|
||||
| **Collections** | No HashMap, Set, Queue, Stack | Critical | List-of-pairs with O(n) lookup is the only option |
|
||||
| **Sorting** | No List.sort or List.sortBy | High | Must implement insertion sort manually |
|
||||
| **Date/Time** | Only `Time.now()` (epoch ms), no parsing/formatting | High | blu-site does string-based date formatting manually |
|
||||
| **Markdown** | No markdown parser | High | blu-site has 300+ lines of hand-rolled markdown |
|
||||
| **XML/RSS** | No XML generation | High | Can't generate RSS feeds or sitemaps |
|
||||
| **Regex** | No pattern matching on strings | High | Character-by-character scanning required |
|
||||
| **Path** | No file path utilities | Medium | basename/dirname manually reimplemented |
|
||||
| **YAML/TOML** | No config file parsing (beyond JSON) | Medium | Frontmatter parsing is manual |
|
||||
| **Template** | No string templating | Medium | HTML built via raw string concatenation |
|
||||
| **URL** | No URL parsing/encoding | Medium | http-client has basic urlEncode but no parser |
|
||||
| **Crypto** | No hashing (SHA256, etc.) | Medium | Can't do checksums, content hashing |
|
||||
| **Base64** | No encoding/decoding | Low | Needed for data URIs, some auth |
|
||||
| **CSV** | No CSV parsing | Low | Common data format |
|
||||
| **UUID** | No UUID generation | Low | Useful for IDs |
|
||||
| **Logging** | No structured logging | Low | Just Console.print |
|
||||
| **CLI** | No argument parsing library | Low | Manual arg handling |
|
||||
|
||||
### What Should Be Stdlib vs Package
|
||||
|
||||
**Should be stdlib additions** (too fundamental to be packages):
|
||||
- HashMap / Map type (requires runtime support)
|
||||
- List.sort / List.sortBy (fundamental operation)
|
||||
- Better Time module (date parsing, formatting)
|
||||
- Regex (needs runtime/C support for performance)
|
||||
- Path module (cross-platform file path handling)
|
||||
|
||||
**Should be packages** (application-level, opinionated, composable):
|
||||
- markdown
|
||||
- xml
|
||||
- rss/atom
|
||||
- frontmatter
|
||||
- template
|
||||
- csv
|
||||
- crypto
|
||||
- ssg (static site generator framework)
|
||||
|
||||
---
|
||||
|
||||
## Priority Package Plans
|
||||
|
||||
Ordered by what unblocks blu-site fixes first, then general ecosystem value.
|
||||
|
||||
---
|
||||
|
||||
### Package 1: `markdown` (Priority: HIGHEST)
|
||||
|
||||
**Why:** The 300-line markdown parser in blu-site's main.lux is general-purpose code that belongs in a reusable package. It's also the most complex part of blu-site and has known bugs (e.g., `### ` inside list items renders literally).
|
||||
|
||||
**Scope:**
|
||||
```
|
||||
markdown/
|
||||
lux.toml
|
||||
lib.lux # Public API: parse, parseInline
|
||||
src/
|
||||
inline.lux # Inline parsing (bold, italic, links, images, code)
|
||||
block.lux # Block parsing (headings, lists, code blocks, blockquotes, hr)
|
||||
types.lux # AST types (optional - could emit HTML directly)
|
||||
```
|
||||
|
||||
**Public API:**
|
||||
```lux
|
||||
// Convert markdown string to HTML string
|
||||
pub fn toHtml(markdown: String): String
|
||||
|
||||
// Convert inline markdown only (no blocks)
|
||||
pub fn inlineToHtml(text: String): String
|
||||
|
||||
// Escape HTML entities
|
||||
pub fn escapeHtml(s: String): String
|
||||
```
|
||||
|
||||
**Improvements over current blu-site code:**
|
||||
- Fix heading-inside-list-item rendering (`- ### Title` should work)
|
||||
- Support nested lists (currently flat only)
|
||||
- Support reference-style links `[text][ref]`
|
||||
- Handle edge cases (empty lines in code blocks, nested blockquotes)
|
||||
- Proper HTML entity escaping in more contexts
|
||||
|
||||
**Depends on:** Nothing (pure string processing)
|
||||
|
||||
**Estimated size:** ~400-500 lines of Lux
|
||||
|
||||
---
|
||||
|
||||
### Package 2: `xml` (Priority: HIGH)
|
||||
|
||||
**Why:** Needed for RSS/Atom feed generation, sitemap.xml, and robots.txt generation. General-purpose XML builder that doesn't try to parse XML (which would need regex), just emits it.
|
||||
|
||||
**Scope:**
|
||||
```
|
||||
xml/
|
||||
lux.toml
|
||||
lib.lux # Public API: element, document, serialize
|
||||
```
|
||||
|
||||
**Public API:**
|
||||
```lux
|
||||
type XmlNode =
|
||||
| Element(String, List<XmlAttr>, List<XmlNode>)
|
||||
| Text(String)
|
||||
| CData(String)
|
||||
| Comment(String)
|
||||
| Declaration(String, String) // version, encoding
|
||||
|
||||
type XmlAttr =
|
||||
| Attr(String, String)
|
||||
|
||||
// Build an XML element
|
||||
pub fn element(tag: String, attrs: List<XmlAttr>, children: List<XmlNode>): XmlNode
|
||||
|
||||
// Build a text node (auto-escapes)
|
||||
pub fn text(content: String): XmlNode
|
||||
|
||||
// Build a CDATA section
|
||||
pub fn cdata(content: String): XmlNode
|
||||
|
||||
// Serialize XML tree to string
|
||||
pub fn serialize(node: XmlNode): String
|
||||
|
||||
// Serialize with XML declaration header
|
||||
pub fn document(version: String, encoding: String, root: XmlNode): String
|
||||
|
||||
// Convenience: self-closing element
|
||||
pub fn selfClosing(tag: String, attrs: List<XmlAttr>): XmlNode
|
||||
```
|
||||
|
||||
**Depends on:** Nothing
|
||||
|
||||
**Estimated size:** ~150-200 lines
|
||||
|
||||
---
|
||||
|
||||
### Package 3: `rss` (Priority: HIGH)
|
||||
|
||||
**Why:** Directly needed for blu-site's #6 priority fix (add RSS feed). Builds on `xml` package.
|
||||
|
||||
**Scope:**
|
||||
```
|
||||
rss/
|
||||
lux.toml # depends on xml
|
||||
lib.lux # Public API: feed, item, toXml, toAtom
|
||||
```
|
||||
|
||||
**Public API:**
|
||||
```lux
|
||||
type FeedInfo =
|
||||
| FeedInfo(String, String, String, String, String)
|
||||
// title, link, description, language, lastBuildDate
|
||||
|
||||
type FeedItem =
|
||||
| FeedItem(String, String, String, String, String, String)
|
||||
// title, link, description, pubDate, guid, categories (comma-separated)
|
||||
|
||||
// Generate RSS 2.0 XML string
|
||||
pub fn toRss(info: FeedInfo, items: List<FeedItem>): String
|
||||
|
||||
// Generate Atom 1.0 XML string
|
||||
pub fn toAtom(info: FeedInfo, items: List<FeedItem>): String
|
||||
```
|
||||
|
||||
**Depends on:** `xml`
|
||||
|
||||
**Estimated size:** ~100-150 lines
|
||||
|
||||
---
|
||||
|
||||
### Package 4: `frontmatter` (Priority: HIGH)
|
||||
|
||||
**Why:** blu-site has ~50 lines of fragile frontmatter parsing. This is a common need for any content-driven Lux project. The current parser uses `String.indexOf(line, ": ")` which breaks on values containing `: `.
|
||||
|
||||
**Scope:**
|
||||
```
|
||||
frontmatter/
|
||||
lux.toml
|
||||
lib.lux # Public API: parse
|
||||
```
|
||||
|
||||
**Public API:**
|
||||
```lux
|
||||
type FrontmatterResult =
|
||||
| FrontmatterResult(List<(String, String)>, String)
|
||||
// key-value pairs, remaining body
|
||||
|
||||
// Parse frontmatter from a string (--- delimited YAML-like header)
|
||||
pub fn parse(content: String): FrontmatterResult
|
||||
|
||||
// Get a value by key from parsed frontmatter
|
||||
pub fn get(pairs: List<(String, String)>, key: String): Option<String>
|
||||
|
||||
// Get a value or default
|
||||
pub fn getOrDefault(pairs: List<(String, String)>, key: String, default: String): String
|
||||
|
||||
// Parse a space-separated tag string into a list
|
||||
pub fn parseTags(tagString: String): List<String>
|
||||
```
|
||||
|
||||
**Improvements over current blu-site code:**
|
||||
- Handle values with `: ` in them (only split on first `: `)
|
||||
- Handle multi-line values (indented continuation)
|
||||
- Handle quoted values with embedded newlines
|
||||
- Strip quotes from values consistently
|
||||
|
||||
**Depends on:** Nothing
|
||||
|
||||
**Estimated size:** ~100-150 lines
|
||||
|
||||
---
|
||||
|
||||
### Package 5: `path` (Priority: MEDIUM)
|
||||
|
||||
**Why:** blu-site manually implements `basename` and `dirname`. Any file-processing Lux program needs these. Tiny but universally useful.
|
||||
|
||||
**Scope:**
|
||||
```
|
||||
path/
|
||||
lux.toml
|
||||
lib.lux
|
||||
```
|
||||
|
||||
**Public API:**
|
||||
```lux
|
||||
// Get filename from path: "/foo/bar.txt" -> "bar.txt"
|
||||
pub fn basename(p: String): String
|
||||
|
||||
// Get directory from path: "/foo/bar.txt" -> "/foo"
|
||||
pub fn dirname(p: String): String
|
||||
|
||||
// Get file extension: "file.txt" -> "txt", "file" -> ""
|
||||
pub fn extension(p: String): String
|
||||
|
||||
// Remove file extension: "file.txt" -> "file"
|
||||
pub fn stem(p: String): String
|
||||
|
||||
// Join path segments: join("foo", "bar") -> "foo/bar"
|
||||
pub fn join(a: String, b: String): String
|
||||
|
||||
// Normalize path: "foo//bar/../baz" -> "foo/baz"
|
||||
pub fn normalize(p: String): String
|
||||
|
||||
// Check if path is absolute
|
||||
pub fn isAbsolute(p: String): Bool
|
||||
```
|
||||
|
||||
**Depends on:** Nothing
|
||||
|
||||
**Estimated size:** ~80-120 lines
|
||||
|
||||
---
|
||||
|
||||
### Package 6: `sitemap` (Priority: MEDIUM)
|
||||
|
||||
**Why:** Directly needed for blu-site's #9 priority fix. Simple package that generates sitemap.xml.
|
||||
|
||||
**Scope:**
|
||||
```
|
||||
sitemap/
|
||||
lux.toml # depends on xml
|
||||
lib.lux
|
||||
```
|
||||
|
||||
**Public API:**
|
||||
```lux
|
||||
type SitemapEntry =
|
||||
| SitemapEntry(String, String, String, String)
|
||||
// url, lastmod (ISO date), changefreq, priority
|
||||
|
||||
// Generate sitemap.xml string
|
||||
pub fn generate(entries: List<SitemapEntry>): String
|
||||
|
||||
// Generate a simple robots.txt pointing to the sitemap
|
||||
pub fn robotsTxt(sitemapUrl: String): String
|
||||
```
|
||||
|
||||
**Depends on:** `xml`
|
||||
|
||||
**Estimated size:** ~50-70 lines
|
||||
|
||||
---
|
||||
|
||||
### Package 7: `ssg` (Priority: LOW - future)
|
||||
|
||||
**Why:** Once markdown, frontmatter, rss, sitemap, and path packages exist, the remaining logic in blu-site's main.lux is generic SSG framework code: read content dirs, parse posts, sort by date, generate section indexes, generate tag pages, copy static assets. This could be extracted into a framework package that other Lux users could use to build their own static sites.
|
||||
|
||||
**This should wait** until the foundation packages above are stable and battle-tested through blu-site usage.
|
||||
|
||||
---
|
||||
|
||||
## Non-Package Stdlib Improvements Needed
|
||||
|
||||
These gaps are too fundamental to be packages and should be added to the Lux language itself:
|
||||
|
||||
### HashMap (Critical)
|
||||
Every package above that needs key-value lookups (frontmatter, xml attributes, etc.) is working around the lack of HashMap with `List<(String, String)>`. This is O(n) per lookup and makes code verbose. A stdlib `Map` module would transform the ecosystem.
|
||||
|
||||
### List.sort / List.sortBy (High)
|
||||
blu-site implements insertion sort manually. Every content-driven app needs sorting. This should be a stdlib function.
|
||||
|
||||
### Time.format / Time.parse (High)
|
||||
blu-site manually parses "2025-01-15" by substring extraction and maps month numbers to names. A proper date/time library (even just ISO 8601 parsing and basic formatting) would help every package above.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
```
|
||||
Phase 1 (unblock blu-site fixes):
|
||||
1. markdown - extract from blu-site, fix bugs, publish
|
||||
2. frontmatter - extract from blu-site, improve robustness
|
||||
3. path - tiny, universally useful
|
||||
4. xml - needed by rss and sitemap
|
||||
|
||||
Phase 2 (complete blu-site features):
|
||||
5. rss - depends on xml
|
||||
6. sitemap - depends on xml
|
||||
|
||||
Phase 3 (ecosystem growth):
|
||||
7. template - string templating (mustache-like)
|
||||
8. csv - data processing
|
||||
9. cli - argument parsing
|
||||
10. ssg - framework extraction from blu-site
|
||||
```
|
||||
|
||||
Each package should be developed in its own directory under `~/src/`, published to the git.qrty.ink registry, and tested by integrating it into blu-site.
|
||||
84
README.md
84
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
|
||||
|
||||
@@ -120,17 +127,35 @@ fn main(): Unit with {Console} =
|
||||
|
||||
## Status
|
||||
|
||||
**Current Phase: Prototype Implementation**
|
||||
**Core Language:** Complete
|
||||
- Full type system with Hindley-Milner inference
|
||||
- Pattern matching with exhaustiveness checking
|
||||
- Algebraic data types, generics, string interpolation
|
||||
- Effect system with handlers
|
||||
- Behavioral types (pure, total, idempotent, deterministic, commutative)
|
||||
- Schema evolution with version tracking
|
||||
|
||||
The interpreter is functional with:
|
||||
- Core language (functions, closures, pattern matching)
|
||||
- Effect system (declare effects, use operations, handle with handlers)
|
||||
- Type checking with effect tracking
|
||||
- REPL for interactive development
|
||||
**Compilation Targets:**
|
||||
- Interpreter (full-featured)
|
||||
- C backend (functions, closures, pattern matching, lists, reference counting)
|
||||
- JavaScript backend (full language, browser & Node.js, DOM, TEA runtime)
|
||||
|
||||
**Tooling:**
|
||||
- REPL with history
|
||||
- LSP server (diagnostics, hover, completions, go-to-definition)
|
||||
- Formatter (`lux fmt`)
|
||||
- Package manager (`lux pkg`)
|
||||
- Watch mode / hot reload
|
||||
|
||||
**Standard Library:**
|
||||
- String, List, Option, Result, Math, JSON modules
|
||||
- Console, File, Http, Random, Time, Process effects
|
||||
- SQL effect (SQLite with transactions)
|
||||
- PostgreSQL effect (connection pooling ready)
|
||||
- DOM effect (40+ browser operations)
|
||||
|
||||
See:
|
||||
- [SKILLS.md](./SKILLS.md) — Language specification and implementation roadmap
|
||||
- [docs/VISION.md](./docs/VISION.md) — Problems Lux solves and development roadmap
|
||||
- [docs/ROADMAP.md](./docs/ROADMAP.md) — Development roadmap and feature status
|
||||
- [docs/OVERVIEW.md](./docs/OVERVIEW.md) — Use cases, pros/cons, complexity analysis
|
||||
|
||||
## Design Goals
|
||||
@@ -150,20 +175,31 @@ See:
|
||||
|
||||
## Building
|
||||
|
||||
### With Nix (recommended)
|
||||
|
||||
```bash
|
||||
# Build
|
||||
nix build
|
||||
|
||||
# Run the REPL
|
||||
nix run
|
||||
|
||||
# Enter development shell
|
||||
nix develop
|
||||
|
||||
# Run tests
|
||||
nix develop --command cargo test
|
||||
```
|
||||
|
||||
### With Cargo
|
||||
|
||||
Requires Rust 1.70+:
|
||||
|
||||
```bash
|
||||
# Build the interpreter
|
||||
cargo build --release
|
||||
|
||||
# Run the REPL
|
||||
cargo run
|
||||
|
||||
# Run a file
|
||||
cargo run -- examples/hello.lux
|
||||
|
||||
# Run tests
|
||||
cargo test
|
||||
./target/release/lux # REPL
|
||||
./target/release/lux file.lux # Run a file
|
||||
cargo test # Tests
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1,148 +1,140 @@
|
||||
# Lux Language Benchmark Results
|
||||
|
||||
Generated: Sat Feb 14 2026
|
||||
Generated: Feb 16 2026
|
||||
|
||||
## Environment
|
||||
- **Platform**: Linux x86_64
|
||||
- **Lux**: Compiled to native via C (gcc -O2)
|
||||
- **Rust**: rustc 1.92.0 with -O
|
||||
- **C**: gcc -O2
|
||||
- **Go**: go 1.25.5
|
||||
- **Node.js**: v16.20.2 (V8 JIT)
|
||||
- **Bun**: 1.3.5 (JavaScriptCore)
|
||||
- **Python**: 3.13.5
|
||||
- **Platform**: Linux x86_64 (NixOS)
|
||||
- **Lux**: Compiled via C backend + gcc -O3
|
||||
- **Tools**: hyperfine, poop
|
||||
- **Comparison**: C (gcc), Rust (rustc+LLVM), Zig (LLVM)
|
||||
|
||||
## Summary
|
||||
## Quick Start
|
||||
|
||||
Lux compiles to native code via C and achieves performance comparable to Rust and C, while being significantly faster than interpreted/JIT languages.
|
||||
```bash
|
||||
nix run .#bench # Full hyperfine comparison
|
||||
nix run .#bench-poop # Detailed CPU metrics
|
||||
nix run .#bench-quick # Just Lux vs C
|
||||
```
|
||||
|
||||
| Benchmark | Lux | Rust | C | Go | Node.js | Bun | Python |
|
||||
|-----------|-----|------|---|-----|---------|-----|--------|
|
||||
| Fibonacci (fib 35) | 0.015s | 0.018s | 0.014s | 0.041s | 0.110s | 0.065s | 0.928s |
|
||||
| Prime Counting (10k) | 0.002s | 0.002s | 0.001s | 0.002s | 0.034s | 0.012s | 0.023s |
|
||||
| Sum Loop (10M) | 0.004s | 0.002s | 0.004s | 0.009s | 0.042s | 0.023s | 0.384s |
|
||||
| Ackermann (3,10) | 0.020s | 0.029s | 0.020s | 0.107s | 0.207s | 0.121s | 5.716s |
|
||||
| Selection Sort (1k) | 0.003s | 0.002s | 0.001s | 0.002s | 0.039s | 0.021s | 0.032s |
|
||||
| List Operations (10k) | 0.002s | - | - | - | 0.030s | 0.016s | - |
|
||||
## CPU Benchmark Results
|
||||
|
||||
### Performance Rankings (Average)
|
||||
### hyperfine (Statistical Timing)
|
||||
|
||||
1. **C** - Baseline (fastest)
|
||||
2. **Rust** - ~1.0-1.5x of C
|
||||
3. **Lux** - ~1.0-1.5x of C (matches Rust)
|
||||
4. **Go** - ~2-5x of C
|
||||
5. **Bun** - ~10-20x of C
|
||||
6. **Node.js** - ~15-30x of C
|
||||
7. **Python** - ~30-300x of C
|
||||
```
|
||||
Summary
|
||||
/tmp/fib_lux ran
|
||||
1.03 ± 0.08 times faster than /tmp/fib_c
|
||||
1.47 ± 0.04 times faster than /tmp/fib_rust
|
||||
1.67 ± 0.05 times faster than /tmp/fib_zig
|
||||
```
|
||||
|
||||
## Benchmark Details
|
||||
| Binary | Mean | Std Dev | vs Lux |
|
||||
|--------|------|---------|--------|
|
||||
| **Lux (compiled)** | 28.1ms | ±0.6ms | baseline |
|
||||
| C (gcc -O3) | 29.0ms | ±2.1ms | 1.03x slower |
|
||||
| Rust | 41.2ms | ±0.6ms | 1.47x slower |
|
||||
| Zig | 47.0ms | ±1.1ms | 1.67x slower |
|
||||
|
||||
### 1. Fibonacci (fib 35)
|
||||
**Tests**: Recursive function calls
|
||||
### poop (Detailed CPU Metrics)
|
||||
|
||||
| Language | Time (s) | vs Lux |
|
||||
|----------|----------|--------|
|
||||
| C | 0.014 | 0.93x |
|
||||
| Lux | 0.015 | 1.00x |
|
||||
| Rust | 0.018 | 1.20x |
|
||||
| Go | 0.041 | 2.73x |
|
||||
| Bun | 0.065 | 4.33x |
|
||||
| Node.js | 0.110 | 7.33x |
|
||||
| Python | 0.928 | 61.87x |
|
||||
| Metric | C | Lux | Rust | Zig |
|
||||
|--------|---|-----|------|-----|
|
||||
| Wall Time | 29.0ms | 29.2ms | 42.0ms | 48.1ms |
|
||||
| CPU Cycles | 53.1M | 53.2M | 78.2M | 90.4M |
|
||||
| Instructions | 293M | 292M | 302M | 317M |
|
||||
| Cache Misses | 4.39K | 4.62K | 6.47K | 340 |
|
||||
| Branch Misses | 28.3K | 32.0K | 33.5K | 29.6K |
|
||||
| Peak RSS | 1.56MB | 1.63MB | 2.00MB | 1.07MB |
|
||||
|
||||
Lux matches C and beats Rust in this recursive function call benchmark.
|
||||
## Why Lux Matches/Beats C, Rust, Zig
|
||||
|
||||
### 2. Prime Counting (up to 10000)
|
||||
**Tests**: Loops and conditionals
|
||||
### The Key: gcc's Recursion Transformation
|
||||
|
||||
| Language | Time (s) | vs Lux |
|
||||
|----------|----------|--------|
|
||||
| C | 0.001 | 0.50x |
|
||||
| Lux | 0.002 | 1.00x |
|
||||
| Rust | 0.002 | 1.00x |
|
||||
| Go | 0.002 | 1.00x |
|
||||
| Bun | 0.012 | 6.00x |
|
||||
| Python | 0.023 | 11.50x |
|
||||
| Node.js | 0.034 | 17.00x |
|
||||
Lux compiles to C, which gcc optimizes aggressively. For the Fibonacci benchmark:
|
||||
|
||||
Lux matches Rust and Go for tight loop-based code.
|
||||
**Rust/Zig (LLVM)** keeps recursive calls:
|
||||
```asm
|
||||
call fib ; actual recursive call in hot path
|
||||
```
|
||||
|
||||
### 3. Sum Loop (10 million iterations)
|
||||
**Tests**: Tight numeric loop (tail-recursive in Lux)
|
||||
**Lux/C (gcc)** transforms to loops:
|
||||
```asm
|
||||
; No recursive calls - fully loop-transformed
|
||||
; Uses registers as accumulators
|
||||
```
|
||||
|
||||
| Language | Time (s) | vs Lux |
|
||||
|----------|----------|--------|
|
||||
| Rust | 0.002 | 0.50x |
|
||||
| C | 0.004 | 1.00x |
|
||||
| Lux | 0.004 | 1.00x |
|
||||
| Go | 0.009 | 2.25x |
|
||||
| Bun | 0.023 | 5.75x |
|
||||
| Node.js | 0.042 | 10.50x |
|
||||
| Python | 0.384 | 96.00x |
|
||||
### Instruction Count Tells the Story
|
||||
|
||||
Lux's tail-call optimization achieves C-level performance.
|
||||
- **Lux/C**: 292-293M instructions executed
|
||||
- **Rust**: 302M instructions (+3%)
|
||||
- **Zig**: 317M instructions (+8%)
|
||||
|
||||
### 4. Ackermann (3, 10)
|
||||
**Tests**: Deep recursion (stack-heavy)
|
||||
More instructions = more work = slower execution.
|
||||
|
||||
| Language | Time (s) | vs Lux |
|
||||
|----------|----------|--------|
|
||||
| C | 0.020 | 1.00x |
|
||||
| Lux | 0.020 | 1.00x |
|
||||
| Rust | 0.029 | 1.45x |
|
||||
| Go | 0.107 | 5.35x |
|
||||
| Bun | 0.121 | 6.05x |
|
||||
| Node.js | 0.207 | 10.35x |
|
||||
| Python | 5.716 | 285.80x |
|
||||
## HTTP Benchmarks
|
||||
|
||||
Lux matches C and beats Rust in deep recursion, demonstrating excellent function call overhead.
|
||||
For HTTP server benchmarks, use established tools:
|
||||
|
||||
### 5. Selection Sort (1000 elements)
|
||||
**Tests**: Sorting algorithm simulation
|
||||
### TechEmpower Framework Benchmarks
|
||||
The industry standard: https://www.techempower.com/benchmarks/
|
||||
|
||||
| Language | Time (s) | vs Lux |
|
||||
|----------|----------|--------|
|
||||
| C | 0.001 | 0.33x |
|
||||
| Go | 0.002 | 0.67x |
|
||||
| Rust | 0.002 | 0.67x |
|
||||
| Lux | 0.003 | 1.00x |
|
||||
| Bun | 0.021 | 7.00x |
|
||||
| Python | 0.032 | 10.67x |
|
||||
| Node.js | 0.039 | 13.00x |
|
||||
### Standard HTTP Benchmark Tools
|
||||
|
||||
### 6. List Operations (10000 elements)
|
||||
**Tests**: map/filter/fold on functional lists with closures
|
||||
```bash
|
||||
# wrk - modern HTTP benchmarking
|
||||
wrk -t4 -c100 -d10s http://localhost:8080/
|
||||
|
||||
| Language | Time (s) | vs Lux |
|
||||
|----------|----------|--------|
|
||||
| Lux | 0.002 | 1.00x |
|
||||
| Bun | 0.016 | 8.00x |
|
||||
| Node.js | 0.030 | 15.00x |
|
||||
# ab (Apache Bench) - classic tool
|
||||
ab -n 10000 -c 100 http://localhost:8080/
|
||||
|
||||
This benchmark showcases Lux's functional programming capabilities with FBIP optimization:
|
||||
- **20,006 allocations, 20,006 frees** (no memory leaks)
|
||||
- **2 FBIP reuses, 0 copies** (efficient memory reuse)
|
||||
# hey - written in Go
|
||||
hey -n 10000 -c 100 http://localhost:8080/
|
||||
```
|
||||
|
||||
## Key Observations
|
||||
### Reference Implementations
|
||||
|
||||
1. **Native Performance**: Lux consistently matches or beats Rust and C across benchmarks
|
||||
2. **Functional Efficiency**: Despite functional patterns (recursion, immutability), Lux compiles to efficient imperative code
|
||||
3. **Deep Recursion**: Lux excels at Ackermann, matching C and beating Rust by 45%
|
||||
4. **vs JavaScript**: Lux is **7-15x faster than Node.js** and **4-8x faster than Bun**
|
||||
5. **vs Python**: Lux is **10-285x faster than Python**
|
||||
6. **vs Go**: Lux is **2-5x faster than Go** in most benchmarks
|
||||
7. **Zero Memory Leaks**: Reference counting ensures all allocations are freed
|
||||
For fair HTTP comparisons, use minimal stdlib servers:
|
||||
|
||||
## Compilation Strategy
|
||||
| Language | Command |
|
||||
|----------|---------|
|
||||
| Go | `go run` with `net/http` |
|
||||
| Rust | `cargo run` with `std::net` or hyper |
|
||||
| Node.js | `node` with `http` module |
|
||||
| Python | `python -m http.server` |
|
||||
|
||||
Lux uses a sophisticated compilation pipeline:
|
||||
1. Parse Lux source code
|
||||
2. Type inference and checking
|
||||
3. Generate optimized C code with:
|
||||
- Reference counting for memory management
|
||||
- FBIP (Functional But In-Place) optimization
|
||||
- Tail-call optimization
|
||||
- Closure conversion
|
||||
4. Compile C code with gcc -O2
|
||||
HTTP benchmarks measure I/O patterns more than language speed. Use established frameworks for meaningful comparisons.
|
||||
|
||||
This approach combines the ergonomics of a high-level functional language with the performance of systems languages.
|
||||
## Reproducing Results
|
||||
|
||||
```bash
|
||||
# Enter dev shell
|
||||
nix develop
|
||||
|
||||
# Compile all
|
||||
cargo run --release -- compile benchmarks/fib.lux -o /tmp/fib_lux
|
||||
gcc -O3 benchmarks/fib.c -o /tmp/fib_c
|
||||
rustc -C opt-level=3 -C lto benchmarks/fib.rs -o /tmp/fib_rust
|
||||
zig build-exe benchmarks/fib.zig -O ReleaseFast -femit-bin=/tmp/fib_zig
|
||||
|
||||
# Run benchmarks
|
||||
hyperfine --warmup 3 --runs 10 '/tmp/fib_lux' '/tmp/fib_c' '/tmp/fib_rust' '/tmp/fib_zig'
|
||||
poop '/tmp/fib_c' '/tmp/fib_lux' '/tmp/fib_rust' '/tmp/fib_zig'
|
||||
```
|
||||
|
||||
## Caveats
|
||||
|
||||
1. **Micro-benchmark**: Fibonacci tests recursion optimization, not general performance
|
||||
2. **gcc-specific**: Results depend on gcc's aggressive loop transformation
|
||||
3. **No allocation**: fib doesn't test memory management (Perceus RC)
|
||||
4. **Single-threaded**: No concurrency testing
|
||||
5. **Linux-specific**: poop requires Linux perf counters
|
||||
|
||||
## When Lux Won't Be Fastest
|
||||
|
||||
| Scenario | Likely Winner | Why |
|
||||
|----------|---------------|-----|
|
||||
| Simple recursion | **Lux/C** | gcc's strength |
|
||||
| SIMD/vectorization | Rust/Zig | Explicit intrinsics |
|
||||
| Async I/O | Rust (tokio) | Mature runtime |
|
||||
| Memory-heavy | Zig | Allocator control |
|
||||
| Unsafe operations | C | No safety checks |
|
||||
|
||||
13
benchmarks/ackermann.zig
Normal file
13
benchmarks/ackermann.zig
Normal file
@@ -0,0 +1,13 @@
|
||||
// Ackermann function benchmark - deep recursion
|
||||
const std = @import("std");
|
||||
|
||||
fn ackermann(m: i64, n: i64) i64 {
|
||||
if (m == 0) return n + 1;
|
||||
if (n == 0) return ackermann(m - 1, 1);
|
||||
return ackermann(m - 1, ackermann(m, n - 1));
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
const result = ackermann(3, 10);
|
||||
std.debug.print("ackermann(3, 10) = {d}\n", .{result});
|
||||
}
|
||||
12
benchmarks/fib.zig
Normal file
12
benchmarks/fib.zig
Normal file
@@ -0,0 +1,12 @@
|
||||
// Fibonacci benchmark - recursive implementation
|
||||
const std = @import("std");
|
||||
|
||||
fn fib(n: i64) i64 {
|
||||
if (n <= 1) return n;
|
||||
return fib(n - 1) + fib(n - 2);
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
const result = fib(35);
|
||||
std.debug.print("fib(35) = {d}\n", .{result});
|
||||
}
|
||||
37
benchmarks/http_benchmark.lux
Normal file
37
benchmarks/http_benchmark.lux
Normal file
@@ -0,0 +1,37 @@
|
||||
// HTTP Server Benchmark
|
||||
//
|
||||
// A minimal HTTP server for benchmarking request throughput.
|
||||
// Run with: lux benchmarks/http_benchmark.lux
|
||||
//
|
||||
// Test with:
|
||||
// wrk -t4 -c100 -d10s http://localhost:8080/
|
||||
// OR
|
||||
// ab -n 10000 -c 100 http://localhost:8080/
|
||||
//
|
||||
// Expected: > 50k req/sec on modern hardware
|
||||
|
||||
fn handleRequest(request: { method: String, path: String, body: String }): { status: Int, body: String } with {Console} = {
|
||||
// Minimal JSON response for benchmarking
|
||||
{ status: 200, body: "{\"status\":\"ok\"}" }
|
||||
}
|
||||
|
||||
fn serveLoop(): Unit with {Console, HttpServer} = {
|
||||
let request = HttpServer.accept()
|
||||
let response = handleRequest(request)
|
||||
HttpServer.respond(response.status, response.body)
|
||||
serveLoop()
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console, HttpServer} = {
|
||||
Console.print("HTTP Benchmark Server")
|
||||
Console.print("Listening on port 8080...")
|
||||
Console.print("")
|
||||
Console.print("Test with:")
|
||||
Console.print(" wrk -t4 -c100 -d10s http://localhost:8080/")
|
||||
Console.print(" ab -n 10000 -c 100 http://localhost:8080/")
|
||||
Console.print("")
|
||||
HttpServer.listen(8080)
|
||||
serveLoop()
|
||||
}
|
||||
|
||||
let result = run main() with {}
|
||||
47
benchmarks/http_server.c
Normal file
47
benchmarks/http_server.c
Normal file
@@ -0,0 +1,47 @@
|
||||
// Minimal HTTP server benchmark - C version (single-threaded, poll-based)
|
||||
// Compile: gcc -O3 -o http_c http_server.c
|
||||
// Test: wrk -t2 -c50 -d5s http://localhost:8080/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
|
||||
#define PORT 8080
|
||||
#define RESPONSE "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 15\r\n\r\n{\"status\":\"ok\"}"
|
||||
|
||||
int main() {
|
||||
int server_fd, client_fd;
|
||||
struct sockaddr_in address;
|
||||
int opt = 1;
|
||||
char buffer[1024];
|
||||
socklen_t addrlen = sizeof(address);
|
||||
|
||||
server_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
setsockopt(server_fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt));
|
||||
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_addr.s_addr = INADDR_ANY;
|
||||
address.sin_port = htons(PORT);
|
||||
|
||||
bind(server_fd, (struct sockaddr*)&address, sizeof(address));
|
||||
listen(server_fd, 1024);
|
||||
|
||||
printf("C HTTP server listening on port %d\n", PORT);
|
||||
fflush(stdout);
|
||||
|
||||
while (1) {
|
||||
client_fd = accept(server_fd, (struct sockaddr*)&address, &addrlen);
|
||||
if (client_fd < 0) continue;
|
||||
|
||||
read(client_fd, buffer, sizeof(buffer));
|
||||
write(client_fd, RESPONSE, strlen(RESPONSE));
|
||||
close(client_fd);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
21
benchmarks/http_server.rs
Normal file
21
benchmarks/http_server.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
// Minimal HTTP server benchmark - Rust version (single-threaded)
|
||||
// Compile: rustc -C opt-level=3 -o http_rust http_server.rs
|
||||
// Test: wrk -t2 -c50 -d5s http://localhost:8081/
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpListener;
|
||||
|
||||
const RESPONSE: &[u8] = b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 15\r\n\r\n{\"status\":\"ok\"}";
|
||||
|
||||
fn main() {
|
||||
let listener = TcpListener::bind("0.0.0.0:8081").unwrap();
|
||||
println!("Rust HTTP server listening on port 8081");
|
||||
|
||||
for stream in listener.incoming() {
|
||||
if let Ok(mut stream) = stream {
|
||||
let mut buffer = [0u8; 1024];
|
||||
let _ = stream.read(&mut buffer);
|
||||
let _ = stream.write_all(RESPONSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
benchmarks/http_server.zig
Normal file
25
benchmarks/http_server.zig
Normal file
@@ -0,0 +1,25 @@
|
||||
// Minimal HTTP server benchmark - Zig version (single-threaded)
|
||||
// Compile: zig build-exe -O ReleaseFast http_server.zig
|
||||
// Test: wrk -t2 -c50 -d5s http://localhost:8082/
|
||||
|
||||
const std = @import("std");
|
||||
const net = std.net;
|
||||
|
||||
const response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 15\r\n\r\n{\"status\":\"ok\"}";
|
||||
|
||||
pub fn main() !void {
|
||||
const address = net.Address.initIp4(.{ 0, 0, 0, 0 }, 8082);
|
||||
var server = try address.listen(.{ .reuse_address = true });
|
||||
defer server.deinit();
|
||||
|
||||
std.debug.print("Zig HTTP server listening on port 8082\n", .{});
|
||||
|
||||
while (true) {
|
||||
var connection = server.accept() catch continue;
|
||||
defer connection.stream.close();
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
_ = connection.stream.read(&buf) catch continue;
|
||||
_ = connection.stream.write(response) catch continue;
|
||||
}
|
||||
}
|
||||
81
benchmarks/json_benchmark.lux
Normal file
81
benchmarks/json_benchmark.lux
Normal file
@@ -0,0 +1,81 @@
|
||||
// JSON Parsing Benchmark
|
||||
//
|
||||
// Benchmarks JSON parsing performance.
|
||||
// Run with: lux benchmarks/json_benchmark.lux
|
||||
//
|
||||
// This benchmark:
|
||||
// 1. Generates a large JSON string
|
||||
// 2. Parses it multiple times
|
||||
// 3. Reports timing
|
||||
|
||||
// Generate a JSON string with n objects
|
||||
fn generateJsonObject(i: Int): String = {
|
||||
"{\"id\":" + toString(i) + ",\"name\":\"item" + toString(i) + "\",\"active\":true,\"value\":" + toString(i * 100) + "}"
|
||||
}
|
||||
|
||||
fn generateJsonArray(n: Int, i: Int, acc: String): String = {
|
||||
if i >= n then acc
|
||||
else {
|
||||
let obj = generateJsonObject(i)
|
||||
let sep = if i == 0 then "" else ","
|
||||
generateJsonArray(n, i + 1, acc + sep + obj)
|
||||
}
|
||||
}
|
||||
|
||||
fn generateLargeJson(n: Int): String = {
|
||||
"[" + generateJsonArray(n, 0, "") + "]"
|
||||
}
|
||||
|
||||
// Simple JSON token counting (simulates parsing)
|
||||
fn countJsonTokens(json: String, i: Int, count: Int): Int = {
|
||||
if i >= String.length(json) then count
|
||||
else {
|
||||
let char = String.substring(json, i, i + 1)
|
||||
let newCount =
|
||||
if char == "{" then count + 1
|
||||
else if char == "}" then count + 1
|
||||
else if char == "[" then count + 1
|
||||
else if char == "]" then count + 1
|
||||
else if char == ":" then count + 1
|
||||
else if char == "," then count + 1
|
||||
else count
|
||||
countJsonTokens(json, i + 1, newCount)
|
||||
}
|
||||
}
|
||||
|
||||
// Run benchmark n times
|
||||
fn runBenchmark(json: String, n: Int, totalTokens: Int): Int = {
|
||||
if n <= 0 then totalTokens
|
||||
else {
|
||||
let tokens = countJsonTokens(json, 0, 0)
|
||||
runBenchmark(json, n - 1, totalTokens + tokens)
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console, Time} = {
|
||||
Console.print("JSON Parsing Benchmark")
|
||||
Console.print("======================")
|
||||
Console.print("")
|
||||
|
||||
// Generate large JSON (~100 objects)
|
||||
Console.print("Generating JSON data...")
|
||||
let json = generateLargeJson(100)
|
||||
Console.print(" JSON size: " + toString(String.length(json)) + " bytes")
|
||||
Console.print("")
|
||||
|
||||
// Benchmark parsing
|
||||
Console.print("Running benchmark (1000 iterations)...")
|
||||
let startTime = Time.now()
|
||||
let totalTokens = runBenchmark(json, 1000, 0)
|
||||
let endTime = Time.now()
|
||||
let elapsed = endTime - startTime
|
||||
|
||||
Console.print("")
|
||||
Console.print("Results:")
|
||||
Console.print(" Total tokens parsed: " + toString(totalTokens))
|
||||
Console.print(" Time: " + toString(elapsed) + " ms")
|
||||
Console.print(" Iterations per second: " + toString((1000 * 1000) / elapsed))
|
||||
Console.print("")
|
||||
}
|
||||
|
||||
let result = run main() with {}
|
||||
27
benchmarks/primes.zig
Normal file
27
benchmarks/primes.zig
Normal file
@@ -0,0 +1,27 @@
|
||||
// Prime counting benchmark
|
||||
const std = @import("std");
|
||||
|
||||
fn isPrime(n: i64) bool {
|
||||
if (n < 2) return false;
|
||||
if (n == 2) return true;
|
||||
if (@mod(n, 2) == 0) return false;
|
||||
var i: i64 = 3;
|
||||
while (i * i <= n) : (i += 2) {
|
||||
if (@mod(n, i) == 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
fn countPrimes(max: i64) i64 {
|
||||
var count: i64 = 0;
|
||||
var i: i64 = 2;
|
||||
while (i <= max) : (i += 1) {
|
||||
if (isPrime(i)) count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
const count = countPrimes(10000);
|
||||
std.debug.print("Primes up to 10000: {d}\n", .{count});
|
||||
}
|
||||
16
benchmarks/sumloop.zig
Normal file
16
benchmarks/sumloop.zig
Normal file
@@ -0,0 +1,16 @@
|
||||
// Sum loop benchmark - tight numeric loop
|
||||
const std = @import("std");
|
||||
|
||||
fn sumTo(n: i64) i64 {
|
||||
var sum: i64 = 0;
|
||||
var i: i64 = 1;
|
||||
while (i <= n) : (i += 1) {
|
||||
sum += i;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
const result = sumTo(10000000);
|
||||
std.debug.print("Sum 1 to 10M: {d}\n", .{result});
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -9,8 +9,10 @@ Lux should compile to native code with zero-cost effects AND compile to JavaScri
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| Interpreter | Full-featured, all language constructs |
|
||||
| C Backend | Complete (functions, closures, pattern matching, lists, RC) |
|
||||
| JS Backend | Complete (full language, browser & Node.js, DOM, TEA) |
|
||||
| JIT (Cranelift) | Integer arithmetic only, ~160x speedup |
|
||||
| Targets | Native (via Cranelift JIT) |
|
||||
| Targets | Native (via C), JavaScript, JIT |
|
||||
|
||||
## Target Architecture
|
||||
|
||||
@@ -296,45 +298,33 @@ Tree increment(Tree tree) {
|
||||
|
||||
## Milestones
|
||||
|
||||
### v0.2.0 - C Backend (Basic)
|
||||
- [ ] Integer/bool expressions → C
|
||||
- [ ] Functions → C functions
|
||||
- [ ] If/else → C conditionals
|
||||
- [ ] Let bindings → C variables
|
||||
- [ ] Basic main() generation
|
||||
- [ ] Build with GCC/Clang
|
||||
### C Backend - COMPLETE
|
||||
- [x] Integer/bool expressions → C
|
||||
- [x] Functions → C functions
|
||||
- [x] If/else → C conditionals
|
||||
- [x] Let bindings → C variables
|
||||
- [x] Basic main() generation
|
||||
- [x] Build with GCC/Clang
|
||||
- [x] Strings → C strings
|
||||
- [x] Pattern matching → Switch/if chains
|
||||
- [x] Lists → Linked structures
|
||||
- [x] Closures
|
||||
- [x] Reference counting (lists, boxed values)
|
||||
|
||||
### v0.3.0 - C Backend (Full)
|
||||
- [ ] Strings → C strings
|
||||
- [ ] Records → C structs
|
||||
- [ ] ADTs → Tagged unions
|
||||
- [ ] Pattern matching → Switch/if chains
|
||||
- [ ] Lists → Linked structures
|
||||
- [ ] Effect compilation (basic)
|
||||
### JavaScript Backend - COMPLETE
|
||||
- [x] Basic expressions → JS
|
||||
- [x] Functions → JS functions
|
||||
- [x] Effects → Direct DOM/API calls
|
||||
- [x] Standard library (String, List, Option, Result, Math, JSON)
|
||||
- [x] DOM effect (40+ operations)
|
||||
- [x] Html module (type-safe HTML)
|
||||
- [x] TEA runtime (Elm Architecture)
|
||||
- [x] Browser & Node.js support
|
||||
|
||||
### v0.4.0 - Evidence Passing
|
||||
- [ ] Effect analysis
|
||||
- [ ] Evidence vector generation
|
||||
- [ ] Transform effect ops to direct calls
|
||||
- [ ] Handler compilation
|
||||
|
||||
### v0.5.0 - JavaScript Backend
|
||||
- [ ] Basic expressions → JS
|
||||
- [ ] Functions → JS functions
|
||||
- [ ] Effects → Direct DOM/API calls
|
||||
- [ ] No runtime bundle
|
||||
|
||||
### v0.6.0 - Reactive Frontend
|
||||
- [ ] Reactive primitives
|
||||
- [ ] Fine-grained DOM updates
|
||||
- [ ] Compile-time dependency tracking
|
||||
- [ ] Svelte-like output
|
||||
|
||||
### v0.7.0 - Memory Optimization
|
||||
- [ ] Reference counting insertion
|
||||
- [ ] Reuse analysis
|
||||
- [ ] FBIP detection
|
||||
- [ ] In-place updates
|
||||
### Remaining Work
|
||||
- [ ] Evidence passing for zero-cost effects
|
||||
- [ ] FBIP (Functional But In-Place) optimization
|
||||
- [ ] WASM backend (deprioritized)
|
||||
|
||||
## References
|
||||
|
||||
|
||||
400
docs/COMPILER_OPTIMIZATIONS.md
Normal file
400
docs/COMPILER_OPTIMIZATIONS.md
Normal file
@@ -0,0 +1,400 @@
|
||||
# Compiler Optimizations from Behavioral Types
|
||||
|
||||
This document describes optimization opportunities enabled by Lux's behavioral type system. When functions are annotated with properties like `is pure`, `is total`, `is idempotent`, `is deterministic`, or `is commutative`, the compiler gains knowledge that enables aggressive optimizations.
|
||||
|
||||
## Overview
|
||||
|
||||
| Property | Key Optimizations |
|
||||
|----------|-------------------|
|
||||
| `is pure` | Memoization, CSE, dead code elimination, auto-parallelization |
|
||||
| `is total` | No exception handling, aggressive inlining, loop unrolling |
|
||||
| `is deterministic` | Result caching, test reproducibility, parallel execution |
|
||||
| `is idempotent` | Duplicate call elimination, retry optimization |
|
||||
| `is commutative` | Argument reordering, parallel reduction, algebraic simplification |
|
||||
|
||||
## Pure Function Optimizations
|
||||
|
||||
When a function is marked `is pure`:
|
||||
|
||||
### 1. Memoization (Automatic Caching)
|
||||
|
||||
```lux
|
||||
fn fib(n: Int): Int is pure =
|
||||
if n <= 1 then n else fib(n - 1) + fib(n - 2)
|
||||
```
|
||||
|
||||
**Optimization**: The compiler can automatically memoize results. Since `fib` is pure, `fib(10)` will always return the same value, so we can cache it.
|
||||
|
||||
**Implementation approach**:
|
||||
- Maintain a hash map of argument → result mappings
|
||||
- Before computing, check if result exists
|
||||
- Store results after computation
|
||||
- Use LRU eviction for memory management
|
||||
|
||||
**Impact**: Reduces exponential recursive calls to linear time.
|
||||
|
||||
### 2. Common Subexpression Elimination (CSE)
|
||||
|
||||
```lux
|
||||
fn compute(x: Int): Int is pure =
|
||||
expensive(x) + expensive(x) // Same call twice
|
||||
```
|
||||
|
||||
**Optimization**: The compiler recognizes both calls are identical and computes `expensive(x)` only once.
|
||||
|
||||
**Transformed to**:
|
||||
```lux
|
||||
fn compute(x: Int): Int is pure =
|
||||
let temp = expensive(x)
|
||||
temp + temp
|
||||
```
|
||||
|
||||
**Impact**: Eliminates redundant computation.
|
||||
|
||||
### 3. Dead Code Elimination
|
||||
|
||||
```lux
|
||||
fn example(): Int is pure = {
|
||||
let unused = expensiveComputation() // Result not used
|
||||
42
|
||||
}
|
||||
```
|
||||
|
||||
**Optimization**: Since `expensiveComputation` is pure (no side effects), and its result is unused, the entire call can be eliminated.
|
||||
|
||||
**Impact**: Removes unnecessary work.
|
||||
|
||||
### 4. Auto-Parallelization
|
||||
|
||||
```lux
|
||||
fn processAll(items: List<Item>): List<Result> is pure =
|
||||
List.map(items, processItem) // processItem is pure
|
||||
```
|
||||
|
||||
**Optimization**: Since `processItem` is pure, each invocation is independent. The compiler can automatically parallelize the map operation.
|
||||
|
||||
**Implementation approach**:
|
||||
- Detect pure functions in map/filter/fold operations
|
||||
- Split work across available cores
|
||||
- Merge results (order-preserving for map)
|
||||
|
||||
**Impact**: Linear speedup with core count for CPU-bound operations.
|
||||
|
||||
### 5. Speculative Execution
|
||||
|
||||
```lux
|
||||
fn decide(cond: Bool, a: Int, b: Int): Int is pure =
|
||||
if cond then computeA(a) else computeB(b)
|
||||
```
|
||||
|
||||
**Optimization**: Both branches can be computed in parallel before the condition is known, since neither has side effects.
|
||||
|
||||
**Impact**: Reduced latency when condition evaluation is slow.
|
||||
|
||||
## Total Function Optimizations
|
||||
|
||||
When a function is marked `is total`:
|
||||
|
||||
### 1. Exception Handling Elimination
|
||||
|
||||
```lux
|
||||
fn safeCompute(x: Int): Int is total =
|
||||
complexCalculation(x)
|
||||
```
|
||||
|
||||
**Optimization**: No try/catch blocks needed around calls to `safeCompute`. The compiler knows it will never throw or fail.
|
||||
|
||||
**Generated code difference**:
|
||||
```c
|
||||
// Without is total - needs error checking
|
||||
Result result = safeCompute(x);
|
||||
if (result.is_error) { handle_error(); }
|
||||
|
||||
// With is total - direct call
|
||||
int result = safeCompute(x);
|
||||
```
|
||||
|
||||
**Impact**: Reduced code size, better branch prediction.
|
||||
|
||||
### 2. Aggressive Inlining
|
||||
|
||||
```lux
|
||||
fn square(x: Int): Int is total = x * x
|
||||
|
||||
fn sumOfSquares(a: Int, b: Int): Int is total =
|
||||
square(a) + square(b)
|
||||
```
|
||||
|
||||
**Optimization**: Total functions are safe to inline aggressively because:
|
||||
- They won't change control flow unexpectedly
|
||||
- They won't introduce exception handling complexity
|
||||
- Their termination is guaranteed
|
||||
|
||||
**Impact**: Eliminates function call overhead, enables further optimizations.
|
||||
|
||||
### 3. Loop Unrolling
|
||||
|
||||
```lux
|
||||
fn sumList(xs: List<Int>): Int is total =
|
||||
List.fold(xs, 0, fn(acc: Int, x: Int): Int is total => acc + x)
|
||||
```
|
||||
|
||||
**Optimization**: When the list size is known at compile time and the fold function is total, the loop can be fully unrolled.
|
||||
|
||||
**Impact**: Eliminates loop overhead, enables vectorization.
|
||||
|
||||
### 4. Termination Assumptions
|
||||
|
||||
```lux
|
||||
fn processRecursive(data: Tree): Result is total =
|
||||
match data {
|
||||
Leaf(v) => Result.single(v),
|
||||
Node(left, right) => {
|
||||
let l = processRecursive(left)
|
||||
let r = processRecursive(right)
|
||||
Result.merge(l, r)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Optimization**: The compiler can assume this recursion terminates, allowing optimizations like:
|
||||
- Converting recursion to iteration
|
||||
- Allocating fixed stack space
|
||||
- Tail call optimization
|
||||
|
||||
**Impact**: Stack safety, predictable memory usage.
|
||||
|
||||
## Deterministic Function Optimizations
|
||||
|
||||
When a function is marked `is deterministic`:
|
||||
|
||||
### 1. Compile-Time Evaluation
|
||||
|
||||
```lux
|
||||
fn hashConstant(s: String): Int is deterministic = computeHash(s)
|
||||
|
||||
let key = hashConstant("api_key") // Constant input
|
||||
```
|
||||
|
||||
**Optimization**: Since the input is a compile-time constant and the function is deterministic, the result can be computed at compile time.
|
||||
|
||||
**Transformed to**:
|
||||
```lux
|
||||
let key = 7823491 // Pre-computed
|
||||
```
|
||||
|
||||
**Impact**: Zero runtime cost for constant computations.
|
||||
|
||||
### 2. Result Caching Across Runs
|
||||
|
||||
```lux
|
||||
fn parseConfig(path: String): Config is deterministic with {File} =
|
||||
Json.parse(File.read(path))
|
||||
```
|
||||
|
||||
**Optimization**: Results can be cached persistently. If the file hasn't changed, the cached result is valid.
|
||||
|
||||
**Implementation approach**:
|
||||
- Hash inputs (including file contents)
|
||||
- Store results in persistent cache
|
||||
- Validate cache on next run
|
||||
|
||||
**Impact**: Faster startup times, reduced I/O.
|
||||
|
||||
### 3. Reproducible Parallel Execution
|
||||
|
||||
```lux
|
||||
fn renderImages(images: List<Image>): List<Bitmap> is deterministic =
|
||||
List.map(images, render)
|
||||
```
|
||||
|
||||
**Optimization**: Deterministic parallel execution guarantees same results regardless of scheduling order. This enables:
|
||||
- Work stealing without synchronization concerns
|
||||
- Speculative execution without rollback complexity
|
||||
- Distributed computation across machines
|
||||
|
||||
**Impact**: Easier parallelization, simpler distributed systems.
|
||||
|
||||
## Idempotent Function Optimizations
|
||||
|
||||
When a function is marked `is idempotent`:
|
||||
|
||||
### 1. Duplicate Call Elimination
|
||||
|
||||
```lux
|
||||
fn setFlag(config: Config, flag: Bool): Config is idempotent =
|
||||
{ ...config, enabled: flag }
|
||||
|
||||
fn configure(c: Config): Config is idempotent =
|
||||
c |> setFlag(true) |> setFlag(true) |> setFlag(true)
|
||||
```
|
||||
|
||||
**Optimization**: Multiple consecutive calls with the same arguments can be collapsed to one.
|
||||
|
||||
**Transformed to**:
|
||||
```lux
|
||||
fn configure(c: Config): Config is idempotent =
|
||||
setFlag(c, true)
|
||||
```
|
||||
|
||||
**Impact**: Eliminates redundant operations.
|
||||
|
||||
### 2. Retry Optimization
|
||||
|
||||
```lux
|
||||
fn sendRequest(data: Request): Response is idempotent with {Http} =
|
||||
Http.put("/api/resource", data)
|
||||
|
||||
fn reliableSend(data: Request): Response with {Http} =
|
||||
retry(3, fn(): Response => sendRequest(data))
|
||||
```
|
||||
|
||||
**Optimization**: The retry mechanism knows the operation is safe to retry without side effects accumulating.
|
||||
|
||||
**Implementation approach**:
|
||||
- No need for transaction logs
|
||||
- No need for "already processed" checks
|
||||
- Simple retry loop
|
||||
|
||||
**Impact**: Simpler error recovery, reduced complexity.
|
||||
|
||||
### 3. Convergent Computation
|
||||
|
||||
```lux
|
||||
fn normalize(value: Float): Float is idempotent =
|
||||
clamp(round(value, 2), 0.0, 1.0)
|
||||
```
|
||||
|
||||
**Optimization**: In iterative algorithms, the compiler can detect when a value has converged (applying the function no longer changes it).
|
||||
|
||||
```lux
|
||||
// Can terminate early when values stop changing
|
||||
fn iterateUntilStable(values: List<Float>): List<Float> =
|
||||
let normalized = List.map(values, normalize)
|
||||
if normalized == values then values
|
||||
else iterateUntilStable(normalized)
|
||||
```
|
||||
|
||||
**Impact**: Early termination of iterative algorithms.
|
||||
|
||||
## Commutative Function Optimizations
|
||||
|
||||
When a function is marked `is commutative`:
|
||||
|
||||
### 1. Argument Reordering
|
||||
|
||||
```lux
|
||||
fn multiply(a: Int, b: Int): Int is commutative = a * b
|
||||
|
||||
// In a computation
|
||||
multiply(expensiveA(), cheapB())
|
||||
```
|
||||
|
||||
**Optimization**: Evaluate the cheaper argument first to enable short-circuit optimizations or better register allocation.
|
||||
|
||||
**Impact**: Improved instruction scheduling.
|
||||
|
||||
### 2. Parallel Reduction
|
||||
|
||||
```lux
|
||||
fn add(a: Int, b: Int): Int is commutative = a + b
|
||||
|
||||
fn sum(xs: List<Int>): Int =
|
||||
List.fold(xs, 0, add)
|
||||
```
|
||||
|
||||
**Optimization**: Since `add` is commutative (and associative), the fold can be parallelized:
|
||||
|
||||
```
|
||||
[1, 2, 3, 4, 5, 6, 7, 8]
|
||||
↓ parallel reduce
|
||||
[(1+2), (3+4), (5+6), (7+8)]
|
||||
↓ parallel reduce
|
||||
[(3+7), (11+15)]
|
||||
↓ parallel reduce
|
||||
[36]
|
||||
```
|
||||
|
||||
**Impact**: O(log n) parallel reduction instead of O(n) sequential.
|
||||
|
||||
### 3. Algebraic Simplification
|
||||
|
||||
```lux
|
||||
fn add(a: Int, b: Int): Int is commutative = a + b
|
||||
|
||||
// Expression: add(x, add(y, z))
|
||||
```
|
||||
|
||||
**Optimization**: Commutative operations can be reordered for simplification:
|
||||
- `add(x, 0)` → `x`
|
||||
- `add(add(x, 1), add(y, 1))` → `add(add(x, y), 2)`
|
||||
|
||||
**Impact**: Constant folding, strength reduction.
|
||||
|
||||
## Combined Property Optimizations
|
||||
|
||||
Properties can be combined for even more powerful optimizations:
|
||||
|
||||
### Pure + Deterministic + Total
|
||||
|
||||
```lux
|
||||
fn computeKey(data: String): Int
|
||||
is pure
|
||||
is deterministic
|
||||
is total = {
|
||||
// Hash computation
|
||||
List.fold(String.chars(data), 0, fn(acc: Int, c: Char): Int =>
|
||||
acc * 31 + Char.code(c))
|
||||
}
|
||||
```
|
||||
|
||||
**Enabled optimizations**:
|
||||
- Compile-time evaluation for constants
|
||||
- Automatic memoization at runtime
|
||||
- Parallel execution in batch operations
|
||||
- No exception handling needed
|
||||
- Safe to inline anywhere
|
||||
|
||||
### Idempotent + Commutative
|
||||
|
||||
```lux
|
||||
fn setUnionItem<T>(set: Set<T>, item: T): Set<T>
|
||||
is idempotent
|
||||
is commutative = {
|
||||
Set.add(set, item)
|
||||
}
|
||||
```
|
||||
|
||||
**Enabled optimizations**:
|
||||
- Parallel set building (order doesn't matter)
|
||||
- Duplicate insertions are free (idempotent)
|
||||
- Reorder insertions for cache locality
|
||||
|
||||
## Implementation Status
|
||||
|
||||
| Optimization | Status |
|
||||
|--------------|--------|
|
||||
| Pure: CSE | Planned |
|
||||
| Pure: Dead code elimination | Partial (basic) |
|
||||
| Pure: Auto-parallelization | Planned |
|
||||
| Total: Exception elimination | Planned |
|
||||
| Total: Aggressive inlining | Partial |
|
||||
| Deterministic: Compile-time eval | Planned |
|
||||
| Idempotent: Duplicate elimination | Planned |
|
||||
| Commutative: Parallel reduction | Planned |
|
||||
|
||||
## Adding New Optimizations
|
||||
|
||||
When implementing new optimizations based on behavioral types:
|
||||
|
||||
1. **Verify the property is correct**: The optimization is only valid if the property holds
|
||||
2. **Consider combinations**: Multiple properties together enable more optimizations
|
||||
3. **Measure impact**: Profile before and after to ensure benefit
|
||||
4. **Handle `assume`**: Functions using `assume` bypass verification but still enable optimizations (risk is on the programmer)
|
||||
|
||||
## Future Work
|
||||
|
||||
1. **Inter-procedural analysis**: Track properties across function boundaries
|
||||
2. **Automatic property inference**: Derive properties when not explicitly stated
|
||||
3. **Profile-guided optimization**: Use runtime data to decide when to apply optimizations
|
||||
4. **LLVM integration**: Pass behavioral hints to LLVM for backend optimizations
|
||||
1048
docs/COMPREHENSIVE_ROADMAP.md
Normal file
1048
docs/COMPREHENSIVE_ROADMAP.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -124,22 +124,19 @@ String interpolation is fully working:
|
||||
- Escape sequences: `\{`, `\}`, `\n`, `\t`, `\"`, `\\`
|
||||
|
||||
#### 1.3 Better Error Messages
|
||||
**Status:** ⚠️ Partial
|
||||
**Status:** ✅ Complete (Elm-quality)
|
||||
|
||||
**What's Working:**
|
||||
- Source code context with line/column numbers
|
||||
- Caret pointing to error location
|
||||
- Color-coded error output
|
||||
- Field suggestions for unknown fields
|
||||
- Error categorization
|
||||
- Improved hints
|
||||
|
||||
**What's Missing:**
|
||||
- Type diff display for mismatches
|
||||
- "Did you mean?" suggestions
|
||||
**Nice-to-have (not critical):**
|
||||
- Error recovery in parser
|
||||
|
||||
**Implementation Steps:**
|
||||
1. Add Levenshtein distance for suggestions
|
||||
2. Implement error recovery in parser
|
||||
3. Add type diff visualization
|
||||
- Type diff visualization
|
||||
|
||||
### Priority 2: Effect System Completion
|
||||
|
||||
@@ -198,8 +195,10 @@ fn withRetry<E>(action: fn(): T with E, attempts: Int): T with E = ...
|
||||
- Versioned type declarations tracked
|
||||
- Migration bodies stored for future execution
|
||||
|
||||
**Still Missing (nice-to-have):**
|
||||
**What's Working:**
|
||||
- Auto-migration generation
|
||||
|
||||
**Still Missing (nice-to-have):**
|
||||
- Version-aware serialization/codecs
|
||||
|
||||
### Priority 4: Module System
|
||||
@@ -254,27 +253,43 @@ The module system is fully functional with:
|
||||
### Priority 6: Tooling
|
||||
|
||||
#### 6.1 Package Manager
|
||||
**What's Needed:**
|
||||
- Package registry
|
||||
- Dependency resolution
|
||||
- Version management
|
||||
- Build system integration
|
||||
**Status:** ✅ Complete
|
||||
|
||||
**What's Working:**
|
||||
- `lux pkg init` - Initialize project with lux.toml
|
||||
- `lux pkg add/remove` - Manage dependencies
|
||||
- `lux pkg install` - Install from lux.toml
|
||||
- Git and local path dependencies
|
||||
- Package registry (`lux registry`)
|
||||
- CLI commands (search, publish)
|
||||
|
||||
**Still Missing (nice-to-have):**
|
||||
- Version conflict resolution
|
||||
|
||||
#### 6.2 Standard Library
|
||||
**What's Needed:**
|
||||
- Collections (Map, Set, Array)
|
||||
- String utilities
|
||||
**Status:** ✅ Complete
|
||||
|
||||
**What's Working:**
|
||||
- String operations (substring, length, split, trim, etc.)
|
||||
- List operations (map, filter, fold, etc.)
|
||||
- Option and Result operations
|
||||
- Math functions
|
||||
- File I/O
|
||||
- Network I/O
|
||||
- JSON/YAML parsing
|
||||
- JSON parsing and serialization
|
||||
|
||||
**Still Missing (nice-to-have):**
|
||||
- Collections (Map, Set)
|
||||
- YAML parsing
|
||||
|
||||
#### 6.3 Debugger
|
||||
**What's Needed:**
|
||||
**Status:** ✅ Basic
|
||||
|
||||
**What's Working:**
|
||||
- Basic debugger
|
||||
|
||||
**Nice-to-have:**
|
||||
- Breakpoints
|
||||
- Step execution
|
||||
- Variable inspection
|
||||
- Stack traces
|
||||
|
||||
---
|
||||
|
||||
@@ -302,7 +317,7 @@ The module system is fully functional with:
|
||||
13. ~~**Idempotent verification**~~ ✅ Done - Pattern-based analysis
|
||||
14. ~~**Deterministic verification**~~ ✅ Done - Effect-based analysis
|
||||
15. ~~**Commutative verification**~~ ✅ Done - Operator analysis
|
||||
16. **Where clause enforcement** - Constraint checking (basic parsing done)
|
||||
16. ~~**Where clause enforcement**~~ ✅ Done - Property constraints
|
||||
|
||||
### Phase 5: Schema Evolution (Data)
|
||||
17. ~~**Type system version tracking**~~ ✅ Done
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
433
docs/LSP.md
Normal file
433
docs/LSP.md
Normal file
@@ -0,0 +1,433 @@
|
||||
# Lux Language Server Protocol (LSP)
|
||||
|
||||
Lux includes a built-in language server that provides IDE features for any editor that supports the Language Server Protocol.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Starting the LSP Server
|
||||
|
||||
```bash
|
||||
lux lsp
|
||||
```
|
||||
|
||||
The server communicates via stdio (stdin/stdout), following the standard LSP protocol.
|
||||
|
||||
## Supported Features
|
||||
|
||||
| Feature | Status | Description |
|
||||
|---------|--------|-------------|
|
||||
| Diagnostics | Full | Real-time parse and type errors |
|
||||
| Hover | Full | Type information and documentation |
|
||||
| Completions | Full | Context-aware code completion |
|
||||
| Go to Definition | Full | Jump to function/type definitions |
|
||||
| Formatting | CLI only | Code formatting via `lux fmt` (not exposed via LSP) |
|
||||
| Rename | Planned | Rename symbols |
|
||||
| Find References | Planned | Find all usages |
|
||||
|
||||
## Editor Setup
|
||||
|
||||
### VS Code
|
||||
|
||||
1. Install the Lux extension from the VS Code marketplace (coming soon)
|
||||
|
||||
2. Or configure manually in `settings.json`:
|
||||
```json
|
||||
{
|
||||
"lux.lspPath": "/path/to/lux",
|
||||
"lux.enableLsp": true
|
||||
}
|
||||
```
|
||||
|
||||
3. Or use a generic LSP client extension like [vscode-languageclient](https://github.com/microsoft/vscode-languageserver-node):
|
||||
|
||||
```json
|
||||
{
|
||||
"languageServerExample.serverPath": "lux",
|
||||
"languageServerExample.serverArgs": ["lsp"]
|
||||
}
|
||||
```
|
||||
|
||||
### Neovim (with nvim-lspconfig)
|
||||
|
||||
Add to your Neovim config (`init.lua`):
|
||||
|
||||
```lua
|
||||
local lspconfig = require('lspconfig')
|
||||
local configs = require('lspconfig.configs')
|
||||
|
||||
-- Register Lux as a new LSP
|
||||
if not configs.lux then
|
||||
configs.lux = {
|
||||
default_config = {
|
||||
cmd = { 'lux', 'lsp' },
|
||||
filetypes = { 'lux' },
|
||||
root_dir = function(fname)
|
||||
return lspconfig.util.find_git_ancestor(fname) or vim.fn.getcwd()
|
||||
end,
|
||||
settings = {},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
-- Enable the server
|
||||
lspconfig.lux.setup{}
|
||||
```
|
||||
|
||||
Also add filetype detection in `~/.config/nvim/ftdetect/lux.vim`:
|
||||
|
||||
```vim
|
||||
au BufRead,BufNewFile *.lux set filetype=lux
|
||||
```
|
||||
|
||||
### Neovim (with built-in LSP)
|
||||
|
||||
```lua
|
||||
vim.api.nvim_create_autocmd('FileType', {
|
||||
pattern = 'lux',
|
||||
callback = function()
|
||||
vim.lsp.start({
|
||||
name = 'lux',
|
||||
cmd = { 'lux', 'lsp' },
|
||||
root_dir = vim.fs.dirname(vim.fs.find({ '.git', 'lux.toml' }, { upward = true })[1]),
|
||||
})
|
||||
end,
|
||||
})
|
||||
```
|
||||
|
||||
### Emacs (with lsp-mode)
|
||||
|
||||
Add to your Emacs config:
|
||||
|
||||
```elisp
|
||||
(use-package lsp-mode
|
||||
:hook (lux-mode . lsp)
|
||||
:config
|
||||
(add-to-list 'lsp-language-id-configuration '(lux-mode . "lux"))
|
||||
(lsp-register-client
|
||||
(make-lsp-client
|
||||
:new-connection (lsp-stdio-connection '("lux" "lsp"))
|
||||
:major-modes '(lux-mode)
|
||||
:server-id 'lux-lsp)))
|
||||
```
|
||||
|
||||
### Emacs (with eglot)
|
||||
|
||||
```elisp
|
||||
(add-to-list 'eglot-server-programs '(lux-mode . ("lux" "lsp")))
|
||||
```
|
||||
|
||||
### Helix
|
||||
|
||||
Add to `~/.config/helix/languages.toml`:
|
||||
|
||||
```toml
|
||||
[[language]]
|
||||
name = "lux"
|
||||
scope = "source.lux"
|
||||
injection-regex = "lux"
|
||||
file-types = ["lux"]
|
||||
roots = ["lux.toml", ".git"]
|
||||
comment-token = "//"
|
||||
indent = { tab-width = 4, unit = " " }
|
||||
language-server = { command = "lux", args = ["lsp"] }
|
||||
```
|
||||
|
||||
### Sublime Text (with LSP package)
|
||||
|
||||
Add to `Preferences > Package Settings > LSP > Settings`:
|
||||
|
||||
```json
|
||||
{
|
||||
"clients": {
|
||||
"lux": {
|
||||
"enabled": true,
|
||||
"command": ["lux", "lsp"],
|
||||
"selector": "source.lux"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Zed
|
||||
|
||||
Add to your Zed settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"lsp": {
|
||||
"lux": {
|
||||
"binary": {
|
||||
"path": "lux",
|
||||
"arguments": ["lsp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Feature Details
|
||||
|
||||
### Diagnostics
|
||||
|
||||
The LSP server provides real-time diagnostics for:
|
||||
|
||||
- **Parse errors**: Syntax issues, missing tokens, unexpected input
|
||||
- **Type errors**: Type mismatches, missing fields, unknown identifiers
|
||||
- **Effect errors**: Missing effect declarations, unhandled effects
|
||||
- **Behavioral type errors**: Violations of `is pure`, `is total`, etc.
|
||||
|
||||
Diagnostics appear as you type, typically within 100ms of changes.
|
||||
|
||||
Example diagnostic:
|
||||
```
|
||||
error[E0308]: mismatched types
|
||||
--> src/main.lux:10:5
|
||||
|
|
||||
10 | return "hello"
|
||||
| ^^^^^^^ expected Int, found String
|
||||
```
|
||||
|
||||
### Hover Information
|
||||
|
||||
Hover over any symbol to see:
|
||||
|
||||
- **Functions**: Signature and documentation
|
||||
- **Types**: Type definition
|
||||
- **Keywords**: Syntax explanation
|
||||
- **Effects**: Effect operations
|
||||
|
||||
Example hover for `List.map`:
|
||||
```markdown
|
||||
```lux
|
||||
List.map(list: List<A>, f: A -> B): List<B>
|
||||
```
|
||||
|
||||
Transform each element in a list by applying a function.
|
||||
```
|
||||
|
||||
### Completions
|
||||
|
||||
The LSP provides context-aware completions:
|
||||
|
||||
#### Module Member Completions
|
||||
|
||||
After typing a module name and `.`, you get relevant members:
|
||||
|
||||
```lux
|
||||
List. // Shows: map, filter, fold, reverse, etc.
|
||||
String. // Shows: length, split, join, trim, etc.
|
||||
Console. // Shows: print, readLine, readInt
|
||||
```
|
||||
|
||||
#### Keyword Completions
|
||||
|
||||
At the start of statements:
|
||||
|
||||
```lux
|
||||
fn // Function declaration
|
||||
let // Variable binding
|
||||
type // Type declaration
|
||||
effect // Effect declaration
|
||||
match // Pattern matching
|
||||
```
|
||||
|
||||
#### Type Completions
|
||||
|
||||
In type position:
|
||||
|
||||
```lux
|
||||
Int, Float, Bool, String, Unit
|
||||
List, Option, Result
|
||||
```
|
||||
|
||||
### Go to Definition
|
||||
|
||||
Jump to the definition of:
|
||||
|
||||
- **Functions**: Goes to the `fn` declaration
|
||||
- **Types**: Goes to the `type` declaration
|
||||
- **Effects**: Goes to the `effect` declaration
|
||||
- **Let bindings**: Goes to the `let` statement
|
||||
- **Imports**: Goes to the imported module
|
||||
|
||||
Works with:
|
||||
- `Ctrl+Click` / `Cmd+Click` in most editors
|
||||
- `gd` in Vim/Neovim
|
||||
- `M-.` in Emacs
|
||||
|
||||
## Module Completions Reference
|
||||
|
||||
### List Module
|
||||
|
||||
| Method | Signature | Description |
|
||||
|--------|-----------|-------------|
|
||||
| `length` | `(list: List<A>): Int` | Get list length |
|
||||
| `head` | `(list: List<A>): Option<A>` | First element |
|
||||
| `tail` | `(list: List<A>): List<A>` | All but first |
|
||||
| `map` | `(list: List<A>, f: A -> B): List<B>` | Transform elements |
|
||||
| `filter` | `(list: List<A>, p: A -> Bool): List<A>` | Keep matching |
|
||||
| `fold` | `(list: List<A>, init: B, f: (B, A) -> B): B` | Reduce |
|
||||
| `reverse` | `(list: List<A>): List<A>` | Reverse order |
|
||||
| `concat` | `(a: List<A>, b: List<A>): List<A>` | Join lists |
|
||||
| `range` | `(start: Int, end: Int): List<Int>` | Create range |
|
||||
| `get` | `(list: List<A>, index: Int): Option<A>` | Get at index |
|
||||
| `find` | `(list: List<A>, p: A -> Bool): Option<A>` | Find first match |
|
||||
| `isEmpty` | `(list: List<A>): Bool` | Check empty |
|
||||
| `take` | `(list: List<A>, n: Int): List<A>` | Take n elements |
|
||||
| `drop` | `(list: List<A>, n: Int): List<A>` | Drop n elements |
|
||||
| `any` | `(list: List<A>, p: A -> Bool): Bool` | Any matches |
|
||||
| `all` | `(list: List<A>, p: A -> Bool): Bool` | All match |
|
||||
|
||||
### String Module
|
||||
|
||||
| Method | Signature | Description |
|
||||
|--------|-----------|-------------|
|
||||
| `length` | `(s: String): Int` | String length |
|
||||
| `split` | `(s: String, delim: String): List<String>` | Split by delimiter |
|
||||
| `join` | `(list: List<String>, delim: String): String` | Join with delimiter |
|
||||
| `trim` | `(s: String): String` | Remove whitespace |
|
||||
| `contains` | `(s: String, sub: String): Bool` | Check contains |
|
||||
| `replace` | `(s: String, from: String, to: String): String` | Replace |
|
||||
| `chars` | `(s: String): List<String>` | To char list |
|
||||
| `lines` | `(s: String): List<String>` | Split into lines |
|
||||
| `toUpper` | `(s: String): String` | Uppercase |
|
||||
| `toLower` | `(s: String): String` | Lowercase |
|
||||
|
||||
### Console Effect
|
||||
|
||||
| Operation | Signature | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| `print` | `(msg: String): Unit` | Print message |
|
||||
| `readLine` | `(): String` | Read line |
|
||||
| `readInt` | `(): Int` | Read integer |
|
||||
|
||||
### Math Module
|
||||
|
||||
| Function | Signature | Description |
|
||||
|----------|-----------|-------------|
|
||||
| `abs` | `(x: Int): Int` | Absolute value |
|
||||
| `min` | `(a: Int, b: Int): Int` | Minimum |
|
||||
| `max` | `(a: Int, b: Int): Int` | Maximum |
|
||||
| `pow` | `(base: Float, exp: Float): Float` | Exponentiation |
|
||||
| `sqrt` | `(x: Float): Float` | Square root |
|
||||
| `floor` | `(x: Float): Int` | Floor |
|
||||
| `ceil` | `(x: Float): Int` | Ceiling |
|
||||
| `round` | `(x: Float): Int` | Round |
|
||||
|
||||
### File Effect
|
||||
|
||||
| Operation | Signature | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| `read` | `(path: String): String` | Read file |
|
||||
| `write` | `(path: String, content: String): Unit` | Write file |
|
||||
| `exists` | `(path: String): Bool` | Check exists |
|
||||
| `delete` | `(path: String): Bool` | Delete file |
|
||||
| `listDir` | `(path: String): List<String>` | List directory |
|
||||
| `mkdir` | `(path: String): Bool` | Create directory |
|
||||
|
||||
### Http Effect
|
||||
|
||||
| Operation | Signature | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| `get` | `(url: String): Result<String, String>` | HTTP GET |
|
||||
| `post` | `(url: String, body: String): Result<String, String>` | HTTP POST |
|
||||
| `put` | `(url: String, body: String): Result<String, String>` | HTTP PUT |
|
||||
| `delete` | `(url: String): Result<String, String>` | HTTP DELETE |
|
||||
|
||||
### Random Effect
|
||||
|
||||
| Operation | Signature | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| `int` | `(min: Int, max: Int): Int` | Random integer |
|
||||
| `float` | `(): Float` | Random float 0-1 |
|
||||
| `bool` | `(): Bool` | Random boolean |
|
||||
|
||||
### Time Effect
|
||||
|
||||
| Operation | Signature | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| `now` | `(): Int` | Current timestamp (ms) |
|
||||
| `sleep` | `(ms: Int): Unit` | Sleep for ms |
|
||||
|
||||
### Sql Effect
|
||||
|
||||
| Operation | Signature | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| `connect` | `(url: String): Connection` | Connect to database |
|
||||
| `query` | `(conn: Connection, sql: String): List<Row>` | Execute query |
|
||||
| `execute` | `(conn: Connection, sql: String): Int` | Execute statement |
|
||||
| `transaction` | `(conn: Connection, f: () -> A): A` | Run in transaction |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### LSP Not Starting
|
||||
|
||||
1. Verify Lux is installed: `lux --version`
|
||||
2. Check the server starts: `lux lsp` (should wait for input)
|
||||
3. Check editor logs for connection errors
|
||||
|
||||
### No Completions
|
||||
|
||||
1. Ensure file has `.lux` extension
|
||||
2. Check file is valid (no parse errors)
|
||||
3. Verify LSP is connected (check status bar)
|
||||
|
||||
### Slow Diagnostics
|
||||
|
||||
The LSP re-parses and type-checks on every change. For large files:
|
||||
|
||||
1. Consider splitting into modules
|
||||
2. Check for complex recursive types
|
||||
3. Report performance issues on GitHub
|
||||
|
||||
### Debug Logging
|
||||
|
||||
Enable verbose logging:
|
||||
|
||||
```bash
|
||||
LUX_LSP_LOG=debug lux lsp
|
||||
```
|
||||
|
||||
Logs are written to stderr.
|
||||
|
||||
## Protocol Details
|
||||
|
||||
The Lux LSP server implements LSP version 3.17 with the following capabilities:
|
||||
|
||||
```json
|
||||
{
|
||||
"capabilities": {
|
||||
"textDocumentSync": "full",
|
||||
"hoverProvider": true,
|
||||
"completionProvider": {
|
||||
"triggerCharacters": ["."]
|
||||
},
|
||||
"definitionProvider": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Supported Methods
|
||||
|
||||
| Method | Support |
|
||||
|--------|---------|
|
||||
| `initialize` | Full |
|
||||
| `shutdown` | Full |
|
||||
| `textDocument/didOpen` | Full |
|
||||
| `textDocument/didChange` | Full |
|
||||
| `textDocument/didClose` | Full |
|
||||
| `textDocument/hover` | Full |
|
||||
| `textDocument/completion` | Full |
|
||||
| `textDocument/definition` | Full |
|
||||
| `textDocument/publishDiagnostics` | Full (server-initiated) |
|
||||
|
||||
## Contributing
|
||||
|
||||
The LSP implementation is in `src/lsp.rs`. To add new features:
|
||||
|
||||
1. Add capability to `ServerCapabilities`
|
||||
2. Implement handler in `handle_request`
|
||||
3. Add tests in `tests/lsp_tests.rs`
|
||||
4. Update this documentation
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup.
|
||||
@@ -150,6 +150,15 @@ Time.sleep(1000) // milliseconds
|
||||
let obj = Json.parse("{\"name\": \"Alice\"}")
|
||||
let str = Json.stringify(obj)
|
||||
|
||||
// SQL database effects
|
||||
let db = Sql.openMemory()
|
||||
Sql.execute(db, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
|
||||
Sql.execute(db, "INSERT INTO users (name) VALUES ('Alice')")
|
||||
let users = Sql.query(db, "SELECT * FROM users")
|
||||
Sql.beginTx(db)
|
||||
Sql.commit(db) // or Sql.rollback(db)
|
||||
Sql.close(db)
|
||||
|
||||
// Module system
|
||||
import mymodule
|
||||
import utils/helpers as h
|
||||
@@ -179,10 +188,6 @@ fn processModern(x: Int @v2+): Int = x // v2 or later
|
||||
fn processAny(x: Int @latest): Int = x // any version
|
||||
```
|
||||
|
||||
### Planned (Not Yet Fully Implemented)
|
||||
|
||||
- **Auto-migration Generation**: Migration bodies stored, execution pending
|
||||
|
||||
---
|
||||
|
||||
## Primary Use Cases
|
||||
@@ -235,7 +240,7 @@ Quick iteration with type inference and a REPL.
|
||||
|------------|-------------|
|
||||
| **New Paradigm** | Effects require learning new concepts |
|
||||
| **Small Ecosystem** | Community packages just starting |
|
||||
| **No Package Registry** | Can share code via git/path, no central registry yet |
|
||||
| **Young Package Registry** | Package registry available, but small ecosystem |
|
||||
| **Early Stage** | Bugs likely, features incomplete |
|
||||
|
||||
---
|
||||
@@ -374,18 +379,10 @@ Values + Effects C Code → GCC/Clang
|
||||
- ✅ Formatter
|
||||
|
||||
**In Progress:**
|
||||
1. **Schema Evolution** - Type-declared migrations working, auto-generation pending
|
||||
2. **Error Message Quality** - Context lines shown, suggestions partial
|
||||
3. **Memory Management** - RC working for lists/boxed, closures/ADTs pending
|
||||
|
||||
**Recently Completed:**
|
||||
- ✅ **JavaScript Backend** - Full language support, browser & Node.js
|
||||
- ✅ **Dom Effect** - 40+ browser manipulation operations
|
||||
- ✅ **Html Module** - Type-safe HTML construction (Elm-style)
|
||||
- ✅ **TEA Runtime** - The Elm Architecture for web apps
|
||||
- ✅ **Package Manager** - `lux pkg` with git/path dependencies, module integration
|
||||
1. **Memory Management** - RC working for lists/boxed, closures/ADTs pending
|
||||
2. **Serialization Codecs** - JSON codec generation for versioned types
|
||||
|
||||
**Planned:**
|
||||
4. **SQL Effect** - Database access (as a package)
|
||||
5. **Package Registry** - Central repository for sharing packages
|
||||
6. **Behavioral Type Verification** - Total, idempotent, deterministic checking
|
||||
- **Connection Pooling** - Pool database connections
|
||||
- **WASM Backend** - WebAssembly compilation target
|
||||
- **Stream Processing** - Stream effect for data pipelines
|
||||
|
||||
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.
|
||||
334
docs/PRIORITY_PLAN.md
Normal file
334
docs/PRIORITY_PLAN.md
Normal file
@@ -0,0 +1,334 @@
|
||||
# Lux Priority Implementation Plan
|
||||
|
||||
*Based on analysis of what makes developers love languages*
|
||||
|
||||
## Core Insight
|
||||
|
||||
From studying successful languages (Rust, Elm, Go, Gleam), the pattern is clear:
|
||||
|
||||
1. **Elm** has 0% runtime exceptions and legendary error messages → developers evangelize it
|
||||
2. **Rust** has "if it compiles, it works" confidence → 72% admiration
|
||||
3. **Go** has simplicity and fast feedback → massive adoption
|
||||
4. **Gleam** has type safety on BEAM → 70% admiration (2nd highest)
|
||||
|
||||
**Lux's unique pitch**: Effects you can see. Tests you can trust. No mocks needed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Make Developers Smile (Highest Impact)
|
||||
|
||||
### 1.1 Elm-Quality Error Messages
|
||||
**Why**: Elm's error messages are the #1 reason people recommend it. This is free marketing.
|
||||
|
||||
**Current state**: Basic errors with location
|
||||
**Target state**: Conversational, helpful, with suggestions
|
||||
|
||||
```
|
||||
Current:
|
||||
Type error at 5:12: Cannot unify Int with String
|
||||
|
||||
Target:
|
||||
── TYPE MISMATCH ─────────────────────────── src/main.lux
|
||||
|
||||
The `age` field expects an Int, but I found a String:
|
||||
|
||||
5│ age: "twenty-five"
|
||||
^^^^^^^^^^^^
|
||||
|
||||
Hint: Did you mean to use String.parseInt?
|
||||
|
||||
age: String.parseInt("twenty-five") |> Option.getOrElse(0)
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
- Add error code catalog (E001, E002, etc.)
|
||||
- "Did you mean?" suggestions using Levenshtein distance
|
||||
- Show expected vs actual with visual diff
|
||||
- Context-aware hints based on error type
|
||||
|
||||
**Effort**: 2-3 weeks
|
||||
**Impact**: HIGH - This is what people tweet about
|
||||
|
||||
### 1.2 HTTP Framework (Routing + Middleware)
|
||||
**Why**: Web services are the most common use case. Without this, Lux is a toy.
|
||||
|
||||
```lux
|
||||
// Target API
|
||||
let app = Router.new()
|
||||
|> Router.get("/users", listUsers)
|
||||
|> Router.get("/users/:id", getUser)
|
||||
|> Router.post("/users", createUser)
|
||||
|> Router.use(loggerMiddleware)
|
||||
|> Router.use(authMiddleware)
|
||||
|
||||
fn main(): Unit with {HttpServer} =
|
||||
HttpServer.serve(app, 3000)
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
- Path pattern matching with params
|
||||
- Middleware composition
|
||||
- Request/Response types
|
||||
- JSON body parsing integration
|
||||
|
||||
**Effort**: 2 weeks
|
||||
**Impact**: HIGH - Enables real projects
|
||||
|
||||
### 1.3 PostgreSQL Driver
|
||||
**Why**: SQLite is nice for demos, real apps need Postgres/MySQL.
|
||||
|
||||
```lux
|
||||
effect Postgres {
|
||||
fn connect(url: String): Connection
|
||||
fn query(conn: Connection, sql: String, params: List<Value>): List<Row>
|
||||
fn execute(conn: Connection, sql: String, params: List<Value>): Int
|
||||
fn transaction<T>(conn: Connection, f: fn(): T with {Postgres}): T
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
- Native Rust driver (tokio-postgres)
|
||||
- Connection pooling
|
||||
- Parameterized queries (SQL injection prevention)
|
||||
- Transaction support
|
||||
|
||||
**Effort**: 2 weeks
|
||||
**Impact**: HIGH - Enables production backends
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Showcase Unique Features
|
||||
|
||||
### 2.1 Property-Based Testing Framework
|
||||
**Why**: This showcases behavioral types - Lux's most unique feature.
|
||||
|
||||
```lux
|
||||
test "reverse is involutive" =
|
||||
forAll(listOf(int), fn(xs) =>
|
||||
List.reverse(List.reverse(xs)) == xs
|
||||
) is pure is total // Compiler verifies!
|
||||
|
||||
test "sort produces sorted output" =
|
||||
forAll(listOf(int), fn(xs) =>
|
||||
let sorted = List.sort(xs)
|
||||
isSorted(sorted) && sameElements(xs, sorted)
|
||||
) where result is idempotent // Compiler verifies!
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
- Generator combinators (int, string, listOf, oneOf, etc.)
|
||||
- Shrinking for minimal failing cases
|
||||
- Integration with behavioral type checker
|
||||
- Nice failure output
|
||||
|
||||
**Effort**: 2 weeks
|
||||
**Impact**: HIGH - Unique selling point
|
||||
|
||||
### 2.2 Schema Evolution Showcase
|
||||
**Why**: This is unique to Lux. Need a compelling demo.
|
||||
|
||||
**Build**: Database migration tool that generates SQL from Lux types
|
||||
|
||||
```lux
|
||||
type User @v1 = { name: String, email: String }
|
||||
type User @v2 = { name: String, email: String, age: Option<Int> }
|
||||
from @v1 = fn(u) => { ...u, age: None }
|
||||
|
||||
// Tool generates:
|
||||
// ALTER TABLE users ADD COLUMN age INTEGER;
|
||||
```
|
||||
|
||||
**Effort**: 1 week
|
||||
**Impact**: MEDIUM - Differentiator for data teams
|
||||
|
||||
### 2.3 Effect Visualization
|
||||
**Why**: Make the invisible visible. Show effect flow in code.
|
||||
|
||||
```
|
||||
lux visualize src/main.lux
|
||||
|
||||
processOrder: Order -> Receipt
|
||||
├── Database.query (line 12)
|
||||
│ └── SQL: "SELECT * FROM inventory"
|
||||
├── PaymentGateway.charge (line 18)
|
||||
│ └── Amount: order.total
|
||||
└── Email.send (line 25)
|
||||
└── To: order.customer.email
|
||||
```
|
||||
|
||||
**Effort**: 1 week
|
||||
**Impact**: MEDIUM - Educational, impressive in demos
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Developer Experience Polish
|
||||
|
||||
### 3.1 Better REPL
|
||||
**Why**: First impression matters. REPL is how people try the language.
|
||||
|
||||
**Add**:
|
||||
- Syntax highlighting
|
||||
- Multi-line editing
|
||||
- Tab completion for modules
|
||||
- `:doc` command for documentation
|
||||
- `:browse Module` to list exports
|
||||
- Pretty-printed output
|
||||
|
||||
**Effort**: 1 week
|
||||
**Impact**: MEDIUM
|
||||
|
||||
### 3.2 LSP Improvements
|
||||
**Why**: IDE experience is expected in 2025.
|
||||
|
||||
**Add**:
|
||||
- Inlay hints (show inferred types)
|
||||
- Code actions (import suggestions, fix suggestions)
|
||||
- Rename symbol
|
||||
- Find all references
|
||||
- Semantic highlighting
|
||||
|
||||
**Effort**: 2 weeks
|
||||
**Impact**: MEDIUM
|
||||
|
||||
### 3.3 Documentation Generator
|
||||
**Why**: Rust's docs.rs is beloved. Good docs = adoption.
|
||||
|
||||
```lux
|
||||
/// Calculate the factorial of a number.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// factorial(5) // => 120
|
||||
/// ```
|
||||
///
|
||||
/// # Properties
|
||||
/// - factorial(n) is pure
|
||||
/// - factorial(n) is total for n >= 0
|
||||
fn factorial(n: Int): Int is pure is total = ...
|
||||
```
|
||||
|
||||
**Effort**: 1 week
|
||||
**Impact**: MEDIUM
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Performance & Production
|
||||
|
||||
### 4.1 Performance Benchmarks
|
||||
**Why**: Need to prove Lux is fast. Numbers matter.
|
||||
|
||||
**Targets**:
|
||||
| Benchmark | Target | Comparison |
|
||||
|-----------|--------|------------|
|
||||
| Fibonacci(40) | < 1s | Rust: 0.3s |
|
||||
| HTTP req/sec | > 50k | Go: 100k |
|
||||
| JSON parse 1MB | < 50ms | Node: 30ms |
|
||||
|
||||
**Effort**: 1 week
|
||||
**Impact**: MEDIUM - Removes adoption blocker
|
||||
|
||||
### 4.2 Production Hardening
|
||||
**Why**: Memory leaks and crashes kill adoption.
|
||||
|
||||
**Add**:
|
||||
- Memory leak detection in debug mode
|
||||
- Graceful shutdown handling
|
||||
- Signal handling (SIGTERM, SIGINT)
|
||||
- Structured logging
|
||||
|
||||
**Effort**: 2 weeks
|
||||
**Impact**: MEDIUM
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Ecosystem Growth
|
||||
|
||||
### 5.1 Package Registry
|
||||
**Why**: Central place to share code.
|
||||
|
||||
- Host at packages.lux-lang.org
|
||||
- `lux pkg publish`
|
||||
- `lux pkg search`
|
||||
- Version resolution
|
||||
|
||||
**Effort**: 2 weeks
|
||||
**Impact**: HIGH long-term
|
||||
|
||||
### 5.2 Starter Templates
|
||||
**Why**: Reduce friction for new projects.
|
||||
|
||||
```bash
|
||||
lux new my-api --template web-api
|
||||
lux new my-cli --template cli-tool
|
||||
lux new my-lib --template library
|
||||
```
|
||||
|
||||
**Effort**: 1 week
|
||||
**Impact**: LOW-MEDIUM
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order (Bang for Buck)
|
||||
|
||||
| Priority | Feature | Effort | Impact | Why First |
|
||||
|----------|---------|--------|--------|-----------|
|
||||
| P0 | Elm-quality errors | 2-3 weeks | HIGH | Free marketing, retention |
|
||||
| P0 | HTTP framework | 2 weeks | HIGH | Enables real projects |
|
||||
| P0 | PostgreSQL driver | 2 weeks | HIGH | Production database |
|
||||
| P1 | Property testing | 2 weeks | HIGH | Unique selling point |
|
||||
| P1 | Better REPL | 1 week | MEDIUM | First impression |
|
||||
| P1 | Performance benchmarks | 1 week | MEDIUM | Removes doubt |
|
||||
| P2 | LSP improvements | 2 weeks | MEDIUM | Developer experience |
|
||||
| P2 | Documentation generator | 1 week | MEDIUM | Ecosystem |
|
||||
| P2 | Schema evolution tool | 1 week | MEDIUM | Differentiator |
|
||||
| P3 | Effect visualization | 1 week | MEDIUM | Demos |
|
||||
| P3 | Package registry | 2 weeks | HIGH | Long-term ecosystem |
|
||||
| P3 | Production hardening | 2 weeks | MEDIUM | Enterprise readiness |
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Short-term (3 months)
|
||||
- [ ] 10 example projects building without issues
|
||||
- [ ] Error messages rated "helpful" by 80% of users
|
||||
- [ ] HTTP "hello world" benchmark > 30k req/sec
|
||||
- [ ] 5 external contributors
|
||||
|
||||
### Medium-term (6 months)
|
||||
- [ ] 1000 GitHub stars
|
||||
- [ ] 50 packages on registry
|
||||
- [ ] 3 production users
|
||||
- [ ] 1 conference talk
|
||||
|
||||
### Long-term (1 year)
|
||||
- [ ] Self-hosted compiler
|
||||
- [ ] 100 packages
|
||||
- [ ] 10 production users
|
||||
- [ ] Featured in "State of Developer Ecosystem" survey
|
||||
|
||||
---
|
||||
|
||||
## What NOT to Build (Yet)
|
||||
|
||||
| Feature | Why Skip |
|
||||
|---------|----------|
|
||||
| WASM backend | JS backend covers browser use case |
|
||||
| Mobile targets | Small market, high effort |
|
||||
| GUI framework | Web handles most UI needs |
|
||||
| AI/ML libraries | Python dominates, wrong battle |
|
||||
| Distributed systems | Need core stability first |
|
||||
| Advanced optimizations | Correctness before speed |
|
||||
|
||||
---
|
||||
|
||||
## The Pitch After Phase 1
|
||||
|
||||
> **Lux**: A functional language where the compiler tells you exactly what your code does.
|
||||
>
|
||||
> - **See effects in signatures**: `fn save(user: User): Unit with {Database, Email}`
|
||||
> - **Test without mocks**: Just swap the handler. No DI framework needed.
|
||||
> - **Evolve your schemas**: Types track versions. Migrations are code.
|
||||
> - **Compiler catches more**: Pure, total, idempotent - verified, not hoped.
|
||||
>
|
||||
> *Effects you can see. Tests you can trust.*
|
||||
@@ -153,27 +153,26 @@ A sequential guide to learning Lux, from basics to advanced topics.
|
||||
8. [Error Handling](guide/08-errors.md) - Fail effect, Option, Result
|
||||
9. [Standard Library](guide/09-stdlib.md) - Built-in functions
|
||||
10. [Advanced Topics](guide/10-advanced.md) - Traits, generics, optimization
|
||||
11. [Databases](guide/11-databases.md) - SQL, transactions, testing with handlers
|
||||
|
||||
### [Language Reference](reference/syntax.md)
|
||||
Complete syntax and semantics reference.
|
||||
|
||||
- [Syntax](reference/syntax.md) - Grammar and syntax rules
|
||||
- [Types](reference/types.md) - Type system details
|
||||
- [Effects](reference/effects.md) - Effect system reference
|
||||
- [Standard Library](reference/stdlib.md) - All built-in functions
|
||||
- [Standard Library](guide/09-stdlib.md) - Built-in functions and modules
|
||||
|
||||
### [Tutorials](tutorials/README.md)
|
||||
Project-based learning.
|
||||
|
||||
**Standard Programs:**
|
||||
- [Calculator](tutorials/calculator.md) - Basic REPL calculator
|
||||
- [Todo App](tutorials/todo.md) - File I/O and data structures
|
||||
- [HTTP Client](tutorials/http-client.md) - Fetching web data
|
||||
|
||||
**Effect Showcases:**
|
||||
- [Dependency Injection](tutorials/dependency-injection.md) - Testing with effects
|
||||
- [State Machines](tutorials/state-machines.md) - Modeling state with effects
|
||||
- [Parser Combinators](tutorials/parsers.md) - Effects for backtracking
|
||||
- [Project Ideas](tutorials/project-ideas.md) - Ideas for building with Lux
|
||||
|
||||
### Design Documents
|
||||
- [Packages](PACKAGES.md) - Package manager and dependencies
|
||||
- [SQL Design Analysis](SQL_DESIGN_ANALYSIS.md) - SQL as built-in vs package
|
||||
- [Roadmap](ROADMAP.md) - Development priorities and status
|
||||
- [Website Plan](WEBSITE_PLAN.md) - Website architecture and content
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
| Runtime versioned values | ✅ Complete |
|
||||
| Schema registry & compatibility checking | ✅ Complete |
|
||||
| Basic migration execution | ✅ Complete |
|
||||
| Type system integration | ⚠️ Partial (versions ignored in typechecker) |
|
||||
| Auto-migration generation | ❌ Missing |
|
||||
| Type system integration | ✅ Complete |
|
||||
| Auto-migration generation | ✅ Complete |
|
||||
| Serialization/codec support | ❌ Missing |
|
||||
|
||||
### Behavioral Types
|
||||
@@ -24,10 +24,11 @@
|
||||
| Parser (`is pure`, `is total`, etc.) | ✅ Complete |
|
||||
| AST & PropertySet | ✅ Complete |
|
||||
| Pure function checking (no effects) | ✅ Complete |
|
||||
| Total verification | ❌ Missing |
|
||||
| Idempotent verification | ❌ Missing |
|
||||
| Deterministic verification | ❌ Missing |
|
||||
| Where clause enforcement | ❌ Missing |
|
||||
| Total verification (no Fail, structural recursion) | ✅ Complete |
|
||||
| Idempotent verification (pattern-based) | ✅ Complete |
|
||||
| Deterministic verification (no Random/Time) | ✅ Complete |
|
||||
| Commutative verification (2 params, commutative op) | ✅ Complete |
|
||||
| Where clause property constraints | ✅ Complete |
|
||||
|
||||
---
|
||||
|
||||
@@ -49,9 +50,10 @@
|
||||
|
||||
| Task | Priority | Effort | Status |
|
||||
|------|----------|--------|--------|
|
||||
| SQL effect (query, execute) | P1 | 2 weeks | ❌ Missing |
|
||||
| SQL effect (query, execute) | P1 | 2 weeks | ✅ Complete |
|
||||
| Transaction effect | P2 | 1 week | ✅ Complete |
|
||||
| Connection pooling | P2 | 1 week | ❌ Missing |
|
||||
| Transaction effect | P2 | 1 week | ❌ Missing |
|
||||
| PostgreSQL support | P1 | 2 weeks | ✅ Complete |
|
||||
|
||||
### Phase 1.3: Web Server Framework
|
||||
|
||||
@@ -66,7 +68,7 @@
|
||||
|
||||
| Task | Priority | Effort | Status |
|
||||
|------|----------|--------|--------|
|
||||
| Elm-quality error messages | P1 | 2 weeks | ⚠️ Partial (context shown, suggestions missing) |
|
||||
| Elm-quality error messages | P1 | 2 weeks | ✅ Complete (field suggestions, error categories) |
|
||||
| Full JS compilation | P2 | 4 weeks | ✅ Complete |
|
||||
| Hot reload / watch mode | P2 | — | ✅ Complete |
|
||||
| Debugger improvements | P3 | 2 weeks | ✅ Basic |
|
||||
@@ -88,15 +90,16 @@
|
||||
|
||||
| Task | Priority | Effort | Status |
|
||||
|------|----------|--------|--------|
|
||||
| Total function verification | P1 | 2 weeks | ❌ Missing |
|
||||
| Idempotent verification | P1 | 2 weeks | ❌ Missing |
|
||||
| Deterministic verification | P1 | 1 week | ❌ Missing |
|
||||
| Where clause enforcement | P1 | 1 week | ❌ Missing |
|
||||
| Total function verification | P1 | 2 weeks | ✅ Complete |
|
||||
| Idempotent verification | P1 | 2 weeks | ✅ Complete |
|
||||
| Deterministic verification | P1 | 1 week | ✅ Complete |
|
||||
| Where clause enforcement | P1 | 1 week | ✅ Complete |
|
||||
|
||||
**Implementation approach:**
|
||||
- **Total:** Restrict to structural recursion, require termination proof for general recursion
|
||||
- **Idempotent:** Pattern-based (setter patterns, specific effect combinations)
|
||||
- **Deterministic:** Effect analysis (no Random, Time, or non-deterministic IO)
|
||||
**Implementation approach (completed):**
|
||||
- **Total:** Checks for no Fail effect + structural recursion for termination
|
||||
- **Idempotent:** Pattern-based recognition (constants, identity, clamping, abs, projections)
|
||||
- **Deterministic:** Effect analysis (no Random or Time effects)
|
||||
- **Commutative:** Requires 2 params with commutative operation (+, *, min, max, etc.)
|
||||
|
||||
### Phase 2.2: Refinement Types (Stretch Goal)
|
||||
|
||||
@@ -130,10 +133,10 @@
|
||||
|
||||
| Task | Priority | Effort | Status |
|
||||
|------|----------|--------|--------|
|
||||
| Type system version tracking | P1 | 1 week | ⚠️ Partial |
|
||||
| Auto-migration generation | P1 | 2 weeks | ❌ Missing |
|
||||
| Version compatibility errors | P1 | 1 week | ❌ Missing |
|
||||
| Migration chain optimization | P2 | 1 week | ⚠️ Basic |
|
||||
| Type system version tracking | P1 | 1 week | ✅ Complete |
|
||||
| Auto-migration generation | P1 | 2 weeks | ✅ Complete |
|
||||
| Version compatibility errors | P1 | 1 week | ✅ Complete |
|
||||
| Migration chain optimization | P2 | 1 week | ✅ Complete |
|
||||
|
||||
### Phase 3.2: Serialization Support
|
||||
|
||||
@@ -205,8 +208,11 @@
|
||||
|------|----------|--------|--------|
|
||||
| Package manager (lux pkg) | P1 | 3 weeks | ✅ Complete |
|
||||
| Module loader integration | P1 | 1 week | ✅ Complete |
|
||||
| Package registry | P2 | 2 weeks | ❌ Missing |
|
||||
| Dependency resolution | P2 | 2 weeks | ❌ Missing |
|
||||
| Package registry server | P2 | 2 weeks | ✅ Complete |
|
||||
| Registry CLI (search, publish) | P2 | 1 week | ✅ Complete |
|
||||
| Lock file generation | P1 | 1 week | ✅ Complete |
|
||||
| Version constraint parsing | P1 | 1 week | ✅ Complete |
|
||||
| Transitive dependency resolution | P2 | 2 weeks | ⚠️ Basic (direct deps only) |
|
||||
|
||||
**Package Manager Features:**
|
||||
- `lux pkg init` - Initialize project with lux.toml
|
||||
@@ -219,8 +225,8 @@
|
||||
|
||||
| Task | Priority | Effort | Status |
|
||||
|------|----------|--------|--------|
|
||||
| LSP completions | P1 | 1 week | ⚠️ Basic |
|
||||
| LSP go-to-definition | P1 | 1 week | ⚠️ Partial |
|
||||
| LSP completions | P1 | 1 week | ✅ Complete (module-specific completions) |
|
||||
| LSP go-to-definition | P1 | 1 week | ✅ Complete (functions, lets, types) |
|
||||
| Formatter | P2 | — | ✅ Complete |
|
||||
| Documentation generator | P2 | 1 week | ❌ Missing |
|
||||
|
||||
@@ -253,21 +259,21 @@
|
||||
3. ~~**File effect**~~ ✅ Done
|
||||
4. ~~**HTTP client effect**~~ ✅ Done
|
||||
5. ~~**JSON support**~~ ✅ Done
|
||||
6. **Elm-quality errors** — ⚠️ In progress
|
||||
6. ~~**Elm-quality errors**~~ ✅ Done
|
||||
|
||||
### Quarter 2: Backend Services (Use Case 1)
|
||||
### Quarter 2: Backend Services (Use Case 1) ✅ COMPLETE
|
||||
|
||||
7. **HTTP server effect** — Build APIs
|
||||
8. **SQL effect** — Database access
|
||||
9. **Full JS compilation** — Deployment
|
||||
10. **Package manager** — Code sharing
|
||||
7. ~~**HTTP server effect**~~ ✅ Done
|
||||
8. ~~**SQL effect**~~ ✅ Done
|
||||
9. ~~**Full JS compilation**~~ ✅ Done
|
||||
10. ~~**Package manager**~~ ✅ Done
|
||||
|
||||
### Quarter 3: Reliability (Use Case 2)
|
||||
### Quarter 3: Reliability (Use Case 2) ✅ COMPLETE
|
||||
|
||||
11. **Behavioral type verification** — Total, idempotent, deterministic
|
||||
12. **Where clause enforcement** — Type-level guarantees
|
||||
13. **Schema evolution completion** — Version tracking in types
|
||||
14. **Auto-migration generation** — Reduce boilerplate
|
||||
11. ~~**Behavioral type verification**~~ ✅ Done
|
||||
12. ~~**Where clause enforcement**~~ ✅ Done
|
||||
13. ~~**Schema evolution completion**~~ ✅ Done
|
||||
14. ~~**Auto-migration generation**~~ ✅ Done
|
||||
|
||||
### Quarter 4: Polish (Use Cases 3 & 4)
|
||||
|
||||
@@ -298,6 +304,8 @@
|
||||
- ✅ Random effect (int, float, range, bool)
|
||||
- ✅ Time effect (now, sleep)
|
||||
- ✅ Test effect (assert, assertEqual, assertTrue, assertFalse)
|
||||
- ✅ SQL effect (SQLite with transactions)
|
||||
- ✅ Postgres effect (PostgreSQL connections)
|
||||
|
||||
**Module System:**
|
||||
- ✅ Imports, exports, aliases
|
||||
@@ -317,14 +325,14 @@
|
||||
- ✅ C backend (functions, closures, pattern matching, lists)
|
||||
- ✅ JS backend (full language support, browser & Node.js)
|
||||
- ✅ REPL with history
|
||||
- ✅ Basic LSP server
|
||||
- ✅ LSP server (diagnostics, hover, completions, go-to-definition, references, symbols)
|
||||
- ✅ Formatter
|
||||
- ✅ Watch mode
|
||||
- ✅ Debugger (basic)
|
||||
|
||||
**Advanced (Parsing Only):**
|
||||
- ✅ Schema evolution (parsing, runtime values)
|
||||
- ✅ Behavioral types (parsing, pure checking only)
|
||||
**Advanced:**
|
||||
- ✅ Schema evolution (parser, runtime, migrations, compatibility checking)
|
||||
- ✅ Behavioral types (pure, total, idempotent, deterministic, commutative verification)
|
||||
|
||||
---
|
||||
|
||||
|
||||
330
docs/SQL_DESIGN_ANALYSIS.md
Normal file
330
docs/SQL_DESIGN_ANALYSIS.md
Normal file
@@ -0,0 +1,330 @@
|
||||
# SQL in Lux: Built-in Effect vs Package
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document analyzes whether SQL database access should be a built-in language feature (as it currently is) or a separate package. After comparing approaches across 12+ languages, the recommendation is:
|
||||
|
||||
**Keep SQL as a built-in effect, but refactor the implementation to be more modular.**
|
||||
|
||||
## Current Implementation
|
||||
|
||||
Lux currently implements SQL as a built-in effect:
|
||||
|
||||
```lux
|
||||
fn main(): Unit with {Console, Sql} = {
|
||||
let db = Sql.openMemory()
|
||||
Sql.execute(db, "CREATE TABLE users (...)")
|
||||
let users = Sql.query(db, "SELECT * FROM users")
|
||||
Sql.close(db)
|
||||
}
|
||||
```
|
||||
|
||||
The implementation uses rusqlite (SQLite) compiled directly into the Lux binary.
|
||||
|
||||
## How Other Languages Handle Database Access
|
||||
|
||||
### Languages with Built-in Database Support
|
||||
|
||||
| Language | Approach | Notes |
|
||||
|----------|----------|-------|
|
||||
| **Python** | `sqlite3` in stdlib | Most languages have SQLite in stdlib |
|
||||
| **Ruby** | `sqlite3` gem + AR are common | ActiveRecord is de facto standard |
|
||||
| **Go** | `database/sql` interface in stdlib | Drivers are packages |
|
||||
| **Elixir** | Ecto as separate package | But universally used |
|
||||
| **PHP** | PDO in core | Multiple backends |
|
||||
|
||||
### Languages with Package-Only Database Support
|
||||
|
||||
| Language | Approach | Notes |
|
||||
|----------|----------|-------|
|
||||
| **Rust** | rusqlite, diesel, sqlx packages | No stdlib database |
|
||||
| **Node.js** | pg, mysql2, better-sqlite3 | Packages only |
|
||||
| **Haskell** | postgresql-simple, persistent | Packages only |
|
||||
| **OCaml** | caqti, postgresql-ocaml | Packages only |
|
||||
|
||||
### Analysis of Each Approach
|
||||
|
||||
#### Go's Model: Interface in Stdlib + Driver Packages
|
||||
|
||||
```go
|
||||
import (
|
||||
"database/sql"
|
||||
_ "github.com/lib/pq" // PostgreSQL driver
|
||||
)
|
||||
|
||||
db, _ := sql.Open("postgres", "...")
|
||||
rows, _ := db.Query("SELECT * FROM users")
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Standard interface for all databases
|
||||
- Type-safe at compile time
|
||||
- Drivers are swappable
|
||||
|
||||
**Cons:**
|
||||
- Requires understanding interfaces
|
||||
- Need external packages for actual database
|
||||
|
||||
#### Python's Model: SQLite in Stdlib
|
||||
|
||||
```python
|
||||
import sqlite3
|
||||
conn = sqlite3.connect('example.db')
|
||||
c = conn.cursor()
|
||||
c.execute('SELECT * FROM users')
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Zero dependencies for getting started
|
||||
- Great for learning/prototyping
|
||||
- Always available
|
||||
|
||||
**Cons:**
|
||||
- Other databases need packages
|
||||
- stdlib vs package API differences
|
||||
|
||||
#### Rust's Model: Everything is Packages
|
||||
|
||||
```rust
|
||||
use rusqlite::{Connection, Result};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let conn = Connection::open("test.db")?;
|
||||
conn.execute("CREATE TABLE users (...)", [])?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Minimal core language
|
||||
- Best-in-class implementations
|
||||
- Clear ownership
|
||||
|
||||
**Cons:**
|
||||
- Cargo.toml management
|
||||
- Version conflicts possible
|
||||
- Learning curve for package ecosystem
|
||||
|
||||
#### Elixir's Model: Strong Package Ecosystem
|
||||
|
||||
```elixir
|
||||
# Ecto is technically a package but universally used
|
||||
Repo.all(from u in User, where: u.age > 18)
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Best API emerges naturally
|
||||
- Core team can focus on language
|
||||
- Community ownership
|
||||
|
||||
**Cons:**
|
||||
- Package can become outdated
|
||||
- Multiple competing solutions
|
||||
|
||||
## Arguments For Built-in SQL
|
||||
|
||||
### 1. Effect System Integration
|
||||
|
||||
The most compelling argument: **SQL fits naturally into Lux's effect system.**
|
||||
|
||||
```lux
|
||||
// The effect signature documents database access
|
||||
fn fetchUser(id: Int): User with {Sql} = { ... }
|
||||
|
||||
// Handlers enable testing without mocks
|
||||
handler testDatabase(): Sql { ... }
|
||||
```
|
||||
|
||||
This is harder to achieve with packages - they'd need to integrate deeply with the effect system.
|
||||
|
||||
### 2. Zero-Dependency Getting Started
|
||||
|
||||
New users can immediately:
|
||||
- Follow tutorials that use databases
|
||||
- Build real applications
|
||||
- Learn effects with practical examples
|
||||
|
||||
```bash
|
||||
lux run database_example.lux
|
||||
# Just works - no package installation
|
||||
```
|
||||
|
||||
### 3. Guaranteed API Stability
|
||||
|
||||
Built-in effects have stable, documented APIs. Package APIs can change between versions.
|
||||
|
||||
### 4. Teaching Functional Effects
|
||||
|
||||
SQL is an excellent teaching example for effects:
|
||||
- Clear side effects (I/O to database)
|
||||
- Handler swapping for testing
|
||||
- Transaction scoping
|
||||
|
||||
### 5. Practical Utility
|
||||
|
||||
90%+ of real applications need database access. Making it trivial benefits most users.
|
||||
|
||||
## Arguments For SQL as Package
|
||||
|
||||
### 1. Smaller Binary Size
|
||||
|
||||
rusqlite adds significant binary size (~2-3MB). Package-based approach lets users opt-in.
|
||||
|
||||
### 2. Database Backend Choice
|
||||
|
||||
Currently locked to SQLite. A package ecosystem could offer:
|
||||
- `lux-sqlite`
|
||||
- `lux-postgres`
|
||||
- `lux-mysql`
|
||||
- `lux-mongodb`
|
||||
|
||||
### 3. Faster Core Language Evolution
|
||||
|
||||
Core team focuses on language; community builds integrations.
|
||||
|
||||
### 4. Better Specialization
|
||||
|
||||
Dedicated package maintainers might build better database tooling than core team.
|
||||
|
||||
### 5. Multiple Competing Implementations
|
||||
|
||||
Competition drives quality. The best SQL package wins adoption.
|
||||
|
||||
## Comparison Matrix
|
||||
|
||||
| Factor | Built-in | Package |
|
||||
|--------|----------|---------|
|
||||
| Effect integration | Excellent | Needs design work |
|
||||
| Learning curve | Low | Medium |
|
||||
| Binary size | Larger | User controls |
|
||||
| Database options | Limited | Unlimited |
|
||||
| API stability | Guaranteed | Version-dependent |
|
||||
| Getting started | Instant | Requires install |
|
||||
| Testing story | Built-in handlers | Package-specific |
|
||||
| Maintenance burden | Core team | Community |
|
||||
|
||||
## Recommendation
|
||||
|
||||
### Keep SQL as Built-in Effect, With Changes
|
||||
|
||||
**Rationale:**
|
||||
|
||||
1. **Effect system is Lux's differentiator** - SQL showcases it perfectly
|
||||
2. **Practicality matters** - 90% of apps need databases
|
||||
3. **Teaching value** - SQL is ideal for learning effects
|
||||
4. **Handler testing** - Built-in integration enables powerful testing
|
||||
|
||||
### Proposed Architecture
|
||||
|
||||
```
|
||||
Core Lux
|
||||
├── Sql effect (interface only)
|
||||
│ ├── open/close
|
||||
│ ├── execute/query
|
||||
│ └── transaction operations
|
||||
│
|
||||
└── Default SQLite handler (built-in)
|
||||
└── Uses rusqlite
|
||||
|
||||
Future packages (optional)
|
||||
├── lux-postgres -- PostgreSQL handler
|
||||
├── lux-mysql -- MySQL handler
|
||||
└── lux-redis -- Redis (key-value, not Sql)
|
||||
```
|
||||
|
||||
### Specific Changes to Consider
|
||||
|
||||
1. **Make SQLite compilation optional**
|
||||
```toml
|
||||
# Cargo.toml
|
||||
[features]
|
||||
default = ["sqlite"]
|
||||
sqlite = ["rusqlite"]
|
||||
```
|
||||
|
||||
2. **Define stable Sql effect interface**
|
||||
```lux
|
||||
effect Sql {
|
||||
fn open(path: String): SqlConn
|
||||
fn close(conn: SqlConn): Unit
|
||||
fn execute(conn: SqlConn, sql: String): Int
|
||||
fn query(conn: SqlConn, sql: String): List<SqlRow>
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
3. **Allow package handlers to implement Sql**
|
||||
```lux
|
||||
// In lux-postgres package
|
||||
handler postgresHandler(connStr: String): Sql { ... }
|
||||
|
||||
// Usage
|
||||
run myApp() with {
|
||||
Sql -> postgresHandler("postgres://...")
|
||||
}
|
||||
```
|
||||
|
||||
4. **Add connection pooling to core**
|
||||
Important for production, should be standard.
|
||||
|
||||
## Comparison to Similar Decisions
|
||||
|
||||
### Console Effect
|
||||
|
||||
Console is built-in. Nobody questions this because:
|
||||
- Universally needed
|
||||
- Simple interface
|
||||
- Hard to get wrong
|
||||
|
||||
SQL is similar but more complex.
|
||||
|
||||
### HTTP Effect
|
||||
|
||||
HTTP client is built-in in Lux. This was the right call because:
|
||||
- Most apps need HTTP
|
||||
- Complex to implement well
|
||||
- Effect system integration important
|
||||
|
||||
SQL follows same reasoning.
|
||||
|
||||
### File Effect
|
||||
|
||||
File I/O is built-in. Same rationale applies.
|
||||
|
||||
## What Other Effect-System Languages Do
|
||||
|
||||
| Language | Database | Built-in? |
|
||||
|----------|----------|-----------|
|
||||
| **Koka** | No database support | N/A |
|
||||
| **Eff** | No database support | N/A |
|
||||
| **Frank** | No database support | N/A |
|
||||
| **Unison** | Abilities + packages | Both |
|
||||
|
||||
Lux is pioneering practical effects. Built-in SQL makes sense.
|
||||
|
||||
## Conclusion
|
||||
|
||||
SQL should remain a built-in effect in Lux because:
|
||||
|
||||
1. It demonstrates the power of effects for real-world use
|
||||
2. It enables the handler-based testing story
|
||||
3. It removes friction for most applications
|
||||
4. It serves as a teaching example for effects
|
||||
|
||||
However, the implementation should evolve to:
|
||||
- Support multiple database backends via handlers
|
||||
- Make SQLite optional for minimal binaries
|
||||
- Provide connection pooling
|
||||
- Add parameterized query support
|
||||
|
||||
This hybrid approach gives users the best of both worlds: immediate productivity with built-in SQLite, and flexibility through package-provided handlers for other databases.
|
||||
|
||||
---
|
||||
|
||||
## Future Work
|
||||
|
||||
1. **Parameterized queries** - Critical for SQL injection prevention
|
||||
2. **Connection pooling** - Required for production servers
|
||||
3. **PostgreSQL handler** - Most requested database
|
||||
4. **Migration support** - Schema evolution tooling
|
||||
5. **Type-safe queries** - Compile-time SQL checking (ambitious)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,122 +1,230 @@
|
||||
# Lux Performance Benchmarks
|
||||
|
||||
This document compares Lux's performance against other languages on common benchmarks.
|
||||
This document provides comprehensive performance measurements comparing Lux to other languages.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Run full benchmark suite
|
||||
nix run .#bench
|
||||
|
||||
# Run quick Lux vs C comparison
|
||||
nix run .#bench-quick
|
||||
|
||||
# Run detailed CPU metrics with poop
|
||||
nix run .#bench-poop
|
||||
```
|
||||
|
||||
## Execution Modes
|
||||
|
||||
Lux supports two execution modes:
|
||||
|
||||
1. **Compiled** (`lux compile`): Generates C code, compiles with gcc -O3. Native performance.
|
||||
2. **Interpreted** (`lux run`): Tree-walking interpreter. Slower but instant startup.
|
||||
|
||||
## Benchmark Environment
|
||||
|
||||
- **Platform**: Linux x86_64
|
||||
- **Lux**: Compiled to native via C backend with `-O2` optimization
|
||||
- **Node.js**: v16.x (V8 JIT)
|
||||
- **Rust**: rustc with `-O` (release optimization)
|
||||
- **Platform**: Linux x86_64 (NixOS)
|
||||
- **Lux**: v0.1.0 (compiled via C backend)
|
||||
- **C**: gcc with -O3
|
||||
- **Rust**: rustc with -C opt-level=3 -C lto
|
||||
- **Zig**: zig with -O ReleaseFast
|
||||
- **Tools**: hyperfine, poop
|
||||
|
||||
## Results Summary
|
||||
|
||||
| Benchmark | Lux (native) | Node.js | Rust (native) |
|
||||
|-----------|-------------|---------|---------------|
|
||||
| Fibonacci(35) | **0.013s** | 0.111s | 0.022s |
|
||||
| List Ops (10k) | **0.001s** | 0.029s | 0.001s |
|
||||
| Prime Count (10k) | **0.001s** | 0.031s | 0.001s |
|
||||
### hyperfine Results
|
||||
|
||||
### Key Findings
|
||||
```
|
||||
Benchmark 1: /tmp/fib_lux
|
||||
Time (mean ± σ): 28.1 ms ± 0.6 ms
|
||||
|
||||
1. **Lux matches or beats Rust** on these benchmarks
|
||||
2. **Lux is 8-30x faster than Node.js** depending on workload
|
||||
3. **Native compilation pays off** - AOT compilation to C produces highly optimized code
|
||||
Benchmark 2: /tmp/fib_c
|
||||
Time (mean ± σ): 29.0 ms ± 2.1 ms
|
||||
|
||||
## Benchmark Details
|
||||
Benchmark 3: /tmp/fib_rust
|
||||
Time (mean ± σ): 41.2 ms ± 0.6 ms
|
||||
|
||||
### Fibonacci (Recursive)
|
||||
Benchmark 4: /tmp/fib_zig
|
||||
Time (mean ± σ): 47.0 ms ± 1.1 ms
|
||||
|
||||
Classic recursive Fibonacci calculation - tests function call overhead and recursion.
|
||||
Summary
|
||||
/tmp/fib_lux ran
|
||||
1.03 ± 0.08 times faster than /tmp/fib_c
|
||||
1.47 ± 0.04 times faster than /tmp/fib_rust
|
||||
1.67 ± 0.05 times faster than /tmp/fib_zig
|
||||
```
|
||||
|
||||
```lux
|
||||
fn fib(n: Int): Int = {
|
||||
if n <= 1 then n
|
||||
else fib(n - 1) + fib(n - 2)
|
||||
| Benchmark | C (gcc -O3) | Rust | Zig | **Lux (compiled)** | Lux (interp) |
|
||||
|-----------|-------------|------|-----|---------------------|--------------|
|
||||
| Fibonacci(35) | 29.0ms | 41.2ms | 47.0ms | **28.1ms** | 254ms |
|
||||
|
||||
### poop Results (Detailed CPU Metrics)
|
||||
|
||||
| Metric | C | Lux | Rust | Zig |
|
||||
|--------|---|-----|------|-----|
|
||||
| **Wall Time** | 29.0ms | 29.2ms (+0.8%) | 42.0ms (+45%) | 48.1ms (+66%) |
|
||||
| **CPU Cycles** | 53.1M | 53.2M (+0.2%) | 78.2M (+47%) | 90.4M (+70%) |
|
||||
| **Instructions** | 293M | 292M (-0.5%) | 302M (+3.2%) | 317M (+8.1%) |
|
||||
| **Cache Refs** | 11.4K | 11.7K (+3.1%) | 17.8K (+57%) | 1.87K (-84%) |
|
||||
| **Cache Misses** | 4.39K | 4.62K (+5.3%) | 6.47K (+47%) | 340 (-92%) |
|
||||
| **Branch Misses** | 28.3K | 32.0K (+13%) | 33.5K (+18%) | 29.6K (+4.7%) |
|
||||
| **Peak RSS** | 1.56MB | 1.63MB (+4.7%) | 2.00MB (+29%) | 1.07MB (-32%) |
|
||||
|
||||
### Key Observations
|
||||
|
||||
1. **Lux matches C**: Within measurement noise (0.8% difference)
|
||||
2. **Lux beats Rust by 47%**: Fewer CPU cycles, fewer instructions
|
||||
3. **Lux beats Zig by 67%**: Despite Zig's excellent cache efficiency
|
||||
4. **Instruction efficiency**: Lux executes fewer instructions than Rust/Zig
|
||||
|
||||
## Why Compiled Lux is Fast
|
||||
|
||||
### 1. gcc's Aggressive Recursion Optimization
|
||||
|
||||
When Lux compiles to C, gcc transforms the recursive Fibonacci into highly optimized loops:
|
||||
|
||||
**Rust (LLVM) keeps one recursive call:**
|
||||
```asm
|
||||
a640: lea -0x1(%r14),%rdi
|
||||
a644: call a630 ; <-- recursive call
|
||||
a649: lea -0x2(%r14),%rdi
|
||||
a657: ja a640 ; loop for fib(n-2)
|
||||
```
|
||||
|
||||
**Lux/C (gcc) transforms to pure loops:**
|
||||
```asm
|
||||
; No 'call fib' in the hot path
|
||||
; Uses r12-r15, rbx as accumulators
|
||||
; Complex but efficient loop structure
|
||||
```
|
||||
|
||||
### 2. Compiler Optimization Strategies
|
||||
|
||||
| Compiler | Backend | Strategy |
|
||||
|----------|---------|----------|
|
||||
| **gcc -O3** | Native | Aggressive recursion elimination, loop unrolling |
|
||||
| **LLVM (Rust/Zig)** | Native | Conservative, preserves some recursion |
|
||||
|
||||
gcc has decades of optimization work specifically for transforming recursive C code into efficient loops. By generating clean C, Lux inherits this optimization automatically.
|
||||
|
||||
### 3. Why More Instructions = Slower (Rust/Zig)
|
||||
|
||||
The poop results show:
|
||||
- **C/Lux**: 293M instructions, 53M cycles
|
||||
- **Rust**: 302M instructions (+3%), 78M cycles (+47%)
|
||||
- **Zig**: 317M instructions (+8%), 90M cycles (+70%)
|
||||
|
||||
The extra instructions in Rust/Zig come from:
|
||||
- Recursive call setup/teardown overhead
|
||||
- Additional bounds checking
|
||||
- Stack frame management for each recursion level
|
||||
|
||||
### 4. Direct C Generation
|
||||
|
||||
Lux generates straightforward C code:
|
||||
```c
|
||||
int64_t fib_lux(int64_t n) {
|
||||
if (n <= 1) return n;
|
||||
return fib_lux(n - 1) + fib_lux(n - 2);
|
||||
}
|
||||
```
|
||||
|
||||
- **Lux**: 0.013s (fastest)
|
||||
- **Rust**: 0.022s
|
||||
- **Node.js**: 0.111s
|
||||
This gives gcc maximum freedom to optimize without fighting language-specific abstractions.
|
||||
|
||||
Lux's C backend generates efficient code with proper tail-call optimization where applicable.
|
||||
### 5. Perceus Reference Counting
|
||||
|
||||
### List Operations
|
||||
Lux implements Koka-style Perceus reference counting:
|
||||
- FBIP (Functional But In-Place) optimization
|
||||
- Compile-time reference tracking where possible
|
||||
- Minimal runtime overhead for memory management
|
||||
|
||||
Tests functional programming primitives: map, filter, fold on 10,000 elements.
|
||||
For the fib benchmark (which doesn't allocate), this adds zero overhead.
|
||||
|
||||
```lux
|
||||
let nums = List.range(1, 10001)
|
||||
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
||||
let evens = List.filter(doubled, fn(x: Int): Bool => x % 4 == 0)
|
||||
let sum = List.fold(evens, 0, fn(acc: Int, x: Int): Int => acc + x)
|
||||
```
|
||||
## Comparison Context
|
||||
|
||||
- **Lux**: 0.001s
|
||||
- **Rust**: 0.001s
|
||||
- **Node.js**: 0.029s
|
||||
| Language | fib(35) | Type | vs Lux |
|
||||
|----------|---------|------|--------|
|
||||
| **Lux (compiled)** | 28.1ms | Compiled (via C) | baseline |
|
||||
| C (gcc -O3) | 29.0ms | Compiled | 1.03x slower |
|
||||
| Rust | 41.2ms | Compiled | 1.47x slower |
|
||||
| Zig | 47.0ms | Compiled | 1.67x slower |
|
||||
| Go | ~50ms | Compiled | ~1.8x slower |
|
||||
| LuaJIT | ~150ms | JIT | ~5x slower |
|
||||
| V8 (JS) | ~200ms | JIT | ~7x slower |
|
||||
| Lux (interp) | 254ms | Interpreted | 9x slower |
|
||||
| Python | ~3000ms | Interpreted | ~107x slower |
|
||||
|
||||
Lux's FBIP (Functional But In-Place) optimization allows list reuse when reference count is 1.
|
||||
## When Lux Won't Be Fastest
|
||||
|
||||
### Prime Counting
|
||||
This benchmark is favorable to gcc's optimization patterns. Other scenarios:
|
||||
|
||||
Count primes up to 10,000 using trial division - tests loops and conditionals.
|
||||
|
||||
```lux
|
||||
fn isPrime(n: Int): Bool = {
|
||||
if n < 2 then false
|
||||
else if n == 2 then true
|
||||
else if n % 2 == 0 then false
|
||||
else isPrimeHelper(n, 3)
|
||||
}
|
||||
```
|
||||
|
||||
- **Lux**: 0.001s
|
||||
- **Rust**: 0.001s
|
||||
- **Node.js**: 0.031s
|
||||
|
||||
## Why Lux is Fast
|
||||
|
||||
### 1. Native Compilation via C
|
||||
|
||||
Lux compiles to C and then to native code using the system C compiler (gcc/clang). This means:
|
||||
- Full access to C compiler optimizations (-O2, -O3)
|
||||
- No interpreter overhead
|
||||
- Direct CPU instruction generation
|
||||
|
||||
### 2. Reference Counting with FBIP
|
||||
|
||||
Lux uses Perceus-inspired reference counting with FBIP optimizations:
|
||||
- **In-place mutation** when reference count is 1
|
||||
- **No garbage collector pauses**
|
||||
- **Predictable memory usage**
|
||||
|
||||
### 3. Efficient Function Calls
|
||||
|
||||
- Closures are allocated once and reused
|
||||
- Ownership transfer avoids unnecessary reference counting
|
||||
- Drop specialization inlines type-specific cleanup
|
||||
| Scenario | Likely Winner | Why |
|
||||
|----------|---------------|-----|
|
||||
| Simple recursion | **Lux/C** | gcc's strength |
|
||||
| SIMD/vectorization | Rust/Zig | Explicit SIMD intrinsics |
|
||||
| Async I/O | Rust (tokio) | Mature async runtime |
|
||||
| Memory-heavy workloads | Zig | Fine-grained allocator control |
|
||||
| Hot loops with bounds checks | C | No safety overhead |
|
||||
|
||||
## Running Benchmarks
|
||||
|
||||
```bash
|
||||
# Run all benchmarks
|
||||
./benchmarks/run_benchmarks.sh
|
||||
### Using Nix Flake Commands
|
||||
|
||||
# Run individual benchmark
|
||||
cargo run --release -- compile benchmarks/fib.lux -o /tmp/fib && /tmp/fib
|
||||
```bash
|
||||
# Full hyperfine benchmark (Lux vs C vs Rust vs Zig)
|
||||
nix run .#bench
|
||||
|
||||
# Quick Lux vs C comparison
|
||||
nix run .#bench-quick
|
||||
|
||||
# Detailed CPU metrics with poop
|
||||
nix run .#bench-poop
|
||||
```
|
||||
|
||||
## Comparison Notes
|
||||
### Manual Benchmark
|
||||
|
||||
- **vs Rust**: Lux is comparable because both compile to native code with similar optimizations
|
||||
- **vs Node.js**: Lux is much faster because V8's JIT can't match AOT compilation for compute-heavy tasks
|
||||
- **vs Python**: Would be even more dramatic (Python is typically 10-100x slower than Node.js)
|
||||
```bash
|
||||
# Enter development shell (includes hyperfine, poop)
|
||||
nix develop
|
||||
|
||||
## Future Improvements
|
||||
# Compile all versions
|
||||
cargo run --release -- compile benchmarks/fib.lux -o /tmp/fib_lux
|
||||
gcc -O3 benchmarks/fib.c -o /tmp/fib_c
|
||||
rustc -C opt-level=3 -C lto benchmarks/fib.rs -o /tmp/fib_rust
|
||||
zig build-exe benchmarks/fib.zig -O ReleaseFast -femit-bin=/tmp/fib_zig
|
||||
|
||||
- Add more benchmarks (sorting, tree operations, string processing)
|
||||
- Compare against more languages (Go, Java, OCaml, Haskell)
|
||||
- Add memory usage benchmarks
|
||||
- Profile and optimize hot paths
|
||||
# Run hyperfine
|
||||
hyperfine --warmup 3 '/tmp/fib_lux' '/tmp/fib_c' '/tmp/fib_rust' '/tmp/fib_zig'
|
||||
|
||||
# Run poop for detailed metrics
|
||||
poop '/tmp/fib_c' '/tmp/fib_lux' '/tmp/fib_rust' '/tmp/fib_zig'
|
||||
```
|
||||
|
||||
## Benchmark Files
|
||||
|
||||
All benchmarks are in `/benchmarks/`:
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `fib.lux`, `fib.c`, `fib.rs`, `fib.zig` | Fibonacci (recursive) |
|
||||
| `ackermann.lux`, etc. | Ackermann function |
|
||||
| `primes.lux`, etc. | Prime counting |
|
||||
| `sumloop.lux`, etc. | Tight numeric loops |
|
||||
|
||||
## The Case for Lux
|
||||
|
||||
Performance is excellent when compiled. But Lux also prioritizes:
|
||||
|
||||
1. **Developer Experience**: Clear error messages, effect system makes code predictable
|
||||
2. **Correctness**: Types catch bugs, effects are explicit in signatures
|
||||
3. **Simplicity**: No null pointers, no exceptions, no hidden control flow
|
||||
4. **Testability**: Effects can be mocked without DI frameworks
|
||||
|
||||
## Methodology Notes
|
||||
|
||||
- All benchmarks run on same machine, same session
|
||||
- hyperfine uses 3 warmup runs, 10 measured runs
|
||||
- poop provides Linux perf-based metrics
|
||||
- Compiler flags documented for reproducibility
|
||||
- Results may vary on different hardware/OS
|
||||
|
||||
@@ -53,6 +53,10 @@ Lux provides several built-in effects:
|
||||
| `Process` | `exec`, `env`, `args`, `cwd`, `exit` | System processes |
|
||||
| `Http` | `get`, `post`, `put`, `delete` | HTTP client |
|
||||
| `HttpServer` | `listen`, `accept`, `respond`, `stop` | HTTP server |
|
||||
| `Sql` | `open`, `openMemory`, `close`, `execute`, `query`, `queryOne`, `beginTx`, `commit`, `rollback` | SQLite database |
|
||||
| `Postgres` | `connect`, `close`, `execute`, `query`, `queryOne` | PostgreSQL database |
|
||||
| `Concurrent` | `spawn`, `await`, `yield`, `sleep`, `cancel`, `isRunning`, `taskCount` | Concurrent tasks |
|
||||
| `Channel` | `create`, `send`, `receive`, `tryReceive`, `close` | Inter-task communication |
|
||||
| `Test` | `assert`, `assertEqual`, `assertTrue`, `assertFalse` | Testing |
|
||||
|
||||
Example usage:
|
||||
|
||||
@@ -320,6 +320,114 @@ fn example(): Int with {Fail} = {
|
||||
}
|
||||
```
|
||||
|
||||
### Sql (SQLite)
|
||||
|
||||
```lux
|
||||
fn example(): Unit with {Sql, Console} = {
|
||||
let conn = Sql.open("mydb.sqlite") // Open database file
|
||||
// Or: let conn = Sql.openMemory() // In-memory database
|
||||
|
||||
// Execute statements (returns row count)
|
||||
Sql.execute(conn, "CREATE TABLE users (id INTEGER, name TEXT)")
|
||||
Sql.execute(conn, "INSERT INTO users VALUES (1, 'Alice')")
|
||||
|
||||
// Query returns list of rows
|
||||
let rows = Sql.query(conn, "SELECT * FROM users")
|
||||
|
||||
// Query for single row
|
||||
let user = Sql.queryOne(conn, "SELECT * FROM users WHERE id = 1")
|
||||
|
||||
// Transactions
|
||||
Sql.beginTx(conn)
|
||||
Sql.execute(conn, "UPDATE users SET name = 'Bob' WHERE id = 1")
|
||||
Sql.commit(conn) // Or: Sql.rollback(conn)
|
||||
|
||||
Sql.close(conn)
|
||||
}
|
||||
```
|
||||
|
||||
### Postgres (PostgreSQL)
|
||||
|
||||
```lux
|
||||
fn example(): Unit with {Postgres, Console} = {
|
||||
let conn = Postgres.connect("postgres://user:pass@localhost/mydb")
|
||||
|
||||
// Execute statements
|
||||
Postgres.execute(conn, "INSERT INTO users (name) VALUES ('Alice')")
|
||||
|
||||
// Query returns list of rows
|
||||
let rows = Postgres.query(conn, "SELECT * FROM users")
|
||||
|
||||
// Query for single row
|
||||
let user = Postgres.queryOne(conn, "SELECT * FROM users WHERE id = 1")
|
||||
|
||||
Postgres.close(conn)
|
||||
}
|
||||
```
|
||||
|
||||
### Concurrent (Parallel Tasks)
|
||||
|
||||
```lux
|
||||
fn example(): Unit with {Concurrent, Console} = {
|
||||
// Spawn concurrent tasks
|
||||
let task1 = Concurrent.spawn(fn(): Int => expensiveComputation(1))
|
||||
let task2 = Concurrent.spawn(fn(): Int => expensiveComputation(2))
|
||||
|
||||
// Do other work while tasks run
|
||||
Console.print("Tasks spawned, doing other work...")
|
||||
|
||||
// Wait for tasks to complete
|
||||
let result1 = Concurrent.await(task1)
|
||||
let result2 = Concurrent.await(task2)
|
||||
|
||||
Console.print("Results: " + toString(result1) + ", " + toString(result2))
|
||||
|
||||
// Check task status
|
||||
if Concurrent.isRunning(task1) then
|
||||
Concurrent.cancel(task1)
|
||||
|
||||
// Non-blocking sleep
|
||||
Concurrent.sleep(100) // 100ms
|
||||
|
||||
// Yield to allow other tasks to run
|
||||
Concurrent.yield()
|
||||
|
||||
// Get active task count
|
||||
let count = Concurrent.taskCount()
|
||||
}
|
||||
```
|
||||
|
||||
### Channel (Inter-Task Communication)
|
||||
|
||||
```lux
|
||||
fn example(): Unit with {Concurrent, Channel, Console} = {
|
||||
// Create a channel for communication
|
||||
let ch = Channel.create()
|
||||
|
||||
// Spawn producer task
|
||||
let producer = Concurrent.spawn(fn(): Unit => {
|
||||
Channel.send(ch, 1)
|
||||
Channel.send(ch, 2)
|
||||
Channel.send(ch, 3)
|
||||
Channel.close(ch)
|
||||
})
|
||||
|
||||
// Consumer receives values
|
||||
match Channel.receive(ch) {
|
||||
Some(value) => Console.print("Received: " + toString(value)),
|
||||
None => Console.print("Channel closed")
|
||||
}
|
||||
|
||||
// Non-blocking receive
|
||||
match Channel.tryReceive(ch) {
|
||||
Some(value) => Console.print("Got: " + toString(value)),
|
||||
None => Console.print("No value available")
|
||||
}
|
||||
|
||||
Concurrent.await(producer)
|
||||
}
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
Native testing framework:
|
||||
@@ -360,6 +468,10 @@ fn main(): Unit with {Console} = {
|
||||
| Random | int, float, bool |
|
||||
| State | get, put |
|
||||
| Fail | fail |
|
||||
| Sql | open, openMemory, close, execute, query, queryOne, beginTx, commit, rollback |
|
||||
| Postgres | connect, close, execute, query, queryOne |
|
||||
| Concurrent | spawn, await, yield, sleep, cancel, isRunning, taskCount |
|
||||
| Channel | create, send, receive, tryReceive, close |
|
||||
| Test | assert, assertEqual, assertTrue, assertFalse |
|
||||
|
||||
## Next
|
||||
|
||||
450
docs/guide/11-databases.md
Normal file
450
docs/guide/11-databases.md
Normal file
@@ -0,0 +1,450 @@
|
||||
# Working with Databases
|
||||
|
||||
Lux includes built-in support for SQL databases through the `Sql` effect. This guide covers how to connect to databases, execute queries, handle transactions, and best practices for database operations.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```lux
|
||||
fn main(): Unit with {Console, Sql} = {
|
||||
// Open an in-memory SQLite database
|
||||
let db = Sql.openMemory()
|
||||
|
||||
// Create a table
|
||||
Sql.execute(db, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
|
||||
|
||||
// Insert data
|
||||
Sql.execute(db, "INSERT INTO users (name) VALUES ('Alice')")
|
||||
Sql.execute(db, "INSERT INTO users (name) VALUES ('Bob')")
|
||||
|
||||
// Query data
|
||||
let users = Sql.query(db, "SELECT * FROM users")
|
||||
Console.print("Found users: " ++ toString(users))
|
||||
|
||||
// Clean up
|
||||
Sql.close(db)
|
||||
}
|
||||
|
||||
run main() with {}
|
||||
```
|
||||
|
||||
## Connecting to Databases
|
||||
|
||||
### In-Memory Database
|
||||
|
||||
For testing and temporary data:
|
||||
|
||||
```lux
|
||||
let db = Sql.openMemory()
|
||||
// Database exists only in memory, lost when closed
|
||||
```
|
||||
|
||||
### File-Based Database
|
||||
|
||||
For persistent storage:
|
||||
|
||||
```lux
|
||||
let db = Sql.open("./data/app.db")
|
||||
// Creates file if it doesn't exist
|
||||
```
|
||||
|
||||
### Connection Lifecycle
|
||||
|
||||
Always close connections when done:
|
||||
|
||||
```lux
|
||||
fn withDatabase<T>(path: String, f: fn(SqlConn): T with Sql): T with Sql = {
|
||||
let db = Sql.open(path)
|
||||
let result = f(db)
|
||||
Sql.close(db)
|
||||
result
|
||||
}
|
||||
```
|
||||
|
||||
## Executing Queries
|
||||
|
||||
### Non-Returning Queries (INSERT, UPDATE, DELETE, CREATE)
|
||||
|
||||
Use `Sql.execute` for statements that don't return rows:
|
||||
|
||||
```lux
|
||||
// Create table
|
||||
Sql.execute(db, "CREATE TABLE IF NOT EXISTS posts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
)")
|
||||
|
||||
// Insert
|
||||
Sql.execute(db, "INSERT INTO posts (title, content) VALUES ('Hello', 'World')")
|
||||
|
||||
// Update
|
||||
Sql.execute(db, "UPDATE posts SET title = 'Updated' WHERE id = 1")
|
||||
|
||||
// Delete
|
||||
Sql.execute(db, "DELETE FROM posts WHERE id = 1")
|
||||
```
|
||||
|
||||
### Queries that Return Rows (SELECT)
|
||||
|
||||
Use `Sql.query` to get all matching rows:
|
||||
|
||||
```lux
|
||||
// Returns List<SqlRow>
|
||||
let users = Sql.query(db, "SELECT * FROM users")
|
||||
|
||||
// Each row is a record-like structure
|
||||
for user in users {
|
||||
Console.print("User: " ++ user.name)
|
||||
}
|
||||
```
|
||||
|
||||
Use `Sql.queryOne` to get a single row (or None):
|
||||
|
||||
```lux
|
||||
// Returns Option<SqlRow>
|
||||
let maybeUser = Sql.queryOne(db, "SELECT * FROM users WHERE id = 1")
|
||||
|
||||
match maybeUser {
|
||||
Some(user) => Console.print("Found: " ++ user.name),
|
||||
None => Console.print("User not found")
|
||||
}
|
||||
```
|
||||
|
||||
## Transactions
|
||||
|
||||
Transactions ensure multiple operations succeed or fail together.
|
||||
|
||||
### Basic Transaction
|
||||
|
||||
```lux
|
||||
// Start transaction
|
||||
Sql.beginTx(db)
|
||||
|
||||
// Do operations
|
||||
Sql.execute(db, "INSERT INTO accounts (name, balance) VALUES ('Alice', 1000)")
|
||||
Sql.execute(db, "INSERT INTO accounts (name, balance) VALUES ('Bob', 1000)")
|
||||
|
||||
// Commit changes
|
||||
Sql.commit(db)
|
||||
```
|
||||
|
||||
### Rollback on Error
|
||||
|
||||
```lux
|
||||
Sql.beginTx(db)
|
||||
|
||||
let result = transferFunds(db, fromId, toId, amount)
|
||||
|
||||
match result {
|
||||
Ok(_) => Sql.commit(db),
|
||||
Err(_) => Sql.rollback(db) // Undo all changes
|
||||
}
|
||||
```
|
||||
|
||||
### Transaction Helper
|
||||
|
||||
Here's a pattern for safe transactions:
|
||||
|
||||
```lux
|
||||
fn transaction<T>(db: SqlConn, f: fn(): T with Sql): Result<T, String> with Sql = {
|
||||
Sql.beginTx(db)
|
||||
|
||||
// Execute the function
|
||||
// In a real implementation, you'd catch errors here
|
||||
let result = f()
|
||||
|
||||
Sql.commit(db)
|
||||
Ok(result)
|
||||
}
|
||||
```
|
||||
|
||||
## Working with Results
|
||||
|
||||
Query results are returned as `List<SqlRow>` where each row behaves like a record:
|
||||
|
||||
```lux
|
||||
let rows = Sql.query(db, "SELECT id, name, age FROM users")
|
||||
|
||||
for row in rows {
|
||||
// Access columns by name
|
||||
let id = row.id // Int
|
||||
let name = row.name // String
|
||||
let age = row.age // Int or null
|
||||
|
||||
Console.print("{name} (age {age})")
|
||||
}
|
||||
```
|
||||
|
||||
### Handling NULL values
|
||||
|
||||
NULL values from the database are represented as options:
|
||||
|
||||
```lux
|
||||
let row = Sql.queryOne(db, "SELECT email FROM users WHERE id = 1")
|
||||
|
||||
match row {
|
||||
Some(r) => {
|
||||
match r.email {
|
||||
Some(email) => Console.print("Email: " ++ email),
|
||||
None => Console.print("No email set")
|
||||
}
|
||||
},
|
||||
None => Console.print("User not found")
|
||||
}
|
||||
```
|
||||
|
||||
## SQL Injection Prevention
|
||||
|
||||
**IMPORTANT**: The current API passes SQL strings directly. For production use, always:
|
||||
|
||||
1. Validate and sanitize user input
|
||||
2. Use parameterized queries (when available)
|
||||
3. Never concatenate user input into SQL strings
|
||||
|
||||
```lux
|
||||
// DANGEROUS - Never do this!
|
||||
let query = "SELECT * FROM users WHERE name = '" ++ userInput ++ "'"
|
||||
|
||||
// SAFER - Validate input first
|
||||
fn safeUserLookup(db: SqlConn, userId: Int): Option<SqlRow> with Sql = {
|
||||
// Integers are safe to interpolate
|
||||
Sql.queryOne(db, "SELECT * FROM users WHERE id = " ++ toString(userId))
|
||||
}
|
||||
|
||||
// For strings, escape quotes at minimum
|
||||
fn escapeString(s: String): String = {
|
||||
String.replace(s, "'", "''")
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Repository Pattern
|
||||
|
||||
Encapsulate database operations:
|
||||
|
||||
```lux
|
||||
type User = { id: Int, name: String, email: Option<String> }
|
||||
|
||||
fn createUser(db: SqlConn, name: String): Int with Sql = {
|
||||
Sql.execute(db, "INSERT INTO users (name) VALUES ('" ++ escapeString(name) ++ "')")
|
||||
// Get last inserted ID
|
||||
let row = Sql.queryOne(db, "SELECT last_insert_rowid() as id")
|
||||
match row {
|
||||
Some(r) => r.id,
|
||||
None => -1
|
||||
}
|
||||
}
|
||||
|
||||
fn findUserById(db: SqlConn, id: Int): Option<User> with Sql = {
|
||||
let row = Sql.queryOne(db, "SELECT * FROM users WHERE id = " ++ toString(id))
|
||||
match row {
|
||||
Some(r) => Some({ id: r.id, name: r.name, email: r.email }),
|
||||
None => None
|
||||
}
|
||||
}
|
||||
|
||||
fn findAllUsers(db: SqlConn): List<User> with Sql = {
|
||||
let rows = Sql.query(db, "SELECT * FROM users ORDER BY name")
|
||||
List.map(rows, fn(r) => { id: r.id, name: r.name, email: r.email })
|
||||
}
|
||||
```
|
||||
|
||||
### Testing with In-Memory Database
|
||||
|
||||
```lux
|
||||
fn testUserRepository(): Unit with {Test, Sql} = {
|
||||
// Each test gets a fresh database
|
||||
let db = Sql.openMemory()
|
||||
|
||||
// Set up schema
|
||||
Sql.execute(db, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
|
||||
|
||||
// Test
|
||||
let id = createUser(db, "Test User")
|
||||
Test.assertTrue(id > 0, "Should return valid ID")
|
||||
|
||||
let user = findUserById(db, id)
|
||||
Test.assertTrue(Option.isSome(user), "Should find created user")
|
||||
|
||||
Sql.close(db)
|
||||
}
|
||||
```
|
||||
|
||||
### Handler for Testing (Mock Database)
|
||||
|
||||
The effect system lets you swap database implementations:
|
||||
|
||||
```lux
|
||||
// Production handler uses real SQLite
|
||||
handler realDatabase(): Sql {
|
||||
// Uses actual rusqlite implementation
|
||||
}
|
||||
|
||||
// Test handler uses in-memory storage
|
||||
handler mockDatabase(): Sql {
|
||||
let storage = ref []
|
||||
|
||||
fn execute(conn, sql) = {
|
||||
// Parse and simulate SQL
|
||||
}
|
||||
|
||||
fn query(conn, sql) = {
|
||||
// Return mock data
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
// Run with mock database in tests
|
||||
run userService() with {
|
||||
Sql -> mockDatabase()
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Types
|
||||
|
||||
```lux
|
||||
type SqlConn // Opaque connection handle
|
||||
type SqlRow // Row result with named column access
|
||||
```
|
||||
|
||||
### Operations
|
||||
|
||||
| Function | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `Sql.open(path)` | `String -> SqlConn` | Open database file |
|
||||
| `Sql.openMemory()` | `() -> SqlConn` | Open in-memory database |
|
||||
| `Sql.close(conn)` | `SqlConn -> Unit` | Close connection |
|
||||
| `Sql.execute(conn, sql)` | `(SqlConn, String) -> Int` | Execute statement, return affected rows |
|
||||
| `Sql.query(conn, sql)` | `(SqlConn, String) -> List<SqlRow>` | Query, return all rows |
|
||||
| `Sql.queryOne(conn, sql)` | `(SqlConn, String) -> Option<SqlRow>` | Query, return first row |
|
||||
| `Sql.beginTx(conn)` | `SqlConn -> Unit` | Begin transaction |
|
||||
| `Sql.commit(conn)` | `SqlConn -> Unit` | Commit transaction |
|
||||
| `Sql.rollback(conn)` | `SqlConn -> Unit` | Rollback transaction |
|
||||
|
||||
## Error Handling
|
||||
|
||||
Database operations can fail. In the current implementation, errors cause runtime failures. Future versions will support returning `Result` types:
|
||||
|
||||
```lux
|
||||
// Future API (not yet implemented)
|
||||
fn safeQuery(db: SqlConn, sql: String): Result<List<SqlRow>, SqlError> with Sql = {
|
||||
Sql.tryQuery(db, sql)
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Batch inserts in transactions** - Much faster than individual inserts:
|
||||
```lux
|
||||
Sql.beginTx(db)
|
||||
for item in items {
|
||||
Sql.execute(db, "INSERT INTO data (value) VALUES (" ++ toString(item) ++ ")")
|
||||
}
|
||||
Sql.commit(db)
|
||||
```
|
||||
|
||||
2. **Use `queryOne` for single results** - More efficient than `query` when you only need one row
|
||||
|
||||
3. **Create indexes** for frequently queried columns:
|
||||
```lux
|
||||
Sql.execute(db, "CREATE INDEX idx_users_email ON users(email)")
|
||||
```
|
||||
|
||||
4. **Close connections** when done to free resources
|
||||
|
||||
## PostgreSQL Support
|
||||
|
||||
Lux also provides native PostgreSQL support through the `Postgres` effect. This is ideal for production applications requiring a full-featured relational database.
|
||||
|
||||
### Connecting to PostgreSQL
|
||||
|
||||
```lux
|
||||
fn main(): Unit with {Console, Postgres} = {
|
||||
// Connect using a connection string
|
||||
let connStr = "host=localhost user=myuser password=mypass dbname=mydb"
|
||||
let conn = Postgres.connect(connStr)
|
||||
|
||||
Console.print("Connected to PostgreSQL!")
|
||||
|
||||
// ... use the connection ...
|
||||
|
||||
Postgres.close(conn)
|
||||
}
|
||||
```
|
||||
|
||||
### PostgreSQL Operations
|
||||
|
||||
The PostgreSQL API mirrors the SQLite API:
|
||||
|
||||
```lux
|
||||
// Execute non-returning queries
|
||||
Postgres.execute(conn, "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)")
|
||||
Postgres.execute(conn, "INSERT INTO users (name) VALUES ('Alice')")
|
||||
|
||||
// Query multiple rows
|
||||
let users = Postgres.query(conn, "SELECT * FROM users")
|
||||
for user in users {
|
||||
Console.print("User: " ++ user.name)
|
||||
}
|
||||
|
||||
// Query single row
|
||||
match Postgres.queryOne(conn, "SELECT * FROM users WHERE id = 1") {
|
||||
Some(user) => Console.print("Found: " ++ user.name),
|
||||
None => Console.print("Not found")
|
||||
}
|
||||
```
|
||||
|
||||
### PostgreSQL Transactions
|
||||
|
||||
```lux
|
||||
// Start transaction
|
||||
Postgres.beginTx(conn)
|
||||
|
||||
// Make changes
|
||||
Postgres.execute(conn, "UPDATE accounts SET balance = balance - 100 WHERE id = 1")
|
||||
Postgres.execute(conn, "UPDATE accounts SET balance = balance + 100 WHERE id = 2")
|
||||
|
||||
// Commit or rollback
|
||||
Postgres.commit(conn)
|
||||
// Or: Postgres.rollback(conn)
|
||||
```
|
||||
|
||||
### PostgreSQL API Reference
|
||||
|
||||
| Function | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `Postgres.connect(connStr)` | `String -> Int` | Connect to PostgreSQL, returns connection ID |
|
||||
| `Postgres.close(conn)` | `Int -> Unit` | Close connection |
|
||||
| `Postgres.execute(conn, sql)` | `(Int, String) -> Int` | Execute statement, return affected rows |
|
||||
| `Postgres.query(conn, sql)` | `(Int, String) -> List<Row>` | Query, return all rows |
|
||||
| `Postgres.queryOne(conn, sql)` | `(Int, String) -> Option<Row>` | Query, return first row |
|
||||
| `Postgres.beginTx(conn)` | `Int -> Unit` | Begin transaction |
|
||||
| `Postgres.commit(conn)` | `Int -> Unit` | Commit transaction |
|
||||
| `Postgres.rollback(conn)` | `Int -> Unit` | Rollback transaction |
|
||||
|
||||
### When to Use SQLite vs PostgreSQL
|
||||
|
||||
| Feature | SQLite | PostgreSQL |
|
||||
|---------|--------|------------|
|
||||
| Setup | No setup needed | Requires server |
|
||||
| Deployment | Single file | Server process |
|
||||
| Concurrency | Limited | Excellent |
|
||||
| Scale | Small-medium | Large |
|
||||
| Features | Basic | Full RDBMS |
|
||||
| Use case | Embedded, testing | Production |
|
||||
|
||||
## Limitations
|
||||
|
||||
- No connection pooling yet
|
||||
- No prepared statements / parameterized queries yet
|
||||
- Limited type mapping (basic Int, String, Float)
|
||||
|
||||
## See Also
|
||||
|
||||
- [Effects Guide](./05-effects.md) - Understanding the effect system
|
||||
- [Testing Guide](../testing.md) - Writing tests with mock handlers
|
||||
- [Roadmap](../ROADMAP.md) - Planned database features
|
||||
449
docs/guide/12-behavioral-types.md
Normal file
449
docs/guide/12-behavioral-types.md
Normal file
@@ -0,0 +1,449 @@
|
||||
# Chapter 12: Behavioral Types
|
||||
|
||||
Lux's behavioral types let you make **compile-time guarantees** about function behavior. Unlike comments or documentation, these are actually verified by the compiler.
|
||||
|
||||
## Why Behavioral Types Matter
|
||||
|
||||
Consider these real-world scenarios:
|
||||
|
||||
1. **Payment processing**: You retry a failed charge. If the function isn't idempotent, you might charge the customer twice.
|
||||
|
||||
2. **Caching**: You cache a computation. If the function isn't deterministic, you'll serve stale/wrong results.
|
||||
|
||||
3. **Parallelization**: You run tasks in parallel. If they aren't pure, you'll have race conditions.
|
||||
|
||||
4. **Infinite loops**: A function never returns. If it was supposed to be total, you have a bug.
|
||||
|
||||
**Behavioral types catch these bugs at compile time.**
|
||||
|
||||
## The Five Properties
|
||||
|
||||
### 1. Pure (`is pure`)
|
||||
|
||||
A pure function has **no side effects**. It only depends on its inputs.
|
||||
|
||||
```lux
|
||||
// GOOD: No effects, just computation
|
||||
fn add(a: Int, b: Int): Int is pure = a + b
|
||||
|
||||
fn double(x: Int): Int is pure = x * 2
|
||||
|
||||
fn greet(name: String): String is pure = "Hello, " + name
|
||||
|
||||
// ERROR: Pure function cannot have effects
|
||||
fn impure(x: Int): Int is pure with {Console} =
|
||||
Console.print("x = " + toString(x)) // Compiler error!
|
||||
x
|
||||
```
|
||||
|
||||
**What the compiler checks:**
|
||||
- Function must have an empty effect set
|
||||
- No calls to effectful operations
|
||||
|
||||
**When to use `is pure`:**
|
||||
- Mathematical functions
|
||||
- Data transformations
|
||||
- Any function that should be cacheable
|
||||
|
||||
**Compiler optimizations enabled:**
|
||||
- Memoization (cache results)
|
||||
- Common subexpression elimination
|
||||
- Parallel execution
|
||||
- Dead code elimination (if result unused)
|
||||
|
||||
### 2. Total (`is total`)
|
||||
|
||||
A total function **always terminates** and **never fails**. It produces a value for every valid input.
|
||||
|
||||
```lux
|
||||
// GOOD: Always terminates (structural recursion)
|
||||
fn factorial(n: Int): Int is total =
|
||||
if n <= 1 then 1 else n * factorial(n - 1)
|
||||
|
||||
// GOOD: Non-recursive is always total
|
||||
fn max(a: Int, b: Int): Int is total =
|
||||
if a > b then a else b
|
||||
|
||||
// GOOD: List operations that terminate
|
||||
fn length<T>(list: List<T>): Int is total =
|
||||
match list {
|
||||
[] => 0,
|
||||
[_, ...rest] => 1 + length(rest) // Structurally decreasing
|
||||
}
|
||||
|
||||
// ERROR: Uses Fail effect
|
||||
fn divide(a: Int, b: Int): Int is total with {Fail} =
|
||||
if b == 0 then Fail.fail("division by zero") // Compiler error!
|
||||
else a / b
|
||||
|
||||
// ERROR: May not terminate (not structurally decreasing)
|
||||
fn collatz(n: Int): Int is total =
|
||||
if n == 1 then 1
|
||||
else if n % 2 == 0 then collatz(n / 2)
|
||||
else collatz(3 * n + 1) // Not structurally smaller!
|
||||
```
|
||||
|
||||
**What the compiler checks:**
|
||||
- No `Fail` effect used
|
||||
- Recursive calls must have at least one structurally decreasing argument
|
||||
|
||||
**When to use `is total`:**
|
||||
- Core business logic that must never crash
|
||||
- Mathematical functions
|
||||
- Data structure operations
|
||||
|
||||
**Compiler optimizations enabled:**
|
||||
- No exception handling overhead
|
||||
- Aggressive inlining
|
||||
- Removal of termination checks
|
||||
|
||||
### 3. Deterministic (`is deterministic`)
|
||||
|
||||
A deterministic function produces the **same output for the same input**, every time.
|
||||
|
||||
```lux
|
||||
// GOOD: Same input = same output
|
||||
fn hash(s: String): Int is deterministic =
|
||||
List.fold(String.chars(s), 0, fn(acc: Int, c: String): Int => acc * 31 + charCode(c))
|
||||
|
||||
fn formatDate(year: Int, month: Int, day: Int): String is deterministic =
|
||||
toString(year) + "-" + padZero(month) + "-" + padZero(day)
|
||||
|
||||
// ERROR: Random is non-deterministic
|
||||
fn generateId(): String is deterministic with {Random} =
|
||||
"id-" + toString(Random.int(0, 1000000)) // Compiler error!
|
||||
|
||||
// ERROR: Time is non-deterministic
|
||||
fn timestamp(): Int is deterministic with {Time} =
|
||||
Time.now() // Compiler error!
|
||||
```
|
||||
|
||||
**What the compiler checks:**
|
||||
- No `Random` effect
|
||||
- No `Time` effect
|
||||
|
||||
**When to use `is deterministic`:**
|
||||
- Hashing functions
|
||||
- Serialization/formatting
|
||||
- Test helpers
|
||||
|
||||
**Compiler optimizations enabled:**
|
||||
- Result caching
|
||||
- Parallel execution with consistent results
|
||||
- Test reproducibility
|
||||
|
||||
### 4. Idempotent (`is idempotent`)
|
||||
|
||||
An idempotent function satisfies: `f(f(x)) == f(x)`. Applying it multiple times has the same effect as applying it once.
|
||||
|
||||
```lux
|
||||
// GOOD: Pattern 1 - Constants
|
||||
fn alwaysZero(x: Int): Int is idempotent = 0
|
||||
|
||||
// GOOD: Pattern 2 - Identity
|
||||
fn identity<T>(x: T): T is idempotent = x
|
||||
|
||||
// GOOD: Pattern 3 - Projection
|
||||
fn getName(person: Person): String is idempotent = person.name
|
||||
|
||||
// GOOD: Pattern 4 - Clamping
|
||||
fn clampPositive(x: Int): Int is idempotent =
|
||||
if x < 0 then 0 else x
|
||||
|
||||
// GOOD: Pattern 5 - Absolute value
|
||||
fn abs(x: Int): Int is idempotent =
|
||||
if x < 0 then 0 - x else x
|
||||
|
||||
// ERROR: Not idempotent (increment changes value each time)
|
||||
fn increment(x: Int): Int is idempotent = x + 1 // f(f(1)) = 3, not 2
|
||||
|
||||
// If you're certain a function is idempotent but the compiler can't verify:
|
||||
fn normalize(s: String): String assume is idempotent =
|
||||
String.toLower(String.trim(s))
|
||||
```
|
||||
|
||||
**What the compiler checks:**
|
||||
- Pattern recognition: constants, identity, projections, clamping, abs
|
||||
|
||||
**When to use `is idempotent`:**
|
||||
- Setting configuration
|
||||
- Database upserts
|
||||
- API PUT/DELETE operations (REST semantics)
|
||||
- Retry-safe operations
|
||||
|
||||
**Real-world example - safe retries:**
|
||||
|
||||
```lux
|
||||
// Payment processing with safe retries
|
||||
fn chargeCard(amount: Int, cardId: String): Receipt
|
||||
is idempotent
|
||||
with {Payment, Logger} = {
|
||||
Logger.log("Charging card " + cardId)
|
||||
Payment.charge(amount, cardId)
|
||||
}
|
||||
|
||||
// Safe to retry because chargeCard is idempotent
|
||||
fn processWithRetry(amount: Int, cardId: String): Receipt with {Payment, Logger, Fail} = {
|
||||
let result = retry(3, fn(): Receipt => chargeCard(amount, cardId))
|
||||
match result {
|
||||
Ok(receipt) => receipt,
|
||||
Err(e) => Fail.fail("Payment failed after 3 attempts: " + e)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Commutative (`is commutative`)
|
||||
|
||||
A commutative function satisfies: `f(a, b) == f(b, a)`. The order of arguments doesn't matter.
|
||||
|
||||
```lux
|
||||
// GOOD: Addition is commutative
|
||||
fn add(a: Int, b: Int): Int is commutative = a + b
|
||||
|
||||
// GOOD: Multiplication is commutative
|
||||
fn multiply(a: Int, b: Int): Int is commutative = a * b
|
||||
|
||||
// GOOD: Min/max are commutative
|
||||
fn minimum(a: Int, b: Int): Int is commutative =
|
||||
if a < b then a else b
|
||||
|
||||
// ERROR: Subtraction is not commutative (3 - 2 != 2 - 3)
|
||||
fn subtract(a: Int, b: Int): Int is commutative = a - b // Compiler error!
|
||||
|
||||
// ERROR: Wrong number of parameters
|
||||
fn triple(a: Int, b: Int, c: Int): Int is commutative = a + b + c // Must have exactly 2
|
||||
```
|
||||
|
||||
**What the compiler checks:**
|
||||
- Must have exactly 2 parameters
|
||||
- Body must be a commutative operation (+, *, min, max, ==, !=, &&, ||)
|
||||
|
||||
**When to use `is commutative`:**
|
||||
- Mathematical operations
|
||||
- Set operations (union, intersection)
|
||||
- Merging/combining functions
|
||||
|
||||
**Compiler optimizations enabled:**
|
||||
- Argument reordering for efficiency
|
||||
- Parallel reduction
|
||||
- Algebraic simplifications
|
||||
|
||||
## Combining Properties
|
||||
|
||||
Properties can be combined for stronger guarantees:
|
||||
|
||||
```lux
|
||||
// Pure + deterministic + total = perfect for caching
|
||||
fn computeHash(data: String): Int
|
||||
is pure
|
||||
is deterministic
|
||||
is total = {
|
||||
List.fold(String.chars(data), 0, fn(acc: Int, c: String): Int =>
|
||||
acc * 31 + charCode(c)
|
||||
)
|
||||
}
|
||||
|
||||
// Pure + idempotent = safe transformation
|
||||
fn normalizeEmail(email: String): String
|
||||
is pure
|
||||
is idempotent = {
|
||||
String.toLower(String.trim(email))
|
||||
}
|
||||
|
||||
// Commutative + pure = parallel reduction friendly
|
||||
fn merge(a: Record, b: Record): Record
|
||||
is pure
|
||||
is commutative = {
|
||||
{ ...a, ...b } // Last wins, but both contribute
|
||||
}
|
||||
```
|
||||
|
||||
## Property Constraints in Where Clauses
|
||||
|
||||
You can require function arguments to have certain properties:
|
||||
|
||||
```lux
|
||||
// Higher-order function that requires a pure function
|
||||
fn map<T, U>(list: List<T>, f: fn(T): U is pure): List<U> is pure =
|
||||
match list {
|
||||
[] => [],
|
||||
[x, ...rest] => [f(x), ...map(rest, f)]
|
||||
}
|
||||
|
||||
// Only accepts idempotent functions - safe to retry
|
||||
fn retry<T>(times: Int, action: fn(): T is idempotent): Result<T, String> = {
|
||||
if times <= 0 then Err("No attempts left")
|
||||
else {
|
||||
match tryCall(action) {
|
||||
Ok(result) => Ok(result),
|
||||
Err(e) => retry(times - 1, action) // Safe because action is idempotent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only accepts deterministic functions - safe to cache
|
||||
fn memoize<K, V>(f: fn(K): V is deterministic): fn(K): V = {
|
||||
let cache = HashMap.new()
|
||||
fn(key: K): V => {
|
||||
match cache.get(key) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
let v = f(key)
|
||||
cache.set(key, v)
|
||||
v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
let cachedHash = memoize(computeHash) // OK: computeHash is deterministic
|
||||
let badCache = memoize(generateRandom) // ERROR: generateRandom is not deterministic
|
||||
```
|
||||
|
||||
## The `assume` Escape Hatch
|
||||
|
||||
Sometimes you know a function has a property but the compiler can't verify it. Use `assume`:
|
||||
|
||||
```lux
|
||||
// Compiler can't verify this is idempotent, but we know it is
|
||||
fn setUserStatus(userId: String, status: String): Unit
|
||||
assume is idempotent
|
||||
with {Database} = {
|
||||
Database.execute("UPDATE users SET status = ? WHERE id = ?", [status, userId])
|
||||
}
|
||||
|
||||
// Use assume sparingly - it bypasses compiler checks!
|
||||
// If you're wrong, you may have subtle bugs.
|
||||
```
|
||||
|
||||
**Warning**: `assume` tells the compiler to trust you. If you're wrong, the optimization or guarantee may be invalid.
|
||||
|
||||
## Compiler Optimizations
|
||||
|
||||
When the compiler knows behavioral properties, it can optimize aggressively:
|
||||
|
||||
| Property | Optimizations |
|
||||
|----------|---------------|
|
||||
| `is pure` | Memoization, CSE, dead code elimination, parallelization |
|
||||
| `is total` | No exception handling, aggressive inlining |
|
||||
| `is deterministic` | Result caching, parallel execution |
|
||||
| `is idempotent` | Retry optimization, duplicate call elimination |
|
||||
| `is commutative` | Argument reordering, parallel reduction |
|
||||
|
||||
### Example: Automatic Memoization
|
||||
|
||||
```lux
|
||||
fn expensiveComputation(n: Int): Int
|
||||
is pure
|
||||
is deterministic
|
||||
is total = {
|
||||
// Complex calculation...
|
||||
fib(n)
|
||||
}
|
||||
|
||||
// The compiler may automatically cache results because:
|
||||
// - pure: no side effects, so caching is safe
|
||||
// - deterministic: same input = same output
|
||||
// - total: will always return a value
|
||||
```
|
||||
|
||||
### Example: Safe Parallelization
|
||||
|
||||
```lux
|
||||
fn processItems(items: List<Item>): List<Result>
|
||||
is pure = {
|
||||
List.map(items, processItem)
|
||||
}
|
||||
|
||||
// If processItem is pure, the compiler can parallelize this automatically
|
||||
```
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Example 1: Financial Calculations
|
||||
|
||||
```lux
|
||||
// Interest calculation - pure, deterministic, total
|
||||
fn calculateInterest(principal: Int, rate: Float, years: Int): Float
|
||||
is pure
|
||||
is deterministic
|
||||
is total = {
|
||||
let r = rate / 100.0
|
||||
Float.fromInt(principal) * Math.pow(1.0 + r, Float.fromInt(years))
|
||||
}
|
||||
|
||||
// Transaction validation - pure, total
|
||||
fn validateTransaction(tx: Transaction): Result<Transaction, String>
|
||||
is pure
|
||||
is total = {
|
||||
if tx.amount <= 0 then Err("Amount must be positive")
|
||||
else if tx.from == tx.to then Err("Cannot transfer to self")
|
||||
else Ok(tx)
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Data Processing Pipeline
|
||||
|
||||
```lux
|
||||
// Each step is pure and deterministic
|
||||
fn cleanData(raw: String): String is pure is deterministic =
|
||||
raw |> String.trim |> String.toLower
|
||||
|
||||
fn parseRecord(line: String): Result<Record, String> is pure is deterministic =
|
||||
match String.split(line, ",") {
|
||||
[name, age, email] => Ok({ name, age: parseInt(age), email }),
|
||||
_ => Err("Invalid format")
|
||||
}
|
||||
|
||||
fn validateRecord(record: Record): Bool is pure is deterministic is total =
|
||||
String.length(record.name) > 0 && record.age > 0
|
||||
|
||||
// Pipeline can be parallelized because all functions are pure + deterministic
|
||||
fn processFile(contents: String): List<Record> is pure is deterministic = {
|
||||
contents
|
||||
|> String.lines
|
||||
|> List.map(cleanData)
|
||||
|> List.map(parseRecord)
|
||||
|> List.filterMap(fn(r: Result<Record, String>): Option<Record> =>
|
||||
match r { Ok(v) => Some(v), Err(_) => None })
|
||||
|> List.filter(validateRecord)
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Idempotent API Handlers
|
||||
|
||||
```lux
|
||||
// PUT /users/:id - idempotent by REST semantics
|
||||
fn handlePutUser(id: String, data: UserData): Response
|
||||
is idempotent
|
||||
with {Database, Logger} = {
|
||||
Logger.log("PUT /users/" + id)
|
||||
Database.upsert("users", id, data)
|
||||
Response.ok({ id, ...data })
|
||||
}
|
||||
|
||||
// DELETE /users/:id - idempotent by REST semantics
|
||||
fn handleDeleteUser(id: String): Response
|
||||
is idempotent
|
||||
with {Database, Logger} = {
|
||||
Logger.log("DELETE /users/" + id)
|
||||
Database.delete("users", id) // Safe to call multiple times
|
||||
Response.noContent()
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
| Property | Meaning | Compiler Checks | Use Case |
|
||||
|----------|---------|-----------------|----------|
|
||||
| `is pure` | No effects | Empty effect set | Caching, parallelization |
|
||||
| `is total` | Always terminates | No Fail, structural recursion | Core logic |
|
||||
| `is deterministic` | Same in = same out | No Random/Time | Caching, testing |
|
||||
| `is idempotent` | f(f(x)) = f(x) | Pattern recognition | Retries, APIs |
|
||||
| `is commutative` | f(a,b) = f(b,a) | 2 params, commutative op | Math, merging |
|
||||
|
||||
## What's Next?
|
||||
|
||||
- [Chapter 13: Schema Evolution](./13-schema-evolution.md) - Version your data types
|
||||
- [Tutorials](../tutorials/README.md) - Practical projects
|
||||
573
docs/guide/13-schema-evolution.md
Normal file
573
docs/guide/13-schema-evolution.md
Normal file
@@ -0,0 +1,573 @@
|
||||
# Chapter 13: Schema Evolution
|
||||
|
||||
Data structures change over time. Fields get added, removed, or renamed. Types get split or merged. Without careful handling, these changes break systems—old data can't be read, services fail, migrations corrupt data.
|
||||
|
||||
Lux's **schema evolution** system makes these changes safe and automatic.
|
||||
|
||||
## The Problem
|
||||
|
||||
Consider a real scenario:
|
||||
|
||||
```lux
|
||||
// Version 1: Simple user
|
||||
type User {
|
||||
name: String
|
||||
}
|
||||
|
||||
// Later, you need email addresses
|
||||
type User {
|
||||
name: String,
|
||||
email: String // Breaking change! Old data doesn't have this.
|
||||
}
|
||||
```
|
||||
|
||||
In most languages, this breaks everything. Existing users in your database don't have email addresses. Deserializing old data fails. Services crash.
|
||||
|
||||
Lux solves this with **versioned types** and **automatic migrations**.
|
||||
|
||||
## Versioned Types
|
||||
|
||||
Add a version annotation to any type:
|
||||
|
||||
```lux
|
||||
// Version 1: Original definition
|
||||
type User @v1 {
|
||||
name: String
|
||||
}
|
||||
|
||||
// Version 2: Added email field
|
||||
type User @v2 {
|
||||
name: String,
|
||||
email: String,
|
||||
|
||||
// How to migrate from v1
|
||||
from @v1 = { name: old.name, email: "unknown@example.com" }
|
||||
}
|
||||
|
||||
// Version 3: Split name into first/last
|
||||
type User @v3 {
|
||||
firstName: String,
|
||||
lastName: String,
|
||||
email: String,
|
||||
|
||||
// How to migrate from v2
|
||||
from @v2 = {
|
||||
firstName: String.split(old.name, " ") |> List.head |> Option.getOrElse(""),
|
||||
lastName: String.split(old.name, " ") |> List.tail |> List.head |> Option.getOrElse(""),
|
||||
email: old.email
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `@latest` alias always refers to the most recent version:
|
||||
|
||||
```lux
|
||||
type User @latest {
|
||||
firstName: String,
|
||||
lastName: String,
|
||||
email: String,
|
||||
|
||||
from @v2 = { ... }
|
||||
}
|
||||
|
||||
// These are equivalent:
|
||||
fn createUser(first: String, last: String, email: String): User@latest = ...
|
||||
fn createUser(first: String, last: String, email: String): User@v3 = ...
|
||||
```
|
||||
|
||||
## Migration Syntax
|
||||
|
||||
### Basic Migration
|
||||
|
||||
```lux
|
||||
type Config @v2 {
|
||||
theme: String,
|
||||
fontSize: Int,
|
||||
|
||||
// 'old' refers to the v1 value
|
||||
from @v1 = {
|
||||
theme: old.theme,
|
||||
fontSize: 14 // New field with default
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Computed Fields
|
||||
|
||||
```lux
|
||||
type Order @v2 {
|
||||
items: List<Item>,
|
||||
total: Int,
|
||||
itemCount: Int, // New computed field
|
||||
|
||||
from @v1 = {
|
||||
items: old.items,
|
||||
total: old.total,
|
||||
itemCount: List.length(old.items)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Removing Fields
|
||||
|
||||
When removing fields, simply don't include them in the new version:
|
||||
|
||||
```lux
|
||||
type Settings @v1 {
|
||||
theme: String,
|
||||
legacyMode: Bool, // To be removed
|
||||
volume: Int
|
||||
}
|
||||
|
||||
type Settings @v2 {
|
||||
theme: String,
|
||||
volume: Int,
|
||||
|
||||
// legacyMode is dropped - just don't migrate it
|
||||
from @v1 = {
|
||||
theme: old.theme,
|
||||
volume: old.volume
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Renaming Fields
|
||||
|
||||
```lux
|
||||
type Product @v1 {
|
||||
name: String,
|
||||
cost: Int // Old field name
|
||||
}
|
||||
|
||||
type Product @v2 {
|
||||
name: String,
|
||||
price: Int, // Renamed from 'cost'
|
||||
|
||||
from @v1 = {
|
||||
name: old.name,
|
||||
price: old.cost // Map old field to new name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Complex Transformations
|
||||
|
||||
```lux
|
||||
type Address @v1 {
|
||||
fullAddress: String // "123 Main St, New York, NY 10001"
|
||||
}
|
||||
|
||||
type Address @v2 {
|
||||
street: String,
|
||||
city: String,
|
||||
state: String,
|
||||
zip: String,
|
||||
|
||||
from @v1 = {
|
||||
let parts = String.split(old.fullAddress, ", ")
|
||||
{
|
||||
street: List.get(parts, 0) |> Option.getOrElse(""),
|
||||
city: List.get(parts, 1) |> Option.getOrElse(""),
|
||||
state: List.get(parts, 2)
|
||||
|> Option.map(fn(s: String): String => String.split(s, " ") |> List.head |> Option.getOrElse(""))
|
||||
|> Option.getOrElse(""),
|
||||
zip: List.get(parts, 2)
|
||||
|> Option.map(fn(s: String): String => String.split(s, " ") |> List.last |> Option.getOrElse(""))
|
||||
|> Option.getOrElse("")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Working with Versioned Values
|
||||
|
||||
The `Schema` module provides runtime operations for versioned values:
|
||||
|
||||
### Creating Versioned Values
|
||||
|
||||
```lux
|
||||
// Create a value tagged with a specific version
|
||||
let userV1 = Schema.versioned("User", 1, { name: "Alice" })
|
||||
let userV2 = Schema.versioned("User", 2, { name: "Alice", email: "alice@example.com" })
|
||||
```
|
||||
|
||||
### Checking Versions
|
||||
|
||||
```lux
|
||||
let user = Schema.versioned("User", 1, { name: "Alice" })
|
||||
let version = Schema.getVersion(user) // Returns 1
|
||||
|
||||
// Version-aware logic
|
||||
if version < 2 then
|
||||
Console.print("Legacy user format")
|
||||
else
|
||||
Console.print("Modern user format")
|
||||
```
|
||||
|
||||
### Migrating Values
|
||||
|
||||
```lux
|
||||
// Migrate to a specific version
|
||||
let userV1 = Schema.versioned("User", 1, { name: "Alice" })
|
||||
let userV2 = Schema.migrate(userV1, 2) // Uses declared migration
|
||||
|
||||
let version = Schema.getVersion(userV2) // Now 2
|
||||
|
||||
// Chain migrations (v1 -> v2 -> v3)
|
||||
let userV3 = Schema.migrate(userV1, 3) // Applies v1->v2, then v2->v3
|
||||
```
|
||||
|
||||
## Auto-Generated Migrations
|
||||
|
||||
For simple changes, Lux can **automatically generate** migrations:
|
||||
|
||||
```lux
|
||||
type Profile @v1 {
|
||||
name: String
|
||||
}
|
||||
|
||||
// Adding a field with a default? Migration is auto-generated
|
||||
type Profile @v2 {
|
||||
name: String,
|
||||
bio: String = "" // Default value provided
|
||||
}
|
||||
|
||||
// The compiler generates this for you:
|
||||
// from @v1 = { name: old.name, bio: "" }
|
||||
```
|
||||
|
||||
Auto-migration works for:
|
||||
- Adding fields with default values
|
||||
- Keeping existing fields unchanged
|
||||
|
||||
You must write explicit migrations for:
|
||||
- Field renaming
|
||||
- Field removal (to confirm intent)
|
||||
- Type changes
|
||||
- Computed/derived fields
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Example 1: API Response Versioning
|
||||
|
||||
```lux
|
||||
type ApiResponse @v1 {
|
||||
status: String,
|
||||
data: String
|
||||
}
|
||||
|
||||
type ApiResponse @v2 {
|
||||
status: String,
|
||||
data: String,
|
||||
meta: { timestamp: Int, version: String },
|
||||
|
||||
from @v1 = {
|
||||
status: old.status,
|
||||
data: old.data,
|
||||
meta: { timestamp: 0, version: "legacy" }
|
||||
}
|
||||
}
|
||||
|
||||
// Version-aware API client
|
||||
fn handleResponse(raw: ApiResponse@v1): ApiResponse@v2 = {
|
||||
Schema.migrate(Schema.versioned("ApiResponse", 1, raw), 2)
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Database Record Evolution
|
||||
|
||||
```lux
|
||||
// Original schema
|
||||
type Customer @v1 {
|
||||
name: String,
|
||||
address: String
|
||||
}
|
||||
|
||||
// Split address into components
|
||||
type Customer @v2 {
|
||||
name: String,
|
||||
street: String,
|
||||
city: String,
|
||||
country: String,
|
||||
|
||||
from @v1 = {
|
||||
let parts = String.split(old.address, ", ")
|
||||
{
|
||||
name: old.name,
|
||||
street: List.get(parts, 0) |> Option.getOrElse(old.address),
|
||||
city: List.get(parts, 1) |> Option.getOrElse("Unknown"),
|
||||
country: List.get(parts, 2) |> Option.getOrElse("Unknown")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load and migrate on read
|
||||
fn loadCustomer(id: String): Customer@v2 with {Database} = {
|
||||
let record = Database.query("SELECT * FROM customers WHERE id = ?", [id])
|
||||
let version = record.schema_version // Stored version
|
||||
|
||||
if version == 1 then
|
||||
let v1 = Schema.versioned("Customer", 1, {
|
||||
name: record.name,
|
||||
address: record.address
|
||||
})
|
||||
Schema.migrate(v1, 2)
|
||||
else
|
||||
{ name: record.name, street: record.street, city: record.city, country: record.country }
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Configuration Files
|
||||
|
||||
```lux
|
||||
type AppConfig @v1 {
|
||||
debug: Bool,
|
||||
port: Int
|
||||
}
|
||||
|
||||
type AppConfig @v2 {
|
||||
debug: Bool,
|
||||
port: Int,
|
||||
logLevel: String, // New in v2
|
||||
|
||||
from @v1 = {
|
||||
debug: old.debug,
|
||||
port: old.port,
|
||||
logLevel: if old.debug then "debug" else "info"
|
||||
}
|
||||
}
|
||||
|
||||
type AppConfig @v3 {
|
||||
environment: String, // Replaces debug flag
|
||||
port: Int,
|
||||
logLevel: String,
|
||||
|
||||
from @v2 = {
|
||||
environment: if old.debug then "development" else "production",
|
||||
port: old.port,
|
||||
logLevel: old.logLevel
|
||||
}
|
||||
}
|
||||
|
||||
// Load config with automatic migration
|
||||
fn loadConfig(path: String): AppConfig@v3 with {File} = {
|
||||
let json = File.read(path)
|
||||
let parsed = Json.parse(json)
|
||||
let version = Json.getInt(parsed, "version") |> Option.getOrElse(1)
|
||||
|
||||
match version {
|
||||
1 => {
|
||||
let v1 = Schema.versioned("AppConfig", 1, {
|
||||
debug: Json.getBool(parsed, "debug") |> Option.getOrElse(false),
|
||||
port: Json.getInt(parsed, "port") |> Option.getOrElse(8080)
|
||||
})
|
||||
Schema.migrate(v1, 3)
|
||||
},
|
||||
2 => {
|
||||
let v2 = Schema.versioned("AppConfig", 2, {
|
||||
debug: Json.getBool(parsed, "debug") |> Option.getOrElse(false),
|
||||
port: Json.getInt(parsed, "port") |> Option.getOrElse(8080),
|
||||
logLevel: Json.getString(parsed, "logLevel") |> Option.getOrElse("info")
|
||||
})
|
||||
Schema.migrate(v2, 3)
|
||||
},
|
||||
_ => {
|
||||
// Already v3
|
||||
{
|
||||
environment: Json.getString(parsed, "environment") |> Option.getOrElse("production"),
|
||||
port: Json.getInt(parsed, "port") |> Option.getOrElse(8080),
|
||||
logLevel: Json.getString(parsed, "logLevel") |> Option.getOrElse("info")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 4: Event Sourcing
|
||||
|
||||
```lux
|
||||
// Event types evolve over time
|
||||
type UserCreated @v1 {
|
||||
userId: String,
|
||||
name: String,
|
||||
timestamp: Int
|
||||
}
|
||||
|
||||
type UserCreated @v2 {
|
||||
userId: String,
|
||||
name: String,
|
||||
email: String,
|
||||
createdAt: Int, // Renamed from timestamp
|
||||
|
||||
from @v1 = {
|
||||
userId: old.userId,
|
||||
name: old.name,
|
||||
email: "", // Not captured in v1
|
||||
createdAt: old.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
// Process events regardless of version
|
||||
fn processEvent(event: UserCreated@v1 | UserCreated@v2): Unit with {Console} = {
|
||||
let normalized = Schema.migrate(event, 2) // Always work with v2
|
||||
Console.print("User created: " + normalized.name + " at " + toString(normalized.createdAt))
|
||||
}
|
||||
```
|
||||
|
||||
## Compile-Time Safety
|
||||
|
||||
The compiler catches schema evolution errors:
|
||||
|
||||
```lux
|
||||
type User @v2 {
|
||||
name: String,
|
||||
email: String
|
||||
|
||||
// ERROR: Migration references non-existent field
|
||||
from @v1 = { name: old.username, email: old.email }
|
||||
// ^^^^^^^^ 'username' does not exist in User@v1
|
||||
}
|
||||
```
|
||||
|
||||
```lux
|
||||
type User @v2 {
|
||||
name: String,
|
||||
email: String
|
||||
|
||||
// ERROR: Migration missing required field
|
||||
from @v1 = { name: old.name }
|
||||
// ^ Missing 'email' field
|
||||
}
|
||||
```
|
||||
|
||||
```lux
|
||||
type User @v2 {
|
||||
name: String,
|
||||
age: Int
|
||||
|
||||
// ERROR: Type mismatch in migration
|
||||
from @v1 = { name: old.name, age: old.birthYear }
|
||||
// ^^^^^^^^^^^^^ Expected Int, found String
|
||||
}
|
||||
```
|
||||
|
||||
## Compatibility Checking
|
||||
|
||||
Lux tracks compatibility between versions:
|
||||
|
||||
| Change Type | Backward Compatible | Forward Compatible |
|
||||
|-------------|--------------------|--------------------|
|
||||
| Add optional field (with default) | Yes | Yes |
|
||||
| Add required field | No | Yes (with migration) |
|
||||
| Remove field | Yes (with migration) | No |
|
||||
| Rename field | No | No (need migration) |
|
||||
| Change field type | No | No (need migration) |
|
||||
|
||||
The compiler warns about breaking changes:
|
||||
|
||||
```lux
|
||||
type User @v1 {
|
||||
name: String,
|
||||
email: String
|
||||
}
|
||||
|
||||
type User @v2 {
|
||||
name: String
|
||||
// Warning: Removing 'email' is a breaking change
|
||||
// Existing v2 consumers expect this field
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Version Production Types
|
||||
|
||||
```lux
|
||||
// Good: Versioned from the start
|
||||
type Order @v1 {
|
||||
id: String,
|
||||
items: List<Item>,
|
||||
total: Int
|
||||
}
|
||||
|
||||
// Bad: Unversioned type is hard to evolve
|
||||
type Order {
|
||||
id: String,
|
||||
items: List<Item>,
|
||||
total: Int
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Keep Migrations Simple
|
||||
|
||||
```lux
|
||||
// Good: Simple, direct mapping
|
||||
from @v1 = {
|
||||
name: old.name,
|
||||
email: old.email |> Option.getOrElse("")
|
||||
}
|
||||
|
||||
// Avoid: Complex logic in migrations
|
||||
from @v1 = {
|
||||
name: old.name,
|
||||
email: {
|
||||
// Don't put complex business logic here
|
||||
let domain = inferDomainFromName(old.name)
|
||||
let local = String.toLower(String.replace(old.name, " ", "."))
|
||||
local + "@" + domain
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Test Migrations
|
||||
|
||||
```lux
|
||||
fn testUserMigration(): Unit with {Test} = {
|
||||
let v1User = Schema.versioned("User", 1, { name: "Alice" })
|
||||
let v2User = Schema.migrate(v1User, 2)
|
||||
|
||||
Test.assertEqual(v2User.name, "Alice")
|
||||
Test.assertEqual(v2User.email, "unknown@example.com")
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Document Breaking Changes
|
||||
|
||||
```lux
|
||||
type User @v3 {
|
||||
// BREAKING: 'name' split into firstName/lastName
|
||||
// Migration: name.split(" ")[0] -> firstName, name.split(" ")[1] -> lastName
|
||||
firstName: String,
|
||||
lastName: String,
|
||||
email: String,
|
||||
|
||||
from @v2 = { ... }
|
||||
}
|
||||
```
|
||||
|
||||
## Schema Module Reference
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `Schema.versioned(typeName, version, value)` | Create a versioned value |
|
||||
| `Schema.getVersion(value)` | Get the version of a value |
|
||||
| `Schema.migrate(value, targetVersion)` | Migrate to a target version |
|
||||
| `Schema.isCompatible(v1, v2)` | Check if versions are compatible |
|
||||
|
||||
## Summary
|
||||
|
||||
Schema evolution in Lux provides:
|
||||
|
||||
- **Versioned types** with `@v1`, `@v2`, `@latest` annotations
|
||||
- **Explicit migrations** with `from @vN = { ... }` syntax
|
||||
- **Automatic migrations** for simple field additions with defaults
|
||||
- **Runtime operations** via the `Schema` module
|
||||
- **Compile-time safety** catching migration errors early
|
||||
- **Migration chaining** for multi-step upgrades
|
||||
|
||||
This system ensures your data can evolve safely over time, without breaking existing code or losing information.
|
||||
|
||||
## What's Next?
|
||||
|
||||
- [Tutorials](../tutorials/README.md) - Build real projects
|
||||
- [Standard Library Reference](../stdlib/README.md) - Complete API docs
|
||||
300
docs/guide/14-property-testing.md
Normal file
300
docs/guide/14-property-testing.md
Normal file
@@ -0,0 +1,300 @@
|
||||
# Property-Based Testing
|
||||
|
||||
Property-based testing is a powerful testing technique where you define properties that should hold for all inputs, and the testing framework generates random inputs to verify those properties. This guide shows how to use property-based testing in Lux.
|
||||
|
||||
## Overview
|
||||
|
||||
Instead of writing tests with specific inputs:
|
||||
|
||||
```lux
|
||||
fn test_reverse(): Unit with {Test} = {
|
||||
Test.assertEqual([3, 2, 1], List.reverse([1, 2, 3]))
|
||||
Test.assertEqual([], List.reverse([]))
|
||||
}
|
||||
```
|
||||
|
||||
Property-based testing verifies general properties:
|
||||
|
||||
```lux
|
||||
// Property: Reversing a list twice gives back the original list
|
||||
fn testReverseInvolutive(n: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then true
|
||||
else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.reverse(List.reverse(xs)) == xs then
|
||||
testReverseInvolutive(n - 1)
|
||||
else
|
||||
false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Generators
|
||||
|
||||
Generators create random test data. The `Random` effect provides the building blocks.
|
||||
|
||||
### Basic Generators
|
||||
|
||||
```lux
|
||||
// Generate random integer in range
|
||||
fn genInt(min: Int, max: Int): Int with {Random} =
|
||||
Random.int(min, max)
|
||||
|
||||
// Generate random boolean
|
||||
fn genBool(): Bool with {Random} =
|
||||
Random.bool()
|
||||
|
||||
// Generate random float
|
||||
fn genFloat(): Float with {Random} =
|
||||
Random.float()
|
||||
```
|
||||
|
||||
### String Generators
|
||||
|
||||
```lux
|
||||
let CHARS = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
// Generate random character
|
||||
fn genChar(): String with {Random} = {
|
||||
let idx = Random.int(0, 25)
|
||||
String.substring(CHARS, idx, idx + 1)
|
||||
}
|
||||
|
||||
// Generate random string up to maxLen characters
|
||||
fn genString(maxLen: Int): String with {Random} = {
|
||||
let len = Random.int(0, maxLen)
|
||||
genStringHelper(len)
|
||||
}
|
||||
|
||||
fn genStringHelper(len: Int): String with {Random} = {
|
||||
if len <= 0 then ""
|
||||
else genChar() + genStringHelper(len - 1)
|
||||
}
|
||||
```
|
||||
|
||||
### List Generators
|
||||
|
||||
```lux
|
||||
// Generate random list of integers
|
||||
fn genIntList(min: Int, max: Int, maxLen: Int): List<Int> with {Random} = {
|
||||
let len = Random.int(0, maxLen)
|
||||
genIntListHelper(min, max, len)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
```
|
||||
|
||||
## Writing Property Tests
|
||||
|
||||
### Pattern: Recursive Test Function
|
||||
|
||||
The most common pattern is a recursive function that runs N iterations:
|
||||
|
||||
```lux
|
||||
fn testProperty(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
Console.print(" PASS property_name (" + toString(count) + " tests)")
|
||||
true
|
||||
} else {
|
||||
// Generate random inputs
|
||||
let x = genInt(0, 100)
|
||||
let y = genInt(0, 100)
|
||||
|
||||
// Check property
|
||||
if x + y == y + x then
|
||||
testProperty(n - 1, count)
|
||||
else {
|
||||
Console.print(" FAIL property_name")
|
||||
Console.print(" Counterexample: x=" + toString(x) + ", y=" + toString(y))
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Common Properties
|
||||
|
||||
Here are properties commonly verified in property-based testing:
|
||||
|
||||
**Involution** - Applying a function twice returns the original value:
|
||||
```lux
|
||||
// f(f(x)) == x
|
||||
List.reverse(List.reverse(xs)) == xs
|
||||
```
|
||||
|
||||
**Idempotence** - Applying a function multiple times has the same effect as once:
|
||||
```lux
|
||||
// f(f(x)) == f(x)
|
||||
List.sort(List.sort(xs)) == List.sort(xs)
|
||||
```
|
||||
|
||||
**Commutativity** - Order of arguments doesn't matter:
|
||||
```lux
|
||||
// f(a, b) == f(b, a)
|
||||
a + b == b + a
|
||||
a * b == b * a
|
||||
```
|
||||
|
||||
**Associativity** - Grouping doesn't matter:
|
||||
```lux
|
||||
// f(f(a, b), c) == f(a, f(b, c))
|
||||
(a + b) + c == a + (b + c)
|
||||
(a * b) * c == a * (b * c)
|
||||
```
|
||||
|
||||
**Identity** - An element that doesn't change the result:
|
||||
```lux
|
||||
x + 0 == x
|
||||
x * 1 == x
|
||||
List.concat(xs, []) == xs
|
||||
```
|
||||
|
||||
**Length Preservation**:
|
||||
```lux
|
||||
List.length(List.reverse(xs)) == List.length(xs)
|
||||
List.length(List.map(xs, f)) == List.length(xs)
|
||||
```
|
||||
|
||||
**Length Addition**:
|
||||
```lux
|
||||
List.length(List.concat(xs, ys)) == List.length(xs) + List.length(ys)
|
||||
String.length(s1 + s2) == String.length(s1) + String.length(s2)
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```lux
|
||||
// Property-Based Testing Example
|
||||
// Run with: lux examples/property_testing.lux
|
||||
|
||||
let CHARS = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
// Generators
|
||||
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)
|
||||
genIntListHelper(min, max, len)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
// Test helper
|
||||
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)
|
||||
}
|
||||
|
||||
// Property: reverse(reverse(xs)) == xs
|
||||
fn testReverseInvolutive(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("reverse(reverse(xs)) == xs", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.reverse(List.reverse(xs)) == xs then
|
||||
testReverseInvolutive(n - 1, count)
|
||||
else {
|
||||
printResult("reverse(reverse(xs)) == xs", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Property: addition is commutative
|
||||
fn testAddCommutative(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("a + b == b + a", true, count)
|
||||
true
|
||||
} else {
|
||||
let a = genInt(-1000, 1000)
|
||||
let b = genInt(-1000, 1000)
|
||||
if a + b == b + a then
|
||||
testAddCommutative(n - 1, count)
|
||||
else {
|
||||
printResult("a + b == b + a", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console, Random} = {
|
||||
Console.print("Property-Based Testing Demo")
|
||||
Console.print("")
|
||||
Console.print("Running 100 iterations per property...")
|
||||
Console.print("")
|
||||
|
||||
testReverseInvolutive(100, 100)
|
||||
testAddCommutative(100, 100)
|
||||
|
||||
Console.print("")
|
||||
Console.print("All tests completed!")
|
||||
}
|
||||
|
||||
let result = run main() with {}
|
||||
```
|
||||
|
||||
## Stdlib Testing Module
|
||||
|
||||
The `stdlib/testing.lux` module provides pre-built generators:
|
||||
|
||||
```lux
|
||||
import stdlib.testing
|
||||
|
||||
// Available generators:
|
||||
genInt(min, max) // Random integer in range
|
||||
genIntUpTo(max) // Random integer 0 to max
|
||||
genPositiveInt(max) // Random integer 1 to max
|
||||
genBool() // Random boolean
|
||||
genChar() // Random lowercase letter
|
||||
genAlphaNum() // Random alphanumeric character
|
||||
genString(maxLen) // Random string
|
||||
genStringOfLength(len) // Random string of exact length
|
||||
genIntList(min, max, maxLen) // Random list of integers
|
||||
genBoolList(maxLen) // Random list of booleans
|
||||
genStringList(maxStrLen, maxLen) // Random list of strings
|
||||
|
||||
// Helper functions:
|
||||
shrinkInt(n) // Shrink integer toward zero
|
||||
shrinkList(xs) // Shrink list by removing elements
|
||||
shrinkString(s) // Shrink string by removing characters
|
||||
isSorted(xs) // Check if list is sorted
|
||||
sameElements(xs, ys) // Check if lists have same elements
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Start with many iterations**: Use 100+ iterations per property to catch edge cases.
|
||||
|
||||
2. **Test edge cases explicitly**: Property tests are great for general cases, but also write unit tests for known edge cases.
|
||||
|
||||
3. **Keep properties simple**: Each property should test one thing. Complex properties are harder to debug.
|
||||
|
||||
4. **Use good generators**: Match the distribution of your generators to realistic inputs.
|
||||
|
||||
5. **Print counterexamples**: When a test fails, print the failing inputs to help debugging.
|
||||
|
||||
6. **Combine with shrinking**: Shrinking finds minimal failing inputs, making debugging easier.
|
||||
|
||||
## Limitations
|
||||
|
||||
Current limitations of property-based testing in Lux:
|
||||
|
||||
- No automatic shrinking (must be done manually)
|
||||
- No seed control for reproducible tests
|
||||
- No integration with `lux test` command (uses `Random` effect)
|
||||
|
||||
## See Also
|
||||
|
||||
- [Testing Guide](../testing.md) - Unit testing with the Test effect
|
||||
- [Effects Guide](./05-effects.md) - Understanding the Random effect
|
||||
- [Example](../../examples/property_testing.lux) - Complete working example
|
||||
@@ -1,36 +1,19 @@
|
||||
// Demonstrating behavioral properties in Lux
|
||||
// Behavioral properties are compile-time guarantees about function behavior
|
||||
//
|
||||
// Expected output:
|
||||
// add(5, 3) = 8
|
||||
// factorial(5) = 120
|
||||
// multiply(7, 6) = 42
|
||||
// abs(-5) = 5
|
||||
fn add(a: Int, b: Int): Int is pure = a + b
|
||||
|
||||
// A pure function - no side effects, same input always gives same output
|
||||
fn add(a: Int, b: Int): Int is pure =
|
||||
a + b
|
||||
fn factorial(n: Int): Int is deterministic = if n <= 1 then 1 else n * factorial(n - 1)
|
||||
|
||||
// A deterministic function - same input always gives same output
|
||||
fn factorial(n: Int): Int is deterministic =
|
||||
if n <= 1 then 1
|
||||
else n * factorial(n - 1)
|
||||
fn multiply(a: Int, b: Int): Int is commutative = a * b
|
||||
|
||||
// A commutative function - order of arguments doesn't matter
|
||||
fn multiply(a: Int, b: Int): Int is commutative =
|
||||
a * b
|
||||
fn abs(x: Int): Int is idempotent = if x < 0 then 0 - x else x
|
||||
|
||||
// An idempotent function - absolute value
|
||||
fn abs(x: Int): Int is idempotent =
|
||||
if x < 0 then 0 - x else x
|
||||
|
||||
// Test the functions
|
||||
let sumResult = add(5, 3)
|
||||
|
||||
let factResult = factorial(5)
|
||||
|
||||
let productResult = multiply(7, 6)
|
||||
|
||||
let absResult = abs(0 - 5)
|
||||
|
||||
// Print results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("add(5, 3) = " + toString(sumResult))
|
||||
Console.print("factorial(5) = " + toString(factResult))
|
||||
|
||||
@@ -1,82 +1,42 @@
|
||||
// Behavioral Types Demo
|
||||
// Demonstrates compile-time verification of function properties
|
||||
|
||||
// ============================================================
|
||||
// PART 1: Pure Functions
|
||||
// ============================================================
|
||||
|
||||
// Pure functions have no side effects
|
||||
fn add(a: Int, b: Int): Int is pure = a + b
|
||||
|
||||
fn subtract(a: Int, b: Int): Int is pure = a - b
|
||||
|
||||
// ============================================================
|
||||
// PART 2: Commutative Functions
|
||||
// ============================================================
|
||||
|
||||
// Commutative functions: f(a, b) = f(b, a)
|
||||
fn multiply(a: Int, b: Int): Int is commutative = a * b
|
||||
|
||||
fn sum(a: Int, b: Int): Int is commutative = a + b
|
||||
|
||||
// ============================================================
|
||||
// PART 3: Idempotent Functions
|
||||
// ============================================================
|
||||
|
||||
// Idempotent functions: f(f(x)) = f(x)
|
||||
fn abs(x: Int): Int is idempotent =
|
||||
if x < 0 then 0 - x else x
|
||||
fn abs(x: Int): Int is idempotent = if x < 0 then 0 - x else x
|
||||
|
||||
fn identity(x: Int): Int is idempotent = x
|
||||
|
||||
// ============================================================
|
||||
// PART 4: Deterministic Functions
|
||||
// ============================================================
|
||||
fn factorial(n: Int): Int is deterministic = if n <= 1 then 1 else n * factorial(n - 1)
|
||||
|
||||
// Deterministic functions always produce the same output for the same input
|
||||
fn factorial(n: Int): Int is deterministic =
|
||||
if n <= 1 then 1 else n * factorial(n - 1)
|
||||
fn fib(n: Int): Int is deterministic = if n <= 1 then n else fib(n - 1) + fib(n - 2)
|
||||
|
||||
fn fib(n: Int): Int is deterministic =
|
||||
if n <= 1 then n else fib(n - 1) + fib(n - 2)
|
||||
fn sumTo(n: Int): Int is total = if n <= 0 then 0 else n + sumTo(n - 1)
|
||||
|
||||
// ============================================================
|
||||
// PART 5: Total Functions
|
||||
// ============================================================
|
||||
|
||||
// Total functions are defined for all inputs (no infinite loops, no exceptions)
|
||||
fn sumTo(n: Int): Int is total =
|
||||
if n <= 0 then 0 else n + sumTo(n - 1)
|
||||
|
||||
fn power(base: Int, exp: Int): Int is total =
|
||||
if exp <= 0 then 1 else base * power(base, exp - 1)
|
||||
|
||||
// ============================================================
|
||||
// RESULTS
|
||||
// ============================================================
|
||||
fn power(base: Int, exp: Int): Int is total = if exp <= 0 then 1 else base * power(base, exp - 1)
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Behavioral Types Demo ===")
|
||||
Console.print("")
|
||||
|
||||
Console.print("Part 1: Pure functions")
|
||||
Console.print(" add(5, 3) = " + toString(add(5, 3)))
|
||||
Console.print(" subtract(10, 4) = " + toString(subtract(10, 4)))
|
||||
Console.print("")
|
||||
|
||||
Console.print("Part 2: Commutative functions")
|
||||
Console.print(" multiply(7, 6) = " + toString(multiply(7, 6)))
|
||||
Console.print(" sum(10, 20) = " + toString(sum(10, 20)))
|
||||
Console.print("")
|
||||
|
||||
Console.print("Part 3: Idempotent functions")
|
||||
Console.print(" abs(-42) = " + toString(abs(0 - 42)))
|
||||
Console.print(" identity(100) = " + toString(identity(100)))
|
||||
Console.print("")
|
||||
|
||||
Console.print("Part 4: Deterministic functions")
|
||||
Console.print(" factorial(5) = " + toString(factorial(5)))
|
||||
Console.print(" fib(10) = " + toString(fib(10)))
|
||||
Console.print("")
|
||||
|
||||
Console.print("Part 5: Total functions")
|
||||
Console.print(" sumTo(10) = " + toString(sumTo(10)))
|
||||
Console.print(" power(2, 8) = " + toString(power(2, 8)))
|
||||
|
||||
@@ -1,31 +1,7 @@
|
||||
// Demonstrating built-in effects in Lux
|
||||
//
|
||||
// Lux provides several built-in effects:
|
||||
// - Console: print and read from terminal
|
||||
// - Fail: early termination with error
|
||||
// - State: get/put mutable state (requires runtime initialization)
|
||||
// - Reader: read-only environment access (requires runtime initialization)
|
||||
//
|
||||
// This example demonstrates Console and Fail effects.
|
||||
//
|
||||
// Expected output:
|
||||
// Starting computation...
|
||||
// Step 1: validating input
|
||||
// Step 2: processing
|
||||
// Result: 42
|
||||
// Done!
|
||||
fn safeDivide(a: Int, b: Int): Int with {Fail} = if b == 0 then Fail.fail("Division by zero") else a / b
|
||||
|
||||
// A function that can fail
|
||||
fn safeDivide(a: Int, b: Int): Int with {Fail} =
|
||||
if b == 0 then Fail.fail("Division by zero")
|
||||
else a / b
|
||||
fn validatePositive(n: Int): Int with {Fail} = if n < 0 then Fail.fail("Negative number not allowed") else n
|
||||
|
||||
// A function that validates input
|
||||
fn validatePositive(n: Int): Int with {Fail} =
|
||||
if n < 0 then Fail.fail("Negative number not allowed")
|
||||
else n
|
||||
|
||||
// A computation that uses multiple effects
|
||||
fn compute(input: Int): Int with {Console, Fail} = {
|
||||
Console.print("Starting computation...")
|
||||
Console.print("Step 1: validating input")
|
||||
@@ -36,7 +12,6 @@ fn compute(input: Int): Int with {Console, Fail} = {
|
||||
result
|
||||
}
|
||||
|
||||
// Main function
|
||||
fn main(): Unit with {Console} = {
|
||||
let result = run compute(21) with {}
|
||||
Console.print("Done!")
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
// Counter Example - A simple interactive counter using TEA pattern
|
||||
//
|
||||
// This example demonstrates:
|
||||
// - Model-View-Update architecture (TEA)
|
||||
// - Html DSL for describing UI (inline version)
|
||||
// - Message-based state updates
|
||||
|
||||
// ============================================================================
|
||||
// Html Types (subset of stdlib/html)
|
||||
// ============================================================================
|
||||
|
||||
type Html<M> =
|
||||
| Element(String, List<Attr<M>>, List<Html<M>>)
|
||||
| Text(String)
|
||||
@@ -19,86 +8,56 @@ type Attr<M> =
|
||||
| Id(String)
|
||||
| OnClick(M)
|
||||
|
||||
// Html builder helpers
|
||||
fn div<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("div", attrs, children)
|
||||
fn div<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = Element("div", attrs, children)
|
||||
|
||||
fn span<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("span", attrs, children)
|
||||
fn span<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = Element("span", attrs, children)
|
||||
|
||||
fn h1<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("h1", attrs, children)
|
||||
fn h1<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = Element("h1", attrs, children)
|
||||
|
||||
fn button<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("button", attrs, children)
|
||||
fn button<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = Element("button", attrs, children)
|
||||
|
||||
fn text<M>(content: String): Html<M> =
|
||||
Text(content)
|
||||
fn text<M>(content: String): Html<M> = Text(content)
|
||||
|
||||
fn class<M>(name: String): Attr<M> =
|
||||
Class(name)
|
||||
fn class<M>(name: String): Attr<M> = Class(name)
|
||||
|
||||
fn onClick<M>(msg: M): Attr<M> =
|
||||
OnClick(msg)
|
||||
|
||||
// ============================================================================
|
||||
// Model - The application state (using ADT wrapper)
|
||||
// ============================================================================
|
||||
fn onClick<M>(msg: M): Attr<M> = OnClick(msg)
|
||||
|
||||
type Model =
|
||||
| Counter(Int)
|
||||
|
||||
fn getCount(model: Model): Int =
|
||||
match model {
|
||||
Counter(n) => n
|
||||
}
|
||||
Counter(n) => n,
|
||||
}
|
||||
|
||||
fn init(): Model = Counter(0)
|
||||
|
||||
// ============================================================================
|
||||
// Messages - Events that can occur
|
||||
// ============================================================================
|
||||
|
||||
type Msg =
|
||||
| Increment
|
||||
| Decrement
|
||||
| Reset
|
||||
|
||||
// ============================================================================
|
||||
// Update - State transitions
|
||||
// ============================================================================
|
||||
|
||||
fn update(model: Model, msg: Msg): Model =
|
||||
match msg {
|
||||
Increment => Counter(getCount(model) + 1),
|
||||
Decrement => Counter(getCount(model) - 1),
|
||||
Reset => Counter(0)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// View - Render the UI
|
||||
// ============================================================================
|
||||
Reset => Counter(0),
|
||||
}
|
||||
|
||||
fn viewCounter(count: Int): Html<Msg> = {
|
||||
let countText = text(toString(count))
|
||||
let countSpan = span([class("count")], [countText])
|
||||
let displayDiv = div([class("counter-display")], [countSpan])
|
||||
|
||||
let minusBtn = button([onClick(Decrement), class("btn")], [text("-")])
|
||||
let resetBtn = button([onClick(Reset), class("btn btn-reset")], [text("Reset")])
|
||||
let plusBtn = button([onClick(Increment), class("btn")], [text("+")])
|
||||
let buttonsDiv = div([class("counter-buttons")], [minusBtn, resetBtn, plusBtn])
|
||||
|
||||
let title = h1([], [text("Counter")])
|
||||
div([class("counter-app")], [title, displayDiv, buttonsDiv])
|
||||
}
|
||||
|
||||
fn view(model: Model): Html<Msg> = viewCounter(getCount(model))
|
||||
|
||||
// ============================================================================
|
||||
// Debug: Print Html structure
|
||||
// ============================================================================
|
||||
|
||||
fn showAttr(attr: Attr<Msg>): String =
|
||||
match attr {
|
||||
Class(s) => "class=\"" + s + "\"",
|
||||
@@ -106,27 +65,27 @@ fn showAttr(attr: Attr<Msg>): String =
|
||||
OnClick(msg) => match msg {
|
||||
Increment => "onclick=\"Increment\"",
|
||||
Decrement => "onclick=\"Decrement\"",
|
||||
Reset => "onclick=\"Reset\""
|
||||
}
|
||||
}
|
||||
Reset => "onclick=\"Reset\"",
|
||||
},
|
||||
}
|
||||
|
||||
fn showAttrs(attrs: List<Attr<Msg>>): String =
|
||||
match List.head(attrs) {
|
||||
None => "",
|
||||
Some(a) => match List.tail(attrs) {
|
||||
None => showAttr(a),
|
||||
Some(rest) => showAttr(a) + " " + showAttrs(rest)
|
||||
}
|
||||
}
|
||||
Some(rest) => showAttr(a) + " " + showAttrs(rest),
|
||||
},
|
||||
}
|
||||
|
||||
fn showChildren(children: List<Html<Msg>>, indent: Int): String =
|
||||
match List.head(children) {
|
||||
None => "",
|
||||
Some(c) => match List.tail(children) {
|
||||
None => showHtml(c, indent),
|
||||
Some(rest) => showHtml(c, indent) + showChildren(rest, indent)
|
||||
}
|
||||
}
|
||||
Some(rest) => showHtml(c, indent) + showChildren(rest, indent),
|
||||
},
|
||||
}
|
||||
|
||||
fn showHtml(html: Html<Msg>, indent: Int): String =
|
||||
match html {
|
||||
@@ -137,12 +96,8 @@ fn showHtml(html: Html<Msg>, indent: Int): String =
|
||||
let attrPart = if String.length(attrStr) > 0 then " " + attrStr else ""
|
||||
let childStr = showChildren(children, indent + 2)
|
||||
"<" + tag + attrPart + ">" + childStr + "</" + tag + ">"
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Entry point
|
||||
// ============================================================================
|
||||
},
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
let model = init()
|
||||
@@ -150,24 +105,19 @@ fn main(): Unit with {Console} = {
|
||||
Console.print("")
|
||||
Console.print("Initial count: " + toString(getCount(model)))
|
||||
Console.print("")
|
||||
|
||||
let m1 = update(model, Increment)
|
||||
Console.print("After Increment: " + toString(getCount(m1)))
|
||||
|
||||
let m2 = update(m1, Increment)
|
||||
Console.print("After Increment: " + toString(getCount(m2)))
|
||||
|
||||
let m3 = update(m2, Increment)
|
||||
Console.print("After Increment: " + toString(getCount(m3)))
|
||||
|
||||
let m4 = update(m3, Decrement)
|
||||
Console.print("After Decrement: " + toString(getCount(m4)))
|
||||
|
||||
let m5 = update(m4, Reset)
|
||||
Console.print("After Reset: " + toString(getCount(m5)))
|
||||
|
||||
Console.print("")
|
||||
Console.print("=== View (HTML Structure) ===")
|
||||
Console.print(showHtml(view(m2), 0))
|
||||
}
|
||||
|
||||
let output = run main() with {}
|
||||
|
||||
@@ -1,57 +1,37 @@
|
||||
// Demonstrating algebraic data types and pattern matching
|
||||
//
|
||||
// Expected output:
|
||||
// Tree sum: 8
|
||||
// Tree depth: 3
|
||||
// Safe divide 10/2: Result: 5
|
||||
// Safe divide 10/0: Division by zero!
|
||||
|
||||
// Define a binary tree
|
||||
type Tree =
|
||||
| Leaf(Int)
|
||||
| Node(Tree, Tree)
|
||||
|
||||
// Sum all values in a tree
|
||||
fn sumTree(tree: Tree): Int =
|
||||
match tree {
|
||||
Leaf(n) => n,
|
||||
Node(left, right) => sumTree(left) + sumTree(right)
|
||||
}
|
||||
Node(left, right) => sumTree(left) + sumTree(right),
|
||||
}
|
||||
|
||||
// Find the depth of a tree
|
||||
fn depth(tree: Tree): Int =
|
||||
match tree {
|
||||
Leaf(_) => 1,
|
||||
Node(left, right) => {
|
||||
let leftDepth = depth(left)
|
||||
let rightDepth = depth(right)
|
||||
1 + (if leftDepth > rightDepth then leftDepth else rightDepth)
|
||||
}
|
||||
}
|
||||
|
||||
// Example tree:
|
||||
// Node
|
||||
// / \
|
||||
// Node Leaf(5)
|
||||
// / \
|
||||
// Leaf(1) Leaf(2)
|
||||
1 + if leftDepth > rightDepth then leftDepth else rightDepth
|
||||
},
|
||||
}
|
||||
|
||||
let myTree = Node(Node(Leaf(1), Leaf(2)), Leaf(5))
|
||||
|
||||
let treeSum = sumTree(myTree)
|
||||
|
||||
let treeDepth = depth(myTree)
|
||||
|
||||
// Option type example
|
||||
fn safeDivide(a: Int, b: Int): Option<Int> =
|
||||
if b == 0 then None
|
||||
else Some(a / b)
|
||||
fn safeDivide(a: Int, b: Int): Option<Int> = if b == 0 then None else Some(a / b)
|
||||
|
||||
fn showResult(result: Option<Int>): String =
|
||||
match result {
|
||||
None => "Division by zero!",
|
||||
Some(n) => "Result: " + toString(n)
|
||||
}
|
||||
Some(n) => "Result: " + toString(n),
|
||||
}
|
||||
|
||||
// Print results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("Tree sum: " + toString(treeSum))
|
||||
Console.print("Tree depth: " + toString(treeDepth))
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
// Demonstrating algebraic effects in Lux
|
||||
//
|
||||
// Expected output:
|
||||
// [info] Processing data...
|
||||
// [debug] Result computed
|
||||
// Final result: 42
|
||||
|
||||
// Define a custom logging effect
|
||||
effect Logger {
|
||||
fn log(level: String, msg: String): Unit
|
||||
fn getLevel(): String
|
||||
}
|
||||
|
||||
// A function that uses the Logger effect
|
||||
fn processData(data: Int): Int with {Logger} = {
|
||||
Logger.log("info", "Processing data...")
|
||||
let result = data * 2
|
||||
@@ -19,17 +10,15 @@ fn processData(data: Int): Int with {Logger} = {
|
||||
result
|
||||
}
|
||||
|
||||
// A handler that prints logs to console
|
||||
handler consoleLogger: Logger {
|
||||
fn log(level, msg) = Console.print("[" + level + "] " + msg)
|
||||
fn getLevel() = "debug"
|
||||
}
|
||||
|
||||
// Run and print
|
||||
fn main(): Unit with {Console} = {
|
||||
let result = run processData(21) with {
|
||||
Logger = consoleLogger
|
||||
}
|
||||
Logger = consoleLogger,
|
||||
}
|
||||
Console.print("Final result: " + toString(result))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
// Factorial function demonstrating recursion
|
||||
//
|
||||
// Expected output: 10! = 3628800
|
||||
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
|
||||
|
||||
fn factorial(n: Int): Int =
|
||||
if n <= 1 then 1
|
||||
else n * factorial(n - 1)
|
||||
|
||||
// Calculate factorial of 10
|
||||
let result = factorial(10)
|
||||
|
||||
// Print result using Console effect
|
||||
fn showResult(): Unit with {Console} =
|
||||
Console.print("10! = " + toString(result))
|
||||
fn showResult(): Unit with {Console} = Console.print("10! = " + toString(result))
|
||||
|
||||
let output = run showResult() with {}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
// File I/O example - demonstrates the File effect
|
||||
//
|
||||
// This script reads a file, counts lines/words, and writes a report
|
||||
|
||||
fn countLines(content: String): Int = {
|
||||
let lines = String.split(content, "\n")
|
||||
let lines = String.split(content, "
|
||||
")
|
||||
List.length(lines)
|
||||
}
|
||||
|
||||
@@ -14,35 +11,28 @@ fn countWords(content: String): Int = {
|
||||
|
||||
fn analyzeFile(path: String): Unit with {File, Console} = {
|
||||
Console.print("Analyzing file: " + path)
|
||||
|
||||
if File.exists(path) then {
|
||||
let content = File.read(path)
|
||||
let lines = countLines(content)
|
||||
let words = countWords(content)
|
||||
let chars = String.length(content)
|
||||
|
||||
Console.print(" Lines: " + toString(lines))
|
||||
Console.print(" Words: " + toString(words))
|
||||
Console.print(" Chars: " + toString(chars))
|
||||
} else {
|
||||
} else {
|
||||
Console.print(" Error: File not found!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {File, Console} = {
|
||||
Console.print("=== Lux File Analyzer ===")
|
||||
Console.print("")
|
||||
|
||||
// Analyze this file itself
|
||||
analyzeFile("examples/file_io.lux")
|
||||
Console.print("")
|
||||
|
||||
// Analyze hello.lux
|
||||
analyzeFile("examples/hello.lux")
|
||||
Console.print("")
|
||||
|
||||
// Write a report
|
||||
let report = "File analysis complete.\nAnalyzed 2 files."
|
||||
let report = "File analysis complete.
|
||||
Analyzed 2 files."
|
||||
File.write("/tmp/lux_report.txt", report)
|
||||
Console.print("Report written to /tmp/lux_report.txt")
|
||||
}
|
||||
|
||||
@@ -1,55 +1,39 @@
|
||||
// Demonstrating functional programming features
|
||||
//
|
||||
// Expected output:
|
||||
// apply(double, 21) = 42
|
||||
// compose(addOne, double)(5) = 11
|
||||
// pipe: 5 |> double |> addOne |> square = 121
|
||||
// curried add5(10) = 15
|
||||
// partial times3(7) = 21
|
||||
// record transform = 30
|
||||
|
||||
// Higher-order functions
|
||||
fn apply(f: fn(Int): Int, x: Int): Int = f(x)
|
||||
|
||||
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int =
|
||||
fn(x: Int): Int => f(g(x))
|
||||
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int = fn(x: Int): Int => f(g(x))
|
||||
|
||||
// Basic functions
|
||||
fn double(x: Int): Int = x * 2
|
||||
|
||||
fn addOne(x: Int): Int = x + 1
|
||||
|
||||
fn square(x: Int): Int = x * x
|
||||
|
||||
// Using apply
|
||||
let result1 = apply(double, 21)
|
||||
|
||||
// Using compose
|
||||
let doubleAndAddOne = compose(addOne, double)
|
||||
|
||||
let result2 = doubleAndAddOne(5)
|
||||
|
||||
// Using the pipe operator
|
||||
let result3 = 5 |> double |> addOne |> square
|
||||
let result3 = square(addOne(double(5)))
|
||||
|
||||
// Currying example
|
||||
fn add(a: Int): fn(Int): Int =
|
||||
fn(b: Int): Int => a + b
|
||||
fn add(a: Int): fn(Int): Int = fn(b: Int): Int => a + b
|
||||
|
||||
let add5 = add(5)
|
||||
|
||||
let result4 = add5(10)
|
||||
|
||||
// Partial application simulation
|
||||
fn multiply(a: Int, b: Int): Int = a * b
|
||||
|
||||
let times3 = fn(x: Int): Int => multiply(3, x)
|
||||
|
||||
let result5 = times3(7)
|
||||
|
||||
// Working with records
|
||||
let transform = fn(record: { x: Int, y: Int }): Int =>
|
||||
record.x + record.y
|
||||
let transform = fn(record: { x: Int, y: Int }): Int => record.x + record.y
|
||||
|
||||
let point = { x: 10, y: 20 }
|
||||
|
||||
let recordSum = transform(point)
|
||||
|
||||
// Print all results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("apply(double, 21) = " + toString(result1))
|
||||
Console.print("compose(addOne, double)(5) = " + toString(result2))
|
||||
|
||||
@@ -1,45 +1,34 @@
|
||||
// Demonstrating generic type parameters in Lux
|
||||
//
|
||||
// Expected output:
|
||||
// identity(42) = 42
|
||||
// identity("hello") = hello
|
||||
// first(MkPair(1, "one")) = 1
|
||||
// second(MkPair(1, "one")) = one
|
||||
// map(Some(21), double) = Some(42)
|
||||
|
||||
// Generic identity function
|
||||
fn identity<T>(x: T): T = x
|
||||
|
||||
// Generic pair type
|
||||
type Pair<A, B> =
|
||||
| MkPair(A, B)
|
||||
|
||||
fn first<A, B>(p: Pair<A, B>): A =
|
||||
match p {
|
||||
MkPair(a, _) => a
|
||||
}
|
||||
MkPair(a, _) => a,
|
||||
}
|
||||
|
||||
fn second<A, B>(p: Pair<A, B>): B =
|
||||
match p {
|
||||
MkPair(_, b) => b
|
||||
}
|
||||
MkPair(_, b) => b,
|
||||
}
|
||||
|
||||
// Generic map function for Option
|
||||
fn mapOption<T, U>(opt: Option<T>, f: fn(T): U): Option<U> =
|
||||
match opt {
|
||||
None => None,
|
||||
Some(x) => Some(f(x))
|
||||
}
|
||||
Some(x) => Some(f(x)),
|
||||
}
|
||||
|
||||
// Helper function for testing
|
||||
fn double(x: Int): Int = x * 2
|
||||
|
||||
// Test usage
|
||||
let id_int = identity(42)
|
||||
|
||||
let id_str = identity("hello")
|
||||
|
||||
let pair = MkPair(1, "one")
|
||||
|
||||
let fst = first(pair)
|
||||
|
||||
let snd = second(pair)
|
||||
|
||||
let doubled = mapOption(Some(21), double)
|
||||
@@ -47,8 +36,8 @@ let doubled = mapOption(Some(21), double)
|
||||
fn showOption(opt: Option<Int>): String =
|
||||
match opt {
|
||||
None => "None",
|
||||
Some(x) => "Some(" + toString(x) + ")"
|
||||
}
|
||||
Some(x) => "Some(" + toString(x) + ")",
|
||||
}
|
||||
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("identity(42) = " + toString(id_int))
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
// Demonstrating resumable effect handlers in Lux
|
||||
//
|
||||
// Handlers can use `resume(value)` to return a value to the effect call site
|
||||
// and continue the computation. This enables powerful control flow patterns.
|
||||
//
|
||||
// Expected output:
|
||||
// [INFO] Starting computation
|
||||
// [DEBUG] Intermediate result: 10
|
||||
// [INFO] Computation complete
|
||||
// Final result: 20
|
||||
|
||||
// Define a custom logging effect
|
||||
effect Logger {
|
||||
fn log(level: String, msg: String): Unit
|
||||
fn getLogLevel(): String
|
||||
}
|
||||
|
||||
// A function that uses the Logger effect
|
||||
fn compute(): Int with {Logger} = {
|
||||
Logger.log("INFO", "Starting computation")
|
||||
let x = 10
|
||||
@@ -25,20 +12,19 @@ fn compute(): Int with {Logger} = {
|
||||
result
|
||||
}
|
||||
|
||||
// A handler that prints logs with brackets and resumes with Unit
|
||||
handler prettyLogger: Logger {
|
||||
fn log(level, msg) = {
|
||||
fn log(level, msg) =
|
||||
{
|
||||
Console.print("[" + level + "] " + msg)
|
||||
resume(())
|
||||
}
|
||||
}
|
||||
fn getLogLevel() = resume("DEBUG")
|
||||
}
|
||||
|
||||
// Main function
|
||||
fn main(): Unit with {Console} = {
|
||||
let result = run compute() with {
|
||||
Logger = prettyLogger
|
||||
}
|
||||
Logger = prettyLogger,
|
||||
}
|
||||
Console.print("Final result: " + toString(result))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
// Hello World in Lux
|
||||
// Demonstrates basic effect usage
|
||||
//
|
||||
// Expected output: Hello, World!
|
||||
fn greet(): Unit with {Console} = Console.print("Hello, World!")
|
||||
|
||||
fn greet(): Unit with {Console} =
|
||||
Console.print("Hello, World!")
|
||||
|
||||
// Run the greeting with the Console effect
|
||||
let output = run greet() with {}
|
||||
|
||||
@@ -1,91 +1,72 @@
|
||||
// HTTP example - demonstrates the Http effect
|
||||
//
|
||||
// This script makes HTTP requests and parses JSON responses
|
||||
|
||||
fn main(): Unit with {Console, Http} = {
|
||||
Console.print("=== Lux HTTP Example ===")
|
||||
Console.print("")
|
||||
|
||||
// Make a GET request to a public API
|
||||
Console.print("Fetching data from httpbin.org...")
|
||||
Console.print("")
|
||||
|
||||
match Http.get("https://httpbin.org/get") {
|
||||
Ok(response) => {
|
||||
Console.print("GET request successful!")
|
||||
Console.print(" Status: " + toString(response.status))
|
||||
Console.print(" Body length: " + toString(String.length(response.body)) + " bytes")
|
||||
Console.print("")
|
||||
|
||||
// Parse the JSON response
|
||||
match Json.parse(response.body) {
|
||||
Ok(json) => {
|
||||
Console.print("Parsed JSON response:")
|
||||
match Json.get(json, "origin") {
|
||||
Some(origin) => match Json.asString(origin) {
|
||||
Some(ip) => Console.print(" Your IP: " + ip),
|
||||
None => Console.print(" origin: (not a string)")
|
||||
},
|
||||
None => Console.print(" origin: (not found)")
|
||||
}
|
||||
None => Console.print(" origin: (not a string)"),
|
||||
},
|
||||
None => Console.print(" origin: (not found)"),
|
||||
}
|
||||
match Json.get(json, "url") {
|
||||
Some(url) => match Json.asString(url) {
|
||||
Some(u) => Console.print(" URL: " + u),
|
||||
None => Console.print(" url: (not a string)")
|
||||
},
|
||||
None => Console.print(" url: (not found)")
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("JSON parse error: " + e)
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("GET request failed: " + e)
|
||||
}
|
||||
|
||||
None => Console.print(" url: (not a string)"),
|
||||
},
|
||||
None => Console.print(" url: (not found)"),
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("JSON parse error: " + e),
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("GET request failed: " + e),
|
||||
}
|
||||
Console.print("")
|
||||
Console.print("--- POST Request ---")
|
||||
Console.print("")
|
||||
|
||||
// Make a POST request with JSON body
|
||||
let requestBody = Json.object([("message", Json.string("Hello from Lux!")), ("version", Json.int(1))])
|
||||
Console.print("Sending POST with JSON body...")
|
||||
Console.print(" Body: " + Json.stringify(requestBody))
|
||||
Console.print("")
|
||||
|
||||
match Http.postJson("https://httpbin.org/post", requestBody) {
|
||||
Ok(response) => {
|
||||
Console.print("POST request successful!")
|
||||
Console.print(" Status: " + toString(response.status))
|
||||
|
||||
// Parse and extract what we sent
|
||||
match Json.parse(response.body) {
|
||||
Ok(json) => match Json.get(json, "json") {
|
||||
Some(sentJson) => {
|
||||
Console.print(" Server received:")
|
||||
Console.print(" " + Json.stringify(sentJson))
|
||||
},
|
||||
None => Console.print(" (no json field in response)")
|
||||
},
|
||||
Err(e) => Console.print("JSON parse error: " + e)
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("POST request failed: " + e)
|
||||
}
|
||||
|
||||
},
|
||||
None => Console.print(" (no json field in response)"),
|
||||
},
|
||||
Err(e) => Console.print("JSON parse error: " + e),
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("POST request failed: " + e),
|
||||
}
|
||||
Console.print("")
|
||||
Console.print("--- Headers ---")
|
||||
Console.print("")
|
||||
|
||||
// Show response headers
|
||||
match Http.get("https://httpbin.org/headers") {
|
||||
Ok(response) => {
|
||||
Console.print("Response headers (first 5):")
|
||||
let count = 0
|
||||
// Note: Can't easily iterate with effects in callbacks, so just show count
|
||||
Console.print(" Total headers: " + toString(List.length(response.headers)))
|
||||
},
|
||||
Err(e) => Console.print("Request failed: " + e)
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("Request failed: " + e),
|
||||
}
|
||||
}
|
||||
|
||||
let result = run main() with {}
|
||||
|
||||
121
examples/http_api.lux
Normal file
121
examples/http_api.lux
Normal file
@@ -0,0 +1,121 @@
|
||||
fn httpOk(body: String): { status: Int, body: String } = { status: 200, body: body }
|
||||
|
||||
fn httpCreated(body: String): { status: Int, body: String } = { status: 201, body: body }
|
||||
|
||||
fn httpNotFound(body: String): { status: Int, body: String } = { status: 404, body: body }
|
||||
|
||||
fn httpBadRequest(body: String): { status: Int, body: String } = { status: 400, body: body }
|
||||
|
||||
fn jsonEscape(s: String): String = String.replace(String.replace(s, "\\", "\\\\"), "\"", "\\\"")
|
||||
|
||||
fn jsonStr(key: String, value: String): String = "\"" + jsonEscape(key) + "\":\"" + jsonEscape(value) + "\""
|
||||
|
||||
fn jsonNum(key: String, value: Int): String = "\"" + jsonEscape(key) + "\":" + toString(value)
|
||||
|
||||
fn jsonObj(content: String): String = toString(" + content + ")
|
||||
|
||||
fn jsonArr(content: String): String = "[" + content + "]"
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fn matchParts(pathParts: List<String>, patternParts: List<String>): Bool = {
|
||||
if List.length(pathParts) == 0 then true else {
|
||||
match List.head(pathParts) {
|
||||
None => true,
|
||||
Some(pathPart) => {
|
||||
match List.head(patternParts) {
|
||||
None => true,
|
||||
Some(patternPart) => {
|
||||
let isMatch = if String.startsWith(patternPart, ":") then true else pathPart == patternPart
|
||||
if isMatch then {
|
||||
let restPath = Option.getOrElse(List.tail(pathParts), [])
|
||||
let restPattern = Option.getOrElse(List.tail(patternParts), [])
|
||||
matchParts(restPath, restPattern)
|
||||
} else false
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn getPathSegment(path: String, index: Int): Option<String> = {
|
||||
let parts = String.split(path, "/")
|
||||
List.get(parts, index + 1)
|
||||
}
|
||||
|
||||
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 listUsersHandler(): { status: Int, body: String } = {
|
||||
let user1 = jsonObj(jsonNum("id", 1) + "," + jsonStr("name", "Alice"))
|
||||
let user2 = jsonObj(jsonNum("id", 2) + "," + jsonStr("name", "Bob"))
|
||||
httpOk(jsonArr(user1 + "," + user2))
|
||||
}
|
||||
|
||||
fn getUserHandler(path: String): { status: Int, body: String } = {
|
||||
match getPathSegment(path, 1) {
|
||||
Some(id) => {
|
||||
let body = jsonObj(jsonStr("id", id) + "," + jsonStr("name", "User " + id))
|
||||
httpOk(body)
|
||||
},
|
||||
None => httpNotFound(jsonError("User not found")),
|
||||
}
|
||||
}
|
||||
|
||||
fn createUserHandler(body: String): { status: Int, body: String } = {
|
||||
let newUser = jsonObj(jsonNum("id", 3) + "," + jsonStr("name", "New User"))
|
||||
httpCreated(newUser)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
fn serveLoop(remaining: Int): Unit with {Console, HttpServer} = {
|
||||
if remaining <= 0 then {
|
||||
Console.print("Max requests reached, stopping server.")
|
||||
HttpServer.stop()
|
||||
} else {
|
||||
let req = HttpServer.accept()
|
||||
Console.print(req.method + " " + req.path)
|
||||
let resp = router(req.method, req.path, req.body)
|
||||
HttpServer.respond(resp.status, resp.body)
|
||||
serveLoop(remaining - 1)
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console, HttpServer} = {
|
||||
let port = 8080
|
||||
let maxRequests = 10
|
||||
Console.print("========================================")
|
||||
Console.print(" Lux HTTP API Demo")
|
||||
Console.print("========================================")
|
||||
Console.print("")
|
||||
Console.print("Endpoints:")
|
||||
Console.print(" GET / - API info")
|
||||
Console.print(" GET /health - Health check")
|
||||
Console.print(" GET /users - List users")
|
||||
Console.print(" GET /users/:id - Get user by ID")
|
||||
Console.print(" POST /users - Create user")
|
||||
Console.print("")
|
||||
Console.print("Try:")
|
||||
Console.print(" curl http://localhost:8080/")
|
||||
Console.print(" curl http://localhost:8080/users")
|
||||
Console.print(" curl http://localhost:8080/users/42")
|
||||
Console.print(" curl -X POST http://localhost:8080/users")
|
||||
Console.print("")
|
||||
Console.print("Starting server on port " + toString(port) + "...")
|
||||
HttpServer.listen(port)
|
||||
Console.print("Server listening!")
|
||||
serveLoop(maxRequests)
|
||||
}
|
||||
|
||||
let output = run main() with {}
|
||||
65
examples/http_router.lux
Normal file
65
examples/http_router.lux
Normal file
@@ -0,0 +1,65 @@
|
||||
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")]))
|
||||
let user2 = jsonObject(jsonJoin([jsonNumber("id", 2), jsonString("name", "Bob")]))
|
||||
let users = jsonArray(jsonJoin([user1, user2]))
|
||||
httpOk(users)
|
||||
}
|
||||
|
||||
fn getUserHandler(path: String): { status: Int, body: String } = {
|
||||
match getPathSegment(path, 1) {
|
||||
Some(id) => {
|
||||
let body = jsonObject(jsonJoin([jsonString("id", id), jsonString("name", "User " + id)]))
|
||||
httpOk(body)
|
||||
},
|
||||
None => httpNotFound(jsonErrorMsg("User ID required")),
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
fn serveLoop(remaining: Int): Unit with {Console, HttpServer} = {
|
||||
if remaining <= 0 then {
|
||||
Console.print("Max requests reached, stopping server.")
|
||||
HttpServer.stop()
|
||||
} else {
|
||||
let req = HttpServer.accept()
|
||||
Console.print(req.method + " " + req.path)
|
||||
let resp = router(req.method, req.path, req.body)
|
||||
HttpServer.respond(resp.status, resp.body)
|
||||
serveLoop(remaining - 1)
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console, HttpServer} = {
|
||||
let port = 8080
|
||||
let maxRequests = 10
|
||||
Console.print("========================================")
|
||||
Console.print(" Lux HTTP Router Demo")
|
||||
Console.print("========================================")
|
||||
Console.print("")
|
||||
Console.print("Endpoints:")
|
||||
Console.print(" GET / - Welcome message")
|
||||
Console.print(" GET /health - Health check")
|
||||
Console.print(" GET /users - List all users")
|
||||
Console.print(" GET /users/:id - Get user by ID")
|
||||
Console.print("")
|
||||
Console.print("Try:")
|
||||
Console.print(" curl http://localhost:8080/")
|
||||
Console.print(" curl http://localhost:8080/users")
|
||||
Console.print(" curl http://localhost:8080/users/42")
|
||||
Console.print("")
|
||||
Console.print("Starting server on port " + toString(port) + "...")
|
||||
Console.print("Will handle " + toString(maxRequests) + " requests then stop.")
|
||||
Console.print("")
|
||||
HttpServer.listen(port)
|
||||
Console.print("Server listening!")
|
||||
serveLoop(maxRequests)
|
||||
}
|
||||
|
||||
let output = run main() with {}
|
||||
@@ -1,13 +1,6 @@
|
||||
// Test file for JIT compilation
|
||||
// This uses only features the JIT supports: integers, arithmetic, conditionals, functions
|
||||
fn fib(n: Int): Int = if n <= 1 then n else fib(n - 1) + fib(n - 2)
|
||||
|
||||
fn fib(n: Int): Int =
|
||||
if n <= 1 then n
|
||||
else fib(n - 1) + fib(n - 2)
|
||||
|
||||
fn factorial(n: Int): Int =
|
||||
if n <= 1 then 1
|
||||
else n * factorial(n - 1)
|
||||
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
let fibResult = fib(30)
|
||||
|
||||
@@ -1,107 +1,79 @@
|
||||
// JSON example - demonstrates JSON parsing and manipulation
|
||||
//
|
||||
// This script parses JSON, extracts values, and builds new JSON structures
|
||||
|
||||
fn main(): Unit with {Console, File} = {
|
||||
Console.print("=== Lux JSON Example ===")
|
||||
Console.print("")
|
||||
|
||||
// First, build some JSON programmatically
|
||||
Console.print("=== Building JSON ===")
|
||||
Console.print("")
|
||||
|
||||
let name = Json.string("Alice")
|
||||
let age = Json.int(30)
|
||||
let active = Json.bool(true)
|
||||
let scores = Json.array([Json.int(95), Json.int(87), Json.int(92)])
|
||||
|
||||
let person = Json.object([("name", name), ("age", age), ("active", active), ("scores", scores)])
|
||||
|
||||
Console.print("Built JSON:")
|
||||
let pretty = Json.prettyPrint(person)
|
||||
Console.print(pretty)
|
||||
Console.print("")
|
||||
|
||||
// Stringify to a compact string
|
||||
let jsonStr = Json.stringify(person)
|
||||
Console.print("Compact: " + jsonStr)
|
||||
Console.print("")
|
||||
|
||||
// Write to file and read back to test parsing
|
||||
File.write("/tmp/test.json", jsonStr)
|
||||
Console.print("Written to /tmp/test.json")
|
||||
Console.print("")
|
||||
|
||||
// Read and parse from file
|
||||
Console.print("=== Parsing JSON ===")
|
||||
Console.print("")
|
||||
let content = File.read("/tmp/test.json")
|
||||
Console.print("Read from file: " + content)
|
||||
Console.print("")
|
||||
|
||||
match Json.parse(content) {
|
||||
Ok(json) => {
|
||||
Console.print("Parse succeeded!")
|
||||
Console.print("")
|
||||
|
||||
// Get string field
|
||||
Console.print("Extracting fields:")
|
||||
match Json.get(json, "name") {
|
||||
Some(nameJson) => match Json.asString(nameJson) {
|
||||
Some(n) => Console.print(" name: " + n),
|
||||
None => Console.print(" name: (not a string)")
|
||||
},
|
||||
None => Console.print(" name: (not found)")
|
||||
}
|
||||
|
||||
// Get int field
|
||||
None => Console.print(" name: (not a string)"),
|
||||
},
|
||||
None => Console.print(" name: (not found)"),
|
||||
}
|
||||
match Json.get(json, "age") {
|
||||
Some(ageJson) => match Json.asInt(ageJson) {
|
||||
Some(a) => Console.print(" age: " + toString(a)),
|
||||
None => Console.print(" age: (not an int)")
|
||||
},
|
||||
None => Console.print(" age: (not found)")
|
||||
}
|
||||
|
||||
// Get bool field
|
||||
None => Console.print(" age: (not an int)"),
|
||||
},
|
||||
None => Console.print(" age: (not found)"),
|
||||
}
|
||||
match Json.get(json, "active") {
|
||||
Some(activeJson) => match Json.asBool(activeJson) {
|
||||
Some(a) => Console.print(" active: " + toString(a)),
|
||||
None => Console.print(" active: (not a bool)")
|
||||
},
|
||||
None => Console.print(" active: (not found)")
|
||||
}
|
||||
|
||||
// Get array field
|
||||
None => Console.print(" active: (not a bool)"),
|
||||
},
|
||||
None => Console.print(" active: (not found)"),
|
||||
}
|
||||
match Json.get(json, "scores") {
|
||||
Some(scoresJson) => match Json.asArray(scoresJson) {
|
||||
Some(arr) => {
|
||||
Console.print(" scores: " + toString(List.length(arr)) + " items")
|
||||
// Get first score
|
||||
match Json.getIndex(scoresJson, 0) {
|
||||
Some(firstJson) => match Json.asInt(firstJson) {
|
||||
Some(first) => Console.print(" first score: " + toString(first)),
|
||||
None => Console.print(" first score: (not an int)")
|
||||
},
|
||||
None => Console.print(" (no first element)")
|
||||
}
|
||||
},
|
||||
None => Console.print(" scores: (not an array)")
|
||||
},
|
||||
None => Console.print(" scores: (not found)")
|
||||
}
|
||||
None => Console.print(" first score: (not an int)"),
|
||||
},
|
||||
None => Console.print(" (no first element)"),
|
||||
}
|
||||
},
|
||||
None => Console.print(" scores: (not an array)"),
|
||||
},
|
||||
None => Console.print(" scores: (not found)"),
|
||||
}
|
||||
Console.print("")
|
||||
|
||||
// Get the keys
|
||||
Console.print("Object keys:")
|
||||
match Json.keys(json) {
|
||||
Some(ks) => Console.print(" " + String.join(ks, ", ")),
|
||||
None => Console.print(" (not an object)")
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("Parse error: " + e)
|
||||
}
|
||||
|
||||
None => Console.print(" (not an object)"),
|
||||
}
|
||||
},
|
||||
Err(e) => Console.print("Parse error: " + e),
|
||||
}
|
||||
Console.print("")
|
||||
Console.print("=== JSON Null Check ===")
|
||||
let nullVal = Json.null()
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
// Main program that imports modules
|
||||
import examples/modules/math_utils
|
||||
import examples/modules/string_utils
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Testing Module Imports ===")
|
||||
|
||||
// Use math_utils
|
||||
Console.print("square(5) = " + toString(math_utils.square(5)))
|
||||
Console.print("cube(3) = " + toString(math_utils.cube(3)))
|
||||
Console.print("factorial(6) = " + toString(math_utils.factorial(6)))
|
||||
Console.print("sumRange(1, 10) = " + toString(math_utils.sumRange(1, 10)))
|
||||
|
||||
// Use string_utils
|
||||
Console.print(string_utils.greet("World"))
|
||||
Console.print(string_utils.exclaim("Modules work"))
|
||||
Console.print("repeat(\"ab\", 3) = " + string_utils.repeat("ab", 3))
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
// Test selective imports
|
||||
import examples/modules/math_utils.{square, factorial}
|
||||
import examples/modules/string_utils as str
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Selective & Aliased Imports ===")
|
||||
|
||||
// Direct imports (no module prefix)
|
||||
Console.print("square(7) = " + toString(square(7)))
|
||||
Console.print("factorial(5) = " + toString(factorial(5)))
|
||||
|
||||
// Aliased import
|
||||
Console.print(str.greet("Lux"))
|
||||
Console.print(str.exclaim("Aliased imports work"))
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
// Test wildcard imports
|
||||
import examples/modules/math_utils.*
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Wildcard Imports ===")
|
||||
|
||||
// All functions available directly
|
||||
Console.print("square(4) = " + toString(square(4)))
|
||||
Console.print("cube(4) = " + toString(cube(4)))
|
||||
Console.print("factorial(4) = " + toString(factorial(4)))
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
// Math utilities module
|
||||
// Exports: square, cube, factorial
|
||||
fn square(n: Int): Int = n * n
|
||||
|
||||
pub fn square(n: Int): Int = n * n
|
||||
fn cube(n: Int): Int = n * n * n
|
||||
|
||||
pub fn cube(n: Int): Int = n * n * n
|
||||
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
|
||||
|
||||
pub fn factorial(n: Int): Int =
|
||||
if n <= 1 then 1
|
||||
else n * factorial(n - 1)
|
||||
|
||||
pub fn sumRange(start: Int, end: Int): Int =
|
||||
if start > end then 0
|
||||
else start + sumRange(start + 1, end)
|
||||
fn sumRange(start: Int, end: Int): Int = if start > end then 0 else start + sumRange(start + 1, end)
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
// String utilities module
|
||||
// Exports: repeat, exclaim
|
||||
fn repeat(s: String, n: Int): String = if n <= 0 then "" else s + repeat(s, n - 1)
|
||||
|
||||
pub fn repeat(s: String, n: Int): String =
|
||||
if n <= 0 then ""
|
||||
else s + repeat(s, n - 1)
|
||||
fn exclaim(s: String): String = s + "!"
|
||||
|
||||
pub fn exclaim(s: String): String = s + "!"
|
||||
|
||||
pub fn greet(name: String): String =
|
||||
"Hello, " + name + "!"
|
||||
fn greet(name: String): String = "Hello, " + name + "!"
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
// Example using the standard library
|
||||
import std/prelude.*
|
||||
import std/option as opt
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Using Standard Library ===")
|
||||
|
||||
// Prelude functions
|
||||
Console.print("identity(42) = " + toString(identity(42)))
|
||||
Console.print("not(true) = " + toString(not(true)))
|
||||
Console.print("and(true, false) = " + toString(and(true, false)))
|
||||
Console.print("or(true, false) = " + toString(or(true, false)))
|
||||
|
||||
// Option utilities
|
||||
let x = opt.some(10)
|
||||
let y = opt.none()
|
||||
Console.print("isSome(Some(10)) = " + toString(opt.isSome(x)))
|
||||
|
||||
@@ -1,47 +1,31 @@
|
||||
// Demonstrating the pipe operator and functional data processing
|
||||
//
|
||||
// Expected output:
|
||||
// 5 |> double |> addTen |> square = 400
|
||||
// Pipeline result2 = 42
|
||||
// process(1) = 144
|
||||
// process(2) = 196
|
||||
// process(3) = 256
|
||||
// clamped = 0
|
||||
// composed = 121
|
||||
|
||||
// Basic transformations
|
||||
fn double(x: Int): Int = x * 2
|
||||
|
||||
fn addTen(x: Int): Int = x + 10
|
||||
|
||||
fn square(x: Int): Int = x * x
|
||||
|
||||
fn negate(x: Int): Int = -x
|
||||
|
||||
// Using the pipe operator for data transformation
|
||||
let result1 = 5 |> double |> addTen |> square
|
||||
let result1 = square(addTen(double(5)))
|
||||
|
||||
// Chaining multiple operations
|
||||
let result2 = 3 |> double |> addTen |> double |> addTen
|
||||
let result2 = addTen(double(addTen(double(3))))
|
||||
|
||||
// More complex pipelines
|
||||
fn process(n: Int): Int =
|
||||
n |> double |> addTen |> square
|
||||
fn process(n: Int): Int = square(addTen(double(n)))
|
||||
|
||||
// Multiple values through same pipeline
|
||||
let a = process(1)
|
||||
|
||||
let b = process(2)
|
||||
|
||||
let c = process(3)
|
||||
|
||||
// Conditional in pipeline
|
||||
fn clampPositive(x: Int): Int =
|
||||
if x < 0 then 0 else x
|
||||
fn clampPositive(x: Int): Int = if x < 0 then 0 else x
|
||||
|
||||
let clamped = -5 |> double |> clampPositive
|
||||
let clamped = clampPositive(double(-5))
|
||||
|
||||
// Function composition using pipe
|
||||
fn increment(x: Int): Int = x + 1
|
||||
|
||||
let composed = 5 |> double |> increment |> square
|
||||
let composed = square(increment(double(5)))
|
||||
|
||||
// Print results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("5 |> double |> addTen |> square = " + toString(result1))
|
||||
Console.print("Pipeline result2 = " + toString(result2))
|
||||
|
||||
113
examples/postgres_demo.lux
Normal file
113
examples/postgres_demo.lux
Normal file
@@ -0,0 +1,113 @@
|
||||
fn jsonStr(key: String, value: String): String = "\"" + key + "\":\"" + value + "\""
|
||||
|
||||
fn jsonNum(key: String, value: Int): String = "\"" + key + "\":" + toString(value)
|
||||
|
||||
fn jsonObj(content: String): String = toString(" + content + ")
|
||||
|
||||
fn insertUser(connId: Int, name: String, email: String): Int with {Console, Postgres} = {
|
||||
let sql = "INSERT INTO users (name, email) VALUES ('" + name + "', '" + email + "') RETURNING id"
|
||||
Console.print("Inserting user: " + name)
|
||||
match Postgres.queryOne(connId, sql) {
|
||||
Some(row) => {
|
||||
Console.print(" Inserted with ID: " + toString(row.id))
|
||||
row.id
|
||||
},
|
||||
None => {
|
||||
Console.print(" Insert failed")
|
||||
-1
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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 => {
|
||||
Console.print(" - " + toString(row.id) + ": " + row.name + " <" + row.email + ">")
|
||||
})
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
let affected = Postgres.execute(connId, sql)
|
||||
Console.print(" Rows affected: " + toString(affected))
|
||||
}
|
||||
|
||||
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))
|
||||
let affected = Postgres.execute(connId, sql)
|
||||
Console.print(" Rows affected: " + toString(affected))
|
||||
}
|
||||
|
||||
fn transactionDemo(connId: Int): Unit with {Console, Postgres} = {
|
||||
Console.print("")
|
||||
Console.print("=== Transaction Demo ===")
|
||||
Console.print("Beginning transaction...")
|
||||
Postgres.beginTx(connId)
|
||||
insertUser(connId, "TxUser1", "tx1@example.com")
|
||||
insertUser(connId, "TxUser2", "tx2@example.com")
|
||||
Console.print("Users before commit:")
|
||||
getUsers(connId)
|
||||
Console.print("Committing transaction...")
|
||||
Postgres.commit(connId)
|
||||
Console.print("Transaction committed!")
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console, Postgres} = {
|
||||
Console.print("========================================")
|
||||
Console.print(" PostgreSQL Demo")
|
||||
Console.print("========================================")
|
||||
Console.print("")
|
||||
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("")
|
||||
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("")
|
||||
Console.print("Clearing existing data...")
|
||||
Postgres.execute(connId, "DELETE FROM users")
|
||||
Console.print("")
|
||||
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("")
|
||||
Console.print("=== All Users ===")
|
||||
getUsers(connId)
|
||||
Console.print("")
|
||||
Console.print("=== Single User Lookup ===")
|
||||
getUserById(connId, id2)
|
||||
Console.print("")
|
||||
Console.print("=== Update User ===")
|
||||
updateUserEmail(connId, id2, "bob.new@example.com")
|
||||
getUserById(connId, id2)
|
||||
Console.print("")
|
||||
Console.print("=== Delete User ===")
|
||||
deleteUser(connId, id3)
|
||||
getUsers(connId)
|
||||
Console.print("")
|
||||
transactionDemo(connId)
|
||||
Console.print("")
|
||||
Console.print("=== Final State ===")
|
||||
getUsers(connId)
|
||||
Console.print("")
|
||||
Console.print("Closing connection...")
|
||||
Postgres.close(connId)
|
||||
Console.print("Done!")
|
||||
}
|
||||
|
||||
let output = run main() with {}
|
||||
190
examples/property_testing.lux
Normal file
190
examples/property_testing.lux
Normal file
@@ -0,0 +1,190 @@
|
||||
let CHARS = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
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)
|
||||
genIntListHelper(min, max, len)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
fn genChar(): String with {Random} = {
|
||||
let idx = Random.int(0, 25)
|
||||
String.substring(CHARS, idx, idx + 1)
|
||||
}
|
||||
|
||||
fn genString(maxLen: Int): String with {Random} = {
|
||||
let len = Random.int(0, maxLen)
|
||||
genStringHelper(len)
|
||||
}
|
||||
|
||||
fn genStringHelper(len: Int): String with {Random} = {
|
||||
if len <= 0 then "" else genChar() + genStringHelper(len - 1)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fn testReverseInvolutive(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("reverse(reverse(xs)) == xs", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.reverse(List.reverse(xs)) == xs then testReverseInvolutive(n - 1, count) else {
|
||||
printResult("reverse(reverse(xs)) == xs", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn testReverseLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("length(reverse(xs)) == length(xs)", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.length(List.reverse(xs)) == List.length(xs) then testReverseLength(n - 1, count) else {
|
||||
printResult("length(reverse(xs)) == length(xs)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn testMapLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("length(map(xs, f)) == length(xs)", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.length(List.map(xs, fn(x: _) => x * 2)) == List.length(xs) then testMapLength(n - 1, count) else {
|
||||
printResult("length(map(xs, f)) == length(xs)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn testConcatLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("length(xs ++ ys) == length(xs) + length(ys)", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 50, 10)
|
||||
let ys = genIntList(0, 50, 10)
|
||||
if List.length(List.concat(xs, ys)) == List.length(xs) + List.length(ys) then testConcatLength(n - 1, count) else {
|
||||
printResult("length(xs ++ ys) == length(xs) + length(ys)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn testAddCommutative(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("a + b == b + a", true, count)
|
||||
true
|
||||
} else {
|
||||
let a = genInt(-1000, 1000)
|
||||
let b = genInt(-1000, 1000)
|
||||
if a + b == b + a then testAddCommutative(n - 1, count) else {
|
||||
printResult("a + b == b + a", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn testMulAssociative(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("(a * b) * c == a * (b * c)", true, count)
|
||||
true
|
||||
} else {
|
||||
let a = genInt(-100, 100)
|
||||
let b = genInt(-100, 100)
|
||||
let c = genInt(-100, 100)
|
||||
if a * b * c == a * b * c then testMulAssociative(n - 1, count) else {
|
||||
printResult("(a * b) * c == a * (b * c)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn testStringConcatLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("length(s1 + s2) == length(s1) + length(s2)", true, count)
|
||||
true
|
||||
} else {
|
||||
let s1 = genString(10)
|
||||
let s2 = genString(10)
|
||||
if String.length(s1 + s2) == String.length(s1) + String.length(s2) then testStringConcatLength(n - 1, count) else {
|
||||
printResult("length(s1 + s2) == length(s1) + length(s2)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn testAddIdentity(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("x + 0 == x && 0 + x == x", true, count)
|
||||
true
|
||||
} else {
|
||||
let x = genInt(-10000, 10000)
|
||||
if x + 0 == x && 0 + x == x then testAddIdentity(n - 1, count) else {
|
||||
printResult("x + 0 == x && 0 + x == x", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn testFilterLength(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("length(filter(xs, p)) <= length(xs)", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 20)
|
||||
if List.length(List.filter(xs, fn(x: _) => x > 50)) <= List.length(xs) then testFilterLength(n - 1, count) else {
|
||||
printResult("length(filter(xs, p)) <= length(xs)", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn testConcatIdentity(n: Int, count: Int): Bool with {Console, Random} = {
|
||||
if n <= 0 then {
|
||||
printResult("concat(xs, []) == xs && concat([], xs) == xs", true, count)
|
||||
true
|
||||
} else {
|
||||
let xs = genIntList(0, 100, 10)
|
||||
if List.concat(xs, []) == xs && List.concat([], xs) == xs then testConcatIdentity(n - 1, count) else {
|
||||
printResult("concat(xs, []) == xs && concat([], xs) == xs", false, count - n + 1)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console, Random} = {
|
||||
Console.print("========================================")
|
||||
Console.print(" Property-Based Testing Demo")
|
||||
Console.print("========================================")
|
||||
Console.print("")
|
||||
Console.print("Running 100 iterations per property...")
|
||||
Console.print("")
|
||||
testReverseInvolutive(100, 100)
|
||||
testReverseLength(100, 100)
|
||||
testMapLength(100, 100)
|
||||
testConcatLength(100, 100)
|
||||
testAddCommutative(100, 100)
|
||||
testMulAssociative(100, 100)
|
||||
testStringConcatLength(100, 100)
|
||||
testAddIdentity(100, 100)
|
||||
testFilterLength(100, 100)
|
||||
testConcatIdentity(100, 100)
|
||||
Console.print("")
|
||||
Console.print("========================================")
|
||||
Console.print(" All property tests completed!")
|
||||
Console.print("========================================")
|
||||
}
|
||||
|
||||
let result = run main() with {}
|
||||
@@ -1,39 +1,22 @@
|
||||
// Demonstrating Random and Time effects in Lux
|
||||
//
|
||||
// Expected output (values will vary):
|
||||
// Rolling dice...
|
||||
// Die 1: <random 1-6>
|
||||
// Die 2: <random 1-6>
|
||||
// Die 3: <random 1-6>
|
||||
// Coin flip: <true/false>
|
||||
// Random float: <0.0-1.0>
|
||||
// Current time: <timestamp>
|
||||
|
||||
// Roll a single die (1-6)
|
||||
fn rollDie(): Int with {Random} = Random.int(1, 6)
|
||||
|
||||
// Roll multiple dice and print results
|
||||
fn rollDice(count: Int): Unit with {Random, Console} = {
|
||||
if count > 0 then {
|
||||
let value = rollDie()
|
||||
Console.print("Die " + toString(4 - count) + ": " + toString(value))
|
||||
rollDice(count - 1)
|
||||
} else {
|
||||
} else {
|
||||
()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main function demonstrating random effects
|
||||
fn main(): Unit with {Random, Console, Time} = {
|
||||
Console.print("Rolling dice...")
|
||||
rollDice(3)
|
||||
|
||||
let coin = Random.bool()
|
||||
Console.print("Coin flip: " + toString(coin))
|
||||
|
||||
let f = Random.float()
|
||||
Console.print("Random float: " + toString(f))
|
||||
|
||||
let now = Time.now()
|
||||
Console.print("Current time: " + toString(now))
|
||||
}
|
||||
|
||||
@@ -1,67 +1,41 @@
|
||||
// Schema Evolution Demo
|
||||
// Demonstrates version tracking and automatic migrations
|
||||
|
||||
// ============================================================
|
||||
// PART 1: Type-Declared Migrations
|
||||
// ============================================================
|
||||
|
||||
// Define a versioned type with a migration from v1 to v2
|
||||
type User @v2 {
|
||||
type User = {
|
||||
name: String,
|
||||
email: String,
|
||||
|
||||
// Migration from v1: add default email
|
||||
from @v1 = { name: old.name, email: "unknown@example.com" }
|
||||
}
|
||||
|
||||
// Create a v1 user
|
||||
let v1_user = Schema.versioned("User", 1, { name: "Alice" })
|
||||
let v1_version = Schema.getVersion(v1_user) // 1
|
||||
|
||||
// Migrate to v2 - uses the declared migration automatically
|
||||
let v1_version = Schema.getVersion(v1_user)
|
||||
|
||||
let v2_user = Schema.migrate(v1_user, 2)
|
||||
let v2_version = Schema.getVersion(v2_user) // 2
|
||||
|
||||
// ============================================================
|
||||
// PART 2: Runtime Schema Operations (separate type)
|
||||
// ============================================================
|
||||
let v2_version = Schema.getVersion(v2_user)
|
||||
|
||||
// Create versioned values for a different type (no migration)
|
||||
let config1 = Schema.versioned("Config", 1, "debug")
|
||||
|
||||
let config2 = Schema.versioned("Config", 2, "release")
|
||||
|
||||
// Check versions
|
||||
let c1 = Schema.getVersion(config1) // 1
|
||||
let c2 = Schema.getVersion(config2) // 2
|
||||
let c1 = Schema.getVersion(config1)
|
||||
|
||||
let c2 = Schema.getVersion(config2)
|
||||
|
||||
// Migrate config (auto-migration since no explicit migration defined)
|
||||
let upgradedConfig = Schema.migrate(config1, 2)
|
||||
let upgradedConfigVersion = Schema.getVersion(upgradedConfig) // 2
|
||||
|
||||
// ============================================================
|
||||
// PART 2: Practical Example - API Versioning
|
||||
// ============================================================
|
||||
let upgradedConfigVersion = Schema.getVersion(upgradedConfig)
|
||||
|
||||
// Simulate different API response versions
|
||||
fn createResponseV1(data: String): { version: Int, payload: String } =
|
||||
{ version: 1, payload: data }
|
||||
fn createResponseV1(data: String): { version: Int, payload: String } = { version: 1, payload: data }
|
||||
|
||||
fn createResponseV2(data: String, timestamp: Int): { version: Int, payload: String, meta: { ts: Int } } =
|
||||
{ version: 2, payload: data, meta: { ts: timestamp } }
|
||||
fn createResponseV2(data: String, timestamp: Int): { version: Int, payload: String, meta: { ts: Int } } = { version: 2, payload: data, meta: { ts: timestamp } }
|
||||
|
||||
// Version-aware processing
|
||||
fn getPayload(response: { version: Int, payload: String }): String =
|
||||
response.payload
|
||||
fn getPayload(response: { version: Int, payload: String }): String = response.payload
|
||||
|
||||
let resp1 = createResponseV1("Hello")
|
||||
|
||||
let resp2 = createResponseV2("World", 1234567890)
|
||||
|
||||
let payload1 = getPayload(resp1)
|
||||
let payload2 = resp2.payload
|
||||
|
||||
// ============================================================
|
||||
// RESULTS
|
||||
// ============================================================
|
||||
let payload2 = resp2.payload
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Schema Evolution Demo ===")
|
||||
|
||||
@@ -1,58 +1,43 @@
|
||||
// Shell/Process example - demonstrates the Process effect
|
||||
//
|
||||
// This script runs shell commands and uses environment variables
|
||||
|
||||
fn main(): Unit with {Process, Console} = {
|
||||
Console.print("=== Lux Shell Example ===")
|
||||
Console.print("")
|
||||
|
||||
// Get current working directory
|
||||
let cwd = Process.cwd()
|
||||
Console.print("Current directory: " + cwd)
|
||||
Console.print("")
|
||||
|
||||
// Get environment variables
|
||||
Console.print("Environment variables:")
|
||||
match Process.env("USER") {
|
||||
Some(user) => Console.print(" USER: " + user),
|
||||
None => Console.print(" USER: (not set)")
|
||||
}
|
||||
None => Console.print(" USER: (not set)"),
|
||||
}
|
||||
match Process.env("HOME") {
|
||||
Some(home) => Console.print(" HOME: " + home),
|
||||
None => Console.print(" HOME: (not set)")
|
||||
}
|
||||
None => Console.print(" HOME: (not set)"),
|
||||
}
|
||||
match Process.env("SHELL") {
|
||||
Some(shell) => Console.print(" SHELL: " + shell),
|
||||
None => Console.print(" SHELL: (not set)")
|
||||
}
|
||||
None => Console.print(" SHELL: (not set)"),
|
||||
}
|
||||
Console.print("")
|
||||
|
||||
// Run shell commands
|
||||
Console.print("Running shell commands:")
|
||||
|
||||
let date = Process.exec("date")
|
||||
Console.print(" date: " + String.trim(date))
|
||||
|
||||
let kernel = Process.exec("uname -r")
|
||||
Console.print(" kernel: " + String.trim(kernel))
|
||||
|
||||
let files = Process.exec("ls examples/*.lux | wc -l")
|
||||
Console.print(" .lux files in examples/: " + String.trim(files))
|
||||
Console.print("")
|
||||
|
||||
// Command line arguments
|
||||
Console.print("Command line arguments:")
|
||||
let args = Process.args()
|
||||
let argCount = List.length(args)
|
||||
if argCount == 0 then {
|
||||
Console.print(" (no arguments)")
|
||||
} else {
|
||||
} else {
|
||||
Console.print(" Count: " + toString(argCount))
|
||||
match List.head(args) {
|
||||
Some(first) => Console.print(" First: " + first),
|
||||
None => Console.print(" First: (empty)")
|
||||
}
|
||||
}
|
||||
None => Console.print(" First: (empty)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = run main() with {}
|
||||
|
||||
107
examples/showcase/README.md
Normal file
107
examples/showcase/README.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Task Manager Showcase
|
||||
|
||||
This example demonstrates Lux's three killer features in a practical, real-world context.
|
||||
|
||||
## Running the Example
|
||||
|
||||
```bash
|
||||
lux run examples/showcase/task_manager.lux
|
||||
```
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
### 1. Algebraic Effects
|
||||
|
||||
Every function signature shows exactly what side effects it can perform:
|
||||
|
||||
```lux
|
||||
fn createTask(title: String, priority: String): Task@latest
|
||||
with {TaskStore, Random} = { ... }
|
||||
```
|
||||
|
||||
- `TaskStore` - database operations
|
||||
- `Random` - random number generation
|
||||
- No hidden I/O or surprise calls
|
||||
|
||||
### 2. Behavioral Types
|
||||
|
||||
Compile-time guarantees about function behavior:
|
||||
|
||||
```lux
|
||||
fn formatTask(task: Task@latest): String
|
||||
is pure // No side effects
|
||||
is deterministic // Same input = same output
|
||||
is total // Always terminates
|
||||
```
|
||||
|
||||
```lux
|
||||
fn completeTask(id: String): Option<Task@latest>
|
||||
is idempotent // Safe to retry
|
||||
with {TaskStore}
|
||||
```
|
||||
|
||||
### 3. Schema Evolution
|
||||
|
||||
Versioned types with automatic migration:
|
||||
|
||||
```lux
|
||||
type Task @v2 {
|
||||
id: String,
|
||||
title: String,
|
||||
done: Bool,
|
||||
priority: String, // New in v2
|
||||
|
||||
from @v1 = { ...old, priority: "medium" }
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Handler Swapping (Testing)
|
||||
|
||||
Test without mocks by swapping effect handlers:
|
||||
|
||||
```lux
|
||||
// Production
|
||||
run processOrders() with {
|
||||
TaskStore = PostgresTaskStore,
|
||||
Logger = CloudLogger
|
||||
}
|
||||
|
||||
// Testing
|
||||
run processOrders() with {
|
||||
TaskStore = InMemoryTaskStore,
|
||||
Logger = SilentLogger
|
||||
}
|
||||
```
|
||||
|
||||
## Why This Matters
|
||||
|
||||
| Traditional Languages | Lux |
|
||||
|----------------------|-----|
|
||||
| Side effects are implicit | Effects in type signatures |
|
||||
| Runtime crashes | Compile-time verification |
|
||||
| Complex mocking frameworks | Simple handler swapping |
|
||||
| Manual migration code | Automatic schema evolution |
|
||||
| Hope for retry safety | Verified idempotency |
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
showcase/
|
||||
├── README.md # This file
|
||||
└── task_manager.lux # Main example with all features
|
||||
```
|
||||
|
||||
## Key Sections in the Code
|
||||
|
||||
1. **Versioned Data Types** - `Task @v1`, `@v2`, `@v3` with migrations
|
||||
2. **Pure Functions** - `is pure`, `is total`, `is deterministic`, `is idempotent`
|
||||
3. **Effects** - `effect TaskStore` and `effect Logger`
|
||||
4. **Effect Handlers** - `InMemoryTaskStore`, `ConsoleLogger`
|
||||
5. **Testing** - `runTestScenario()` with swapped handlers
|
||||
6. **Migration Demo** - `demonstrateMigration()`
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Read the [Behavioral Types Guide](../../docs/guide/12-behavioral-types.md)
|
||||
- Read the [Schema Evolution Guide](../../docs/guide/13-schema-evolution.md)
|
||||
- Explore [more examples](../)
|
||||
@@ -1,15 +1,3 @@
|
||||
// The "Ask" Pattern - Resumable Effects
|
||||
//
|
||||
// Unlike exceptions which unwind the stack, effect handlers can
|
||||
// RESUME with a value. This enables "ask the environment" patterns.
|
||||
//
|
||||
// Expected output:
|
||||
// Need config: api_url
|
||||
// Got: https://api.example.com
|
||||
// Need config: timeout
|
||||
// Got: 30
|
||||
// Configured with url=https://api.example.com, timeout=30
|
||||
|
||||
effect Config {
|
||||
fn get(key: String): String
|
||||
}
|
||||
@@ -25,14 +13,13 @@ fn configure(): String with {Config, Console} = {
|
||||
}
|
||||
|
||||
handler envConfig: Config {
|
||||
fn get(key) =
|
||||
if key == "api_url" then resume("https://api.example.com")
|
||||
else if key == "timeout" then resume("30")
|
||||
else resume("unknown")
|
||||
fn get(key) = if key == "api_url" then resume("https://api.example.com") else if key == "timeout" then resume("30") else resume("unknown")
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
let result = run configure() with { Config = envConfig }
|
||||
let result = run configure() with {
|
||||
Config = envConfig,
|
||||
}
|
||||
Console.print(result)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
// Custom Logging with Effects
|
||||
//
|
||||
// This demonstrates how effects let you abstract side effects.
|
||||
// The same code can be run with different logging implementations.
|
||||
//
|
||||
// Expected output:
|
||||
// [INFO] Starting computation
|
||||
// [DEBUG] x = 10
|
||||
// [INFO] Processing
|
||||
// [DEBUG] result = 20
|
||||
// Final: 20
|
||||
|
||||
effect Log {
|
||||
fn info(msg: String): Unit
|
||||
fn debug(msg: String): Unit
|
||||
@@ -26,18 +14,22 @@ fn computation(): Int with {Log} = {
|
||||
}
|
||||
|
||||
handler consoleLogger: Log {
|
||||
fn info(msg) = {
|
||||
fn info(msg) =
|
||||
{
|
||||
Console.print("[INFO] " + msg)
|
||||
resume(())
|
||||
}
|
||||
fn debug(msg) = {
|
||||
}
|
||||
fn debug(msg) =
|
||||
{
|
||||
Console.print("[DEBUG] " + msg)
|
||||
resume(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
let result = run computation() with { Log = consoleLogger }
|
||||
let result = run computation() with {
|
||||
Log = consoleLogger,
|
||||
}
|
||||
Console.print("Final: " + toString(result))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,18 @@
|
||||
// Early Return with Fail Effect
|
||||
//
|
||||
// The Fail effect provides clean early termination.
|
||||
// Functions declare their failure modes in the type signature.
|
||||
//
|
||||
// Expected output:
|
||||
// Parsing "42"...
|
||||
// Result: 42
|
||||
// Parsing "100"...
|
||||
// Result: 100
|
||||
// Dividing 100 by 4...
|
||||
// Result: 25
|
||||
|
||||
fn parsePositive(s: String): Int with {Fail, Console} = {
|
||||
Console.print("Parsing \"" + s + "\"...")
|
||||
if s == "42" then 42
|
||||
else if s == "100" then 100
|
||||
else Fail.fail("Invalid number: " + s)
|
||||
if s == "42" then 42 else if s == "100" then 100 else Fail.fail("Invalid number: " + s)
|
||||
}
|
||||
|
||||
fn safeDivide(a: Int, b: Int): Int with {Fail, Console} = {
|
||||
Console.print("Dividing " + toString(a) + " by " + toString(b) + "...")
|
||||
if b == 0 then Fail.fail("Division by zero")
|
||||
else a / b
|
||||
if b == 0 then Fail.fail("Division by zero") else a / b
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
// These succeed
|
||||
let n1 = run parsePositive("42") with {}
|
||||
Console.print("Result: " + toString(n1))
|
||||
|
||||
let n2 = run parsePositive("100") with {}
|
||||
Console.print("Result: " + toString(n2))
|
||||
|
||||
let n3 = run safeDivide(100, 4) with {}
|
||||
Console.print("Result: " + toString(n3))
|
||||
}
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
// Effect Composition - Combine multiple effects cleanly
|
||||
//
|
||||
// Unlike monad transformers (which have ordering issues),
|
||||
// effects can be freely combined without boilerplate.
|
||||
// Each handler handles its own effect, ignoring others.
|
||||
//
|
||||
// Expected output:
|
||||
// [LOG] Starting computation
|
||||
// Generated: 7
|
||||
// [LOG] Processing value
|
||||
// [LOG] Done
|
||||
// Result: 14
|
||||
|
||||
effect Log {
|
||||
fn log(msg: String): Unit
|
||||
}
|
||||
@@ -30,8 +17,8 @@ handler consoleLog: Log {
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
let result = run computation() with {
|
||||
Log = consoleLog
|
||||
}
|
||||
Log = consoleLog,
|
||||
}
|
||||
Console.print("Generated: " + toString(result / 2))
|
||||
Console.print("Result: " + toString(result))
|
||||
}
|
||||
|
||||
@@ -1,38 +1,19 @@
|
||||
// Higher-Order Functions and Closures
|
||||
//
|
||||
// Functions are first-class values in Lux.
|
||||
// Closures capture their environment.
|
||||
//
|
||||
// Expected output:
|
||||
// Square of 5: 25
|
||||
// Cube of 3: 27
|
||||
// Add 10 to 5: 15
|
||||
// Add 10 to 20: 30
|
||||
// Composed: 15625 (cube(square(5)) = cube(25) = 15625)
|
||||
|
||||
fn apply(f: fn(Int): Int, x: Int): Int = f(x)
|
||||
|
||||
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int =
|
||||
fn(x: Int): Int => f(g(x))
|
||||
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int = fn(x: Int): Int => f(g(x))
|
||||
|
||||
fn square(n: Int): Int = n * n
|
||||
|
||||
fn cube(n: Int): Int = n * n * n
|
||||
|
||||
fn makeAdder(n: Int): fn(Int): Int =
|
||||
fn(x: Int): Int => x + n
|
||||
fn makeAdder(n: Int): fn(Int): Int = fn(x: Int): Int => x + n
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
// Apply functions
|
||||
Console.print("Square of 5: " + toString(apply(square, 5)))
|
||||
Console.print("Cube of 3: " + toString(apply(cube, 3)))
|
||||
|
||||
// Closures
|
||||
let add10 = makeAdder(10)
|
||||
Console.print("Add 10 to 5: " + toString(add10(5)))
|
||||
Console.print("Add 10 to 20: " + toString(add10(20)))
|
||||
|
||||
// Function composition
|
||||
let squareThenCube = compose(cube, square)
|
||||
Console.print("Composed: " + toString(squareThenCube(5)))
|
||||
}
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
// Algebraic Data Types and Pattern Matching
|
||||
//
|
||||
// Lux has powerful ADTs with exhaustive pattern matching.
|
||||
// The type system ensures all cases are handled.
|
||||
//
|
||||
// Expected output:
|
||||
// Evaluating: (2 + 3)
|
||||
// Result: 5
|
||||
// Evaluating: ((1 + 2) * (3 + 4))
|
||||
// Result: 21
|
||||
// Evaluating: (10 - (2 * 3))
|
||||
// Result: 4
|
||||
|
||||
type Expr =
|
||||
| Num(Int)
|
||||
| Add(Expr, Expr)
|
||||
@@ -22,16 +9,16 @@ fn eval(e: Expr): Int =
|
||||
Num(n) => n,
|
||||
Add(a, b) => eval(a) + eval(b),
|
||||
Sub(a, b) => eval(a) - eval(b),
|
||||
Mul(a, b) => eval(a) * eval(b)
|
||||
}
|
||||
Mul(a, b) => eval(a) * eval(b),
|
||||
}
|
||||
|
||||
fn showExpr(e: Expr): String =
|
||||
match e {
|
||||
Num(n) => toString(n),
|
||||
Add(a, b) => "(" + showExpr(a) + " + " + showExpr(b) + ")",
|
||||
Sub(a, b) => "(" + showExpr(a) + " - " + showExpr(b) + ")",
|
||||
Mul(a, b) => "(" + showExpr(a) + " * " + showExpr(b) + ")"
|
||||
}
|
||||
Mul(a, b) => "(" + showExpr(a) + " * " + showExpr(b) + ")",
|
||||
}
|
||||
|
||||
fn evalAndPrint(e: Expr): Unit with {Console} = {
|
||||
Console.print("Evaluating: " + showExpr(e))
|
||||
@@ -39,15 +26,10 @@ fn evalAndPrint(e: Expr): Unit with {Console} = {
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
// (2 + 3)
|
||||
let e1 = Add(Num(2), Num(3))
|
||||
evalAndPrint(e1)
|
||||
|
||||
// ((1 + 2) * (3 + 4))
|
||||
let e2 = Mul(Add(Num(1), Num(2)), Add(Num(3), Num(4)))
|
||||
evalAndPrint(e2)
|
||||
|
||||
// (10 - (2 * 3))
|
||||
let e3 = Sub(Num(10), Mul(Num(2), Num(3)))
|
||||
evalAndPrint(e3)
|
||||
}
|
||||
|
||||
419
examples/showcase/task_manager.lux
Normal file
419
examples/showcase/task_manager.lux
Normal file
@@ -0,0 +1,419 @@
|
||||
// =============================================================================
|
||||
// Task Manager API - A Showcase of Lux's Unique Features
|
||||
// =============================================================================
|
||||
//
|
||||
// This example demonstrates Lux's three killer features:
|
||||
//
|
||||
// 1. ALGEBRAIC EFFECTS - Every side effect is explicit in function signatures
|
||||
// - No hidden I/O, no surprise database calls
|
||||
// - Testing is trivial: just swap handlers
|
||||
//
|
||||
// 2. BEHAVIORAL TYPES - Compile-time guarantees about function behavior
|
||||
// - `is pure` - no side effects, safe to cache
|
||||
// - `is total` - always terminates, never fails
|
||||
// - `is idempotent` - safe to retry without side effects
|
||||
// - `is deterministic` - same input = same output
|
||||
//
|
||||
// 3. SCHEMA EVOLUTION - Versioned types with automatic migration
|
||||
// - Data structures evolve safely over time
|
||||
// - Old data automatically upgrades
|
||||
//
|
||||
// To run: lux run examples/showcase/task_manager.lux
|
||||
// =============================================================================
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// PART 1: VERSIONED DATA TYPES (Schema Evolution)
|
||||
// =============================================================================
|
||||
|
||||
// Task v1: Our original data model (simple)
|
||||
type Task @v1 {
|
||||
id: String,
|
||||
title: String,
|
||||
done: Bool
|
||||
}
|
||||
|
||||
// Task v2: Added priority field
|
||||
// The `from @v1` clause defines how to migrate old data automatically
|
||||
type Task @v2 {
|
||||
id: String,
|
||||
title: String,
|
||||
done: Bool,
|
||||
priority: String, // New field: "low", "medium", "high"
|
||||
|
||||
// Migration: old tasks get "medium" priority by default
|
||||
from @v1 = {
|
||||
id: old.id,
|
||||
title: old.title,
|
||||
done: old.done,
|
||||
priority: "medium"
|
||||
}
|
||||
}
|
||||
|
||||
// Task v3: Added due date and tags
|
||||
// Migrations chain automatically: v1 → v2 → v3
|
||||
type Task @v3 {
|
||||
id: String,
|
||||
title: String,
|
||||
done: Bool,
|
||||
priority: String,
|
||||
dueDate: Option<Int>, // Unix timestamp, optional
|
||||
tags: List<String>, // New: categorization
|
||||
|
||||
from @v2 = {
|
||||
id: old.id,
|
||||
title: old.title,
|
||||
done: old.done,
|
||||
priority: old.priority,
|
||||
dueDate: None, // No due date for migrated tasks
|
||||
tags: [] // Empty tags for migrated tasks
|
||||
}
|
||||
}
|
||||
|
||||
// Use @latest to always refer to the newest version
|
||||
type TaskList = List<Task@latest>
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// PART 2: PURE FUNCTIONS WITH BEHAVIORAL TYPES
|
||||
// =============================================================================
|
||||
|
||||
// Pure function: no side effects, safe to cache, parallelize, eliminate if unused
|
||||
// The compiler verifies `is pure` - if you try to call an effect, it errors.
|
||||
fn formatTask(task: Task@latest): String
|
||||
is pure
|
||||
is deterministic
|
||||
is total = {
|
||||
let status = if task.done then "[x]" else "[ ]"
|
||||
let priority = match task.priority {
|
||||
"high" => "!!",
|
||||
"medium" => "!",
|
||||
_ => ""
|
||||
}
|
||||
status + " " + priority + task.title
|
||||
}
|
||||
|
||||
// Idempotent function: f(f(x)) = f(x)
|
||||
// Safe to apply multiple times without changing the result
|
||||
// Critical for retry logic - the compiler verifies this property
|
||||
fn normalizeTitle(title: String): String
|
||||
is pure
|
||||
is idempotent = {
|
||||
title
|
||||
|> String.trim
|
||||
|> String.toLower
|
||||
}
|
||||
|
||||
// Total function: always terminates, never throws
|
||||
// No Fail effect allowed, recursion must be structurally decreasing
|
||||
fn countCompleted(tasks: TaskList): Int
|
||||
is pure
|
||||
is total = {
|
||||
match tasks {
|
||||
[] => 0,
|
||||
[task, ...rest] =>
|
||||
(if task.done then 1 else 0) + countCompleted(rest)
|
||||
}
|
||||
}
|
||||
|
||||
// Commutative function: f(a, b) = f(b, a)
|
||||
// Enables parallel reduction and argument reordering optimizations
|
||||
fn maxPriority(a: String, b: String): String
|
||||
is pure
|
||||
is commutative = {
|
||||
let priorityValue = fn(p: String): Int =>
|
||||
match p {
|
||||
"high" => 3,
|
||||
"medium" => 2,
|
||||
"low" => 1,
|
||||
_ => 0
|
||||
}
|
||||
if priorityValue(a) > priorityValue(b) then a else b
|
||||
}
|
||||
|
||||
// Filter tasks by criteria - pure, can be cached and parallelized
|
||||
fn filterByPriority(tasks: TaskList, priority: String): TaskList
|
||||
is pure
|
||||
is deterministic = {
|
||||
List.filter(tasks, fn(t: Task@latest): Bool => t.priority == priority)
|
||||
}
|
||||
|
||||
fn filterPending(tasks: TaskList): TaskList
|
||||
is pure
|
||||
is deterministic = {
|
||||
List.filter(tasks, fn(t: Task@latest): Bool => !t.done)
|
||||
}
|
||||
|
||||
fn filterCompleted(tasks: TaskList): TaskList
|
||||
is pure
|
||||
is deterministic = {
|
||||
List.filter(tasks, fn(t: Task@latest): Bool => t.done)
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// PART 3: EFFECTS - EXPLICIT SIDE EFFECTS
|
||||
// =============================================================================
|
||||
|
||||
// Custom effect for task storage
|
||||
// This declares WHAT operations are available, not HOW they work
|
||||
effect TaskStore {
|
||||
fn save(task: Task@latest): Result<Task@latest, String>
|
||||
fn getById(id: String): Option<Task@latest>
|
||||
fn getAll(): TaskList
|
||||
fn delete(id: String): Bool
|
||||
}
|
||||
|
||||
// Service functions declare their effects in the type signature
|
||||
// Anyone reading the signature knows exactly what side effects can occur
|
||||
|
||||
// Create a new task - requires TaskStore and Random effects
|
||||
fn createTask(title: String, priority: String): Task@latest
|
||||
with {TaskStore, Random} = {
|
||||
let id = "task_" + toString(Random.int(10000, 99999))
|
||||
let task = {
|
||||
id: id,
|
||||
title: normalizeTitle(title), // Uses our idempotent normalizer
|
||||
done: false,
|
||||
priority: priority,
|
||||
dueDate: None,
|
||||
tags: []
|
||||
}
|
||||
match TaskStore.save(task) {
|
||||
Ok(saved) => saved,
|
||||
Err(_) => task // Return unsaved if storage fails
|
||||
}
|
||||
}
|
||||
|
||||
// Complete a task - idempotent, safe to retry
|
||||
// If the network fails mid-request, retry is safe
|
||||
fn completeTask(id: String): Option<Task@latest>
|
||||
is idempotent // Compiler verifies this is safe to retry
|
||||
with {TaskStore} = {
|
||||
match TaskStore.getById(id) {
|
||||
None => None,
|
||||
Some(task) => {
|
||||
// Setting done = true is idempotent: already done? stays done
|
||||
let updated = { ...task, done: true }
|
||||
match TaskStore.save(updated) {
|
||||
Ok(saved) => Some(saved),
|
||||
Err(_) => None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get task summary - logging effect, but computation is pure
|
||||
fn getTaskSummary(): { total: Int, completed: Int, pending: Int, highPriority: Int }
|
||||
with {TaskStore, Logger} = {
|
||||
let tasks = TaskStore.getAll()
|
||||
Logger.log("Fetched " + toString(List.length(tasks)) + " tasks")
|
||||
|
||||
// These computations are pure - could be parallelized
|
||||
let completed = countCompleted(tasks)
|
||||
let pending = List.length(tasks) - completed
|
||||
let highPriority = List.length(filterByPriority(tasks, "high"))
|
||||
|
||||
{ total: List.length(tasks), completed: completed, pending: pending, highPriority: highPriority }
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// PART 4: EFFECT HANDLERS - SWAP IMPLEMENTATIONS
|
||||
// =============================================================================
|
||||
|
||||
// In-memory handler for testing
|
||||
// This handler stores tasks in a mutable list - perfect for unit tests
|
||||
handler InMemoryTaskStore: TaskStore {
|
||||
let tasks: List<Task@latest> = []
|
||||
|
||||
fn save(task: Task@latest): Result<Task@latest, String> = {
|
||||
// Remove existing task with same ID (if any), then add new
|
||||
tasks = List.filter(tasks, fn(t: Task@latest): Bool => t.id != task.id)
|
||||
tasks = List.concat(tasks, [task])
|
||||
Ok(task)
|
||||
}
|
||||
|
||||
fn getById(id: String): Option<Task@latest> = {
|
||||
List.find(tasks, fn(t: Task@latest): Bool => t.id == id)
|
||||
}
|
||||
|
||||
fn getAll(): TaskList = tasks
|
||||
|
||||
fn delete(id: String): Bool = {
|
||||
let before = List.length(tasks)
|
||||
tasks = List.filter(tasks, fn(t: Task@latest): Bool => t.id != task.id)
|
||||
List.length(tasks) < before
|
||||
}
|
||||
}
|
||||
|
||||
// Logging handler - wraps another handler with logging
|
||||
handler LoggingTaskStore(inner: TaskStore): TaskStore with {Logger} {
|
||||
fn save(task: Task@latest): Result<Task@latest, String> = {
|
||||
Logger.log("Saving task: " + task.id)
|
||||
inner.save(task)
|
||||
}
|
||||
|
||||
fn getById(id: String): Option<Task@latest> = {
|
||||
Logger.log("Getting task: " + id)
|
||||
inner.getById(id)
|
||||
}
|
||||
|
||||
fn getAll(): TaskList = {
|
||||
Logger.log("Getting all tasks")
|
||||
inner.getAll()
|
||||
}
|
||||
|
||||
fn delete(id: String): Bool = {
|
||||
Logger.log("Deleting task: " + id)
|
||||
inner.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
// Simple logger effect and handler
|
||||
effect Logger {
|
||||
fn log(message: String): Unit
|
||||
}
|
||||
|
||||
handler ConsoleLogger: Logger with {Console} {
|
||||
fn log(message: String): Unit = {
|
||||
Console.print("[LOG] " + message)
|
||||
}
|
||||
}
|
||||
|
||||
handler SilentLogger: Logger {
|
||||
fn log(message: String): Unit = {
|
||||
// Do nothing - useful for tests
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// PART 5: TESTING - SWAP HANDLERS, NO MOCKS NEEDED
|
||||
// =============================================================================
|
||||
|
||||
// Test helper: creates a controlled environment
|
||||
fn runTestScenario(): Unit with {Console} = {
|
||||
Console.print("=== Running Test Scenario ===")
|
||||
Console.print("")
|
||||
|
||||
// Use in-memory storage and silent logging for tests
|
||||
// No database, no file I/O, no network - pure in-memory testing
|
||||
let result = run {
|
||||
// Create some tasks
|
||||
let task1 = createTask("Write documentation", "high")
|
||||
let task2 = createTask("Fix bug #123", "medium")
|
||||
let task3 = createTask("Review PR", "low")
|
||||
|
||||
// Complete one task
|
||||
completeTask(task1.id)
|
||||
|
||||
// Get summary
|
||||
getTaskSummary()
|
||||
} with {
|
||||
TaskStore = InMemoryTaskStore,
|
||||
Logger = SilentLogger,
|
||||
Random = {
|
||||
// Deterministic "random" for tests
|
||||
let counter = 0
|
||||
fn int(min: Int, max: Int): Int = {
|
||||
counter = counter + 1
|
||||
min + (counter * 12345) % (max - min)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.print("Test Results:")
|
||||
Console.print(" Total tasks: " + toString(result.total))
|
||||
Console.print(" Completed: " + toString(result.completed))
|
||||
Console.print(" Pending: " + toString(result.pending))
|
||||
Console.print(" High priority: " + toString(result.highPriority))
|
||||
Console.print("")
|
||||
|
||||
// Verify results
|
||||
if result.total == 3 &&
|
||||
result.completed == 1 &&
|
||||
result.pending == 2 &&
|
||||
result.highPriority == 1 {
|
||||
Console.print("All tests passed!")
|
||||
} else {
|
||||
Console.print("Test failed!")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// PART 6: SCHEMA MIGRATION DEMO
|
||||
// =============================================================================
|
||||
|
||||
fn demonstrateMigration(): Unit with {Console} = {
|
||||
Console.print("=== Schema Evolution Demo ===")
|
||||
Console.print("")
|
||||
|
||||
// Simulate loading a v1 task (from old database/API)
|
||||
let oldTask = Schema.versioned("Task", 1, {
|
||||
id: "legacy_001",
|
||||
title: "Old task from v1",
|
||||
done: false
|
||||
})
|
||||
|
||||
Console.print("Loaded v1 task:")
|
||||
Console.print(" Version: " + toString(Schema.getVersion(oldTask)))
|
||||
Console.print("")
|
||||
|
||||
// Migrate to latest version automatically
|
||||
let migratedTask = Schema.migrate(oldTask, 3)
|
||||
|
||||
Console.print("After migration to v3:")
|
||||
Console.print(" Version: " + toString(Schema.getVersion(migratedTask)))
|
||||
Console.print(" Has priority: " + migratedTask.priority) // Added by v2 migration
|
||||
Console.print(" Has tags: " + toString(List.length(migratedTask.tags)) + " tags") // Added by v3
|
||||
Console.print("")
|
||||
Console.print("Old data seamlessly upgraded!")
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// PART 7: MAIN - PUTTING IT ALL TOGETHER
|
||||
// =============================================================================
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("╔═══════════════════════════════════════════════════════════╗")
|
||||
Console.print("║ Lux Task Manager - Feature Showcase ║")
|
||||
Console.print("╚═══════════════════════════════════════════════════════════╝")
|
||||
Console.print("")
|
||||
|
||||
// Demonstrate pure functions
|
||||
Console.print("--- Pure Functions (Behavioral Types) ---")
|
||||
let sampleTask = {
|
||||
id: "demo",
|
||||
title: "Learn Lux",
|
||||
done: false,
|
||||
priority: "high",
|
||||
dueDate: None,
|
||||
tags: ["learning", "programming"]
|
||||
}
|
||||
Console.print("Formatted task: " + formatTask(sampleTask))
|
||||
Console.print("Normalized title: " + normalizeTitle(" HELLO WORLD "))
|
||||
Console.print("")
|
||||
|
||||
// Demonstrate schema evolution
|
||||
demonstrateMigration()
|
||||
Console.print("")
|
||||
|
||||
// Run tests with swapped handlers
|
||||
runTestScenario()
|
||||
Console.print("")
|
||||
|
||||
Console.print("╔═══════════════════════════════════════════════════════════╗")
|
||||
Console.print("║ Key Takeaways: ║")
|
||||
Console.print("║ ║")
|
||||
Console.print("║ 1. Effects in signatures = no hidden side effects ║")
|
||||
Console.print("║ 2. Behavioral types = compile-time guarantees ║")
|
||||
Console.print("║ 3. Handler swapping = easy testing without mocks ║")
|
||||
Console.print("║ 4. Schema evolution = safe data migrations ║")
|
||||
Console.print("╚═══════════════════════════════════════════════════════════╝")
|
||||
}
|
||||
|
||||
// Run the showcase
|
||||
let _ = run main() with {}
|
||||
@@ -1,14 +1,6 @@
|
||||
// Factorial - compute n!
|
||||
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
|
||||
|
||||
// Recursive version
|
||||
fn factorial(n: Int): Int =
|
||||
if n <= 1 then 1
|
||||
else n * factorial(n - 1)
|
||||
|
||||
// Tail-recursive version (optimized)
|
||||
fn factorialTail(n: Int, acc: Int): Int =
|
||||
if n <= 1 then acc
|
||||
else factorialTail(n - 1, n * acc)
|
||||
fn factorialTail(n: Int, acc: Int): Int = if n <= 1 then acc else factorialTail(n - 1, n * acc)
|
||||
|
||||
fn factorial2(n: Int): Int = factorialTail(n, 1)
|
||||
|
||||
|
||||
@@ -1,22 +1,11 @@
|
||||
// FizzBuzz - print numbers 1-100, but:
|
||||
// - multiples of 3: print "Fizz"
|
||||
// - multiples of 5: print "Buzz"
|
||||
// - multiples of both: print "FizzBuzz"
|
||||
|
||||
fn fizzbuzz(n: Int): String =
|
||||
if n % 15 == 0 then "FizzBuzz"
|
||||
else if n % 3 == 0 then "Fizz"
|
||||
else if n % 5 == 0 then "Buzz"
|
||||
else toString(n)
|
||||
fn fizzbuzz(n: Int): String = if n % 15 == 0 then "FizzBuzz" else if n % 3 == 0 then "Fizz" else if n % 5 == 0 then "Buzz" else toString(n)
|
||||
|
||||
fn printFizzbuzz(i: Int, max: Int): Unit with {Console} =
|
||||
if i > max then ()
|
||||
else {
|
||||
if i > max then () else {
|
||||
Console.print(fizzbuzz(i))
|
||||
printFizzbuzz(i + 1, max)
|
||||
}
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} =
|
||||
printFizzbuzz(1, 100)
|
||||
fn main(): Unit with {Console} = printFizzbuzz(1, 100)
|
||||
|
||||
let output = run main() with {}
|
||||
|
||||
@@ -1,42 +1,17 @@
|
||||
// Number guessing game - demonstrates Random and Console effects
|
||||
//
|
||||
// Expected output:
|
||||
// Welcome to the Guessing Game!
|
||||
// Target number: 42
|
||||
// Simulating guesses...
|
||||
// Guess 50: Too high!
|
||||
// Guess 25: Too low!
|
||||
// Guess 37: Too low!
|
||||
// Guess 43: Too high!
|
||||
// Guess 40: Too low!
|
||||
// Guess 41: Too low!
|
||||
// Guess 42: Correct!
|
||||
// Found in 7 attempts!
|
||||
fn checkGuess(guess: Int, secret: Int): String = if guess == secret then "Correct" else if guess < secret then "Too low" else "Too high"
|
||||
|
||||
// Game logic - check a guess against the secret
|
||||
fn checkGuess(guess: Int, secret: Int): String =
|
||||
if guess == secret then "Correct"
|
||||
else if guess < secret then "Too low"
|
||||
else "Too high"
|
||||
|
||||
// Binary search simulation to find the number
|
||||
fn binarySearch(low: Int, high: Int, secret: Int, attempts: Int): Int with {Console} = {
|
||||
let mid = (low + high) / 2
|
||||
let mid = low + high / 2
|
||||
let result = checkGuess(mid, secret)
|
||||
Console.print("Guess " + toString(mid) + ": " + result + "!")
|
||||
|
||||
if result == "Correct" then attempts
|
||||
else if result == "Too low" then binarySearch(mid + 1, high, secret, attempts + 1)
|
||||
else binarySearch(low, mid - 1, secret, attempts + 1)
|
||||
if result == "Correct" then attempts else if result == "Too low" then binarySearch(mid + 1, high, secret, attempts + 1) else binarySearch(low, mid - 1, secret, attempts + 1)
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("Welcome to the Guessing Game!")
|
||||
// Use a fixed "secret" for reproducible output
|
||||
let secret = 42
|
||||
Console.print("Target number: " + toString(secret))
|
||||
Console.print("Simulating guesses...")
|
||||
|
||||
let attempts = binarySearch(1, 100, secret, 1)
|
||||
Console.print("Found in " + toString(attempts) + " attempts!")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// The classic first program
|
||||
// Expected output: Hello, World!
|
||||
|
||||
fn main(): Unit with {Console} =
|
||||
Console.print("Hello, World!")
|
||||
fn main(): Unit with {Console} = Console.print("Hello, World!")
|
||||
|
||||
let output = run main() with {}
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
// Prime number utilities
|
||||
fn isPrime(n: Int): Bool = if n < 2 then false else isPrimeHelper(n, 2)
|
||||
|
||||
fn isPrime(n: Int): Bool =
|
||||
if n < 2 then false
|
||||
else isPrimeHelper(n, 2)
|
||||
fn isPrimeHelper(n: Int, i: Int): Bool = if i * i > n then true else if n % i == 0 then false else isPrimeHelper(n, i + 1)
|
||||
|
||||
fn isPrimeHelper(n: Int, i: Int): Bool =
|
||||
if i * i > n then true
|
||||
else if n % i == 0 then false
|
||||
else isPrimeHelper(n, i + 1)
|
||||
|
||||
// Find first n primes
|
||||
fn findPrimes(count: Int): Unit with {Console} =
|
||||
findPrimesHelper(2, count)
|
||||
fn findPrimes(count: Int): Unit with {Console} = findPrimesHelper(2, count)
|
||||
|
||||
fn findPrimesHelper(current: Int, remaining: Int): Unit with {Console} =
|
||||
if remaining <= 0 then ()
|
||||
else if isPrime(current) then {
|
||||
if remaining <= 0 then () else if isPrime(current) then {
|
||||
Console.print(toString(current))
|
||||
findPrimesHelper(current + 1, remaining - 1)
|
||||
}
|
||||
else findPrimesHelper(current + 1, remaining)
|
||||
} else findPrimesHelper(current + 1, remaining)
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("First 20 prime numbers:")
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// Standard Library Demo
|
||||
// Demonstrates the built-in modules: List, String, Option, Math
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== List Operations ===")
|
||||
let nums = [1, 2, 3, 4, 5]
|
||||
@@ -11,7 +8,6 @@ fn main(): Unit with {Console} = {
|
||||
Console.print("Length: " + toString(List.length(nums)))
|
||||
Console.print("Reversed: " + toString(List.reverse(nums)))
|
||||
Console.print("Range 1-5: " + toString(List.range(1, 6)))
|
||||
|
||||
Console.print("")
|
||||
Console.print("=== String Operations ===")
|
||||
let text = " Hello, World! "
|
||||
@@ -22,7 +18,6 @@ fn main(): Unit with {Console} = {
|
||||
Console.print("Contains 'World': " + toString(String.contains(text, "World")))
|
||||
Console.print("Split by comma: " + toString(String.split("a,b,c", ",")))
|
||||
Console.print("Join with dash: " + String.join(["x", "y", "z"], "-"))
|
||||
|
||||
Console.print("")
|
||||
Console.print("=== Option Operations ===")
|
||||
let some_val = Some(42)
|
||||
@@ -31,7 +26,6 @@ fn main(): Unit with {Console} = {
|
||||
Console.print("None mapped: " + toString(Option.map(none_val, fn(x: Int): Int => x * 2)))
|
||||
Console.print("Some(42) getOrElse(0): " + toString(Option.getOrElse(some_val, 0)))
|
||||
Console.print("None getOrElse(0): " + toString(Option.getOrElse(none_val, 0)))
|
||||
|
||||
Console.print("")
|
||||
Console.print("=== Math Operations ===")
|
||||
Console.print("abs(-5): " + toString(Math.abs(-5)))
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
// State machine example using algebraic data types
|
||||
// Demonstrates pattern matching for state transitions
|
||||
//
|
||||
// Expected output:
|
||||
// Initial light: red
|
||||
// After transition: green
|
||||
// After two transitions: yellow
|
||||
// Door: Closed -> Open -> Closed -> Locked
|
||||
|
||||
// Traffic light state machine
|
||||
type TrafficLight =
|
||||
| Red
|
||||
| Yellow
|
||||
@@ -17,24 +7,23 @@ fn nextLight(light: TrafficLight): TrafficLight =
|
||||
match light {
|
||||
Red => Green,
|
||||
Green => Yellow,
|
||||
Yellow => Red
|
||||
}
|
||||
Yellow => Red,
|
||||
}
|
||||
|
||||
fn canGo(light: TrafficLight): Bool =
|
||||
match light {
|
||||
Green => true,
|
||||
Yellow => false,
|
||||
Red => false
|
||||
}
|
||||
Red => false,
|
||||
}
|
||||
|
||||
fn lightColor(light: TrafficLight): String =
|
||||
match light {
|
||||
Red => "red",
|
||||
Yellow => "yellow",
|
||||
Green => "green"
|
||||
}
|
||||
Green => "green",
|
||||
}
|
||||
|
||||
// Door state machine
|
||||
type DoorState =
|
||||
| Open
|
||||
| Closed
|
||||
@@ -52,27 +41,30 @@ fn applyAction(state: DoorState, action: DoorAction): DoorState =
|
||||
(Open, CloseDoor) => Closed,
|
||||
(Closed, LockDoor) => Locked,
|
||||
(Locked, UnlockDoor) => Closed,
|
||||
_ => state
|
||||
}
|
||||
_ => state,
|
||||
}
|
||||
|
||||
fn doorStateName(state: DoorState): String =
|
||||
match state {
|
||||
Open => "Open",
|
||||
Closed => "Closed",
|
||||
Locked => "Locked"
|
||||
}
|
||||
Locked => "Locked",
|
||||
}
|
||||
|
||||
// Test the state machines
|
||||
let light1 = Red
|
||||
|
||||
let light2 = nextLight(light1)
|
||||
|
||||
let light3 = nextLight(light2)
|
||||
|
||||
let door1 = Closed
|
||||
|
||||
let door2 = applyAction(door1, OpenDoor)
|
||||
|
||||
let door3 = applyAction(door2, CloseDoor)
|
||||
|
||||
let door4 = applyAction(door3, LockDoor)
|
||||
|
||||
// Print results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("Initial light: " + lightColor(light1))
|
||||
Console.print("After transition: " + lightColor(light2))
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
// Stress test for RC system with large lists
|
||||
// Tests FBIP optimization with single-owner chains
|
||||
|
||||
fn processChain(n: Int): Int = {
|
||||
// Single owner chain - FBIP should reuse lists
|
||||
let nums = List.range(1, n)
|
||||
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
||||
let filtered = List.filter(doubled, fn(x: Int): Bool => x > n)
|
||||
@@ -12,13 +8,10 @@ fn processChain(n: Int): Int = {
|
||||
|
||||
fn main(): Unit = {
|
||||
Console.print("=== RC Stress Test ===")
|
||||
|
||||
// Run multiple iterations of list operations
|
||||
let result1 = processChain(100)
|
||||
let result2 = processChain(200)
|
||||
let result3 = processChain(500)
|
||||
let result4 = processChain(1000)
|
||||
|
||||
Console.print("Completed 4 chains")
|
||||
Console.print("Sizes: 100, 200, 500, 1000")
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
// Stress test for RC system WITH shared references
|
||||
// Forces rc>1 path by keeping aliases
|
||||
|
||||
fn processWithAlias(n: Int): Int = {
|
||||
let nums = List.range(1, n)
|
||||
let alias = nums // This increments rc, forcing copy path
|
||||
let _len = List.length(alias) // Use the alias
|
||||
|
||||
// Now nums has rc>1, so map must allocate new
|
||||
let alias = nums
|
||||
let _len = List.length(alias)
|
||||
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
||||
let filtered = List.filter(doubled, fn(x: Int): Bool => x > n)
|
||||
let reversed = List.reverse(filtered)
|
||||
@@ -15,12 +10,9 @@ fn processWithAlias(n: Int): Int = {
|
||||
|
||||
fn main(): Unit = {
|
||||
Console.print("=== RC Stress Test (Shared Refs) ===")
|
||||
|
||||
// Run multiple iterations with shared references
|
||||
let result1 = processWithAlias(100)
|
||||
let result2 = processWithAlias(200)
|
||||
let result3 = processWithAlias(500)
|
||||
let result4 = processWithAlias(1000)
|
||||
|
||||
Console.print("Completed 4 chains with shared refs")
|
||||
}
|
||||
|
||||
@@ -1,45 +1,25 @@
|
||||
// Demonstrating tail call optimization (TCO) in Lux
|
||||
// TCO allows recursive functions to run in constant stack space
|
||||
//
|
||||
// Expected output:
|
||||
// factorial(20) = 2432902008176640000
|
||||
// fib(30) = 832040
|
||||
// sumTo(1000) = 500500
|
||||
// countdown(10000) completed
|
||||
|
||||
// Factorial with accumulator - tail recursive
|
||||
fn factorialTCO(n: Int, acc: Int): Int =
|
||||
if n <= 1 then acc
|
||||
else factorialTCO(n - 1, n * acc)
|
||||
fn factorialTCO(n: Int, acc: Int): Int = if n <= 1 then acc else factorialTCO(n - 1, n * acc)
|
||||
|
||||
fn factorial(n: Int): Int = factorialTCO(n, 1)
|
||||
|
||||
// Fibonacci with accumulator - tail recursive
|
||||
fn fibTCO(n: Int, a: Int, b: Int): Int =
|
||||
if n <= 0 then a
|
||||
else fibTCO(n - 1, b, a + b)
|
||||
fn fibTCO(n: Int, a: Int, b: Int): Int = if n <= 0 then a else fibTCO(n - 1, b, a + b)
|
||||
|
||||
fn fib(n: Int): Int = fibTCO(n, 0, 1)
|
||||
|
||||
// Count down - simple tail recursion
|
||||
fn countdown(n: Int): Int =
|
||||
if n <= 0 then 0
|
||||
else countdown(n - 1)
|
||||
fn countdown(n: Int): Int = if n <= 0 then 0 else countdown(n - 1)
|
||||
|
||||
// Sum with accumulator - tail recursive
|
||||
fn sumToTCO(n: Int, acc: Int): Int =
|
||||
if n <= 0 then acc
|
||||
else sumToTCO(n - 1, acc + n)
|
||||
fn sumToTCO(n: Int, acc: Int): Int = if n <= 0 then acc else sumToTCO(n - 1, acc + n)
|
||||
|
||||
fn sumTo(n: Int): Int = sumToTCO(n, 0)
|
||||
|
||||
// Test the functions
|
||||
let fact20 = factorial(20)
|
||||
|
||||
let fib30 = fib(30)
|
||||
|
||||
let sum1000 = sumTo(1000)
|
||||
|
||||
let countResult = countdown(10000)
|
||||
|
||||
// Print results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("factorial(20) = " + toString(fact20))
|
||||
Console.print("fib(30) = " + toString(fib30))
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
// This test shows FBIP optimization by comparing allocation counts
|
||||
// With FBIP (rc=1): lists are reused in-place
|
||||
// Without FBIP (rc>1): new lists are allocated
|
||||
|
||||
fn main(): Unit = {
|
||||
Console.print("=== FBIP Allocation Test ===")
|
||||
|
||||
// Case 1: Single owner (FBIP active) - should reuse list
|
||||
let a = List.range(1, 100)
|
||||
let b = List.map(a, fn(x: Int): Int => x * 2)
|
||||
let c = List.filter(b, fn(x: Int): Bool => x > 50)
|
||||
let d = List.reverse(c)
|
||||
Console.print("Single owner chain done")
|
||||
|
||||
// The allocation count will show FBIP is working
|
||||
// if allocations are low relative to operations performed
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
fn main(): Unit = {
|
||||
// Test FBIP without string operations
|
||||
let nums = [1, 2, 3, 4, 5]
|
||||
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
||||
let filtered = List.filter(doubled, fn(x: Int): Bool => x > 4)
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// List Operations Test Suite
|
||||
// Run with: lux test examples/test_lists.lux
|
||||
|
||||
fn test_list_length(): Unit with {Test} = {
|
||||
Test.assertEqual(0, List.length([]))
|
||||
Test.assertEqual(1, List.length([1]))
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// Math Test Suite
|
||||
// Run with: lux test examples/test_math.lux
|
||||
|
||||
fn test_addition(): Unit with {Test} = {
|
||||
Test.assertEqual(4, 2 + 2)
|
||||
Test.assertEqual(0, 0 + 0)
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
// Test demonstrating ownership transfer with aliases
|
||||
// The ownership transfer optimization ensures FBIP still works
|
||||
// even when variables are aliased, because ownership is transferred
|
||||
// rather than reference count being incremented.
|
||||
|
||||
fn main(): Unit = {
|
||||
Console.print("=== Ownership Transfer Test ===")
|
||||
|
||||
let a = List.range(1, 100)
|
||||
// Ownership transfers from 'a' to 'alias', keeping rc=1
|
||||
let alias = a
|
||||
let len1 = List.length(alias)
|
||||
|
||||
// Since ownership transferred, 'a' still has rc=1
|
||||
// FBIP can still optimize map/filter/reverse
|
||||
let b = List.map(a, fn(x: Int): Int => x * 2)
|
||||
let c = List.filter(b, fn(x: Int): Bool => x > 50)
|
||||
let d = List.reverse(c)
|
||||
|
||||
Console.print("Ownership transfer chain done")
|
||||
}
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
fn main(): Unit = {
|
||||
Console.print("=== Allocation Comparison ===")
|
||||
|
||||
// FBIP path (rc=1): list is reused
|
||||
Console.print("Test 1: FBIP path")
|
||||
let a1 = List.range(1, 50)
|
||||
let b1 = List.map(a1, fn(x: Int): Int => x * 2)
|
||||
let c1 = List.reverse(b1)
|
||||
Console.print("FBIP done")
|
||||
|
||||
// To show non-FBIP, we need concat which doesn't have FBIP
|
||||
Console.print("Test 2: Non-FBIP path (concat)")
|
||||
let x = List.range(1, 25)
|
||||
let y = List.range(26, 50)
|
||||
let z = List.concat(x, y) // concat always allocates new
|
||||
let z = List.concat(x, y)
|
||||
Console.print("Concat done")
|
||||
}
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
// Demonstrating type classes (traits) in Lux
|
||||
//
|
||||
// Expected output:
|
||||
// RGB color: rgb(255,128,0)
|
||||
// Red color: red
|
||||
// Green color: green
|
||||
|
||||
// Define a simple Printable trait
|
||||
trait Printable {
|
||||
fn format(value: Int): String
|
||||
}
|
||||
|
||||
// Implement Printable
|
||||
impl Printable for Int {
|
||||
fn format(value: Int): String = "Number: " + toString(value)
|
||||
}
|
||||
|
||||
// A Color type with pattern matching
|
||||
type Color =
|
||||
| Red
|
||||
| Green
|
||||
@@ -27,15 +17,15 @@ fn colorName(c: Color): String =
|
||||
Red => "red",
|
||||
Green => "green",
|
||||
Blue => "blue",
|
||||
RGB(r, g, b) => "rgb(" + toString(r) + "," + toString(g) + "," + toString(b) + ")"
|
||||
}
|
||||
RGB(r, g, b) => "rgb(" + toString(r) + "," + toString(g) + "," + toString(b) + ")",
|
||||
}
|
||||
|
||||
// Test
|
||||
let myColor = RGB(255, 128, 0)
|
||||
|
||||
let redColor = Red
|
||||
|
||||
let greenColor = Green
|
||||
|
||||
// Print results
|
||||
fn printResults(): Unit with {Console} = {
|
||||
Console.print("RGB color: " + colorName(myColor))
|
||||
Console.print("Red color: " + colorName(redColor))
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
// Demonstrating Schema Evolution in Lux
|
||||
//
|
||||
// Lux provides versioned types to help manage data evolution over time.
|
||||
// The Schema module provides functions for creating and migrating versioned values.
|
||||
//
|
||||
// Expected output:
|
||||
// Created user v1: Alice (age unknown)
|
||||
// User version: 1
|
||||
// Migrated to v2: Alice (age unknown)
|
||||
// User version after migration: 2
|
||||
|
||||
// Create a versioned User value at v1
|
||||
fn createUserV1(name: String): Unit with {Console} = {
|
||||
let user = Schema.versioned("User", 1, { name: name })
|
||||
let version = Schema.getVersion(user)
|
||||
@@ -17,7 +5,6 @@ fn createUserV1(name: String): Unit with {Console} = {
|
||||
Console.print("User version: " + toString(version))
|
||||
}
|
||||
|
||||
// Migrate a user to v2
|
||||
fn migrateUserToV2(name: String): Unit with {Console} = {
|
||||
let userV1 = Schema.versioned("User", 1, { name: name })
|
||||
let userV2 = Schema.migrate(userV1, 2)
|
||||
@@ -26,7 +13,6 @@ fn migrateUserToV2(name: String): Unit with {Console} = {
|
||||
Console.print("User version after migration: " + toString(newVersion))
|
||||
}
|
||||
|
||||
// Main
|
||||
fn main(): Unit with {Console} = {
|
||||
createUserV1("Alice")
|
||||
migrateUserToV2("Alice")
|
||||
|
||||
@@ -1,54 +1,30 @@
|
||||
// Simple Counter for Browser
|
||||
// Compile with: lux compile examples/web/counter.lux --target js -o examples/web/counter.js
|
||||
type Model =
|
||||
| Counter(Int)
|
||||
|
||||
// ============================================================================
|
||||
// Model
|
||||
// ============================================================================
|
||||
|
||||
type Model = | Counter(Int)
|
||||
|
||||
fn getCount(m: Model): Int = match m { Counter(n) => n }
|
||||
fn getCount(m: Model): Int =
|
||||
match m {
|
||||
Counter(n) => n,
|
||||
}
|
||||
|
||||
fn init(): Model = Counter(0)
|
||||
|
||||
// ============================================================================
|
||||
// Messages
|
||||
// ============================================================================
|
||||
|
||||
type Msg = | Increment | Decrement | Reset
|
||||
|
||||
// ============================================================================
|
||||
// Update
|
||||
// ============================================================================
|
||||
type Msg =
|
||||
| Increment
|
||||
| Decrement
|
||||
| Reset
|
||||
|
||||
fn update(model: Model, msg: Msg): Model =
|
||||
match msg {
|
||||
Increment => Counter(getCount(model) + 1),
|
||||
Decrement => Counter(getCount(model) - 1),
|
||||
Reset => Counter(0)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// View - Returns HTML string for simplicity
|
||||
// ============================================================================
|
||||
Reset => Counter(0),
|
||||
}
|
||||
|
||||
fn view(model: Model): String = {
|
||||
let count = getCount(model)
|
||||
"<div class=\"counter\">" +
|
||||
"<h1>Lux Counter</h1>" +
|
||||
"<div class=\"display\">" + toString(count) + "</div>" +
|
||||
"<div class=\"buttons\">" +
|
||||
"<button onclick=\"dispatch('Decrement')\">-</button>" +
|
||||
"<button onclick=\"dispatch('Reset')\">Reset</button>" +
|
||||
"<button onclick=\"dispatch('Increment')\">+</button>" +
|
||||
"</div>" +
|
||||
"</div>"
|
||||
"<div class=\"counter\">" + "<h1>Lux Counter</h1>" + "<div class=\"display\">" + toString(count) + "</div>" + "<div class=\"buttons\">" + "<button onclick=\"dispatch('Decrement')\">-</button>" + "<button onclick=\"dispatch('Reset')\">Reset</button>" + "<button onclick=\"dispatch('Increment')\">+</button>" + "</div>" + "</div>"
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Export for browser runtime
|
||||
// ============================================================================
|
||||
|
||||
fn luxInit(): Model = init()
|
||||
|
||||
fn luxUpdate(model: Model, msgName: String): Model =
|
||||
@@ -56,7 +32,7 @@ fn luxUpdate(model: Model, msgName: String): Model =
|
||||
"Increment" => update(model, Increment),
|
||||
"Decrement" => update(model, Decrement),
|
||||
"Reset" => update(model, Reset),
|
||||
_ => model
|
||||
}
|
||||
_ => model,
|
||||
}
|
||||
|
||||
fn luxView(model: Model): String = view(model)
|
||||
|
||||
129
flake.nix
129
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,11 @@
|
||||
rustToolchain
|
||||
cargo-watch
|
||||
cargo-edit
|
||||
pkg-config
|
||||
openssl
|
||||
# Static builds
|
||||
pkgsStatic.stdenv.cc
|
||||
# Benchmark tools
|
||||
hyperfine
|
||||
poop
|
||||
];
|
||||
|
||||
RUST_BACKTRACE = "1";
|
||||
@@ -40,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.4\n"
|
||||
printf "\n"
|
||||
printf " Functional language with first-class effects\n"
|
||||
printf "\n"
|
||||
@@ -58,12 +62,125 @@
|
||||
|
||||
packages.default = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "lux";
|
||||
version = "0.1.0";
|
||||
version = "0.1.4";
|
||||
src = ./.;
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
|
||||
nativeBuildInputs = [ pkgs.pkg-config ];
|
||||
buildInputs = [ pkgs.openssl ];
|
||||
doCheck = false;
|
||||
};
|
||||
|
||||
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.4";
|
||||
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";
|
||||
program = toString (pkgs.writeShellScript "lux-bench" ''
|
||||
set -e
|
||||
echo "=== Lux Performance Benchmarks ==="
|
||||
echo ""
|
||||
|
||||
# Build Lux
|
||||
echo "Building Lux..."
|
||||
cd ${self}
|
||||
${pkgs.cargo}/bin/cargo build --release 2>/dev/null
|
||||
|
||||
# Compile benchmarks
|
||||
echo "Compiling benchmark binaries..."
|
||||
./target/release/lux compile benchmarks/fib.lux -o /tmp/fib_lux 2>/dev/null
|
||||
${pkgs.gcc}/bin/gcc -O3 benchmarks/fib.c -o /tmp/fib_c 2>/dev/null
|
||||
${pkgs.rustc}/bin/rustc -C opt-level=3 -C lto benchmarks/fib.rs -o /tmp/fib_rust 2>/dev/null
|
||||
${pkgs.zig}/bin/zig build-exe benchmarks/fib.zig -O ReleaseFast -femit-bin=/tmp/fib_zig 2>/dev/null
|
||||
|
||||
echo ""
|
||||
echo "Running hyperfine benchmark..."
|
||||
echo ""
|
||||
${pkgs.hyperfine}/bin/hyperfine --warmup 3 --runs 10 \
|
||||
--export-markdown /tmp/bench_results.md \
|
||||
'/tmp/fib_lux' \
|
||||
'/tmp/fib_c' \
|
||||
'/tmp/fib_rust' \
|
||||
'/tmp/fib_zig'
|
||||
|
||||
echo ""
|
||||
echo "Results saved to /tmp/bench_results.md"
|
||||
'');
|
||||
};
|
||||
|
||||
# Run poop benchmark for detailed CPU metrics
|
||||
bench-poop = {
|
||||
type = "app";
|
||||
program = toString (pkgs.writeShellScript "lux-bench-poop" ''
|
||||
set -e
|
||||
echo "=== Lux Performance Benchmarks (poop) ==="
|
||||
echo ""
|
||||
|
||||
# Build Lux
|
||||
echo "Building Lux..."
|
||||
cd ${self}
|
||||
${pkgs.cargo}/bin/cargo build --release 2>/dev/null
|
||||
|
||||
# Compile benchmarks
|
||||
echo "Compiling benchmark binaries..."
|
||||
./target/release/lux compile benchmarks/fib.lux -o /tmp/fib_lux 2>/dev/null
|
||||
${pkgs.gcc}/bin/gcc -O3 benchmarks/fib.c -o /tmp/fib_c 2>/dev/null
|
||||
${pkgs.rustc}/bin/rustc -C opt-level=3 -C lto benchmarks/fib.rs -o /tmp/fib_rust 2>/dev/null
|
||||
${pkgs.zig}/bin/zig build-exe benchmarks/fib.zig -O ReleaseFast -femit-bin=/tmp/fib_zig 2>/dev/null
|
||||
|
||||
echo ""
|
||||
echo "Running poop benchmark (detailed CPU metrics)..."
|
||||
echo ""
|
||||
${pkgs.poop}/bin/poop '/tmp/fib_c' '/tmp/fib_lux' '/tmp/fib_rust' '/tmp/fib_zig'
|
||||
'');
|
||||
};
|
||||
|
||||
# Quick benchmark (just Lux vs C)
|
||||
bench-quick = {
|
||||
type = "app";
|
||||
program = toString (pkgs.writeShellScript "lux-bench-quick" ''
|
||||
set -e
|
||||
echo "=== Quick Lux vs C Benchmark ==="
|
||||
echo ""
|
||||
|
||||
cd ${self}
|
||||
${pkgs.cargo}/bin/cargo build --release 2>/dev/null
|
||||
./target/release/lux compile benchmarks/fib.lux -o /tmp/fib_lux 2>/dev/null
|
||||
${pkgs.gcc}/bin/gcc -O3 benchmarks/fib.c -o /tmp/fib_c 2>/dev/null
|
||||
|
||||
${pkgs.hyperfine}/bin/hyperfine --warmup 3 '/tmp/fib_lux' '/tmp/fib_c'
|
||||
'');
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
225
projects/lux-compiler/ast.lux
Normal file
225
projects/lux-compiler/ast.lux
Normal file
@@ -0,0 +1,225 @@
|
||||
// Lux AST — Self-hosted Abstract Syntax Tree definitions
|
||||
//
|
||||
// Direct translation of src/ast.rs into Lux ADTs.
|
||||
// These types represent the parsed structure of a Lux program.
|
||||
//
|
||||
// Naming conventions to avoid collisions:
|
||||
// Ex = Expr variant, Pat = Pattern, Te = TypeExpr
|
||||
// Td = TypeDef, Vf = VariantFields, Op = Operator
|
||||
// Decl = Declaration, St = Statement
|
||||
|
||||
// === Source Location ===
|
||||
|
||||
type Span = | Span(Int, Int)
|
||||
|
||||
// === Identifiers ===
|
||||
|
||||
type Ident = | Ident(String, Span)
|
||||
|
||||
// === Visibility ===
|
||||
|
||||
type Visibility = | Public | Private
|
||||
|
||||
// === Schema Evolution ===
|
||||
|
||||
type Version = | Version(Int, Span)
|
||||
|
||||
type VersionConstraint =
|
||||
| VcExact(Version)
|
||||
| VcAtLeast(Version)
|
||||
| VcLatest(Span)
|
||||
|
||||
// === Behavioral Types ===
|
||||
|
||||
type BehavioralProperty =
|
||||
| BpPure
|
||||
| BpTotal
|
||||
| BpIdempotent
|
||||
| BpDeterministic
|
||||
| BpCommutative
|
||||
|
||||
// === Trait Bound (needed before WhereClause) ===
|
||||
|
||||
type TraitBound = | TraitBound(Ident, List<TypeExpr>, Span)
|
||||
|
||||
// === Trait Constraint (needed before WhereClause) ===
|
||||
|
||||
type TraitConstraint = | TraitConstraint(Ident, List<TraitBound>, Span)
|
||||
|
||||
// === Where Clauses ===
|
||||
|
||||
type WhereClause =
|
||||
| WcProperty(Ident, BehavioralProperty, Span)
|
||||
| WcResult(Expr, Span)
|
||||
| WcTrait(TraitConstraint)
|
||||
|
||||
// === Module Path ===
|
||||
|
||||
type ModulePath = | ModulePath(List<Ident>, Span)
|
||||
|
||||
// === Import ===
|
||||
|
||||
// path, alias, items, wildcard, span
|
||||
type ImportDecl = | ImportDecl(ModulePath, Option<Ident>, Option<List<Ident>>, Bool, Span)
|
||||
|
||||
// === Program ===
|
||||
|
||||
type Program = | Program(List<ImportDecl>, List<Declaration>)
|
||||
|
||||
// === Declarations ===
|
||||
|
||||
type Declaration =
|
||||
| DeclFunction(FunctionDecl)
|
||||
| DeclEffect(EffectDecl)
|
||||
| DeclType(TypeDecl)
|
||||
| DeclHandler(HandlerDecl)
|
||||
| DeclLet(LetDecl)
|
||||
| DeclTrait(TraitDecl)
|
||||
| DeclImpl(ImplDecl)
|
||||
|
||||
// === Parameter ===
|
||||
|
||||
type Parameter = | Parameter(Ident, TypeExpr, Span)
|
||||
|
||||
// === Effect Operation ===
|
||||
|
||||
type EffectOp = | EffectOp(Ident, List<Parameter>, TypeExpr, Span)
|
||||
|
||||
// === Record Field ===
|
||||
|
||||
type RecordField = | RecordField(Ident, TypeExpr, Span)
|
||||
|
||||
// === Variant Fields ===
|
||||
|
||||
type VariantFields =
|
||||
| VfUnit
|
||||
| VfTuple(List<TypeExpr>)
|
||||
| VfRecord(List<RecordField>)
|
||||
|
||||
// === Variant ===
|
||||
|
||||
type Variant = | Variant(Ident, VariantFields, Span)
|
||||
|
||||
// === Migration ===
|
||||
|
||||
type Migration = | Migration(Version, Expr, Span)
|
||||
|
||||
// === Handler Impl ===
|
||||
|
||||
// op_name, params, resume, body, span
|
||||
type HandlerImpl = | HandlerImpl(Ident, List<Ident>, Option<Ident>, Expr, Span)
|
||||
|
||||
// === Impl Method ===
|
||||
|
||||
// name, params, return_type, body, span
|
||||
type ImplMethod = | ImplMethod(Ident, List<Parameter>, Option<TypeExpr>, Expr, Span)
|
||||
|
||||
// === Trait Method ===
|
||||
|
||||
// name, type_params, params, return_type, default_impl, span
|
||||
type TraitMethod = | TraitMethod(Ident, List<Ident>, List<Parameter>, TypeExpr, Option<Expr>, Span)
|
||||
|
||||
// === Type Expressions ===
|
||||
|
||||
type TypeExpr =
|
||||
| TeNamed(Ident)
|
||||
| TeApp(TypeExpr, List<TypeExpr>)
|
||||
| TeFunction(List<TypeExpr>, TypeExpr, List<Ident>)
|
||||
| TeTuple(List<TypeExpr>)
|
||||
| TeRecord(List<RecordField>)
|
||||
| TeUnit
|
||||
| TeVersioned(TypeExpr, VersionConstraint)
|
||||
|
||||
// === Literal ===
|
||||
|
||||
type LiteralKind =
|
||||
| LitInt(Int)
|
||||
| LitFloat(String)
|
||||
| LitString(String)
|
||||
| LitChar(Char)
|
||||
| LitBool(Bool)
|
||||
| LitUnit
|
||||
|
||||
type Literal = | Literal(LiteralKind, Span)
|
||||
|
||||
// === Binary Operators ===
|
||||
|
||||
type BinaryOp =
|
||||
| OpAdd | OpSub | OpMul | OpDiv | OpMod
|
||||
| OpEq | OpNe | OpLt | OpLe | OpGt | OpGe
|
||||
| OpAnd | OpOr
|
||||
| OpPipe | OpConcat
|
||||
|
||||
// === Unary Operators ===
|
||||
|
||||
type UnaryOp = | OpNeg | OpNot
|
||||
|
||||
// === Statements ===
|
||||
|
||||
type Statement =
|
||||
| StExpr(Expr)
|
||||
| StLet(Ident, Option<TypeExpr>, Expr, Span)
|
||||
|
||||
// === Match Arms ===
|
||||
|
||||
type MatchArm = | MatchArm(Pattern, Option<Expr>, Expr, Span)
|
||||
|
||||
// === Patterns ===
|
||||
|
||||
type Pattern =
|
||||
| PatWildcard(Span)
|
||||
| PatVar(Ident)
|
||||
| PatLiteral(Literal)
|
||||
| PatConstructor(Ident, List<Pattern>, Span)
|
||||
| PatRecord(List<(Ident, Pattern)>, Span)
|
||||
| PatTuple(List<Pattern>, Span)
|
||||
|
||||
// === Function Declaration ===
|
||||
// visibility, doc, name, type_params, params, return_type, effects, properties, where_clauses, body, span
|
||||
type FunctionDecl = | FunctionDecl(Visibility, Option<String>, Ident, List<Ident>, List<Parameter>, TypeExpr, List<Ident>, List<BehavioralProperty>, List<WhereClause>, Expr, Span)
|
||||
|
||||
// === Effect Declaration ===
|
||||
// doc, name, type_params, operations, span
|
||||
type EffectDecl = | EffectDecl(Option<String>, Ident, List<Ident>, List<EffectOp>, Span)
|
||||
|
||||
// === Type Declaration ===
|
||||
// visibility, doc, name, type_params, version, definition, migrations, span
|
||||
type TypeDecl = | TypeDecl(Visibility, Option<String>, Ident, List<Ident>, Option<Version>, TypeDef, List<Migration>, Span)
|
||||
|
||||
// === Handler Declaration ===
|
||||
// name, params, effect, implementations, span
|
||||
type HandlerDecl = | HandlerDecl(Ident, List<Parameter>, Ident, List<HandlerImpl>, Span)
|
||||
|
||||
// === Let Declaration ===
|
||||
// visibility, doc, name, typ, value, span
|
||||
type LetDecl = | LetDecl(Visibility, Option<String>, Ident, Option<TypeExpr>, Expr, Span)
|
||||
|
||||
// === Trait Declaration ===
|
||||
// visibility, doc, name, type_params, super_traits, methods, span
|
||||
type TraitDecl = | TraitDecl(Visibility, Option<String>, Ident, List<Ident>, List<TraitBound>, List<TraitMethod>, Span)
|
||||
|
||||
// === Impl Declaration ===
|
||||
// type_params, constraints, trait_name, trait_args, target_type, methods, span
|
||||
type ImplDecl = | ImplDecl(List<Ident>, List<TraitConstraint>, Ident, List<TypeExpr>, TypeExpr, List<ImplMethod>, Span)
|
||||
|
||||
// === Expressions ===
|
||||
|
||||
type Expr =
|
||||
| ExLiteral(Literal)
|
||||
| ExVar(Ident)
|
||||
| ExBinaryOp(BinaryOp, Expr, Expr, Span)
|
||||
| ExUnaryOp(UnaryOp, Expr, Span)
|
||||
| ExCall(Expr, List<Expr>, Span)
|
||||
| ExEffectOp(Ident, Ident, List<Expr>, Span)
|
||||
| ExField(Expr, Ident, Span)
|
||||
| ExTupleIndex(Expr, Int, Span)
|
||||
| ExLambda(List<Parameter>, Option<TypeExpr>, List<Ident>, Expr, Span)
|
||||
| ExLet(Ident, Option<TypeExpr>, Expr, Expr, Span)
|
||||
| ExIf(Expr, Expr, Expr, Span)
|
||||
| ExMatch(Expr, List<MatchArm>, Span)
|
||||
| ExBlock(List<Statement>, Expr, Span)
|
||||
| ExRecord(Option<Expr>, List<(Ident, Expr)>, Span)
|
||||
| ExTuple(List<Expr>, Span)
|
||||
| ExList(List<Expr>, Span)
|
||||
| ExRun(Expr, List<(Ident, Expr)>, Span)
|
||||
| ExResume(Expr, Span)
|
||||
512
projects/lux-compiler/lexer.lux
Normal file
512
projects/lux-compiler/lexer.lux
Normal file
@@ -0,0 +1,512 @@
|
||||
// Lux Lexer — Self-hosted lexer for the Lux language
|
||||
//
|
||||
// This is the first component of the Lux-in-Lux compiler.
|
||||
// It tokenizes Lux source code into a list of tokens.
|
||||
//
|
||||
// Design:
|
||||
// - Recursive descent character scanning
|
||||
// - Immutable state (ParseState tracks chars + position)
|
||||
// - Pattern matching for all token types
|
||||
|
||||
// === Token types ===
|
||||
|
||||
type TokenKind =
|
||||
// Literals
|
||||
| TkInt(Int)
|
||||
| TkFloat(String)
|
||||
| TkString(String)
|
||||
| TkChar(Char)
|
||||
| TkBool(Bool)
|
||||
// Identifiers
|
||||
| TkIdent(String)
|
||||
// Keywords
|
||||
| TkFn | TkLet | TkIf | TkThen | TkElse | TkMatch
|
||||
| TkWith | TkEffect | TkHandler | TkRun | TkResume
|
||||
| TkType | TkImport | TkPub | TkAs | TkFrom
|
||||
| TkTrait | TkImpl | TkFor
|
||||
// Behavioral
|
||||
| TkIs | TkPure | TkTotal | TkIdempotent
|
||||
| TkDeterministic | TkCommutative
|
||||
| TkWhere | TkAssume
|
||||
// Operators
|
||||
| TkPlus | TkMinus | TkStar | TkSlash | TkPercent
|
||||
| TkEq | TkEqEq | TkNe | TkLt | TkLe | TkGt | TkGe
|
||||
| TkAnd | TkOr | TkNot
|
||||
| TkPipe | TkPipeGt | TkArrow | TkThinArrow
|
||||
| TkDot | TkColon | TkColonColon | TkComma | TkSemi | TkAt
|
||||
// Delimiters
|
||||
| TkLParen | TkRParen | TkLBrace | TkRBrace
|
||||
| TkLBracket | TkRBracket
|
||||
// Special
|
||||
| TkUnderscore | TkNewline | TkEof
|
||||
// Doc comment
|
||||
| TkDocComment(String)
|
||||
|
||||
type Token =
|
||||
| Token(TokenKind, Int, Int) // kind, start, end
|
||||
|
||||
type LexState =
|
||||
| LexState(List<Char>, Int) // chars, position
|
||||
|
||||
type LexResult =
|
||||
| LexOk(Token, LexState)
|
||||
| LexErr(String, Int)
|
||||
|
||||
// === Character utilities ===
|
||||
|
||||
fn peek(state: LexState): Option<Char> =
|
||||
match state {
|
||||
LexState(chars, pos) => List.get(chars, pos)
|
||||
}
|
||||
|
||||
fn peekAt(state: LexState, offset: Int): Option<Char> =
|
||||
match state {
|
||||
LexState(chars, pos) => List.get(chars, pos + offset)
|
||||
}
|
||||
|
||||
fn advance(state: LexState): LexState =
|
||||
match state {
|
||||
LexState(chars, pos) => LexState(chars, pos + 1)
|
||||
}
|
||||
|
||||
fn position(state: LexState): Int =
|
||||
match state { LexState(_, pos) => pos }
|
||||
|
||||
fn isDigit(c: Char): Bool =
|
||||
c == '0' || c == '1' || c == '2' || c == '3' || c == '4' ||
|
||||
c == '5' || c == '6' || c == '7' || c == '8' || c == '9'
|
||||
|
||||
fn isAlpha(c: Char): Bool =
|
||||
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'
|
||||
|
||||
fn isAlphaNumeric(c: Char): Bool =
|
||||
isAlpha(c) || isDigit(c)
|
||||
|
||||
fn isWhitespace(c: Char): Bool =
|
||||
c == ' ' || c == '\t' || c == '\r'
|
||||
|
||||
// === Core lexing ===
|
||||
|
||||
fn skipLineComment(state: LexState): LexState =
|
||||
match peek(state) {
|
||||
None => state,
|
||||
Some(c) =>
|
||||
if c == '\n' then state
|
||||
else skipLineComment(advance(state))
|
||||
}
|
||||
|
||||
fn skipWhitespaceAndComments(state: LexState): LexState =
|
||||
match peek(state) {
|
||||
None => state,
|
||||
Some(c) =>
|
||||
if isWhitespace(c) then
|
||||
skipWhitespaceAndComments(advance(state))
|
||||
else if c == '/' then
|
||||
match peekAt(state, 1) {
|
||||
Some('/') =>
|
||||
// Check for doc comment (///)
|
||||
match peekAt(state, 2) {
|
||||
Some('/') => state, // Don't skip doc comments
|
||||
_ => skipWhitespaceAndComments(skipLineComment(advance(advance(state))))
|
||||
},
|
||||
_ => state
|
||||
}
|
||||
else state
|
||||
}
|
||||
|
||||
// Collect identifier characters
|
||||
fn collectIdent(state: LexState, acc: List<Char>): (List<Char>, LexState) =
|
||||
match peek(state) {
|
||||
None => (acc, state),
|
||||
Some(c) =>
|
||||
if isAlphaNumeric(c) then
|
||||
collectIdent(advance(state), List.concat(acc, [c]))
|
||||
else (acc, state)
|
||||
}
|
||||
|
||||
// Collect number characters (digits only)
|
||||
fn collectDigits(state: LexState, acc: List<Char>): (List<Char>, LexState) =
|
||||
match peek(state) {
|
||||
None => (acc, state),
|
||||
Some(c) =>
|
||||
if isDigit(c) then
|
||||
collectDigits(advance(state), List.concat(acc, [c]))
|
||||
else (acc, state)
|
||||
}
|
||||
|
||||
// Convert list of digit chars to int
|
||||
fn charsToInt(chars: List<Char>): Int =
|
||||
List.fold(chars, 0, fn(acc, c) => acc * 10 + charToDigit(c))
|
||||
|
||||
fn charToDigit(c: Char): Int =
|
||||
if c == '0' then 0
|
||||
else if c == '1' then 1
|
||||
else if c == '2' then 2
|
||||
else if c == '3' then 3
|
||||
else if c == '4' then 4
|
||||
else if c == '5' then 5
|
||||
else if c == '6' then 6
|
||||
else if c == '7' then 7
|
||||
else if c == '8' then 8
|
||||
else 9
|
||||
|
||||
// Map identifier string to keyword token or ident
|
||||
fn identToToken(name: String): TokenKind =
|
||||
if name == "fn" then TkFn
|
||||
else if name == "let" then TkLet
|
||||
else if name == "if" then TkIf
|
||||
else if name == "then" then TkThen
|
||||
else if name == "else" then TkElse
|
||||
else if name == "match" then TkMatch
|
||||
else if name == "with" then TkWith
|
||||
else if name == "effect" then TkEffect
|
||||
else if name == "handler" then TkHandler
|
||||
else if name == "run" then TkRun
|
||||
else if name == "resume" then TkResume
|
||||
else if name == "type" then TkType
|
||||
else if name == "true" then TkBool(true)
|
||||
else if name == "false" then TkBool(false)
|
||||
else if name == "import" then TkImport
|
||||
else if name == "pub" then TkPub
|
||||
else if name == "as" then TkAs
|
||||
else if name == "from" then TkFrom
|
||||
else if name == "trait" then TkTrait
|
||||
else if name == "impl" then TkImpl
|
||||
else if name == "for" then TkFor
|
||||
else if name == "is" then TkIs
|
||||
else if name == "pure" then TkPure
|
||||
else if name == "total" then TkTotal
|
||||
else if name == "idempotent" then TkIdempotent
|
||||
else if name == "deterministic" then TkDeterministic
|
||||
else if name == "commutative" then TkCommutative
|
||||
else if name == "where" then TkWhere
|
||||
else if name == "assume" then TkAssume
|
||||
else TkIdent(name)
|
||||
|
||||
// Lex a string literal (after opening quote consumed)
|
||||
fn lexStringBody(state: LexState, acc: List<Char>): (List<Char>, LexState) =
|
||||
match peek(state) {
|
||||
None => (acc, state),
|
||||
Some(c) =>
|
||||
if c == '"' then (acc, advance(state))
|
||||
else if c == '\\' then
|
||||
match peekAt(state, 1) {
|
||||
Some('n') => lexStringBody(advance(advance(state)), List.concat(acc, ['\n'])),
|
||||
Some('t') => lexStringBody(advance(advance(state)), List.concat(acc, ['\t'])),
|
||||
Some('\\') => lexStringBody(advance(advance(state)), List.concat(acc, ['\\'])),
|
||||
Some('"') => lexStringBody(advance(advance(state)), List.concat(acc, ['"'])),
|
||||
_ => lexStringBody(advance(state), List.concat(acc, [c]))
|
||||
}
|
||||
else lexStringBody(advance(state), List.concat(acc, [c]))
|
||||
}
|
||||
|
||||
// Lex a char literal (after opening quote consumed)
|
||||
fn lexCharLiteral(state: LexState): LexResult =
|
||||
let start = position(state) - 1;
|
||||
match peek(state) {
|
||||
None => LexErr("Unexpected end of input in char literal", start),
|
||||
Some(c) =>
|
||||
if c == '\\' then
|
||||
match peekAt(state, 1) {
|
||||
Some('n') =>
|
||||
match peekAt(state, 2) {
|
||||
Some('\'') => LexOk(Token(TkChar('\n'), start, position(state) + 3), advance(advance(advance(state)))),
|
||||
_ => LexErr("Expected closing quote", position(state))
|
||||
},
|
||||
Some('t') =>
|
||||
match peekAt(state, 2) {
|
||||
Some('\'') => LexOk(Token(TkChar('\t'), start, position(state) + 3), advance(advance(advance(state)))),
|
||||
_ => LexErr("Expected closing quote", position(state))
|
||||
},
|
||||
Some('\\') =>
|
||||
match peekAt(state, 2) {
|
||||
Some('\'') => LexOk(Token(TkChar('\\'), start, position(state) + 3), advance(advance(advance(state)))),
|
||||
_ => LexErr("Expected closing quote", position(state))
|
||||
},
|
||||
_ => LexErr("Unknown escape sequence", position(state))
|
||||
}
|
||||
else
|
||||
match peekAt(state, 1) {
|
||||
Some('\'') => LexOk(Token(TkChar(c), start, position(state) + 2), advance(advance(state))),
|
||||
_ => LexErr("Expected closing quote", position(state))
|
||||
}
|
||||
}
|
||||
|
||||
// Collect doc comment text (after /// consumed)
|
||||
fn collectDocComment(state: LexState, acc: List<Char>): (List<Char>, LexState) =
|
||||
match peek(state) {
|
||||
None => (acc, state),
|
||||
Some(c) =>
|
||||
if c == '\n' then (acc, state)
|
||||
else collectDocComment(advance(state), List.concat(acc, [c]))
|
||||
}
|
||||
|
||||
// Lex a single token
|
||||
fn lexToken(state: LexState): LexResult =
|
||||
let state = skipWhitespaceAndComments(state);
|
||||
let start = position(state);
|
||||
match peek(state) {
|
||||
None => LexOk(Token(TkEof, start, start), state),
|
||||
Some(c) =>
|
||||
if c == '\n' then
|
||||
LexOk(Token(TkNewline, start, start + 1), advance(state))
|
||||
// Numbers
|
||||
else if isDigit(c) then
|
||||
let result = collectDigits(state, []);
|
||||
match result {
|
||||
(digits, nextState) =>
|
||||
// Check for float
|
||||
match peek(nextState) {
|
||||
Some('.') =>
|
||||
match peekAt(nextState, 1) {
|
||||
Some(d) =>
|
||||
if isDigit(d) then
|
||||
let fracResult = collectDigits(advance(nextState), []);
|
||||
match fracResult {
|
||||
(fracDigits, finalState) =>
|
||||
let intPart = String.join(List.map(digits, fn(ch) => String.fromChar(ch)), "");
|
||||
let fracPart = String.join(List.map(fracDigits, fn(ch) => String.fromChar(ch)), "");
|
||||
LexOk(Token(TkFloat(intPart + "." + fracPart), start, position(finalState)), finalState)
|
||||
}
|
||||
else
|
||||
LexOk(Token(TkInt(charsToInt(digits)), start, position(nextState)), nextState),
|
||||
None =>
|
||||
LexOk(Token(TkInt(charsToInt(digits)), start, position(nextState)), nextState)
|
||||
},
|
||||
_ => LexOk(Token(TkInt(charsToInt(digits)), start, position(nextState)), nextState)
|
||||
}
|
||||
}
|
||||
// Identifiers and keywords
|
||||
else if isAlpha(c) then
|
||||
let result = collectIdent(state, []);
|
||||
match result {
|
||||
(chars, nextState) =>
|
||||
let name = String.join(List.map(chars, fn(ch) => String.fromChar(ch)), "");
|
||||
LexOk(Token(identToToken(name), start, position(nextState)), nextState)
|
||||
}
|
||||
// String literals
|
||||
else if c == '"' then
|
||||
let result = lexStringBody(advance(state), []);
|
||||
match result {
|
||||
(chars, nextState) =>
|
||||
let str = String.join(List.map(chars, fn(ch) => String.fromChar(ch)), "");
|
||||
LexOk(Token(TkString(str), start, position(nextState)), nextState)
|
||||
}
|
||||
// Char literals
|
||||
else if c == '\'' then
|
||||
lexCharLiteral(advance(state))
|
||||
// Doc comments (///)
|
||||
else if c == '/' then
|
||||
match peekAt(state, 1) {
|
||||
Some('/') =>
|
||||
match peekAt(state, 2) {
|
||||
Some('/') =>
|
||||
// Skip the "/// " prefix
|
||||
let docState = advance(advance(advance(state)));
|
||||
let docState = match peek(docState) {
|
||||
Some(' ') => advance(docState),
|
||||
_ => docState
|
||||
};
|
||||
let result = collectDocComment(docState, []);
|
||||
match result {
|
||||
(chars, nextState) =>
|
||||
let text = String.join(List.map(chars, fn(ch) => String.fromChar(ch)), "");
|
||||
LexOk(Token(TkDocComment(text), start, position(nextState)), nextState)
|
||||
},
|
||||
_ => LexOk(Token(TkSlash, start, start + 1), advance(state))
|
||||
},
|
||||
_ => LexOk(Token(TkSlash, start, start + 1), advance(state))
|
||||
}
|
||||
// Two-character operators
|
||||
else if c == '=' then
|
||||
match peekAt(state, 1) {
|
||||
Some('=') => LexOk(Token(TkEqEq, start, start + 2), advance(advance(state))),
|
||||
Some('>') => LexOk(Token(TkArrow, start, start + 2), advance(advance(state))),
|
||||
_ => LexOk(Token(TkEq, start, start + 1), advance(state))
|
||||
}
|
||||
else if c == '!' then
|
||||
match peekAt(state, 1) {
|
||||
Some('=') => LexOk(Token(TkNe, start, start + 2), advance(advance(state))),
|
||||
_ => LexOk(Token(TkNot, start, start + 1), advance(state))
|
||||
}
|
||||
else if c == '<' then
|
||||
match peekAt(state, 1) {
|
||||
Some('=') => LexOk(Token(TkLe, start, start + 2), advance(advance(state))),
|
||||
_ => LexOk(Token(TkLt, start, start + 1), advance(state))
|
||||
}
|
||||
else if c == '>' then
|
||||
match peekAt(state, 1) {
|
||||
Some('=') => LexOk(Token(TkGe, start, start + 2), advance(advance(state))),
|
||||
_ => LexOk(Token(TkGt, start, start + 1), advance(state))
|
||||
}
|
||||
else if c == '&' then
|
||||
match peekAt(state, 1) {
|
||||
Some('&') => LexOk(Token(TkAnd, start, start + 2), advance(advance(state))),
|
||||
_ => LexErr("Expected '&&'", start)
|
||||
}
|
||||
else if c == '|' then
|
||||
match peekAt(state, 1) {
|
||||
Some('|') => LexOk(Token(TkOr, start, start + 2), advance(advance(state))),
|
||||
Some('>') => LexOk(Token(TkPipeGt, start, start + 2), advance(advance(state))),
|
||||
_ => LexOk(Token(TkPipe, start, start + 1), advance(state))
|
||||
}
|
||||
else if c == '-' then
|
||||
match peekAt(state, 1) {
|
||||
Some('>') => LexOk(Token(TkThinArrow, start, start + 2), advance(advance(state))),
|
||||
_ => LexOk(Token(TkMinus, start, start + 1), advance(state))
|
||||
}
|
||||
else if c == ':' then
|
||||
match peekAt(state, 1) {
|
||||
Some(':') => LexOk(Token(TkColonColon, start, start + 2), advance(advance(state))),
|
||||
_ => LexOk(Token(TkColon, start, start + 1), advance(state))
|
||||
}
|
||||
// Single-character tokens
|
||||
else if c == '+' then LexOk(Token(TkPlus, start, start + 1), advance(state))
|
||||
else if c == '*' then LexOk(Token(TkStar, start, start + 1), advance(state))
|
||||
else if c == '%' then LexOk(Token(TkPercent, start, start + 1), advance(state))
|
||||
else if c == '.' then LexOk(Token(TkDot, start, start + 1), advance(state))
|
||||
else if c == ',' then LexOk(Token(TkComma, start, start + 1), advance(state))
|
||||
else if c == ';' then LexOk(Token(TkSemi, start, start + 1), advance(state))
|
||||
else if c == '@' then LexOk(Token(TkAt, start, start + 1), advance(state))
|
||||
else if c == '(' then LexOk(Token(TkLParen, start, start + 1), advance(state))
|
||||
else if c == ')' then LexOk(Token(TkRParen, start, start + 1), advance(state))
|
||||
else if c == '{' then LexOk(Token(TkLBrace, start, start + 1), advance(state))
|
||||
else if c == '}' then LexOk(Token(TkRBrace, start, start + 1), advance(state))
|
||||
else if c == '[' then LexOk(Token(TkLBracket, start, start + 1), advance(state))
|
||||
else if c == ']' then LexOk(Token(TkRBracket, start, start + 1), advance(state))
|
||||
else if c == '_' then
|
||||
// Check if it's just underscore or start of ident
|
||||
match peekAt(state, 1) {
|
||||
Some(next) =>
|
||||
if isAlphaNumeric(next) then
|
||||
let result = collectIdent(state, []);
|
||||
match result {
|
||||
(chars, nextState) =>
|
||||
let name = String.join(List.map(chars, fn(ch) => String.fromChar(ch)), "");
|
||||
LexOk(Token(TkIdent(name), start, position(nextState)), nextState)
|
||||
}
|
||||
else LexOk(Token(TkUnderscore, start, start + 1), advance(state)),
|
||||
None => LexOk(Token(TkUnderscore, start, start + 1), advance(state))
|
||||
}
|
||||
else LexErr("Unexpected character: " + String.fromChar(c), start)
|
||||
}
|
||||
|
||||
// Lex all tokens from source
|
||||
fn lexAll(state: LexState, acc: List<Token>): List<Token> =
|
||||
match lexToken(state) {
|
||||
LexErr(msg, pos) =>
|
||||
// On error, skip the character and continue
|
||||
List.concat(acc, [Token(TkEof, pos, pos)]),
|
||||
LexOk(token, nextState) =>
|
||||
match token {
|
||||
Token(TkEof, _, _) => List.concat(acc, [token]),
|
||||
Token(TkNewline, _, _) =>
|
||||
// Skip consecutive newlines
|
||||
lexAll(nextState, List.concat(acc, [token])),
|
||||
_ => lexAll(nextState, List.concat(acc, [token]))
|
||||
}
|
||||
}
|
||||
|
||||
// Public API: tokenize a source string
|
||||
fn tokenize(source: String): List<Token> =
|
||||
let chars = String.chars(source);
|
||||
let state = LexState(chars, 0);
|
||||
lexAll(state, [])
|
||||
|
||||
// === Token display ===
|
||||
|
||||
fn tokenKindToString(kind: TokenKind): String =
|
||||
match kind {
|
||||
TkInt(n) => "Int(" + toString(n) + ")",
|
||||
TkFloat(s) => "Float(" + s + ")",
|
||||
TkString(s) => "String(\"" + s + "\")",
|
||||
TkChar(c) => "Char('" + String.fromChar(c) + "')",
|
||||
TkBool(b) => if b then "true" else "false",
|
||||
TkIdent(name) => "Ident(" + name + ")",
|
||||
TkFn => "fn", TkLet => "let", TkIf => "if",
|
||||
TkThen => "then", TkElse => "else", TkMatch => "match",
|
||||
TkWith => "with", TkEffect => "effect", TkHandler => "handler",
|
||||
TkRun => "run", TkResume => "resume", TkType => "type",
|
||||
TkImport => "import", TkPub => "pub", TkAs => "as",
|
||||
TkFrom => "from", TkTrait => "trait", TkImpl => "impl", TkFor => "for",
|
||||
TkIs => "is", TkPure => "pure", TkTotal => "total",
|
||||
TkIdempotent => "idempotent", TkDeterministic => "deterministic",
|
||||
TkCommutative => "commutative", TkWhere => "where", TkAssume => "assume",
|
||||
TkPlus => "+", TkMinus => "-", TkStar => "*", TkSlash => "/",
|
||||
TkPercent => "%", TkEq => "=", TkEqEq => "==", TkNe => "!=",
|
||||
TkLt => "<", TkLe => "<=", TkGt => ">", TkGe => ">=",
|
||||
TkAnd => "&&", TkOr => "||", TkNot => "!",
|
||||
TkPipe => "|", TkPipeGt => "|>",
|
||||
TkArrow => "=>", TkThinArrow => "->",
|
||||
TkDot => ".", TkColon => ":", TkColonColon => "::",
|
||||
TkComma => ",", TkSemi => ";", TkAt => "@",
|
||||
TkLParen => "(", TkRParen => ")", TkLBrace => "{", TkRBrace => "}",
|
||||
TkLBracket => "[", TkRBracket => "]",
|
||||
TkUnderscore => "_", TkNewline => "\\n", TkEof => "EOF",
|
||||
TkDocComment(text) => "DocComment(\"" + text + "\")",
|
||||
_ => "?"
|
||||
}
|
||||
|
||||
fn tokenToString(token: Token): String =
|
||||
match token {
|
||||
Token(kind, start, end) =>
|
||||
tokenKindToString(kind) + " [" + toString(start) + ".." + toString(end) + "]"
|
||||
}
|
||||
|
||||
// === Tests ===
|
||||
|
||||
fn printTokens(tokens: List<Token>): Unit with {Console} =
|
||||
match List.head(tokens) {
|
||||
None => Console.print(""),
|
||||
Some(t) => {
|
||||
Console.print(" " + tokenToString(t));
|
||||
match List.tail(tokens) {
|
||||
Some(rest) => printTokens(rest),
|
||||
None => Console.print("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn testLexer(label: String, source: String): Unit with {Console} = {
|
||||
Console.print("--- " + label + " ---");
|
||||
Console.print(" Input: \"" + source + "\"");
|
||||
let tokens = tokenize(source);
|
||||
printTokens(tokens)
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("=== Lux Self-Hosted Lexer ===");
|
||||
Console.print("");
|
||||
|
||||
// Basic tokens
|
||||
testLexer("numbers", "42 3");
|
||||
Console.print("");
|
||||
|
||||
// Identifiers and keywords
|
||||
testLexer("keywords", "fn main let x");
|
||||
Console.print("");
|
||||
|
||||
// Operators
|
||||
testLexer("operators", "a + b == c");
|
||||
Console.print("");
|
||||
|
||||
// String literal
|
||||
testLexer("string", "\"hello world\"");
|
||||
Console.print("");
|
||||
|
||||
// Function declaration
|
||||
testLexer("function", "fn add(a: Int, b: Int): Int = a + b");
|
||||
Console.print("");
|
||||
|
||||
// Behavioral properties
|
||||
testLexer("behavioral", "fn add(a: Int): Int is pure = a");
|
||||
Console.print("");
|
||||
|
||||
// Complex expression
|
||||
testLexer("complex", "let result = if x > 0 then x else 0 - x");
|
||||
Console.print("");
|
||||
|
||||
Console.print("=== Lexer test complete ===")
|
||||
}
|
||||
|
||||
let _ = run main() with {}
|
||||
213
scripts/release.sh
Executable file
213
scripts/release.sh
Executable file
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Lux Release Script
|
||||
# Builds a static binary, generates changelog, and creates a Gitea release.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/release.sh # auto-bump patch (0.2.0 → 0.2.1)
|
||||
# ./scripts/release.sh patch # same as above
|
||||
# ./scripts/release.sh minor # bump minor (0.2.0 → 0.3.0)
|
||||
# ./scripts/release.sh major # bump major (0.2.0 → 1.0.0)
|
||||
# ./scripts/release.sh v1.2.3 # explicit version
|
||||
#
|
||||
# Environment:
|
||||
# GITEA_TOKEN - API token for git.qrty.ink (prompted if not set)
|
||||
# GITEA_URL - Gitea instance URL (default: https://git.qrty.ink)
|
||||
|
||||
# cd to repo root (directory containing this script's parent)
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$SCRIPT_DIR/.."
|
||||
|
||||
GITEA_URL="${GITEA_URL:-https://git.qrty.ink}"
|
||||
REPO_OWNER="blu"
|
||||
REPO_NAME="lux"
|
||||
API_BASE="$GITEA_URL/api/v1"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { printf "${CYAN}::${NC} %s\n" "$1"; }
|
||||
ok() { printf "${GREEN}ok${NC} %s\n" "$1"; }
|
||||
warn() { printf "${YELLOW}!!${NC} %s\n" "$1"; }
|
||||
err() { printf "${RED}error:${NC} %s\n" "$1" >&2; exit 1; }
|
||||
|
||||
# --- Determine version ---
|
||||
CURRENT=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||
BUMP="${1:-patch}"
|
||||
|
||||
bump_version() {
|
||||
local ver="$1" part="$2"
|
||||
IFS='.' read -r major minor patch <<< "$ver"
|
||||
case "$part" in
|
||||
major) echo "$((major + 1)).0.0" ;;
|
||||
minor) echo "$major.$((minor + 1)).0" ;;
|
||||
patch) echo "$major.$minor.$((patch + 1))" ;;
|
||||
*) echo "$part" ;; # treat as explicit version
|
||||
esac
|
||||
}
|
||||
|
||||
case "$BUMP" in
|
||||
major|minor|patch)
|
||||
VERSION=$(bump_version "$CURRENT" "$BUMP")
|
||||
info "Bumping $BUMP: $CURRENT → $VERSION"
|
||||
;;
|
||||
*)
|
||||
# Explicit version — strip v prefix if present
|
||||
VERSION="${BUMP#v}"
|
||||
info "Explicit version: $VERSION"
|
||||
;;
|
||||
esac
|
||||
|
||||
TAG="v$VERSION"
|
||||
|
||||
# --- Check for clean working tree ---
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
warn "Working tree has uncommitted changes:"
|
||||
git status --short
|
||||
printf "\n"
|
||||
read -rp "Continue anyway? [y/N] " confirm
|
||||
[[ "$confirm" =~ ^[Yy]$ ]] || exit 1
|
||||
fi
|
||||
|
||||
# --- Check if tag already exists ---
|
||||
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||
err "Tag $TAG already exists. Choose a different version."
|
||||
fi
|
||||
|
||||
# --- Update version in source files ---
|
||||
if [ "$VERSION" != "$CURRENT" ]; then
|
||||
info "Updating version in Cargo.toml and flake.nix..."
|
||||
sed -i "0,/^version = \"$CURRENT\"/s//version = \"$VERSION\"/" Cargo.toml
|
||||
sed -i "s/version = \"$CURRENT\";/version = \"$VERSION\";/g" flake.nix
|
||||
sed -i "s/v$CURRENT/v$VERSION/g" flake.nix
|
||||
git add Cargo.toml flake.nix
|
||||
git commit --no-gpg-sign -m "chore: bump version to $VERSION"
|
||||
ok "Version updated and committed"
|
||||
fi
|
||||
|
||||
# --- Generate changelog ---
|
||||
info "Generating changelog..."
|
||||
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
||||
if [ -n "$LAST_TAG" ]; then
|
||||
RANGE="$LAST_TAG..HEAD"
|
||||
info "Changes since $LAST_TAG:"
|
||||
else
|
||||
RANGE="HEAD"
|
||||
info "First release — summarizing recent commits:"
|
||||
fi
|
||||
|
||||
CHANGELOG=$(git log "$RANGE" --pretty=format:"- %s" --no-merges 2>/dev/null | head -50 || true)
|
||||
if [ -z "$CHANGELOG" ]; then
|
||||
CHANGELOG="- Initial release"
|
||||
fi
|
||||
|
||||
# --- Build static binary ---
|
||||
info "Building static binary (nix build .#static)..."
|
||||
nix build .#static
|
||||
BINARY="result/bin/lux"
|
||||
if [ ! -f "$BINARY" ]; then
|
||||
err "Static binary not found at $BINARY"
|
||||
fi
|
||||
|
||||
BINARY_SIZE=$(ls -lh "$BINARY" | awk '{print $5}')
|
||||
BINARY_TYPE=$(file "$BINARY" | sed 's/.*: //')
|
||||
ok "Binary: $BINARY_SIZE, $BINARY_TYPE"
|
||||
|
||||
# --- Prepare release artifact ---
|
||||
ARTIFACT="/tmp/lux-${TAG}-linux-x86_64"
|
||||
cp "$BINARY" "$ARTIFACT"
|
||||
chmod +x "$ARTIFACT"
|
||||
|
||||
# --- Show release summary ---
|
||||
printf "\n"
|
||||
printf "${BOLD}═══ Release Summary ═══${NC}\n"
|
||||
printf "\n"
|
||||
printf " ${BOLD}Tag:${NC} %s\n" "$TAG"
|
||||
printf " ${BOLD}Binary:${NC} %s (%s)\n" "lux-${TAG}-linux-x86_64" "$BINARY_SIZE"
|
||||
printf " ${BOLD}Commit:${NC} %s\n" "$(git rev-parse --short HEAD)"
|
||||
printf "\n"
|
||||
printf "${BOLD}Changelog:${NC}\n"
|
||||
printf "%s\n" "$CHANGELOG"
|
||||
printf "\n"
|
||||
|
||||
# --- Confirm ---
|
||||
read -rp "Create release $TAG? [y/N] " confirm
|
||||
[[ "$confirm" =~ ^[Yy]$ ]] || { info "Aborted."; exit 0; }
|
||||
|
||||
# --- Get Gitea token ---
|
||||
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||
printf "\n"
|
||||
info "Gitea API token required (create at $GITEA_URL/user/settings/applications)"
|
||||
read -rsp "Token: " GITEA_TOKEN
|
||||
printf "\n"
|
||||
fi
|
||||
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
err "No token provided"
|
||||
fi
|
||||
|
||||
# --- Create and push tag ---
|
||||
info "Creating tag $TAG..."
|
||||
git tag -a "$TAG" -m "Release $TAG" --no-sign
|
||||
ok "Tag created"
|
||||
|
||||
info "Pushing tag to origin..."
|
||||
git push origin "$TAG"
|
||||
ok "Tag pushed"
|
||||
|
||||
# --- Create Gitea release ---
|
||||
info "Creating release on Gitea..."
|
||||
|
||||
RELEASE_BODY=$(printf "## Lux %s\n\n### Changes\n\n%s\n\n### Installation\n\n\`\`\`bash\ncurl -Lo lux %s/%s/%s/releases/download/%s/lux-linux-x86_64\nchmod +x lux\n./lux --version\n\`\`\`" \
|
||||
"$TAG" "$CHANGELOG" "$GITEA_URL" "$REPO_OWNER" "$REPO_NAME" "$TAG")
|
||||
|
||||
RELEASE_JSON=$(jq -n \
|
||||
--arg tag "$TAG" \
|
||||
--arg name "Lux $TAG" \
|
||||
--arg body "$RELEASE_BODY" \
|
||||
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')
|
||||
|
||||
RELEASE_RESPONSE=$(curl -s -X POST \
|
||||
"$API_BASE/repos/$REPO_OWNER/$REPO_NAME/releases" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$RELEASE_JSON")
|
||||
|
||||
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id // empty')
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "$RELEASE_RESPONSE" | jq . 2>/dev/null || echo "$RELEASE_RESPONSE"
|
||||
err "Failed to create release"
|
||||
fi
|
||||
ok "Release created (id: $RELEASE_ID)"
|
||||
|
||||
# --- Upload binary ---
|
||||
info "Uploading binary..."
|
||||
UPLOAD_RESPONSE=$(curl -s -X POST \
|
||||
"$API_BASE/repos/$REPO_OWNER/$REPO_NAME/releases/$RELEASE_ID/assets?name=lux-linux-x86_64" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@$ARTIFACT")
|
||||
|
||||
ASSET_NAME=$(echo "$UPLOAD_RESPONSE" | jq -r '.name // empty')
|
||||
if [ -z "$ASSET_NAME" ]; then
|
||||
echo "$UPLOAD_RESPONSE" | jq . 2>/dev/null || echo "$UPLOAD_RESPONSE"
|
||||
err "Failed to upload binary"
|
||||
fi
|
||||
ok "Binary uploaded: $ASSET_NAME"
|
||||
|
||||
# --- Done ---
|
||||
printf "\n"
|
||||
printf "${GREEN}${BOLD}Release $TAG published!${NC}\n"
|
||||
printf "\n"
|
||||
printf " ${BOLD}URL:${NC} %s/%s/%s/releases/tag/%s\n" "$GITEA_URL" "$REPO_OWNER" "$REPO_NAME" "$TAG"
|
||||
printf " ${BOLD}Download:${NC} %s/%s/%s/releases/download/%s/lux-linux-x86_64\n" "$GITEA_URL" "$REPO_OWNER" "$REPO_NAME" "$TAG"
|
||||
printf "\n"
|
||||
|
||||
# Cleanup
|
||||
rm -f "$ARTIFACT"
|
||||
211
scripts/validate.sh
Executable file
211
scripts/validate.sh
Executable file
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Lux Full Validation Script
|
||||
# Runs all checks: Rust tests, package tests, type checking, example compilation.
|
||||
# Run after every committable change to ensure no regressions.
|
||||
|
||||
# cd to repo root (directory containing this script's parent)
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$SCRIPT_DIR/.."
|
||||
|
||||
LUX="$(pwd)/target/release/lux"
|
||||
PACKAGES_DIR="$(pwd)/../packages"
|
||||
PROJECTS_DIR="$(pwd)/projects"
|
||||
EXAMPLES_DIR="$(pwd)/examples"
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
FAILED=0
|
||||
TOTAL=0
|
||||
|
||||
step() {
|
||||
TOTAL=$((TOTAL + 1))
|
||||
printf "${CYAN}[%d]${NC} %s... " "$TOTAL" "$1"
|
||||
}
|
||||
|
||||
ok() { printf "${GREEN}ok${NC} %s\n" "${1:-}"; }
|
||||
fail() { printf "${RED}FAIL${NC} %s\n" "${1:-}"; FAILED=$((FAILED + 1)); }
|
||||
|
||||
# --- Rust checks ---
|
||||
step "cargo check"
|
||||
if nix develop --command cargo check 2>/dev/null; then ok; else fail; fi
|
||||
|
||||
step "cargo test"
|
||||
OUTPUT=$(nix develop --command cargo test 2>&1 || true)
|
||||
RESULT=$(echo "$OUTPUT" | grep "test result:" || echo "no result")
|
||||
if echo "$RESULT" | grep -q "0 failed"; then ok "$RESULT"; else fail "$RESULT"; fi
|
||||
|
||||
# --- Build release binary ---
|
||||
step "cargo build --release"
|
||||
if nix develop --command cargo build --release 2>/dev/null; then ok; else fail; fi
|
||||
|
||||
# --- Package tests ---
|
||||
for pkg in path frontmatter xml rss markdown; do
|
||||
PKG_DIR="$PACKAGES_DIR/$pkg"
|
||||
if [ -d "$PKG_DIR" ]; then
|
||||
step "lux test ($pkg)"
|
||||
OUTPUT=$(cd "$PKG_DIR" && "$LUX" test 2>&1 || true)
|
||||
RESULT=$(echo "$OUTPUT" | grep "passed" | tail -1 || echo "no result")
|
||||
if echo "$RESULT" | grep -q "passed"; then ok "$RESULT"; else fail "$RESULT"; fi
|
||||
fi
|
||||
done
|
||||
|
||||
# --- Lux check on packages ---
|
||||
for pkg in path frontmatter xml rss markdown; do
|
||||
PKG_DIR="$PACKAGES_DIR/$pkg"
|
||||
if [ -d "$PKG_DIR" ]; then
|
||||
step "lux check ($pkg)"
|
||||
OUTPUT=$(cd "$PKG_DIR" && "$LUX" check 2>&1 || true)
|
||||
RESULT=$(echo "$OUTPUT" | grep "passed" | tail -1 || echo "no result")
|
||||
if echo "$RESULT" | grep -q "passed"; then ok; else fail "$RESULT"; fi
|
||||
fi
|
||||
done
|
||||
|
||||
# --- Project checks ---
|
||||
for proj_dir in "$PROJECTS_DIR"/*/; do
|
||||
proj=$(basename "$proj_dir")
|
||||
if [ -f "$proj_dir/main.lux" ]; then
|
||||
step "lux check (project: $proj)"
|
||||
OUTPUT=$("$LUX" check "$proj_dir/main.lux" 2>&1 || true)
|
||||
if echo "$OUTPUT" | grep -qi "error"; then fail; else ok; fi
|
||||
fi
|
||||
# Check any standalone .lux files in the project
|
||||
for lux_file in "$proj_dir"/*.lux; do
|
||||
[ -f "$lux_file" ] || continue
|
||||
fname=$(basename "$lux_file")
|
||||
[ "$fname" = "main.lux" ] && continue
|
||||
step "lux check (project: $proj/$fname)"
|
||||
OUTPUT=$("$LUX" check "$lux_file" 2>&1 || true)
|
||||
if echo "$OUTPUT" | grep -qi "error"; then fail; else ok; fi
|
||||
done
|
||||
done
|
||||
|
||||
# === Compilation & Interpreter Checks ===
|
||||
|
||||
# --- Interpreter: examples ---
|
||||
# Skip: http_api, http, http_router, http_server (network), postgres_demo (db),
|
||||
# random, property_testing (Random effect), shell (Process), json (File I/O),
|
||||
# file_io (File I/O), test_math, test_lists (Test effect), stress_shared_rc,
|
||||
# test_rc_comparison (internal tests), modules/* (need cwd)
|
||||
INTERP_SKIP="http_api http http_router http_server postgres_demo random property_testing shell json file_io test_math test_lists stress_shared_rc test_rc_comparison"
|
||||
for f in "$EXAMPLES_DIR"/*.lux; do
|
||||
name=$(basename "$f" .lux)
|
||||
skip=false
|
||||
for s in $INTERP_SKIP; do [ "$name" = "$s" ] && skip=true; done
|
||||
$skip && continue
|
||||
step "interpreter (examples/$name)"
|
||||
if timeout 10 "$LUX" "$f" >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- Interpreter: examples/standard ---
|
||||
# Skip: guessing_game (reads stdin)
|
||||
for f in "$EXAMPLES_DIR"/standard/*.lux; do
|
||||
[ -f "$f" ] || continue
|
||||
name=$(basename "$f" .lux)
|
||||
[ "$name" = "guessing_game" ] && continue
|
||||
step "interpreter (standard/$name)"
|
||||
if timeout 10 "$LUX" "$f" >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- Interpreter: examples/showcase ---
|
||||
# Skip: task_manager (parse error in current version)
|
||||
for f in "$EXAMPLES_DIR"/showcase/*.lux; do
|
||||
[ -f "$f" ] || continue
|
||||
name=$(basename "$f" .lux)
|
||||
[ "$name" = "task_manager" ] && continue
|
||||
step "interpreter (showcase/$name)"
|
||||
if timeout 10 "$LUX" "$f" >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- Interpreter: projects ---
|
||||
# Skip: guessing-game (Random), rest-api (HttpServer)
|
||||
PROJ_INTERP_SKIP="guessing-game rest-api"
|
||||
for proj_dir in "$PROJECTS_DIR"/*/; do
|
||||
proj=$(basename "$proj_dir")
|
||||
[ -f "$proj_dir/main.lux" ] || continue
|
||||
skip=false
|
||||
for s in $PROJ_INTERP_SKIP; do [ "$proj" = "$s" ] && skip=true; done
|
||||
$skip && continue
|
||||
step "interpreter (project: $proj)"
|
||||
if timeout 10 "$LUX" "$proj_dir/main.lux" >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- JS compilation: examples ---
|
||||
# Skip files that fail JS compilation (unsupported features)
|
||||
JS_SKIP="http_api http http_router postgres_demo property_testing json test_lists test_rc_comparison"
|
||||
for f in "$EXAMPLES_DIR"/*.lux; do
|
||||
name=$(basename "$f" .lux)
|
||||
skip=false
|
||||
for s in $JS_SKIP; do [ "$name" = "$s" ] && skip=true; done
|
||||
$skip && continue
|
||||
step "compile JS (examples/$name)"
|
||||
if "$LUX" compile "$f" --target js -o /tmp/lux_validate.js >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- JS compilation: examples/standard ---
|
||||
# Skip: stdlib_demo (uses String.toUpper not in JS backend)
|
||||
for f in "$EXAMPLES_DIR"/standard/*.lux; do
|
||||
[ -f "$f" ] || continue
|
||||
name=$(basename "$f" .lux)
|
||||
[ "$name" = "stdlib_demo" ] && continue
|
||||
step "compile JS (standard/$name)"
|
||||
if "$LUX" compile "$f" --target js -o /tmp/lux_validate.js >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- JS compilation: examples/showcase ---
|
||||
# Skip: task_manager (unsupported features)
|
||||
for f in "$EXAMPLES_DIR"/showcase/*.lux; do
|
||||
[ -f "$f" ] || continue
|
||||
name=$(basename "$f" .lux)
|
||||
[ "$name" = "task_manager" ] && continue
|
||||
step "compile JS (showcase/$name)"
|
||||
if "$LUX" compile "$f" --target js -o /tmp/lux_validate.js >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- JS compilation: projects ---
|
||||
# Skip: json-parser, rest-api (unsupported features)
|
||||
JS_PROJ_SKIP="json-parser rest-api"
|
||||
for proj_dir in "$PROJECTS_DIR"/*/; do
|
||||
proj=$(basename "$proj_dir")
|
||||
[ -f "$proj_dir/main.lux" ] || continue
|
||||
skip=false
|
||||
for s in $JS_PROJ_SKIP; do [ "$proj" = "$s" ] && skip=true; done
|
||||
$skip && continue
|
||||
step "compile JS (project: $proj)"
|
||||
if "$LUX" compile "$proj_dir/main.lux" --target js -o /tmp/lux_validate.js >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- C compilation: examples ---
|
||||
# Only compile examples known to work with C backend
|
||||
C_EXAMPLES="hello factorial pipelines tailcall jit_test"
|
||||
for name in $C_EXAMPLES; do
|
||||
f="$EXAMPLES_DIR/$name.lux"
|
||||
[ -f "$f" ] || continue
|
||||
step "compile C (examples/$name)"
|
||||
if "$LUX" compile "$f" -o /tmp/lux_validate_bin >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- C compilation: examples/standard ---
|
||||
C_STD_EXAMPLES="hello_world factorial fizzbuzz primes guessing_game"
|
||||
for name in $C_STD_EXAMPLES; do
|
||||
f="$EXAMPLES_DIR/standard/$name.lux"
|
||||
[ -f "$f" ] || continue
|
||||
step "compile C (standard/$name)"
|
||||
if "$LUX" compile "$f" -o /tmp/lux_validate_bin >/dev/null 2>&1; then ok; else fail; fi
|
||||
done
|
||||
|
||||
# --- Cleanup ---
|
||||
rm -f /tmp/lux_validate.js /tmp/lux_validate_bin
|
||||
|
||||
# --- Summary ---
|
||||
printf "\n${BOLD}═══ Validation Summary ═══${NC}\n"
|
||||
if [ $FAILED -eq 0 ]; then
|
||||
printf "${GREEN}All %d checks passed.${NC}\n" "$TOTAL"
|
||||
else
|
||||
printf "${RED}%d/%d checks failed.${NC}\n" "$FAILED" "$TOTAL"
|
||||
exit 1
|
||||
fi
|
||||
11
src/ast.rs
11
src/ast.rs
@@ -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>,
|
||||
@@ -535,7 +541,9 @@ pub enum Expr {
|
||||
span: Span,
|
||||
},
|
||||
/// Record literal: { name: "Alice", age: 30 }
|
||||
/// With optional spread: { ...base, name: "Bob" }
|
||||
Record {
|
||||
spread: Option<Box<Expr>>,
|
||||
fields: Vec<(Ident, Expr)>,
|
||||
span: Span,
|
||||
},
|
||||
@@ -563,6 +571,7 @@ impl Expr {
|
||||
Expr::Call { span, .. } => *span,
|
||||
Expr::EffectOp { span, .. } => *span,
|
||||
Expr::Field { span, .. } => *span,
|
||||
Expr::TupleIndex { span, .. } => *span,
|
||||
Expr::Lambda { span, .. } => *span,
|
||||
Expr::Let { span, .. } => *span,
|
||||
Expr::If { span, .. } => *span,
|
||||
@@ -615,6 +624,7 @@ pub enum BinaryOp {
|
||||
Or,
|
||||
// Other
|
||||
Pipe, // |>
|
||||
Concat, // ++
|
||||
}
|
||||
|
||||
impl fmt::Display for BinaryOp {
|
||||
@@ -634,6 +644,7 @@ impl fmt::Display for BinaryOp {
|
||||
BinaryOp::And => write!(f, "&&"),
|
||||
BinaryOp::Or => write!(f, "||"),
|
||||
BinaryOp::Pipe => write!(f, "|>"),
|
||||
BinaryOp::Concat => write!(f, "++"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user