Compare commits
42 Commits
26d340b5a3
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
/target
|
||||||
/result
|
/result
|
||||||
|
|
||||||
|
# Claude Code project instructions
|
||||||
|
CLAUDE.md
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
_site/
|
||||||
|
docs/*.html
|
||||||
|
docs/*.css
|
||||||
|
|
||||||
# Test binaries
|
# Test binaries
|
||||||
hello
|
hello
|
||||||
test_rc
|
test_rc
|
||||||
|
|||||||
149
CLAUDE.md
Normal file
149
CLAUDE.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# Lux Project Notes
|
||||||
|
|
||||||
|
## Development Environment
|
||||||
|
|
||||||
|
This is a **Nix environment**. Tools like `cargo`, `rustc`, `clippy`, etc. are not available in the base shell.
|
||||||
|
|
||||||
|
To run Rust/Cargo commands, use one of:
|
||||||
|
```bash
|
||||||
|
nix develop --command cargo test
|
||||||
|
nix develop --command cargo build
|
||||||
|
nix develop --command cargo clippy
|
||||||
|
nix develop --command cargo fmt
|
||||||
|
```
|
||||||
|
|
||||||
|
Or enter the development shell first:
|
||||||
|
```bash
|
||||||
|
nix develop
|
||||||
|
# then run commands normally
|
||||||
|
cargo test
|
||||||
|
```
|
||||||
|
|
||||||
|
The `lux` binary can be run directly if already built:
|
||||||
|
```bash
|
||||||
|
./target/debug/lux test
|
||||||
|
./target/release/lux <file.lux>
|
||||||
|
```
|
||||||
|
|
||||||
|
For additional tools not in the dev shell:
|
||||||
|
```bash
|
||||||
|
nix-shell -p <program>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
When making changes:
|
||||||
|
1. **Always run tests**: `cargo check && cargo test` - fix all errors and warnings
|
||||||
|
2. **Lint the Lux code**: `./target/release/lux lint` - fix warnings
|
||||||
|
3. **Check Lux code**: `./target/release/lux check` - type check + lint in one pass
|
||||||
|
4. **Format Lux code**: `./target/release/lux fmt` - auto-format all .lux files
|
||||||
|
5. **Write tests**: Add tests to cover new code
|
||||||
|
6. **Document features**: Provide documentation and tutorials for new features/frameworks
|
||||||
|
7. **Fix language limitations**: If you encounter parser/type system limitations, fix them (without regressions on guarantees or speed)
|
||||||
|
8. **Git commits**: Always use `--no-gpg-sign` flag
|
||||||
|
|
||||||
|
### Post-work checklist (run after each major piece of work)
|
||||||
|
```bash
|
||||||
|
nix develop --command cargo check # No Rust errors
|
||||||
|
nix develop --command cargo test # All tests pass (currently 381)
|
||||||
|
./target/release/lux check # Type check + lint all .lux files
|
||||||
|
./target/release/lux fmt # Format all .lux files
|
||||||
|
./target/release/lux lint # Standalone lint pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commit after every piece of work
|
||||||
|
**After completing each logical unit of work, commit immediately.** Do not let changes accumulate uncommitted across multiple features. Each commit should be a single logical change (one feature, one bugfix, etc.). Use `--no-gpg-sign` flag for all commits.
|
||||||
|
|
||||||
|
**IMPORTANT: Always verify Lux code you write:**
|
||||||
|
- Run with interpreter: `./target/release/lux file.lux`
|
||||||
|
- Compile to binary: `./target/release/lux compile file.lux`
|
||||||
|
- Both must work before claiming code is functional
|
||||||
|
- The C backend has limited effect support (Console, File only - no HttpServer, Http, etc.)
|
||||||
|
|
||||||
|
## CLI Commands & Aliases
|
||||||
|
|
||||||
|
| Command | Alias | Description |
|
||||||
|
|---------|-------|-------------|
|
||||||
|
| `lux fmt` | `lux f` | Format .lux files |
|
||||||
|
| `lux test` | `lux t` | Run test suite |
|
||||||
|
| `lux check` | `lux k` | Type check + lint |
|
||||||
|
| `lux lint` | `lux l` | Lint only (with `--explain` for detailed help) |
|
||||||
|
| `lux serve` | `lux s` | Static file server |
|
||||||
|
| `lux compile` | `lux c` | Compile to binary |
|
||||||
|
|
||||||
|
## Documenting Lux Language Errors
|
||||||
|
|
||||||
|
When working on any major task that involves writing Lux code, **document every language error, limitation, or surprising behavior** you encounter. This log is optimized for LLM consumption so future sessions can avoid repeating mistakes.
|
||||||
|
|
||||||
|
**File:** Maintain an `ISSUES.md` in the relevant project directory (e.g., `~/src/blu-site/ISSUES.md`).
|
||||||
|
|
||||||
|
**Format for each entry:**
|
||||||
|
```markdown
|
||||||
|
## Issue N: <Short descriptive title>
|
||||||
|
|
||||||
|
**Category**: Parser limitation | Type checker gap | Missing feature | Runtime error | Documentation gap
|
||||||
|
**Severity**: High | Medium | Low
|
||||||
|
**Status**: Open | **Fixed** (commit hash or version)
|
||||||
|
|
||||||
|
<1-2 sentence description of the problem>
|
||||||
|
|
||||||
|
**Reproduction:**
|
||||||
|
```lux
|
||||||
|
// Minimal code that triggers the issue
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error message:** `<exact error text>`
|
||||||
|
|
||||||
|
**Workaround:** <how to accomplish the goal despite the limitation>
|
||||||
|
|
||||||
|
**Fix:** <if fixed, what was changed and where>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
- Add new issues as you encounter them during any task
|
||||||
|
- When a previously documented issue gets fixed, update its status to **Fixed** and note the commit/version
|
||||||
|
- Remove entries that are no longer relevant (e.g., the feature was redesigned entirely)
|
||||||
|
- Keep the summary table at the bottom of ISSUES.md in sync with the entries
|
||||||
|
- Do NOT duplicate issues already documented -- check existing entries first
|
||||||
|
|
||||||
|
## Code Quality
|
||||||
|
|
||||||
|
- Fix all compiler warnings before committing
|
||||||
|
- Ensure all tests pass (currently 381 tests)
|
||||||
|
- Add new tests when adding features
|
||||||
|
- Keep examples and documentation in sync
|
||||||
|
|
||||||
|
## Lux Language Notes
|
||||||
|
|
||||||
|
### Top-level expressions
|
||||||
|
Bare `run` expressions are not allowed at top-level. You must wrap them in a `let` binding:
|
||||||
|
```lux
|
||||||
|
// WRONG: parse error
|
||||||
|
run main() with {}
|
||||||
|
|
||||||
|
// CORRECT
|
||||||
|
let output = run main() with {}
|
||||||
|
```
|
||||||
|
|
||||||
|
### String methods
|
||||||
|
Lux uses module-qualified function calls, not method syntax on primitives:
|
||||||
|
```lux
|
||||||
|
// WRONG: not valid syntax
|
||||||
|
path.endsWith(".html")
|
||||||
|
|
||||||
|
// CORRECT
|
||||||
|
String.endsWith(path, ".html")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available String functions
|
||||||
|
Key string functions (all in `String.` namespace):
|
||||||
|
- `String.length(s)` - get length
|
||||||
|
- `String.startsWith(s, prefix)` - check prefix
|
||||||
|
- `String.endsWith(s, suffix)` - check suffix
|
||||||
|
- `String.split(s, delimiter)` - split into list
|
||||||
|
- `String.join(list, delimiter)` - join list
|
||||||
|
- `String.substring(s, start, end)` - extract substring
|
||||||
|
- `String.indexOf(s, needle)` - find position (returns Option)
|
||||||
|
- `String.replace(s, old, new)` - replace occurrences
|
||||||
|
- `String.trim(s)` - trim whitespace
|
||||||
|
- `String.toLower(s)` / `String.toUpper(s)` - case conversion
|
||||||
683
Cargo.lock
generated
683
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -13,8 +13,10 @@ lsp-types = "0.94"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
||||||
tiny_http = "0.12"
|
tiny_http = "0.12"
|
||||||
|
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||||
|
postgres = "0.19"
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[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.
|
A functional programming language with first-class effects, schema evolution, and behavioral types.
|
||||||
|
|
||||||
## Vision
|
## Philosophy
|
||||||
|
|
||||||
Most programming languages treat three critical concerns as afterthoughts:
|
**Make the important things visible.**
|
||||||
|
|
||||||
1. **Effects** — What can this code do? (Hidden, untraceable, untestable)
|
Most languages hide what matters most: what code can do (effects), how data changes over time (schema evolution), and what guarantees functions provide (behavioral properties). Lux makes all three first-class, compiler-checked language features.
|
||||||
2. **Data Evolution** — Types change, data persists. (Manual migrations, runtime failures)
|
|
||||||
3. **Behavioral Properties** — Is this idempotent? Does it terminate? (Comments and hope)
|
|
||||||
|
|
||||||
Lux makes these first-class language features. The compiler knows what your code does, how your data evolves, and what properties your functions guarantee.
|
| Principle | What it means |
|
||||||
|
|-----------|--------------|
|
||||||
|
| **Explicit over implicit** | Effects in types — see what code does |
|
||||||
|
| **Composition over configuration** | No DI frameworks — effects compose naturally |
|
||||||
|
| **Safety without ceremony** | Type inference + explicit signatures where they matter |
|
||||||
|
| **Practical over academic** | Familiar syntax, ML semantics, no monads |
|
||||||
|
| **One right way** | Opinionated formatter, integrated tooling, built-in test framework |
|
||||||
|
| **Tools are the language** | `lux fmt/lint/check/test/compile` — one binary, not seven tools |
|
||||||
|
|
||||||
|
See [docs/PHILOSOPHY.md](./docs/PHILOSOPHY.md) for the full philosophy with language comparisons and design rationale.
|
||||||
|
|
||||||
## Core Principles
|
## Core Principles
|
||||||
|
|
||||||
@@ -120,17 +127,35 @@ fn main(): Unit with {Console} =
|
|||||||
|
|
||||||
## Status
|
## 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:
|
**Compilation Targets:**
|
||||||
- Core language (functions, closures, pattern matching)
|
- Interpreter (full-featured)
|
||||||
- Effect system (declare effects, use operations, handle with handlers)
|
- C backend (functions, closures, pattern matching, lists, reference counting)
|
||||||
- Type checking with effect tracking
|
- JavaScript backend (full language, browser & Node.js, DOM, TEA runtime)
|
||||||
- REPL for interactive development
|
|
||||||
|
**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:
|
See:
|
||||||
- [SKILLS.md](./SKILLS.md) — Language specification and implementation roadmap
|
- [docs/ROADMAP.md](./docs/ROADMAP.md) — Development roadmap and feature status
|
||||||
- [docs/VISION.md](./docs/VISION.md) — Problems Lux solves and development roadmap
|
|
||||||
- [docs/OVERVIEW.md](./docs/OVERVIEW.md) — Use cases, pros/cons, complexity analysis
|
- [docs/OVERVIEW.md](./docs/OVERVIEW.md) — Use cases, pros/cons, complexity analysis
|
||||||
|
|
||||||
## Design Goals
|
## Design Goals
|
||||||
@@ -150,20 +175,31 @@ See:
|
|||||||
|
|
||||||
## Building
|
## 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+:
|
Requires Rust 1.70+:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build the interpreter
|
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
./target/release/lux # REPL
|
||||||
# Run the REPL
|
./target/release/lux file.lux # Run a file
|
||||||
cargo run
|
cargo test # Tests
|
||||||
|
|
||||||
# Run a file
|
|
||||||
cargo run -- examples/hello.lux
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
cargo test
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|||||||
@@ -1,148 +1,140 @@
|
|||||||
# Lux Language Benchmark Results
|
# Lux Language Benchmark Results
|
||||||
|
|
||||||
Generated: Sat Feb 14 2026
|
Generated: Feb 16 2026
|
||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
- **Platform**: Linux x86_64
|
- **Platform**: Linux x86_64 (NixOS)
|
||||||
- **Lux**: Compiled to native via C (gcc -O2)
|
- **Lux**: Compiled via C backend + gcc -O3
|
||||||
- **Rust**: rustc 1.92.0 with -O
|
- **Tools**: hyperfine, poop
|
||||||
- **C**: gcc -O2
|
- **Comparison**: C (gcc), Rust (rustc+LLVM), Zig (LLVM)
|
||||||
- **Go**: go 1.25.5
|
|
||||||
- **Node.js**: v16.20.2 (V8 JIT)
|
|
||||||
- **Bun**: 1.3.5 (JavaScriptCore)
|
|
||||||
- **Python**: 3.13.5
|
|
||||||
|
|
||||||
## 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 |
|
## CPU Benchmark Results
|
||||||
|-----------|-----|------|---|-----|---------|-----|--------|
|
|
||||||
| 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 | - |
|
|
||||||
|
|
||||||
### Performance Rankings (Average)
|
### hyperfine (Statistical Timing)
|
||||||
|
|
||||||
1. **C** - Baseline (fastest)
|
```
|
||||||
2. **Rust** - ~1.0-1.5x of C
|
Summary
|
||||||
3. **Lux** - ~1.0-1.5x of C (matches Rust)
|
/tmp/fib_lux ran
|
||||||
4. **Go** - ~2-5x of C
|
1.03 ± 0.08 times faster than /tmp/fib_c
|
||||||
5. **Bun** - ~10-20x of C
|
1.47 ± 0.04 times faster than /tmp/fib_rust
|
||||||
6. **Node.js** - ~15-30x of C
|
1.67 ± 0.05 times faster than /tmp/fib_zig
|
||||||
7. **Python** - ~30-300x of C
|
```
|
||||||
|
|
||||||
## 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)
|
### poop (Detailed CPU Metrics)
|
||||||
**Tests**: Recursive function calls
|
|
||||||
|
|
||||||
| Language | Time (s) | vs Lux |
|
| Metric | C | Lux | Rust | Zig |
|
||||||
|----------|----------|--------|
|
|--------|---|-----|------|-----|
|
||||||
| C | 0.014 | 0.93x |
|
| Wall Time | 29.0ms | 29.2ms | 42.0ms | 48.1ms |
|
||||||
| Lux | 0.015 | 1.00x |
|
| CPU Cycles | 53.1M | 53.2M | 78.2M | 90.4M |
|
||||||
| Rust | 0.018 | 1.20x |
|
| Instructions | 293M | 292M | 302M | 317M |
|
||||||
| Go | 0.041 | 2.73x |
|
| Cache Misses | 4.39K | 4.62K | 6.47K | 340 |
|
||||||
| Bun | 0.065 | 4.33x |
|
| Branch Misses | 28.3K | 32.0K | 33.5K | 29.6K |
|
||||||
| Node.js | 0.110 | 7.33x |
|
| Peak RSS | 1.56MB | 1.63MB | 2.00MB | 1.07MB |
|
||||||
| Python | 0.928 | 61.87x |
|
|
||||||
|
|
||||||
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)
|
### The Key: gcc's Recursion Transformation
|
||||||
**Tests**: Loops and conditionals
|
|
||||||
|
|
||||||
| Language | Time (s) | vs Lux |
|
Lux compiles to C, which gcc optimizes aggressively. For the Fibonacci benchmark:
|
||||||
|----------|----------|--------|
|
|
||||||
| 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 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)
|
**Lux/C (gcc)** transforms to loops:
|
||||||
**Tests**: Tight numeric loop (tail-recursive in Lux)
|
```asm
|
||||||
|
; No recursive calls - fully loop-transformed
|
||||||
|
; Uses registers as accumulators
|
||||||
|
```
|
||||||
|
|
||||||
| Language | Time (s) | vs Lux |
|
### Instruction Count Tells the Story
|
||||||
|----------|----------|--------|
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
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)
|
More instructions = more work = slower execution.
|
||||||
**Tests**: Deep recursion (stack-heavy)
|
|
||||||
|
|
||||||
| Language | Time (s) | vs Lux |
|
## HTTP Benchmarks
|
||||||
|----------|----------|--------|
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
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)
|
### TechEmpower Framework Benchmarks
|
||||||
**Tests**: Sorting algorithm simulation
|
The industry standard: https://www.techempower.com/benchmarks/
|
||||||
|
|
||||||
| Language | Time (s) | vs Lux |
|
### Standard HTTP Benchmark Tools
|
||||||
|----------|----------|--------|
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
### 6. List Operations (10000 elements)
|
```bash
|
||||||
**Tests**: map/filter/fold on functional lists with closures
|
# wrk - modern HTTP benchmarking
|
||||||
|
wrk -t4 -c100 -d10s http://localhost:8080/
|
||||||
|
|
||||||
| Language | Time (s) | vs Lux |
|
# ab (Apache Bench) - classic tool
|
||||||
|----------|----------|--------|
|
ab -n 10000 -c 100 http://localhost:8080/
|
||||||
| Lux | 0.002 | 1.00x |
|
|
||||||
| Bun | 0.016 | 8.00x |
|
|
||||||
| Node.js | 0.030 | 15.00x |
|
|
||||||
|
|
||||||
This benchmark showcases Lux's functional programming capabilities with FBIP optimization:
|
# hey - written in Go
|
||||||
- **20,006 allocations, 20,006 frees** (no memory leaks)
|
hey -n 10000 -c 100 http://localhost:8080/
|
||||||
- **2 FBIP reuses, 0 copies** (efficient memory reuse)
|
```
|
||||||
|
|
||||||
## Key Observations
|
### Reference Implementations
|
||||||
|
|
||||||
1. **Native Performance**: Lux consistently matches or beats Rust and C across benchmarks
|
For fair HTTP comparisons, use minimal stdlib servers:
|
||||||
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
|
|
||||||
|
|
||||||
## 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:
|
HTTP benchmarks measure I/O patterns more than language speed. Use established frameworks for meaningful comparisons.
|
||||||
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
|
|
||||||
|
|
||||||
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 |
|
| Component | Status |
|
||||||
|-----------|--------|
|
|-----------|--------|
|
||||||
| Interpreter | Full-featured, all language constructs |
|
| 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 |
|
| JIT (Cranelift) | Integer arithmetic only, ~160x speedup |
|
||||||
| Targets | Native (via Cranelift JIT) |
|
| Targets | Native (via C), JavaScript, JIT |
|
||||||
|
|
||||||
## Target Architecture
|
## Target Architecture
|
||||||
|
|
||||||
@@ -296,45 +298,33 @@ Tree increment(Tree tree) {
|
|||||||
|
|
||||||
## Milestones
|
## Milestones
|
||||||
|
|
||||||
### v0.2.0 - C Backend (Basic)
|
### C Backend - COMPLETE
|
||||||
- [ ] Integer/bool expressions → C
|
- [x] Integer/bool expressions → C
|
||||||
- [ ] Functions → C functions
|
- [x] Functions → C functions
|
||||||
- [ ] If/else → C conditionals
|
- [x] If/else → C conditionals
|
||||||
- [ ] Let bindings → C variables
|
- [x] Let bindings → C variables
|
||||||
- [ ] Basic main() generation
|
- [x] Basic main() generation
|
||||||
- [ ] Build with GCC/Clang
|
- [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)
|
### JavaScript Backend - COMPLETE
|
||||||
- [ ] Strings → C strings
|
- [x] Basic expressions → JS
|
||||||
- [ ] Records → C structs
|
- [x] Functions → JS functions
|
||||||
- [ ] ADTs → Tagged unions
|
- [x] Effects → Direct DOM/API calls
|
||||||
- [ ] Pattern matching → Switch/if chains
|
- [x] Standard library (String, List, Option, Result, Math, JSON)
|
||||||
- [ ] Lists → Linked structures
|
- [x] DOM effect (40+ operations)
|
||||||
- [ ] Effect compilation (basic)
|
- [x] Html module (type-safe HTML)
|
||||||
|
- [x] TEA runtime (Elm Architecture)
|
||||||
|
- [x] Browser & Node.js support
|
||||||
|
|
||||||
### v0.4.0 - Evidence Passing
|
### Remaining Work
|
||||||
- [ ] Effect analysis
|
- [ ] Evidence passing for zero-cost effects
|
||||||
- [ ] Evidence vector generation
|
- [ ] FBIP (Functional But In-Place) optimization
|
||||||
- [ ] Transform effect ops to direct calls
|
- [ ] WASM backend (deprioritized)
|
||||||
- [ ] 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
|
|
||||||
|
|
||||||
## References
|
## 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`, `\"`, `\\`
|
- Escape sequences: `\{`, `\}`, `\n`, `\t`, `\"`, `\\`
|
||||||
|
|
||||||
#### 1.3 Better Error Messages
|
#### 1.3 Better Error Messages
|
||||||
**Status:** ⚠️ Partial
|
**Status:** ✅ Complete (Elm-quality)
|
||||||
|
|
||||||
**What's Working:**
|
**What's Working:**
|
||||||
- Source code context with line/column numbers
|
- Source code context with line/column numbers
|
||||||
- Caret pointing to error location
|
- Caret pointing to error location
|
||||||
- Color-coded error output
|
- Color-coded error output
|
||||||
|
- Field suggestions for unknown fields
|
||||||
|
- Error categorization
|
||||||
|
- Improved hints
|
||||||
|
|
||||||
**What's Missing:**
|
**Nice-to-have (not critical):**
|
||||||
- Type diff display for mismatches
|
|
||||||
- "Did you mean?" suggestions
|
|
||||||
- Error recovery in parser
|
- Error recovery in parser
|
||||||
|
- Type diff visualization
|
||||||
**Implementation Steps:**
|
|
||||||
1. Add Levenshtein distance for suggestions
|
|
||||||
2. Implement error recovery in parser
|
|
||||||
3. Add type diff visualization
|
|
||||||
|
|
||||||
### Priority 2: Effect System Completion
|
### 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
|
- Versioned type declarations tracked
|
||||||
- Migration bodies stored for future execution
|
- Migration bodies stored for future execution
|
||||||
|
|
||||||
**Still Missing (nice-to-have):**
|
**What's Working:**
|
||||||
- Auto-migration generation
|
- Auto-migration generation
|
||||||
|
|
||||||
|
**Still Missing (nice-to-have):**
|
||||||
- Version-aware serialization/codecs
|
- Version-aware serialization/codecs
|
||||||
|
|
||||||
### Priority 4: Module System
|
### Priority 4: Module System
|
||||||
@@ -254,27 +253,43 @@ The module system is fully functional with:
|
|||||||
### Priority 6: Tooling
|
### Priority 6: Tooling
|
||||||
|
|
||||||
#### 6.1 Package Manager
|
#### 6.1 Package Manager
|
||||||
**What's Needed:**
|
**Status:** ✅ Complete
|
||||||
- Package registry
|
|
||||||
- Dependency resolution
|
**What's Working:**
|
||||||
- Version management
|
- `lux pkg init` - Initialize project with lux.toml
|
||||||
- Build system integration
|
- `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
|
#### 6.2 Standard Library
|
||||||
**What's Needed:**
|
**Status:** ✅ Complete
|
||||||
- Collections (Map, Set, Array)
|
|
||||||
- String utilities
|
**What's Working:**
|
||||||
|
- String operations (substring, length, split, trim, etc.)
|
||||||
|
- List operations (map, filter, fold, etc.)
|
||||||
|
- Option and Result operations
|
||||||
- Math functions
|
- Math functions
|
||||||
- File I/O
|
- JSON parsing and serialization
|
||||||
- Network I/O
|
|
||||||
- JSON/YAML parsing
|
**Still Missing (nice-to-have):**
|
||||||
|
- Collections (Map, Set)
|
||||||
|
- YAML parsing
|
||||||
|
|
||||||
#### 6.3 Debugger
|
#### 6.3 Debugger
|
||||||
**What's Needed:**
|
**Status:** ✅ Basic
|
||||||
|
|
||||||
|
**What's Working:**
|
||||||
|
- Basic debugger
|
||||||
|
|
||||||
|
**Nice-to-have:**
|
||||||
- Breakpoints
|
- Breakpoints
|
||||||
- Step execution
|
- Step execution
|
||||||
- Variable inspection
|
- Variable inspection
|
||||||
- Stack traces
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -302,7 +317,7 @@ The module system is fully functional with:
|
|||||||
13. ~~**Idempotent verification**~~ ✅ Done - Pattern-based analysis
|
13. ~~**Idempotent verification**~~ ✅ Done - Pattern-based analysis
|
||||||
14. ~~**Deterministic verification**~~ ✅ Done - Effect-based analysis
|
14. ~~**Deterministic verification**~~ ✅ Done - Effect-based analysis
|
||||||
15. ~~**Commutative verification**~~ ✅ Done - Operator 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)
|
### Phase 5: Schema Evolution (Data)
|
||||||
17. ~~**Type system version tracking**~~ ✅ Done
|
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 obj = Json.parse("{\"name\": \"Alice\"}")
|
||||||
let str = Json.stringify(obj)
|
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
|
// Module system
|
||||||
import mymodule
|
import mymodule
|
||||||
import utils/helpers as h
|
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
|
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
|
## Primary Use Cases
|
||||||
@@ -235,7 +240,7 @@ Quick iteration with type inference and a REPL.
|
|||||||
|------------|-------------|
|
|------------|-------------|
|
||||||
| **New Paradigm** | Effects require learning new concepts |
|
| **New Paradigm** | Effects require learning new concepts |
|
||||||
| **Small Ecosystem** | Community packages just starting |
|
| **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 |
|
| **Early Stage** | Bugs likely, features incomplete |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -374,18 +379,10 @@ Values + Effects C Code → GCC/Clang
|
|||||||
- ✅ Formatter
|
- ✅ Formatter
|
||||||
|
|
||||||
**In Progress:**
|
**In Progress:**
|
||||||
1. **Schema Evolution** - Type-declared migrations working, auto-generation pending
|
1. **Memory Management** - RC working for lists/boxed, closures/ADTs pending
|
||||||
2. **Error Message Quality** - Context lines shown, suggestions partial
|
2. **Serialization Codecs** - JSON codec generation for versioned types
|
||||||
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
|
|
||||||
|
|
||||||
**Planned:**
|
**Planned:**
|
||||||
4. **SQL Effect** - Database access (as a package)
|
- **Connection Pooling** - Pool database connections
|
||||||
5. **Package Registry** - Central repository for sharing packages
|
- **WASM Backend** - WebAssembly compilation target
|
||||||
6. **Behavioral Type Verification** - Total, idempotent, deterministic checking
|
- **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
|
8. [Error Handling](guide/08-errors.md) - Fail effect, Option, Result
|
||||||
9. [Standard Library](guide/09-stdlib.md) - Built-in functions
|
9. [Standard Library](guide/09-stdlib.md) - Built-in functions
|
||||||
10. [Advanced Topics](guide/10-advanced.md) - Traits, generics, optimization
|
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)
|
### [Language Reference](reference/syntax.md)
|
||||||
Complete syntax and semantics reference.
|
Complete syntax and semantics reference.
|
||||||
|
|
||||||
- [Syntax](reference/syntax.md) - Grammar and syntax rules
|
- [Syntax](reference/syntax.md) - Grammar and syntax rules
|
||||||
- [Types](reference/types.md) - Type system details
|
- [Standard Library](guide/09-stdlib.md) - Built-in functions and modules
|
||||||
- [Effects](reference/effects.md) - Effect system reference
|
|
||||||
- [Standard Library](reference/stdlib.md) - All built-in functions
|
|
||||||
|
|
||||||
### [Tutorials](tutorials/README.md)
|
### [Tutorials](tutorials/README.md)
|
||||||
Project-based learning.
|
Project-based learning.
|
||||||
|
|
||||||
**Standard Programs:**
|
|
||||||
- [Calculator](tutorials/calculator.md) - Basic REPL calculator
|
- [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
|
- [Dependency Injection](tutorials/dependency-injection.md) - Testing with effects
|
||||||
- [State Machines](tutorials/state-machines.md) - Modeling state with effects
|
- [Project Ideas](tutorials/project-ideas.md) - Ideas for building with Lux
|
||||||
- [Parser Combinators](tutorials/parsers.md) - Effects for backtracking
|
|
||||||
|
### 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 |
|
| Runtime versioned values | ✅ Complete |
|
||||||
| Schema registry & compatibility checking | ✅ Complete |
|
| Schema registry & compatibility checking | ✅ Complete |
|
||||||
| Basic migration execution | ✅ Complete |
|
| Basic migration execution | ✅ Complete |
|
||||||
| Type system integration | ⚠️ Partial (versions ignored in typechecker) |
|
| Type system integration | ✅ Complete |
|
||||||
| Auto-migration generation | ❌ Missing |
|
| Auto-migration generation | ✅ Complete |
|
||||||
| Serialization/codec support | ❌ Missing |
|
| Serialization/codec support | ❌ Missing |
|
||||||
|
|
||||||
### Behavioral Types
|
### Behavioral Types
|
||||||
@@ -24,10 +24,11 @@
|
|||||||
| Parser (`is pure`, `is total`, etc.) | ✅ Complete |
|
| Parser (`is pure`, `is total`, etc.) | ✅ Complete |
|
||||||
| AST & PropertySet | ✅ Complete |
|
| AST & PropertySet | ✅ Complete |
|
||||||
| Pure function checking (no effects) | ✅ Complete |
|
| Pure function checking (no effects) | ✅ Complete |
|
||||||
| Total verification | ❌ Missing |
|
| Total verification (no Fail, structural recursion) | ✅ Complete |
|
||||||
| Idempotent verification | ❌ Missing |
|
| Idempotent verification (pattern-based) | ✅ Complete |
|
||||||
| Deterministic verification | ❌ Missing |
|
| Deterministic verification (no Random/Time) | ✅ Complete |
|
||||||
| Where clause enforcement | ❌ Missing |
|
| Commutative verification (2 params, commutative op) | ✅ Complete |
|
||||||
|
| Where clause property constraints | ✅ Complete |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -49,9 +50,10 @@
|
|||||||
|
|
||||||
| Task | Priority | Effort | Status |
|
| 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 |
|
| Connection pooling | P2 | 1 week | ❌ Missing |
|
||||||
| Transaction effect | P2 | 1 week | ❌ Missing |
|
| PostgreSQL support | P1 | 2 weeks | ✅ Complete |
|
||||||
|
|
||||||
### Phase 1.3: Web Server Framework
|
### Phase 1.3: Web Server Framework
|
||||||
|
|
||||||
@@ -66,7 +68,7 @@
|
|||||||
|
|
||||||
| Task | Priority | Effort | Status |
|
| 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 |
|
| Full JS compilation | P2 | 4 weeks | ✅ Complete |
|
||||||
| Hot reload / watch mode | P2 | — | ✅ Complete |
|
| Hot reload / watch mode | P2 | — | ✅ Complete |
|
||||||
| Debugger improvements | P3 | 2 weeks | ✅ Basic |
|
| Debugger improvements | P3 | 2 weeks | ✅ Basic |
|
||||||
@@ -88,15 +90,16 @@
|
|||||||
|
|
||||||
| Task | Priority | Effort | Status |
|
| Task | Priority | Effort | Status |
|
||||||
|------|----------|--------|--------|
|
|------|----------|--------|--------|
|
||||||
| Total function verification | P1 | 2 weeks | ❌ Missing |
|
| Total function verification | P1 | 2 weeks | ✅ Complete |
|
||||||
| Idempotent verification | P1 | 2 weeks | ❌ Missing |
|
| Idempotent verification | P1 | 2 weeks | ✅ Complete |
|
||||||
| Deterministic verification | P1 | 1 week | ❌ Missing |
|
| Deterministic verification | P1 | 1 week | ✅ Complete |
|
||||||
| Where clause enforcement | P1 | 1 week | ❌ Missing |
|
| Where clause enforcement | P1 | 1 week | ✅ Complete |
|
||||||
|
|
||||||
**Implementation approach:**
|
**Implementation approach (completed):**
|
||||||
- **Total:** Restrict to structural recursion, require termination proof for general recursion
|
- **Total:** Checks for no Fail effect + structural recursion for termination
|
||||||
- **Idempotent:** Pattern-based (setter patterns, specific effect combinations)
|
- **Idempotent:** Pattern-based recognition (constants, identity, clamping, abs, projections)
|
||||||
- **Deterministic:** Effect analysis (no Random, Time, or non-deterministic IO)
|
- **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)
|
### Phase 2.2: Refinement Types (Stretch Goal)
|
||||||
|
|
||||||
@@ -130,10 +133,10 @@
|
|||||||
|
|
||||||
| Task | Priority | Effort | Status |
|
| Task | Priority | Effort | Status |
|
||||||
|------|----------|--------|--------|
|
|------|----------|--------|--------|
|
||||||
| Type system version tracking | P1 | 1 week | ⚠️ Partial |
|
| Type system version tracking | P1 | 1 week | ✅ Complete |
|
||||||
| Auto-migration generation | P1 | 2 weeks | ❌ Missing |
|
| Auto-migration generation | P1 | 2 weeks | ✅ Complete |
|
||||||
| Version compatibility errors | P1 | 1 week | ❌ Missing |
|
| Version compatibility errors | P1 | 1 week | ✅ Complete |
|
||||||
| Migration chain optimization | P2 | 1 week | ⚠️ Basic |
|
| Migration chain optimization | P2 | 1 week | ✅ Complete |
|
||||||
|
|
||||||
### Phase 3.2: Serialization Support
|
### Phase 3.2: Serialization Support
|
||||||
|
|
||||||
@@ -205,8 +208,11 @@
|
|||||||
|------|----------|--------|--------|
|
|------|----------|--------|--------|
|
||||||
| Package manager (lux pkg) | P1 | 3 weeks | ✅ Complete |
|
| Package manager (lux pkg) | P1 | 3 weeks | ✅ Complete |
|
||||||
| Module loader integration | P1 | 1 week | ✅ Complete |
|
| Module loader integration | P1 | 1 week | ✅ Complete |
|
||||||
| Package registry | P2 | 2 weeks | ❌ Missing |
|
| Package registry server | P2 | 2 weeks | ✅ Complete |
|
||||||
| Dependency resolution | P2 | 2 weeks | ❌ Missing |
|
| Registry CLI (search, publish) | P2 | 1 week | ✅ Complete |
|
||||||
|
| Lock file generation | P1 | 1 week | ✅ Complete |
|
||||||
|
| Version constraint parsing | P1 | 1 week | ✅ Complete |
|
||||||
|
| Transitive dependency resolution | P2 | 2 weeks | ⚠️ Basic (direct deps only) |
|
||||||
|
|
||||||
**Package Manager Features:**
|
**Package Manager Features:**
|
||||||
- `lux pkg init` - Initialize project with lux.toml
|
- `lux pkg init` - Initialize project with lux.toml
|
||||||
@@ -219,8 +225,8 @@
|
|||||||
|
|
||||||
| Task | Priority | Effort | Status |
|
| Task | Priority | Effort | Status |
|
||||||
|------|----------|--------|--------|
|
|------|----------|--------|--------|
|
||||||
| LSP completions | P1 | 1 week | ⚠️ Basic |
|
| LSP completions | P1 | 1 week | ✅ Complete (module-specific completions) |
|
||||||
| LSP go-to-definition | P1 | 1 week | ⚠️ Partial |
|
| LSP go-to-definition | P1 | 1 week | ✅ Complete (functions, lets, types) |
|
||||||
| Formatter | P2 | — | ✅ Complete |
|
| Formatter | P2 | — | ✅ Complete |
|
||||||
| Documentation generator | P2 | 1 week | ❌ Missing |
|
| Documentation generator | P2 | 1 week | ❌ Missing |
|
||||||
|
|
||||||
@@ -253,21 +259,21 @@
|
|||||||
3. ~~**File effect**~~ ✅ Done
|
3. ~~**File effect**~~ ✅ Done
|
||||||
4. ~~**HTTP client effect**~~ ✅ Done
|
4. ~~**HTTP client effect**~~ ✅ Done
|
||||||
5. ~~**JSON support**~~ ✅ 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
|
7. ~~**HTTP server effect**~~ ✅ Done
|
||||||
8. **SQL effect** — Database access
|
8. ~~**SQL effect**~~ ✅ Done
|
||||||
9. **Full JS compilation** — Deployment
|
9. ~~**Full JS compilation**~~ ✅ Done
|
||||||
10. **Package manager** — Code sharing
|
10. ~~**Package manager**~~ ✅ Done
|
||||||
|
|
||||||
### Quarter 3: Reliability (Use Case 2)
|
### Quarter 3: Reliability (Use Case 2) ✅ COMPLETE
|
||||||
|
|
||||||
11. **Behavioral type verification** — Total, idempotent, deterministic
|
11. ~~**Behavioral type verification**~~ ✅ Done
|
||||||
12. **Where clause enforcement** — Type-level guarantees
|
12. ~~**Where clause enforcement**~~ ✅ Done
|
||||||
13. **Schema evolution completion** — Version tracking in types
|
13. ~~**Schema evolution completion**~~ ✅ Done
|
||||||
14. **Auto-migration generation** — Reduce boilerplate
|
14. ~~**Auto-migration generation**~~ ✅ Done
|
||||||
|
|
||||||
### Quarter 4: Polish (Use Cases 3 & 4)
|
### Quarter 4: Polish (Use Cases 3 & 4)
|
||||||
|
|
||||||
@@ -298,6 +304,8 @@
|
|||||||
- ✅ Random effect (int, float, range, bool)
|
- ✅ Random effect (int, float, range, bool)
|
||||||
- ✅ Time effect (now, sleep)
|
- ✅ Time effect (now, sleep)
|
||||||
- ✅ Test effect (assert, assertEqual, assertTrue, assertFalse)
|
- ✅ Test effect (assert, assertEqual, assertTrue, assertFalse)
|
||||||
|
- ✅ SQL effect (SQLite with transactions)
|
||||||
|
- ✅ Postgres effect (PostgreSQL connections)
|
||||||
|
|
||||||
**Module System:**
|
**Module System:**
|
||||||
- ✅ Imports, exports, aliases
|
- ✅ Imports, exports, aliases
|
||||||
@@ -317,14 +325,14 @@
|
|||||||
- ✅ C backend (functions, closures, pattern matching, lists)
|
- ✅ C backend (functions, closures, pattern matching, lists)
|
||||||
- ✅ JS backend (full language support, browser & Node.js)
|
- ✅ JS backend (full language support, browser & Node.js)
|
||||||
- ✅ REPL with history
|
- ✅ REPL with history
|
||||||
- ✅ Basic LSP server
|
- ✅ LSP server (diagnostics, hover, completions, go-to-definition, references, symbols)
|
||||||
- ✅ Formatter
|
- ✅ Formatter
|
||||||
- ✅ Watch mode
|
- ✅ Watch mode
|
||||||
- ✅ Debugger (basic)
|
- ✅ Debugger (basic)
|
||||||
|
|
||||||
**Advanced (Parsing Only):**
|
**Advanced:**
|
||||||
- ✅ Schema evolution (parsing, runtime values)
|
- ✅ Schema evolution (parser, runtime, migrations, compatibility checking)
|
||||||
- ✅ Behavioral types (parsing, pure checking only)
|
- ✅ 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
|
# 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
|
## Benchmark Environment
|
||||||
|
|
||||||
- **Platform**: Linux x86_64
|
- **Platform**: Linux x86_64 (NixOS)
|
||||||
- **Lux**: Compiled to native via C backend with `-O2` optimization
|
- **Lux**: v0.1.0 (compiled via C backend)
|
||||||
- **Node.js**: v16.x (V8 JIT)
|
- **C**: gcc with -O3
|
||||||
- **Rust**: rustc with `-O` (release optimization)
|
- **Rust**: rustc with -C opt-level=3 -C lto
|
||||||
|
- **Zig**: zig with -O ReleaseFast
|
||||||
|
- **Tools**: hyperfine, poop
|
||||||
|
|
||||||
## Results Summary
|
## Results Summary
|
||||||
|
|
||||||
| Benchmark | Lux (native) | Node.js | Rust (native) |
|
### hyperfine Results
|
||||||
|-----------|-------------|---------|---------------|
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
### Key Findings
|
```
|
||||||
|
Benchmark 1: /tmp/fib_lux
|
||||||
|
Time (mean ± σ): 28.1 ms ± 0.6 ms
|
||||||
|
|
||||||
1. **Lux matches or beats Rust** on these benchmarks
|
Benchmark 2: /tmp/fib_c
|
||||||
2. **Lux is 8-30x faster than Node.js** depending on workload
|
Time (mean ± σ): 29.0 ms ± 2.1 ms
|
||||||
3. **Native compilation pays off** - AOT compilation to C produces highly optimized code
|
|
||||||
|
|
||||||
## 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
|
| Benchmark | C (gcc -O3) | Rust | Zig | **Lux (compiled)** | Lux (interp) |
|
||||||
fn fib(n: Int): Int = {
|
|-----------|-------------|------|-----|---------------------|--------------|
|
||||||
if n <= 1 then n
|
| Fibonacci(35) | 29.0ms | 41.2ms | 47.0ms | **28.1ms** | 254ms |
|
||||||
else fib(n - 1) + fib(n - 2)
|
|
||||||
|
### 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)
|
This gives gcc maximum freedom to optimize without fighting language-specific abstractions.
|
||||||
- **Rust**: 0.022s
|
|
||||||
- **Node.js**: 0.111s
|
|
||||||
|
|
||||||
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
|
## Comparison Context
|
||||||
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)
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Lux**: 0.001s
|
| Language | fib(35) | Type | vs Lux |
|
||||||
- **Rust**: 0.001s
|
|----------|---------|------|--------|
|
||||||
- **Node.js**: 0.029s
|
| **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.
|
| Scenario | Likely Winner | Why |
|
||||||
|
|----------|---------------|-----|
|
||||||
```lux
|
| Simple recursion | **Lux/C** | gcc's strength |
|
||||||
fn isPrime(n: Int): Bool = {
|
| SIMD/vectorization | Rust/Zig | Explicit SIMD intrinsics |
|
||||||
if n < 2 then false
|
| Async I/O | Rust (tokio) | Mature async runtime |
|
||||||
else if n == 2 then true
|
| Memory-heavy workloads | Zig | Fine-grained allocator control |
|
||||||
else if n % 2 == 0 then false
|
| Hot loops with bounds checks | C | No safety overhead |
|
||||||
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
|
|
||||||
|
|
||||||
## Running Benchmarks
|
## Running Benchmarks
|
||||||
|
|
||||||
```bash
|
### Using Nix Flake Commands
|
||||||
# Run all benchmarks
|
|
||||||
./benchmarks/run_benchmarks.sh
|
|
||||||
|
|
||||||
# Run individual benchmark
|
```bash
|
||||||
cargo run --release -- compile benchmarks/fib.lux -o /tmp/fib && /tmp/fib
|
# 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
|
```bash
|
||||||
- **vs Node.js**: Lux is much faster because V8's JIT can't match AOT compilation for compute-heavy tasks
|
# Enter development shell (includes hyperfine, poop)
|
||||||
- **vs Python**: Would be even more dramatic (Python is typically 10-100x slower than Node.js)
|
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)
|
# Run hyperfine
|
||||||
- Compare against more languages (Go, Java, OCaml, Haskell)
|
hyperfine --warmup 3 '/tmp/fib_lux' '/tmp/fib_c' '/tmp/fib_rust' '/tmp/fib_zig'
|
||||||
- Add memory usage benchmarks
|
|
||||||
- Profile and optimize hot paths
|
# 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 |
|
| `Process` | `exec`, `env`, `args`, `cwd`, `exit` | System processes |
|
||||||
| `Http` | `get`, `post`, `put`, `delete` | HTTP client |
|
| `Http` | `get`, `post`, `put`, `delete` | HTTP client |
|
||||||
| `HttpServer` | `listen`, `accept`, `respond`, `stop` | HTTP server |
|
| `HttpServer` | `listen`, `accept`, `respond`, `stop` | HTTP server |
|
||||||
|
| `Sql` | `open`, `openMemory`, `close`, `execute`, `query`, `queryOne`, `beginTx`, `commit`, `rollback` | SQLite database |
|
||||||
|
| `Postgres` | `connect`, `close`, `execute`, `query`, `queryOne` | PostgreSQL database |
|
||||||
|
| `Concurrent` | `spawn`, `await`, `yield`, `sleep`, `cancel`, `isRunning`, `taskCount` | Concurrent tasks |
|
||||||
|
| `Channel` | `create`, `send`, `receive`, `tryReceive`, `close` | Inter-task communication |
|
||||||
| `Test` | `assert`, `assertEqual`, `assertTrue`, `assertFalse` | Testing |
|
| `Test` | `assert`, `assertEqual`, `assertTrue`, `assertFalse` | Testing |
|
||||||
|
|
||||||
Example usage:
|
Example usage:
|
||||||
|
|||||||
@@ -320,6 +320,114 @@ fn example(): Int with {Fail} = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Sql (SQLite)
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn example(): Unit with {Sql, Console} = {
|
||||||
|
let conn = Sql.open("mydb.sqlite") // Open database file
|
||||||
|
// Or: let conn = Sql.openMemory() // In-memory database
|
||||||
|
|
||||||
|
// Execute statements (returns row count)
|
||||||
|
Sql.execute(conn, "CREATE TABLE users (id INTEGER, name TEXT)")
|
||||||
|
Sql.execute(conn, "INSERT INTO users VALUES (1, 'Alice')")
|
||||||
|
|
||||||
|
// Query returns list of rows
|
||||||
|
let rows = Sql.query(conn, "SELECT * FROM users")
|
||||||
|
|
||||||
|
// Query for single row
|
||||||
|
let user = Sql.queryOne(conn, "SELECT * FROM users WHERE id = 1")
|
||||||
|
|
||||||
|
// Transactions
|
||||||
|
Sql.beginTx(conn)
|
||||||
|
Sql.execute(conn, "UPDATE users SET name = 'Bob' WHERE id = 1")
|
||||||
|
Sql.commit(conn) // Or: Sql.rollback(conn)
|
||||||
|
|
||||||
|
Sql.close(conn)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Postgres (PostgreSQL)
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn example(): Unit with {Postgres, Console} = {
|
||||||
|
let conn = Postgres.connect("postgres://user:pass@localhost/mydb")
|
||||||
|
|
||||||
|
// Execute statements
|
||||||
|
Postgres.execute(conn, "INSERT INTO users (name) VALUES ('Alice')")
|
||||||
|
|
||||||
|
// Query returns list of rows
|
||||||
|
let rows = Postgres.query(conn, "SELECT * FROM users")
|
||||||
|
|
||||||
|
// Query for single row
|
||||||
|
let user = Postgres.queryOne(conn, "SELECT * FROM users WHERE id = 1")
|
||||||
|
|
||||||
|
Postgres.close(conn)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Concurrent (Parallel Tasks)
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn example(): Unit with {Concurrent, Console} = {
|
||||||
|
// Spawn concurrent tasks
|
||||||
|
let task1 = Concurrent.spawn(fn(): Int => expensiveComputation(1))
|
||||||
|
let task2 = Concurrent.spawn(fn(): Int => expensiveComputation(2))
|
||||||
|
|
||||||
|
// Do other work while tasks run
|
||||||
|
Console.print("Tasks spawned, doing other work...")
|
||||||
|
|
||||||
|
// Wait for tasks to complete
|
||||||
|
let result1 = Concurrent.await(task1)
|
||||||
|
let result2 = Concurrent.await(task2)
|
||||||
|
|
||||||
|
Console.print("Results: " + toString(result1) + ", " + toString(result2))
|
||||||
|
|
||||||
|
// Check task status
|
||||||
|
if Concurrent.isRunning(task1) then
|
||||||
|
Concurrent.cancel(task1)
|
||||||
|
|
||||||
|
// Non-blocking sleep
|
||||||
|
Concurrent.sleep(100) // 100ms
|
||||||
|
|
||||||
|
// Yield to allow other tasks to run
|
||||||
|
Concurrent.yield()
|
||||||
|
|
||||||
|
// Get active task count
|
||||||
|
let count = Concurrent.taskCount()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Channel (Inter-Task Communication)
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn example(): Unit with {Concurrent, Channel, Console} = {
|
||||||
|
// Create a channel for communication
|
||||||
|
let ch = Channel.create()
|
||||||
|
|
||||||
|
// Spawn producer task
|
||||||
|
let producer = Concurrent.spawn(fn(): Unit => {
|
||||||
|
Channel.send(ch, 1)
|
||||||
|
Channel.send(ch, 2)
|
||||||
|
Channel.send(ch, 3)
|
||||||
|
Channel.close(ch)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Consumer receives values
|
||||||
|
match Channel.receive(ch) {
|
||||||
|
Some(value) => Console.print("Received: " + toString(value)),
|
||||||
|
None => Console.print("Channel closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-blocking receive
|
||||||
|
match Channel.tryReceive(ch) {
|
||||||
|
Some(value) => Console.print("Got: " + toString(value)),
|
||||||
|
None => Console.print("No value available")
|
||||||
|
}
|
||||||
|
|
||||||
|
Concurrent.await(producer)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Test
|
### Test
|
||||||
|
|
||||||
Native testing framework:
|
Native testing framework:
|
||||||
@@ -360,6 +468,10 @@ fn main(): Unit with {Console} = {
|
|||||||
| Random | int, float, bool |
|
| Random | int, float, bool |
|
||||||
| State | get, put |
|
| State | get, put |
|
||||||
| Fail | fail |
|
| Fail | fail |
|
||||||
|
| Sql | open, openMemory, close, execute, query, queryOne, beginTx, commit, rollback |
|
||||||
|
| Postgres | connect, close, execute, query, queryOne |
|
||||||
|
| Concurrent | spawn, await, yield, sleep, cancel, isRunning, taskCount |
|
||||||
|
| Channel | create, send, receive, tryReceive, close |
|
||||||
| Test | assert, assertEqual, assertTrue, assertFalse |
|
| Test | assert, assertEqual, assertTrue, assertFalse |
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|||||||
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
|
fn add(a: Int, b: Int): Int is pure = a + b
|
||||||
// Behavioral properties are compile-time guarantees about function behavior
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// add(5, 3) = 8
|
|
||||||
// factorial(5) = 120
|
|
||||||
// multiply(7, 6) = 42
|
|
||||||
// abs(-5) = 5
|
|
||||||
|
|
||||||
// A pure function - no side effects, same input always gives same output
|
fn factorial(n: Int): Int is deterministic = if n <= 1 then 1 else n * factorial(n - 1)
|
||||||
fn add(a: Int, b: Int): Int is pure =
|
|
||||||
a + b
|
|
||||||
|
|
||||||
// A deterministic function - same input always gives same output
|
fn multiply(a: Int, b: Int): Int is commutative = a * b
|
||||||
fn factorial(n: Int): Int is deterministic =
|
|
||||||
if n <= 1 then 1
|
|
||||||
else n * factorial(n - 1)
|
|
||||||
|
|
||||||
// A commutative function - order of arguments doesn't matter
|
fn abs(x: Int): Int is idempotent = if x < 0 then 0 - x else x
|
||||||
fn multiply(a: Int, b: Int): Int is commutative =
|
|
||||||
a * b
|
|
||||||
|
|
||||||
// An idempotent function - absolute value
|
|
||||||
fn abs(x: Int): Int is idempotent =
|
|
||||||
if x < 0 then 0 - x else x
|
|
||||||
|
|
||||||
// Test the functions
|
|
||||||
let sumResult = add(5, 3)
|
let sumResult = add(5, 3)
|
||||||
|
|
||||||
let factResult = factorial(5)
|
let factResult = factorial(5)
|
||||||
|
|
||||||
let productResult = multiply(7, 6)
|
let productResult = multiply(7, 6)
|
||||||
|
|
||||||
let absResult = abs(0 - 5)
|
let absResult = abs(0 - 5)
|
||||||
|
|
||||||
// Print results
|
|
||||||
fn printResults(): Unit with {Console} = {
|
fn printResults(): Unit with {Console} = {
|
||||||
Console.print("add(5, 3) = " + toString(sumResult))
|
Console.print("add(5, 3) = " + toString(sumResult))
|
||||||
Console.print("factorial(5) = " + toString(factResult))
|
Console.print("factorial(5) = " + toString(factResult))
|
||||||
|
|||||||
@@ -1,82 +1,42 @@
|
|||||||
// Behavioral Types Demo
|
|
||||||
// Demonstrates compile-time verification of function properties
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// PART 1: Pure Functions
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Pure functions have no side effects
|
|
||||||
fn add(a: Int, b: Int): Int is pure = a + b
|
fn add(a: Int, b: Int): Int is pure = a + b
|
||||||
|
|
||||||
fn subtract(a: Int, b: Int): Int is pure = a - b
|
fn subtract(a: Int, b: Int): Int is pure = a - b
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// PART 2: Commutative Functions
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Commutative functions: f(a, b) = f(b, a)
|
|
||||||
fn multiply(a: Int, b: Int): Int is commutative = a * b
|
fn multiply(a: Int, b: Int): Int is commutative = a * b
|
||||||
|
|
||||||
fn sum(a: Int, b: Int): Int is commutative = a + b
|
fn sum(a: Int, b: Int): Int is commutative = a + b
|
||||||
|
|
||||||
// ============================================================
|
fn abs(x: Int): Int is idempotent = if x < 0 then 0 - x else x
|
||||||
// PART 3: Idempotent Functions
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Idempotent functions: f(f(x)) = f(x)
|
|
||||||
fn abs(x: Int): Int is idempotent =
|
|
||||||
if x < 0 then 0 - x else x
|
|
||||||
|
|
||||||
fn identity(x: Int): Int is idempotent = x
|
fn identity(x: Int): Int is idempotent = x
|
||||||
|
|
||||||
// ============================================================
|
fn factorial(n: Int): Int is deterministic = if n <= 1 then 1 else n * factorial(n - 1)
|
||||||
// PART 4: Deterministic Functions
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Deterministic functions always produce the same output for the same input
|
fn fib(n: Int): Int is deterministic = if n <= 1 then n else fib(n - 1) + fib(n - 2)
|
||||||
fn factorial(n: Int): Int is deterministic =
|
|
||||||
if n <= 1 then 1 else n * factorial(n - 1)
|
|
||||||
|
|
||||||
fn fib(n: Int): Int is deterministic =
|
fn sumTo(n: Int): Int is total = if n <= 0 then 0 else n + sumTo(n - 1)
|
||||||
if n <= 1 then n else fib(n - 1) + fib(n - 2)
|
|
||||||
|
|
||||||
// ============================================================
|
fn power(base: Int, exp: Int): Int is total = if exp <= 0 then 1 else base * power(base, exp - 1)
|
||||||
// PART 5: Total Functions
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Total functions are defined for all inputs (no infinite loops, no exceptions)
|
|
||||||
fn sumTo(n: Int): Int is total =
|
|
||||||
if n <= 0 then 0 else n + sumTo(n - 1)
|
|
||||||
|
|
||||||
fn power(base: Int, exp: Int): Int is total =
|
|
||||||
if exp <= 0 then 1 else base * power(base, exp - 1)
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// RESULTS
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
Console.print("=== Behavioral Types Demo ===")
|
Console.print("=== Behavioral Types Demo ===")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
Console.print("Part 1: Pure functions")
|
Console.print("Part 1: Pure functions")
|
||||||
Console.print(" add(5, 3) = " + toString(add(5, 3)))
|
Console.print(" add(5, 3) = " + toString(add(5, 3)))
|
||||||
Console.print(" subtract(10, 4) = " + toString(subtract(10, 4)))
|
Console.print(" subtract(10, 4) = " + toString(subtract(10, 4)))
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
Console.print("Part 2: Commutative functions")
|
Console.print("Part 2: Commutative functions")
|
||||||
Console.print(" multiply(7, 6) = " + toString(multiply(7, 6)))
|
Console.print(" multiply(7, 6) = " + toString(multiply(7, 6)))
|
||||||
Console.print(" sum(10, 20) = " + toString(sum(10, 20)))
|
Console.print(" sum(10, 20) = " + toString(sum(10, 20)))
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
Console.print("Part 3: Idempotent functions")
|
Console.print("Part 3: Idempotent functions")
|
||||||
Console.print(" abs(-42) = " + toString(abs(0 - 42)))
|
Console.print(" abs(-42) = " + toString(abs(0 - 42)))
|
||||||
Console.print(" identity(100) = " + toString(identity(100)))
|
Console.print(" identity(100) = " + toString(identity(100)))
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
Console.print("Part 4: Deterministic functions")
|
Console.print("Part 4: Deterministic functions")
|
||||||
Console.print(" factorial(5) = " + toString(factorial(5)))
|
Console.print(" factorial(5) = " + toString(factorial(5)))
|
||||||
Console.print(" fib(10) = " + toString(fib(10)))
|
Console.print(" fib(10) = " + toString(fib(10)))
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
Console.print("Part 5: Total functions")
|
Console.print("Part 5: Total functions")
|
||||||
Console.print(" sumTo(10) = " + toString(sumTo(10)))
|
Console.print(" sumTo(10) = " + toString(sumTo(10)))
|
||||||
Console.print(" power(2, 8) = " + toString(power(2, 8)))
|
Console.print(" power(2, 8) = " + toString(power(2, 8)))
|
||||||
|
|||||||
@@ -1,31 +1,7 @@
|
|||||||
// Demonstrating built-in effects in Lux
|
fn safeDivide(a: Int, b: Int): Int with {Fail} = if b == 0 then Fail.fail("Division by zero") else a / b
|
||||||
//
|
|
||||||
// Lux provides several built-in effects:
|
|
||||||
// - Console: print and read from terminal
|
|
||||||
// - Fail: early termination with error
|
|
||||||
// - State: get/put mutable state (requires runtime initialization)
|
|
||||||
// - Reader: read-only environment access (requires runtime initialization)
|
|
||||||
//
|
|
||||||
// This example demonstrates Console and Fail effects.
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// Starting computation...
|
|
||||||
// Step 1: validating input
|
|
||||||
// Step 2: processing
|
|
||||||
// Result: 42
|
|
||||||
// Done!
|
|
||||||
|
|
||||||
// A function that can fail
|
fn validatePositive(n: Int): Int with {Fail} = if n < 0 then Fail.fail("Negative number not allowed") else n
|
||||||
fn safeDivide(a: Int, b: Int): Int with {Fail} =
|
|
||||||
if b == 0 then Fail.fail("Division by zero")
|
|
||||||
else a / b
|
|
||||||
|
|
||||||
// A function that validates input
|
|
||||||
fn validatePositive(n: Int): Int with {Fail} =
|
|
||||||
if n < 0 then Fail.fail("Negative number not allowed")
|
|
||||||
else n
|
|
||||||
|
|
||||||
// A computation that uses multiple effects
|
|
||||||
fn compute(input: Int): Int with {Console, Fail} = {
|
fn compute(input: Int): Int with {Console, Fail} = {
|
||||||
Console.print("Starting computation...")
|
Console.print("Starting computation...")
|
||||||
Console.print("Step 1: validating input")
|
Console.print("Step 1: validating input")
|
||||||
@@ -36,7 +12,6 @@ fn compute(input: Int): Int with {Console, Fail} = {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main function
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
let result = run compute(21) with {}
|
let result = run compute(21) with {}
|
||||||
Console.print("Done!")
|
Console.print("Done!")
|
||||||
|
|||||||
@@ -1,14 +1,3 @@
|
|||||||
// Counter Example - A simple interactive counter using TEA pattern
|
|
||||||
//
|
|
||||||
// This example demonstrates:
|
|
||||||
// - Model-View-Update architecture (TEA)
|
|
||||||
// - Html DSL for describing UI (inline version)
|
|
||||||
// - Message-based state updates
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Html Types (subset of stdlib/html)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
type Html<M> =
|
type Html<M> =
|
||||||
| Element(String, List<Attr<M>>, List<Html<M>>)
|
| Element(String, List<Attr<M>>, List<Html<M>>)
|
||||||
| Text(String)
|
| Text(String)
|
||||||
@@ -19,130 +8,96 @@ type Attr<M> =
|
|||||||
| Id(String)
|
| Id(String)
|
||||||
| OnClick(M)
|
| OnClick(M)
|
||||||
|
|
||||||
// Html builder helpers
|
fn div<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = Element("div", attrs, children)
|
||||||
fn div<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
||||||
Element("div", attrs, children)
|
|
||||||
|
|
||||||
fn span<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
fn span<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = Element("span", attrs, children)
|
||||||
Element("span", attrs, children)
|
|
||||||
|
|
||||||
fn h1<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
fn h1<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = Element("h1", attrs, children)
|
||||||
Element("h1", attrs, children)
|
|
||||||
|
|
||||||
fn button<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
fn button<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = Element("button", attrs, children)
|
||||||
Element("button", attrs, children)
|
|
||||||
|
|
||||||
fn text<M>(content: String): Html<M> =
|
fn text<M>(content: String): Html<M> = Text(content)
|
||||||
Text(content)
|
|
||||||
|
|
||||||
fn class<M>(name: String): Attr<M> =
|
fn class<M>(name: String): Attr<M> = Class(name)
|
||||||
Class(name)
|
|
||||||
|
|
||||||
fn onClick<M>(msg: M): Attr<M> =
|
fn onClick<M>(msg: M): Attr<M> = OnClick(msg)
|
||||||
OnClick(msg)
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Model - The application state (using ADT wrapper)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
type Model =
|
type Model =
|
||||||
| Counter(Int)
|
| Counter(Int)
|
||||||
|
|
||||||
fn getCount(model: Model): Int =
|
fn getCount(model: Model): Int =
|
||||||
match model {
|
match model {
|
||||||
Counter(n) => n
|
Counter(n) => n,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init(): Model = Counter(0)
|
fn init(): Model = Counter(0)
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Messages - Events that can occur
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
type Msg =
|
type Msg =
|
||||||
| Increment
|
| Increment
|
||||||
| Decrement
|
| Decrement
|
||||||
| Reset
|
| Reset
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Update - State transitions
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
fn update(model: Model, msg: Msg): Model =
|
fn update(model: Model, msg: Msg): Model =
|
||||||
match msg {
|
match msg {
|
||||||
Increment => Counter(getCount(model) + 1),
|
Increment => Counter(getCount(model) + 1),
|
||||||
Decrement => Counter(getCount(model) - 1),
|
Decrement => Counter(getCount(model) - 1),
|
||||||
Reset => Counter(0)
|
Reset => Counter(0),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// View - Render the UI
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
fn viewCounter(count: Int): Html<Msg> = {
|
fn viewCounter(count: Int): Html<Msg> = {
|
||||||
let countText = text(toString(count))
|
let countText = text(toString(count))
|
||||||
let countSpan = span([class("count")], [countText])
|
let countSpan = span([class("count")], [countText])
|
||||||
let displayDiv = div([class("counter-display")], [countSpan])
|
let displayDiv = div([class("counter-display")], [countSpan])
|
||||||
|
|
||||||
let minusBtn = button([onClick(Decrement), class("btn")], [text("-")])
|
let minusBtn = button([onClick(Decrement), class("btn")], [text("-")])
|
||||||
let resetBtn = button([onClick(Reset), class("btn btn-reset")], [text("Reset")])
|
let resetBtn = button([onClick(Reset), class("btn btn-reset")], [text("Reset")])
|
||||||
let plusBtn = button([onClick(Increment), class("btn")], [text("+")])
|
let plusBtn = button([onClick(Increment), class("btn")], [text("+")])
|
||||||
let buttonsDiv = div([class("counter-buttons")], [minusBtn, resetBtn, plusBtn])
|
let buttonsDiv = div([class("counter-buttons")], [minusBtn, resetBtn, plusBtn])
|
||||||
|
|
||||||
let title = h1([], [text("Counter")])
|
let title = h1([], [text("Counter")])
|
||||||
div([class("counter-app")], [title, displayDiv, buttonsDiv])
|
div([class("counter-app")], [title, displayDiv, buttonsDiv])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(model: Model): Html<Msg> = viewCounter(getCount(model))
|
fn view(model: Model): Html<Msg> = viewCounter(getCount(model))
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Debug: Print Html structure
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
fn showAttr(attr: Attr<Msg>): String =
|
fn showAttr(attr: Attr<Msg>): String =
|
||||||
match attr {
|
match attr {
|
||||||
Class(s) => "class=\"" + s + "\"",
|
Class(s) => "class=\"" + s + "\"",
|
||||||
Id(s) => "id=\"" + s + "\"",
|
Id(s) => "id=\"" + s + "\"",
|
||||||
OnClick(msg) => match msg {
|
OnClick(msg) => match msg {
|
||||||
Increment => "onclick=\"Increment\"",
|
Increment => "onclick=\"Increment\"",
|
||||||
Decrement => "onclick=\"Decrement\"",
|
Decrement => "onclick=\"Decrement\"",
|
||||||
Reset => "onclick=\"Reset\""
|
Reset => "onclick=\"Reset\"",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn showAttrs(attrs: List<Attr<Msg>>): String =
|
fn showAttrs(attrs: List<Attr<Msg>>): String =
|
||||||
match List.head(attrs) {
|
match List.head(attrs) {
|
||||||
None => "",
|
None => "",
|
||||||
Some(a) => match List.tail(attrs) {
|
Some(a) => match List.tail(attrs) {
|
||||||
None => showAttr(a),
|
None => showAttr(a),
|
||||||
Some(rest) => showAttr(a) + " " + showAttrs(rest)
|
Some(rest) => showAttr(a) + " " + showAttrs(rest),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn showChildren(children: List<Html<Msg>>, indent: Int): String =
|
fn showChildren(children: List<Html<Msg>>, indent: Int): String =
|
||||||
match List.head(children) {
|
match List.head(children) {
|
||||||
None => "",
|
None => "",
|
||||||
Some(c) => match List.tail(children) {
|
Some(c) => match List.tail(children) {
|
||||||
None => showHtml(c, indent),
|
None => showHtml(c, indent),
|
||||||
Some(rest) => showHtml(c, indent) + showChildren(rest, indent)
|
Some(rest) => showHtml(c, indent) + showChildren(rest, indent),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn showHtml(html: Html<Msg>, indent: Int): String =
|
fn showHtml(html: Html<Msg>, indent: Int): String =
|
||||||
match html {
|
match html {
|
||||||
Empty => "",
|
Empty => "",
|
||||||
Text(s) => s,
|
Text(s) => s,
|
||||||
Element(tag, attrs, children) => {
|
Element(tag, attrs, children) => {
|
||||||
let attrStr = showAttrs(attrs)
|
let attrStr = showAttrs(attrs)
|
||||||
let attrPart = if String.length(attrStr) > 0 then " " + attrStr else ""
|
let attrPart = if String.length(attrStr) > 0 then " " + attrStr else ""
|
||||||
let childStr = showChildren(children, indent + 2)
|
let childStr = showChildren(children, indent + 2)
|
||||||
"<" + tag + attrPart + ">" + childStr + "</" + tag + ">"
|
"<" + tag + attrPart + ">" + childStr + "</" + tag + ">"
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Entry point
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
let model = init()
|
let model = init()
|
||||||
@@ -150,24 +105,19 @@ fn main(): Unit with {Console} = {
|
|||||||
Console.print("")
|
Console.print("")
|
||||||
Console.print("Initial count: " + toString(getCount(model)))
|
Console.print("Initial count: " + toString(getCount(model)))
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
let m1 = update(model, Increment)
|
let m1 = update(model, Increment)
|
||||||
Console.print("After Increment: " + toString(getCount(m1)))
|
Console.print("After Increment: " + toString(getCount(m1)))
|
||||||
|
|
||||||
let m2 = update(m1, Increment)
|
let m2 = update(m1, Increment)
|
||||||
Console.print("After Increment: " + toString(getCount(m2)))
|
Console.print("After Increment: " + toString(getCount(m2)))
|
||||||
|
|
||||||
let m3 = update(m2, Increment)
|
let m3 = update(m2, Increment)
|
||||||
Console.print("After Increment: " + toString(getCount(m3)))
|
Console.print("After Increment: " + toString(getCount(m3)))
|
||||||
|
|
||||||
let m4 = update(m3, Decrement)
|
let m4 = update(m3, Decrement)
|
||||||
Console.print("After Decrement: " + toString(getCount(m4)))
|
Console.print("After Decrement: " + toString(getCount(m4)))
|
||||||
|
|
||||||
let m5 = update(m4, Reset)
|
let m5 = update(m4, Reset)
|
||||||
Console.print("After Reset: " + toString(getCount(m5)))
|
Console.print("After Reset: " + toString(getCount(m5)))
|
||||||
|
|
||||||
Console.print("")
|
Console.print("")
|
||||||
Console.print("=== View (HTML Structure) ===")
|
Console.print("=== View (HTML Structure) ===")
|
||||||
Console.print(showHtml(view(m2), 0))
|
Console.print(showHtml(view(m2), 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = run main() with {}
|
let output = run main() with {}
|
||||||
|
|||||||
@@ -1,57 +1,37 @@
|
|||||||
// Demonstrating algebraic data types and pattern matching
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// Tree sum: 8
|
|
||||||
// Tree depth: 3
|
|
||||||
// Safe divide 10/2: Result: 5
|
|
||||||
// Safe divide 10/0: Division by zero!
|
|
||||||
|
|
||||||
// Define a binary tree
|
|
||||||
type Tree =
|
type Tree =
|
||||||
| Leaf(Int)
|
| Leaf(Int)
|
||||||
| Node(Tree, Tree)
|
| Node(Tree, Tree)
|
||||||
|
|
||||||
// Sum all values in a tree
|
|
||||||
fn sumTree(tree: Tree): Int =
|
fn sumTree(tree: Tree): Int =
|
||||||
match tree {
|
match tree {
|
||||||
Leaf(n) => n,
|
Leaf(n) => n,
|
||||||
Node(left, right) => sumTree(left) + sumTree(right)
|
Node(left, right) => sumTree(left) + sumTree(right),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the depth of a tree
|
|
||||||
fn depth(tree: Tree): Int =
|
fn depth(tree: Tree): Int =
|
||||||
match tree {
|
match tree {
|
||||||
Leaf(_) => 1,
|
Leaf(_) => 1,
|
||||||
Node(left, right) => {
|
Node(left, right) => {
|
||||||
let leftDepth = depth(left)
|
let leftDepth = depth(left)
|
||||||
let rightDepth = depth(right)
|
let rightDepth = depth(right)
|
||||||
1 + (if leftDepth > rightDepth then leftDepth else rightDepth)
|
1 + if leftDepth > rightDepth then leftDepth else rightDepth
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example tree:
|
|
||||||
// Node
|
|
||||||
// / \
|
|
||||||
// Node Leaf(5)
|
|
||||||
// / \
|
|
||||||
// Leaf(1) Leaf(2)
|
|
||||||
|
|
||||||
let myTree = Node(Node(Leaf(1), Leaf(2)), Leaf(5))
|
let myTree = Node(Node(Leaf(1), Leaf(2)), Leaf(5))
|
||||||
|
|
||||||
let treeSum = sumTree(myTree)
|
let treeSum = sumTree(myTree)
|
||||||
|
|
||||||
let treeDepth = depth(myTree)
|
let treeDepth = depth(myTree)
|
||||||
|
|
||||||
// Option type example
|
fn safeDivide(a: Int, b: Int): Option<Int> = if b == 0 then None else Some(a / b)
|
||||||
fn safeDivide(a: Int, b: Int): Option<Int> =
|
|
||||||
if b == 0 then None
|
|
||||||
else Some(a / b)
|
|
||||||
|
|
||||||
fn showResult(result: Option<Int>): String =
|
fn showResult(result: Option<Int>): String =
|
||||||
match result {
|
match result {
|
||||||
None => "Division by zero!",
|
None => "Division by zero!",
|
||||||
Some(n) => "Result: " + toString(n)
|
Some(n) => "Result: " + toString(n),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print results
|
|
||||||
fn printResults(): Unit with {Console} = {
|
fn printResults(): Unit with {Console} = {
|
||||||
Console.print("Tree sum: " + toString(treeSum))
|
Console.print("Tree sum: " + toString(treeSum))
|
||||||
Console.print("Tree depth: " + toString(treeDepth))
|
Console.print("Tree depth: " + toString(treeDepth))
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
// Demonstrating algebraic effects in Lux
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// [info] Processing data...
|
|
||||||
// [debug] Result computed
|
|
||||||
// Final result: 42
|
|
||||||
|
|
||||||
// Define a custom logging effect
|
|
||||||
effect Logger {
|
effect Logger {
|
||||||
fn log(level: String, msg: String): Unit
|
fn log(level: String, msg: String): Unit
|
||||||
fn getLevel(): String
|
fn getLevel(): String
|
||||||
}
|
}
|
||||||
|
|
||||||
// A function that uses the Logger effect
|
|
||||||
fn processData(data: Int): Int with {Logger} = {
|
fn processData(data: Int): Int with {Logger} = {
|
||||||
Logger.log("info", "Processing data...")
|
Logger.log("info", "Processing data...")
|
||||||
let result = data * 2
|
let result = data * 2
|
||||||
@@ -19,17 +10,15 @@ fn processData(data: Int): Int with {Logger} = {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
// A handler that prints logs to console
|
|
||||||
handler consoleLogger: Logger {
|
handler consoleLogger: Logger {
|
||||||
fn log(level, msg) = Console.print("[" + level + "] " + msg)
|
fn log(level, msg) = Console.print("[" + level + "] " + msg)
|
||||||
fn getLevel() = "debug"
|
fn getLevel() = "debug"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run and print
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
let result = run processData(21) with {
|
let result = run processData(21) with {
|
||||||
Logger = consoleLogger
|
Logger = consoleLogger,
|
||||||
}
|
}
|
||||||
Console.print("Final result: " + toString(result))
|
Console.print("Final result: " + toString(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
// Factorial function demonstrating recursion
|
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
|
||||||
//
|
|
||||||
// Expected output: 10! = 3628800
|
|
||||||
|
|
||||||
fn factorial(n: Int): Int =
|
|
||||||
if n <= 1 then 1
|
|
||||||
else n * factorial(n - 1)
|
|
||||||
|
|
||||||
// Calculate factorial of 10
|
|
||||||
let result = factorial(10)
|
let result = factorial(10)
|
||||||
|
|
||||||
// Print result using Console effect
|
fn showResult(): Unit with {Console} = Console.print("10! = " + toString(result))
|
||||||
fn showResult(): Unit with {Console} =
|
|
||||||
Console.print("10! = " + toString(result))
|
|
||||||
|
|
||||||
let output = run showResult() with {}
|
let output = run showResult() with {}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
// File I/O example - demonstrates the File effect
|
|
||||||
//
|
|
||||||
// This script reads a file, counts lines/words, and writes a report
|
|
||||||
|
|
||||||
fn countLines(content: String): Int = {
|
fn countLines(content: String): Int = {
|
||||||
let lines = String.split(content, "\n")
|
let lines = String.split(content, "
|
||||||
|
")
|
||||||
List.length(lines)
|
List.length(lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,35 +11,28 @@ fn countWords(content: String): Int = {
|
|||||||
|
|
||||||
fn analyzeFile(path: String): Unit with {File, Console} = {
|
fn analyzeFile(path: String): Unit with {File, Console} = {
|
||||||
Console.print("Analyzing file: " + path)
|
Console.print("Analyzing file: " + path)
|
||||||
|
|
||||||
if File.exists(path) then {
|
if File.exists(path) then {
|
||||||
let content = File.read(path)
|
let content = File.read(path)
|
||||||
let lines = countLines(content)
|
let lines = countLines(content)
|
||||||
let words = countWords(content)
|
let words = countWords(content)
|
||||||
let chars = String.length(content)
|
let chars = String.length(content)
|
||||||
|
Console.print(" Lines: " + toString(lines))
|
||||||
Console.print(" Lines: " + toString(lines))
|
Console.print(" Words: " + toString(words))
|
||||||
Console.print(" Words: " + toString(words))
|
Console.print(" Chars: " + toString(chars))
|
||||||
Console.print(" Chars: " + toString(chars))
|
} else {
|
||||||
} else {
|
Console.print(" Error: File not found!")
|
||||||
Console.print(" Error: File not found!")
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main(): Unit with {File, Console} = {
|
fn main(): Unit with {File, Console} = {
|
||||||
Console.print("=== Lux File Analyzer ===")
|
Console.print("=== Lux File Analyzer ===")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Analyze this file itself
|
|
||||||
analyzeFile("examples/file_io.lux")
|
analyzeFile("examples/file_io.lux")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Analyze hello.lux
|
|
||||||
analyzeFile("examples/hello.lux")
|
analyzeFile("examples/hello.lux")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
let report = "File analysis complete.
|
||||||
// Write a report
|
Analyzed 2 files."
|
||||||
let report = "File analysis complete.\nAnalyzed 2 files."
|
|
||||||
File.write("/tmp/lux_report.txt", report)
|
File.write("/tmp/lux_report.txt", report)
|
||||||
Console.print("Report written to /tmp/lux_report.txt")
|
Console.print("Report written to /tmp/lux_report.txt")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,39 @@
|
|||||||
// Demonstrating functional programming features
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// apply(double, 21) = 42
|
|
||||||
// compose(addOne, double)(5) = 11
|
|
||||||
// pipe: 5 |> double |> addOne |> square = 121
|
|
||||||
// curried add5(10) = 15
|
|
||||||
// partial times3(7) = 21
|
|
||||||
// record transform = 30
|
|
||||||
|
|
||||||
// Higher-order functions
|
|
||||||
fn apply(f: fn(Int): Int, x: Int): Int = f(x)
|
fn apply(f: fn(Int): Int, x: Int): Int = f(x)
|
||||||
|
|
||||||
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int =
|
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int = fn(x: Int): Int => f(g(x))
|
||||||
fn(x: Int): Int => f(g(x))
|
|
||||||
|
|
||||||
// Basic functions
|
|
||||||
fn double(x: Int): Int = x * 2
|
fn double(x: Int): Int = x * 2
|
||||||
|
|
||||||
fn addOne(x: Int): Int = x + 1
|
fn addOne(x: Int): Int = x + 1
|
||||||
|
|
||||||
fn square(x: Int): Int = x * x
|
fn square(x: Int): Int = x * x
|
||||||
|
|
||||||
// Using apply
|
|
||||||
let result1 = apply(double, 21)
|
let result1 = apply(double, 21)
|
||||||
|
|
||||||
// Using compose
|
|
||||||
let doubleAndAddOne = compose(addOne, double)
|
let doubleAndAddOne = compose(addOne, double)
|
||||||
|
|
||||||
let result2 = doubleAndAddOne(5)
|
let result2 = doubleAndAddOne(5)
|
||||||
|
|
||||||
// Using the pipe operator
|
let result3 = square(addOne(double(5)))
|
||||||
let result3 = 5 |> double |> addOne |> square
|
|
||||||
|
|
||||||
// Currying example
|
fn add(a: Int): fn(Int): Int = fn(b: Int): Int => a + b
|
||||||
fn add(a: Int): fn(Int): Int =
|
|
||||||
fn(b: Int): Int => a + b
|
|
||||||
|
|
||||||
let add5 = add(5)
|
let add5 = add(5)
|
||||||
|
|
||||||
let result4 = add5(10)
|
let result4 = add5(10)
|
||||||
|
|
||||||
// Partial application simulation
|
|
||||||
fn multiply(a: Int, b: Int): Int = a * b
|
fn multiply(a: Int, b: Int): Int = a * b
|
||||||
|
|
||||||
let times3 = fn(x: Int): Int => multiply(3, x)
|
let times3 = fn(x: Int): Int => multiply(3, x)
|
||||||
|
|
||||||
let result5 = times3(7)
|
let result5 = times3(7)
|
||||||
|
|
||||||
// Working with records
|
let transform = fn(record: { x: Int, y: Int }): Int => record.x + record.y
|
||||||
let transform = fn(record: { x: Int, y: Int }): Int =>
|
|
||||||
record.x + record.y
|
|
||||||
|
|
||||||
let point = { x: 10, y: 20 }
|
let point = { x: 10, y: 20 }
|
||||||
|
|
||||||
let recordSum = transform(point)
|
let recordSum = transform(point)
|
||||||
|
|
||||||
// Print all results
|
|
||||||
fn printResults(): Unit with {Console} = {
|
fn printResults(): Unit with {Console} = {
|
||||||
Console.print("apply(double, 21) = " + toString(result1))
|
Console.print("apply(double, 21) = " + toString(result1))
|
||||||
Console.print("compose(addOne, double)(5) = " + toString(result2))
|
Console.print("compose(addOne, double)(5) = " + toString(result2))
|
||||||
|
|||||||
@@ -1,54 +1,43 @@
|
|||||||
// Demonstrating generic type parameters in Lux
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// identity(42) = 42
|
|
||||||
// identity("hello") = hello
|
|
||||||
// first(MkPair(1, "one")) = 1
|
|
||||||
// second(MkPair(1, "one")) = one
|
|
||||||
// map(Some(21), double) = Some(42)
|
|
||||||
|
|
||||||
// Generic identity function
|
|
||||||
fn identity<T>(x: T): T = x
|
fn identity<T>(x: T): T = x
|
||||||
|
|
||||||
// Generic pair type
|
|
||||||
type Pair<A, B> =
|
type Pair<A, B> =
|
||||||
| MkPair(A, B)
|
| MkPair(A, B)
|
||||||
|
|
||||||
fn first<A, B>(p: Pair<A, B>): A =
|
fn first<A, B>(p: Pair<A, B>): A =
|
||||||
match p {
|
match p {
|
||||||
MkPair(a, _) => a
|
MkPair(a, _) => a,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn second<A, B>(p: Pair<A, B>): B =
|
fn second<A, B>(p: Pair<A, B>): B =
|
||||||
match p {
|
match p {
|
||||||
MkPair(_, b) => b
|
MkPair(_, b) => b,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic map function for Option
|
|
||||||
fn mapOption<T, U>(opt: Option<T>, f: fn(T): U): Option<U> =
|
fn mapOption<T, U>(opt: Option<T>, f: fn(T): U): Option<U> =
|
||||||
match opt {
|
match opt {
|
||||||
None => None,
|
None => None,
|
||||||
Some(x) => Some(f(x))
|
Some(x) => Some(f(x)),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function for testing
|
|
||||||
fn double(x: Int): Int = x * 2
|
fn double(x: Int): Int = x * 2
|
||||||
|
|
||||||
// Test usage
|
|
||||||
let id_int = identity(42)
|
let id_int = identity(42)
|
||||||
|
|
||||||
let id_str = identity("hello")
|
let id_str = identity("hello")
|
||||||
|
|
||||||
let pair = MkPair(1, "one")
|
let pair = MkPair(1, "one")
|
||||||
|
|
||||||
let fst = first(pair)
|
let fst = first(pair)
|
||||||
|
|
||||||
let snd = second(pair)
|
let snd = second(pair)
|
||||||
|
|
||||||
let doubled = mapOption(Some(21), double)
|
let doubled = mapOption(Some(21), double)
|
||||||
|
|
||||||
fn showOption(opt: Option<Int>): String =
|
fn showOption(opt: Option<Int>): String =
|
||||||
match opt {
|
match opt {
|
||||||
None => "None",
|
None => "None",
|
||||||
Some(x) => "Some(" + toString(x) + ")"
|
Some(x) => "Some(" + toString(x) + ")",
|
||||||
}
|
}
|
||||||
|
|
||||||
fn printResults(): Unit with {Console} = {
|
fn printResults(): Unit with {Console} = {
|
||||||
Console.print("identity(42) = " + toString(id_int))
|
Console.print("identity(42) = " + toString(id_int))
|
||||||
|
|||||||
@@ -1,21 +1,8 @@
|
|||||||
// Demonstrating resumable effect handlers in Lux
|
|
||||||
//
|
|
||||||
// Handlers can use `resume(value)` to return a value to the effect call site
|
|
||||||
// and continue the computation. This enables powerful control flow patterns.
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// [INFO] Starting computation
|
|
||||||
// [DEBUG] Intermediate result: 10
|
|
||||||
// [INFO] Computation complete
|
|
||||||
// Final result: 20
|
|
||||||
|
|
||||||
// Define a custom logging effect
|
|
||||||
effect Logger {
|
effect Logger {
|
||||||
fn log(level: String, msg: String): Unit
|
fn log(level: String, msg: String): Unit
|
||||||
fn getLogLevel(): String
|
fn getLogLevel(): String
|
||||||
}
|
}
|
||||||
|
|
||||||
// A function that uses the Logger effect
|
|
||||||
fn compute(): Int with {Logger} = {
|
fn compute(): Int with {Logger} = {
|
||||||
Logger.log("INFO", "Starting computation")
|
Logger.log("INFO", "Starting computation")
|
||||||
let x = 10
|
let x = 10
|
||||||
@@ -25,20 +12,19 @@ fn compute(): Int with {Logger} = {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
// A handler that prints logs with brackets and resumes with Unit
|
|
||||||
handler prettyLogger: Logger {
|
handler prettyLogger: Logger {
|
||||||
fn log(level, msg) = {
|
fn log(level, msg) =
|
||||||
Console.print("[" + level + "] " + msg)
|
{
|
||||||
resume(())
|
Console.print("[" + level + "] " + msg)
|
||||||
}
|
resume(())
|
||||||
|
}
|
||||||
fn getLogLevel() = resume("DEBUG")
|
fn getLogLevel() = resume("DEBUG")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main function
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
let result = run compute() with {
|
let result = run compute() with {
|
||||||
Logger = prettyLogger
|
Logger = prettyLogger,
|
||||||
}
|
}
|
||||||
Console.print("Final result: " + toString(result))
|
Console.print("Final result: " + toString(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
// Hello World in Lux
|
fn greet(): Unit with {Console} = Console.print("Hello, World!")
|
||||||
// Demonstrates basic effect usage
|
|
||||||
//
|
|
||||||
// Expected output: Hello, World!
|
|
||||||
|
|
||||||
fn greet(): Unit with {Console} =
|
|
||||||
Console.print("Hello, World!")
|
|
||||||
|
|
||||||
// Run the greeting with the Console effect
|
|
||||||
let output = run greet() with {}
|
let output = run greet() with {}
|
||||||
|
|||||||
@@ -1,91 +1,72 @@
|
|||||||
// HTTP example - demonstrates the Http effect
|
|
||||||
//
|
|
||||||
// This script makes HTTP requests and parses JSON responses
|
|
||||||
|
|
||||||
fn main(): Unit with {Console, Http} = {
|
fn main(): Unit with {Console, Http} = {
|
||||||
Console.print("=== Lux HTTP Example ===")
|
Console.print("=== Lux HTTP Example ===")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Make a GET request to a public API
|
|
||||||
Console.print("Fetching data from httpbin.org...")
|
Console.print("Fetching data from httpbin.org...")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
match Http.get("https://httpbin.org/get") {
|
match Http.get("https://httpbin.org/get") {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
Console.print("GET request successful!")
|
Console.print("GET request successful!")
|
||||||
Console.print(" Status: " + toString(response.status))
|
Console.print(" Status: " + toString(response.status))
|
||||||
Console.print(" Body length: " + toString(String.length(response.body)) + " bytes")
|
Console.print(" Body length: " + toString(String.length(response.body)) + " bytes")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
match Json.parse(response.body) {
|
||||||
// Parse the JSON response
|
Ok(json) => {
|
||||||
match Json.parse(response.body) {
|
Console.print("Parsed JSON response:")
|
||||||
Ok(json) => {
|
match Json.get(json, "origin") {
|
||||||
Console.print("Parsed JSON response:")
|
Some(origin) => match Json.asString(origin) {
|
||||||
match Json.get(json, "origin") {
|
Some(ip) => Console.print(" Your IP: " + ip),
|
||||||
Some(origin) => match Json.asString(origin) {
|
None => Console.print(" origin: (not a string)"),
|
||||||
Some(ip) => Console.print(" Your IP: " + ip),
|
},
|
||||||
None => Console.print(" origin: (not a string)")
|
None => Console.print(" origin: (not found)"),
|
||||||
},
|
}
|
||||||
None => Console.print(" origin: (not found)")
|
match Json.get(json, "url") {
|
||||||
}
|
Some(url) => match Json.asString(url) {
|
||||||
match Json.get(json, "url") {
|
Some(u) => Console.print(" URL: " + u),
|
||||||
Some(url) => match Json.asString(url) {
|
None => Console.print(" url: (not a string)"),
|
||||||
Some(u) => Console.print(" URL: " + u),
|
},
|
||||||
None => Console.print(" url: (not a string)")
|
None => Console.print(" url: (not found)"),
|
||||||
},
|
}
|
||||||
None => Console.print(" url: (not found)")
|
},
|
||||||
}
|
Err(e) => Console.print("JSON parse error: " + e),
|
||||||
},
|
}
|
||||||
Err(e) => Console.print("JSON parse error: " + e)
|
},
|
||||||
}
|
Err(e) => Console.print("GET request failed: " + e),
|
||||||
},
|
}
|
||||||
Err(e) => Console.print("GET request failed: " + e)
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.print("")
|
Console.print("")
|
||||||
Console.print("--- POST Request ---")
|
Console.print("--- POST Request ---")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Make a POST request with JSON body
|
|
||||||
let requestBody = Json.object([("message", Json.string("Hello from Lux!")), ("version", Json.int(1))])
|
let requestBody = Json.object([("message", Json.string("Hello from Lux!")), ("version", Json.int(1))])
|
||||||
Console.print("Sending POST with JSON body...")
|
Console.print("Sending POST with JSON body...")
|
||||||
Console.print(" Body: " + Json.stringify(requestBody))
|
Console.print(" Body: " + Json.stringify(requestBody))
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
match Http.postJson("https://httpbin.org/post", requestBody) {
|
match Http.postJson("https://httpbin.org/post", requestBody) {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
Console.print("POST request successful!")
|
Console.print("POST request successful!")
|
||||||
Console.print(" Status: " + toString(response.status))
|
Console.print(" Status: " + toString(response.status))
|
||||||
|
match Json.parse(response.body) {
|
||||||
// Parse and extract what we sent
|
Ok(json) => match Json.get(json, "json") {
|
||||||
match Json.parse(response.body) {
|
Some(sentJson) => {
|
||||||
Ok(json) => match Json.get(json, "json") {
|
Console.print(" Server received:")
|
||||||
Some(sentJson) => {
|
Console.print(" " + Json.stringify(sentJson))
|
||||||
Console.print(" Server received:")
|
},
|
||||||
Console.print(" " + Json.stringify(sentJson))
|
None => Console.print(" (no json field in response)"),
|
||||||
},
|
},
|
||||||
None => Console.print(" (no json field in response)")
|
Err(e) => Console.print("JSON parse error: " + e),
|
||||||
},
|
}
|
||||||
Err(e) => Console.print("JSON parse error: " + e)
|
},
|
||||||
}
|
Err(e) => Console.print("POST request failed: " + e),
|
||||||
},
|
}
|
||||||
Err(e) => Console.print("POST request failed: " + e)
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.print("")
|
Console.print("")
|
||||||
Console.print("--- Headers ---")
|
Console.print("--- Headers ---")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Show response headers
|
|
||||||
match Http.get("https://httpbin.org/headers") {
|
match Http.get("https://httpbin.org/headers") {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
Console.print("Response headers (first 5):")
|
Console.print("Response headers (first 5):")
|
||||||
let count = 0
|
let count = 0
|
||||||
// Note: Can't easily iterate with effects in callbacks, so just show count
|
Console.print(" Total headers: " + toString(List.length(response.headers)))
|
||||||
Console.print(" Total headers: " + toString(List.length(response.headers)))
|
},
|
||||||
},
|
Err(e) => Console.print("Request failed: " + e),
|
||||||
Err(e) => Console.print("Request failed: " + e)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = run main() with {}
|
let result = run main() with {}
|
||||||
|
|||||||
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
|
fn fib(n: Int): Int = if n <= 1 then n else fib(n - 1) + fib(n - 2)
|
||||||
// This uses only features the JIT supports: integers, arithmetic, conditionals, functions
|
|
||||||
|
|
||||||
fn fib(n: Int): Int =
|
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
|
||||||
if n <= 1 then n
|
|
||||||
else fib(n - 1) + fib(n - 2)
|
|
||||||
|
|
||||||
fn factorial(n: Int): Int =
|
|
||||||
if n <= 1 then 1
|
|
||||||
else n * factorial(n - 1)
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
let fibResult = fib(30)
|
let fibResult = fib(30)
|
||||||
|
|||||||
@@ -1,107 +1,79 @@
|
|||||||
// JSON example - demonstrates JSON parsing and manipulation
|
|
||||||
//
|
|
||||||
// This script parses JSON, extracts values, and builds new JSON structures
|
|
||||||
|
|
||||||
fn main(): Unit with {Console, File} = {
|
fn main(): Unit with {Console, File} = {
|
||||||
Console.print("=== Lux JSON Example ===")
|
Console.print("=== Lux JSON Example ===")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// First, build some JSON programmatically
|
|
||||||
Console.print("=== Building JSON ===")
|
Console.print("=== Building JSON ===")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
let name = Json.string("Alice")
|
let name = Json.string("Alice")
|
||||||
let age = Json.int(30)
|
let age = Json.int(30)
|
||||||
let active = Json.bool(true)
|
let active = Json.bool(true)
|
||||||
let scores = Json.array([Json.int(95), Json.int(87), Json.int(92)])
|
let scores = Json.array([Json.int(95), Json.int(87), Json.int(92)])
|
||||||
|
|
||||||
let person = Json.object([("name", name), ("age", age), ("active", active), ("scores", scores)])
|
let person = Json.object([("name", name), ("age", age), ("active", active), ("scores", scores)])
|
||||||
|
|
||||||
Console.print("Built JSON:")
|
Console.print("Built JSON:")
|
||||||
let pretty = Json.prettyPrint(person)
|
let pretty = Json.prettyPrint(person)
|
||||||
Console.print(pretty)
|
Console.print(pretty)
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Stringify to a compact string
|
|
||||||
let jsonStr = Json.stringify(person)
|
let jsonStr = Json.stringify(person)
|
||||||
Console.print("Compact: " + jsonStr)
|
Console.print("Compact: " + jsonStr)
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Write to file and read back to test parsing
|
|
||||||
File.write("/tmp/test.json", jsonStr)
|
File.write("/tmp/test.json", jsonStr)
|
||||||
Console.print("Written to /tmp/test.json")
|
Console.print("Written to /tmp/test.json")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Read and parse from file
|
|
||||||
Console.print("=== Parsing JSON ===")
|
Console.print("=== Parsing JSON ===")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
let content = File.read("/tmp/test.json")
|
let content = File.read("/tmp/test.json")
|
||||||
Console.print("Read from file: " + content)
|
Console.print("Read from file: " + content)
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
match Json.parse(content) {
|
match Json.parse(content) {
|
||||||
Ok(json) => {
|
Ok(json) => {
|
||||||
Console.print("Parse succeeded!")
|
Console.print("Parse succeeded!")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
Console.print("Extracting fields:")
|
||||||
// Get string field
|
match Json.get(json, "name") {
|
||||||
Console.print("Extracting fields:")
|
Some(nameJson) => match Json.asString(nameJson) {
|
||||||
match Json.get(json, "name") {
|
Some(n) => Console.print(" name: " + n),
|
||||||
Some(nameJson) => match Json.asString(nameJson) {
|
None => Console.print(" name: (not a string)"),
|
||||||
Some(n) => Console.print(" name: " + n),
|
},
|
||||||
None => Console.print(" name: (not a string)")
|
None => Console.print(" name: (not found)"),
|
||||||
},
|
}
|
||||||
None => Console.print(" name: (not found)")
|
match Json.get(json, "age") {
|
||||||
}
|
Some(ageJson) => match Json.asInt(ageJson) {
|
||||||
|
Some(a) => Console.print(" age: " + toString(a)),
|
||||||
// Get int field
|
None => Console.print(" age: (not an int)"),
|
||||||
match Json.get(json, "age") {
|
},
|
||||||
Some(ageJson) => match Json.asInt(ageJson) {
|
None => Console.print(" age: (not found)"),
|
||||||
Some(a) => Console.print(" age: " + toString(a)),
|
}
|
||||||
None => Console.print(" age: (not an int)")
|
match Json.get(json, "active") {
|
||||||
},
|
Some(activeJson) => match Json.asBool(activeJson) {
|
||||||
None => Console.print(" age: (not found)")
|
Some(a) => Console.print(" active: " + toString(a)),
|
||||||
}
|
None => Console.print(" active: (not a bool)"),
|
||||||
|
},
|
||||||
// Get bool field
|
None => Console.print(" active: (not found)"),
|
||||||
match Json.get(json, "active") {
|
}
|
||||||
Some(activeJson) => match Json.asBool(activeJson) {
|
match Json.get(json, "scores") {
|
||||||
Some(a) => Console.print(" active: " + toString(a)),
|
Some(scoresJson) => match Json.asArray(scoresJson) {
|
||||||
None => Console.print(" active: (not a bool)")
|
Some(arr) => {
|
||||||
},
|
Console.print(" scores: " + toString(List.length(arr)) + " items")
|
||||||
None => Console.print(" active: (not found)")
|
match Json.getIndex(scoresJson, 0) {
|
||||||
}
|
Some(firstJson) => match Json.asInt(firstJson) {
|
||||||
|
Some(first) => Console.print(" first score: " + toString(first)),
|
||||||
// Get array field
|
None => Console.print(" first score: (not an int)"),
|
||||||
match Json.get(json, "scores") {
|
},
|
||||||
Some(scoresJson) => match Json.asArray(scoresJson) {
|
None => Console.print(" (no first element)"),
|
||||||
Some(arr) => {
|
}
|
||||||
Console.print(" scores: " + toString(List.length(arr)) + " items")
|
},
|
||||||
// Get first score
|
None => Console.print(" scores: (not an array)"),
|
||||||
match Json.getIndex(scoresJson, 0) {
|
},
|
||||||
Some(firstJson) => match Json.asInt(firstJson) {
|
None => Console.print(" scores: (not found)"),
|
||||||
Some(first) => Console.print(" first score: " + toString(first)),
|
}
|
||||||
None => Console.print(" first score: (not an int)")
|
Console.print("")
|
||||||
},
|
Console.print("Object keys:")
|
||||||
None => Console.print(" (no first element)")
|
match Json.keys(json) {
|
||||||
}
|
Some(ks) => Console.print(" " + String.join(ks, ", ")),
|
||||||
},
|
None => Console.print(" (not an object)"),
|
||||||
None => Console.print(" scores: (not an array)")
|
}
|
||||||
},
|
},
|
||||||
None => Console.print(" scores: (not found)")
|
Err(e) => Console.print("Parse error: " + e),
|
||||||
}
|
}
|
||||||
Console.print("")
|
|
||||||
|
|
||||||
// Get the keys
|
|
||||||
Console.print("Object keys:")
|
|
||||||
match Json.keys(json) {
|
|
||||||
Some(ks) => Console.print(" " + String.join(ks, ", ")),
|
|
||||||
None => Console.print(" (not an object)")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => Console.print("Parse error: " + e)
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.print("")
|
Console.print("")
|
||||||
Console.print("=== JSON Null Check ===")
|
Console.print("=== JSON Null Check ===")
|
||||||
let nullVal = Json.null()
|
let nullVal = Json.null()
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
// Main program that imports modules
|
|
||||||
import examples/modules/math_utils
|
|
||||||
import examples/modules/string_utils
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
Console.print("=== Testing Module Imports ===")
|
Console.print("=== Testing Module Imports ===")
|
||||||
|
|
||||||
// Use math_utils
|
|
||||||
Console.print("square(5) = " + toString(math_utils.square(5)))
|
Console.print("square(5) = " + toString(math_utils.square(5)))
|
||||||
Console.print("cube(3) = " + toString(math_utils.cube(3)))
|
Console.print("cube(3) = " + toString(math_utils.cube(3)))
|
||||||
Console.print("factorial(6) = " + toString(math_utils.factorial(6)))
|
Console.print("factorial(6) = " + toString(math_utils.factorial(6)))
|
||||||
Console.print("sumRange(1, 10) = " + toString(math_utils.sumRange(1, 10)))
|
Console.print("sumRange(1, 10) = " + toString(math_utils.sumRange(1, 10)))
|
||||||
|
|
||||||
// Use string_utils
|
|
||||||
Console.print(string_utils.greet("World"))
|
Console.print(string_utils.greet("World"))
|
||||||
Console.print(string_utils.exclaim("Modules work"))
|
Console.print(string_utils.exclaim("Modules work"))
|
||||||
Console.print("repeat(\"ab\", 3) = " + string_utils.repeat("ab", 3))
|
Console.print("repeat(\"ab\", 3) = " + string_utils.repeat("ab", 3))
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
// Test selective imports
|
|
||||||
import examples/modules/math_utils.{square, factorial}
|
|
||||||
import examples/modules/string_utils as str
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
Console.print("=== Selective & Aliased Imports ===")
|
Console.print("=== Selective & Aliased Imports ===")
|
||||||
|
|
||||||
// Direct imports (no module prefix)
|
|
||||||
Console.print("square(7) = " + toString(square(7)))
|
Console.print("square(7) = " + toString(square(7)))
|
||||||
Console.print("factorial(5) = " + toString(factorial(5)))
|
Console.print("factorial(5) = " + toString(factorial(5)))
|
||||||
|
|
||||||
// Aliased import
|
|
||||||
Console.print(str.greet("Lux"))
|
Console.print(str.greet("Lux"))
|
||||||
Console.print(str.exclaim("Aliased imports work"))
|
Console.print(str.exclaim("Aliased imports work"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
// Test wildcard imports
|
|
||||||
import examples/modules/math_utils.*
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
Console.print("=== Wildcard Imports ===")
|
Console.print("=== Wildcard Imports ===")
|
||||||
|
|
||||||
// All functions available directly
|
|
||||||
Console.print("square(4) = " + toString(square(4)))
|
Console.print("square(4) = " + toString(square(4)))
|
||||||
Console.print("cube(4) = " + toString(cube(4)))
|
Console.print("cube(4) = " + toString(cube(4)))
|
||||||
Console.print("factorial(4) = " + toString(factorial(4)))
|
Console.print("factorial(4) = " + toString(factorial(4)))
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
// Math utilities module
|
fn square(n: Int): Int = n * n
|
||||||
// Exports: square, cube, factorial
|
|
||||||
|
|
||||||
pub fn square(n: Int): Int = n * n
|
fn cube(n: Int): Int = n * n * n
|
||||||
|
|
||||||
pub fn cube(n: Int): Int = n * n * n
|
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
|
||||||
|
|
||||||
pub fn factorial(n: Int): Int =
|
fn sumRange(start: Int, end: Int): Int = if start > end then 0 else start + sumRange(start + 1, end)
|
||||||
if n <= 1 then 1
|
|
||||||
else n * factorial(n - 1)
|
|
||||||
|
|
||||||
pub fn sumRange(start: Int, end: Int): Int =
|
|
||||||
if start > end then 0
|
|
||||||
else start + sumRange(start + 1, end)
|
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
// String utilities module
|
fn repeat(s: String, n: Int): String = if n <= 0 then "" else s + repeat(s, n - 1)
|
||||||
// Exports: repeat, exclaim
|
|
||||||
|
|
||||||
pub fn repeat(s: String, n: Int): String =
|
fn exclaim(s: String): String = s + "!"
|
||||||
if n <= 0 then ""
|
|
||||||
else s + repeat(s, n - 1)
|
|
||||||
|
|
||||||
pub fn exclaim(s: String): String = s + "!"
|
fn greet(name: String): String = "Hello, " + name + "!"
|
||||||
|
|
||||||
pub fn greet(name: String): String =
|
|
||||||
"Hello, " + name + "!"
|
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
// Example using the standard library
|
|
||||||
import std/prelude.*
|
|
||||||
import std/option as opt
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
Console.print("=== Using Standard Library ===")
|
Console.print("=== Using Standard Library ===")
|
||||||
|
|
||||||
// Prelude functions
|
|
||||||
Console.print("identity(42) = " + toString(identity(42)))
|
Console.print("identity(42) = " + toString(identity(42)))
|
||||||
Console.print("not(true) = " + toString(not(true)))
|
Console.print("not(true) = " + toString(not(true)))
|
||||||
Console.print("and(true, false) = " + toString(and(true, false)))
|
Console.print("and(true, false) = " + toString(and(true, false)))
|
||||||
Console.print("or(true, false) = " + toString(or(true, false)))
|
Console.print("or(true, false) = " + toString(or(true, false)))
|
||||||
|
|
||||||
// Option utilities
|
|
||||||
let x = opt.some(10)
|
let x = opt.some(10)
|
||||||
let y = opt.none()
|
let y = opt.none()
|
||||||
Console.print("isSome(Some(10)) = " + toString(opt.isSome(x)))
|
Console.print("isSome(Some(10)) = " + toString(opt.isSome(x)))
|
||||||
|
|||||||
@@ -1,47 +1,31 @@
|
|||||||
// Demonstrating the pipe operator and functional data processing
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// 5 |> double |> addTen |> square = 400
|
|
||||||
// Pipeline result2 = 42
|
|
||||||
// process(1) = 144
|
|
||||||
// process(2) = 196
|
|
||||||
// process(3) = 256
|
|
||||||
// clamped = 0
|
|
||||||
// composed = 121
|
|
||||||
|
|
||||||
// Basic transformations
|
|
||||||
fn double(x: Int): Int = x * 2
|
fn double(x: Int): Int = x * 2
|
||||||
|
|
||||||
fn addTen(x: Int): Int = x + 10
|
fn addTen(x: Int): Int = x + 10
|
||||||
|
|
||||||
fn square(x: Int): Int = x * x
|
fn square(x: Int): Int = x * x
|
||||||
|
|
||||||
fn negate(x: Int): Int = -x
|
fn negate(x: Int): Int = -x
|
||||||
|
|
||||||
// Using the pipe operator for data transformation
|
let result1 = square(addTen(double(5)))
|
||||||
let result1 = 5 |> double |> addTen |> square
|
|
||||||
|
|
||||||
// Chaining multiple operations
|
let result2 = addTen(double(addTen(double(3))))
|
||||||
let result2 = 3 |> double |> addTen |> double |> addTen
|
|
||||||
|
|
||||||
// More complex pipelines
|
fn process(n: Int): Int = square(addTen(double(n)))
|
||||||
fn process(n: Int): Int =
|
|
||||||
n |> double |> addTen |> square
|
|
||||||
|
|
||||||
// Multiple values through same pipeline
|
|
||||||
let a = process(1)
|
let a = process(1)
|
||||||
|
|
||||||
let b = process(2)
|
let b = process(2)
|
||||||
|
|
||||||
let c = process(3)
|
let c = process(3)
|
||||||
|
|
||||||
// Conditional in pipeline
|
fn clampPositive(x: Int): Int = if x < 0 then 0 else x
|
||||||
fn clampPositive(x: Int): Int =
|
|
||||||
if x < 0 then 0 else x
|
|
||||||
|
|
||||||
let clamped = -5 |> double |> clampPositive
|
let clamped = clampPositive(double(-5))
|
||||||
|
|
||||||
// Function composition using pipe
|
|
||||||
fn increment(x: Int): Int = x + 1
|
fn increment(x: Int): Int = x + 1
|
||||||
|
|
||||||
let composed = 5 |> double |> increment |> square
|
let composed = square(increment(double(5)))
|
||||||
|
|
||||||
// Print results
|
|
||||||
fn printResults(): Unit with {Console} = {
|
fn printResults(): Unit with {Console} = {
|
||||||
Console.print("5 |> double |> addTen |> square = " + toString(result1))
|
Console.print("5 |> double |> addTen |> square = " + toString(result1))
|
||||||
Console.print("Pipeline result2 = " + toString(result2))
|
Console.print("Pipeline result2 = " + toString(result2))
|
||||||
|
|||||||
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)
|
fn rollDie(): Int with {Random} = Random.int(1, 6)
|
||||||
|
|
||||||
// Roll multiple dice and print results
|
|
||||||
fn rollDice(count: Int): Unit with {Random, Console} = {
|
fn rollDice(count: Int): Unit with {Random, Console} = {
|
||||||
if count > 0 then {
|
if count > 0 then {
|
||||||
let value = rollDie()
|
let value = rollDie()
|
||||||
Console.print("Die " + toString(4 - count) + ": " + toString(value))
|
Console.print("Die " + toString(4 - count) + ": " + toString(value))
|
||||||
rollDice(count - 1)
|
rollDice(count - 1)
|
||||||
} else {
|
} else {
|
||||||
()
|
()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main function demonstrating random effects
|
|
||||||
fn main(): Unit with {Random, Console, Time} = {
|
fn main(): Unit with {Random, Console, Time} = {
|
||||||
Console.print("Rolling dice...")
|
Console.print("Rolling dice...")
|
||||||
rollDice(3)
|
rollDice(3)
|
||||||
|
|
||||||
let coin = Random.bool()
|
let coin = Random.bool()
|
||||||
Console.print("Coin flip: " + toString(coin))
|
Console.print("Coin flip: " + toString(coin))
|
||||||
|
|
||||||
let f = Random.float()
|
let f = Random.float()
|
||||||
Console.print("Random float: " + toString(f))
|
Console.print("Random float: " + toString(f))
|
||||||
|
|
||||||
let now = Time.now()
|
let now = Time.now()
|
||||||
Console.print("Current time: " + toString(now))
|
Console.print("Current time: " + toString(now))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,41 @@
|
|||||||
// Schema Evolution Demo
|
type User = {
|
||||||
// Demonstrates version tracking and automatic migrations
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// PART 1: Type-Declared Migrations
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Define a versioned type with a migration from v1 to v2
|
|
||||||
type User @v2 {
|
|
||||||
name: String,
|
name: String,
|
||||||
email: String,
|
email: String,
|
||||||
|
|
||||||
// Migration from v1: add default email
|
|
||||||
from @v1 = { name: old.name, email: "unknown@example.com" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a v1 user
|
|
||||||
let v1_user = Schema.versioned("User", 1, { name: "Alice" })
|
let v1_user = Schema.versioned("User", 1, { name: "Alice" })
|
||||||
let v1_version = Schema.getVersion(v1_user) // 1
|
|
||||||
|
|
||||||
// Migrate to v2 - uses the declared migration automatically
|
let v1_version = Schema.getVersion(v1_user)
|
||||||
|
|
||||||
let v2_user = Schema.migrate(v1_user, 2)
|
let v2_user = Schema.migrate(v1_user, 2)
|
||||||
let v2_version = Schema.getVersion(v2_user) // 2
|
|
||||||
|
|
||||||
// ============================================================
|
let v2_version = Schema.getVersion(v2_user)
|
||||||
// PART 2: Runtime Schema Operations (separate type)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Create versioned values for a different type (no migration)
|
|
||||||
let config1 = Schema.versioned("Config", 1, "debug")
|
let config1 = Schema.versioned("Config", 1, "debug")
|
||||||
|
|
||||||
let config2 = Schema.versioned("Config", 2, "release")
|
let config2 = Schema.versioned("Config", 2, "release")
|
||||||
|
|
||||||
// Check versions
|
let c1 = Schema.getVersion(config1)
|
||||||
let c1 = Schema.getVersion(config1) // 1
|
|
||||||
let c2 = Schema.getVersion(config2) // 2
|
let c2 = Schema.getVersion(config2)
|
||||||
|
|
||||||
// Migrate config (auto-migration since no explicit migration defined)
|
|
||||||
let upgradedConfig = Schema.migrate(config1, 2)
|
let upgradedConfig = Schema.migrate(config1, 2)
|
||||||
let upgradedConfigVersion = Schema.getVersion(upgradedConfig) // 2
|
|
||||||
|
|
||||||
// ============================================================
|
let upgradedConfigVersion = Schema.getVersion(upgradedConfig)
|
||||||
// PART 2: Practical Example - API Versioning
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Simulate different API response versions
|
fn createResponseV1(data: String): { version: Int, payload: String } = { version: 1, payload: data }
|
||||||
fn createResponseV1(data: String): { version: Int, payload: String } =
|
|
||||||
{ version: 1, payload: data }
|
|
||||||
|
|
||||||
fn createResponseV2(data: String, timestamp: Int): { version: Int, payload: String, meta: { ts: Int } } =
|
fn createResponseV2(data: String, timestamp: Int): { version: Int, payload: String, meta: { ts: Int } } = { version: 2, payload: data, meta: { ts: timestamp } }
|
||||||
{ version: 2, payload: data, meta: { ts: timestamp } }
|
|
||||||
|
|
||||||
// Version-aware processing
|
fn getPayload(response: { version: Int, payload: String }): String = response.payload
|
||||||
fn getPayload(response: { version: Int, payload: String }): String =
|
|
||||||
response.payload
|
|
||||||
|
|
||||||
let resp1 = createResponseV1("Hello")
|
let resp1 = createResponseV1("Hello")
|
||||||
|
|
||||||
let resp2 = createResponseV2("World", 1234567890)
|
let resp2 = createResponseV2("World", 1234567890)
|
||||||
|
|
||||||
let payload1 = getPayload(resp1)
|
let payload1 = getPayload(resp1)
|
||||||
let payload2 = resp2.payload
|
|
||||||
|
|
||||||
// ============================================================
|
let payload2 = resp2.payload
|
||||||
// RESULTS
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
Console.print("=== Schema Evolution Demo ===")
|
Console.print("=== Schema Evolution Demo ===")
|
||||||
|
|||||||
@@ -1,58 +1,43 @@
|
|||||||
// Shell/Process example - demonstrates the Process effect
|
|
||||||
//
|
|
||||||
// This script runs shell commands and uses environment variables
|
|
||||||
|
|
||||||
fn main(): Unit with {Process, Console} = {
|
fn main(): Unit with {Process, Console} = {
|
||||||
Console.print("=== Lux Shell Example ===")
|
Console.print("=== Lux Shell Example ===")
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Get current working directory
|
|
||||||
let cwd = Process.cwd()
|
let cwd = Process.cwd()
|
||||||
Console.print("Current directory: " + cwd)
|
Console.print("Current directory: " + cwd)
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Get environment variables
|
|
||||||
Console.print("Environment variables:")
|
Console.print("Environment variables:")
|
||||||
match Process.env("USER") {
|
match Process.env("USER") {
|
||||||
Some(user) => Console.print(" USER: " + user),
|
Some(user) => Console.print(" USER: " + user),
|
||||||
None => Console.print(" USER: (not set)")
|
None => Console.print(" USER: (not set)"),
|
||||||
}
|
}
|
||||||
match Process.env("HOME") {
|
match Process.env("HOME") {
|
||||||
Some(home) => Console.print(" HOME: " + home),
|
Some(home) => Console.print(" HOME: " + home),
|
||||||
None => Console.print(" HOME: (not set)")
|
None => Console.print(" HOME: (not set)"),
|
||||||
}
|
}
|
||||||
match Process.env("SHELL") {
|
match Process.env("SHELL") {
|
||||||
Some(shell) => Console.print(" SHELL: " + shell),
|
Some(shell) => Console.print(" SHELL: " + shell),
|
||||||
None => Console.print(" SHELL: (not set)")
|
None => Console.print(" SHELL: (not set)"),
|
||||||
}
|
}
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Run shell commands
|
|
||||||
Console.print("Running shell commands:")
|
Console.print("Running shell commands:")
|
||||||
|
|
||||||
let date = Process.exec("date")
|
let date = Process.exec("date")
|
||||||
Console.print(" date: " + String.trim(date))
|
Console.print(" date: " + String.trim(date))
|
||||||
|
|
||||||
let kernel = Process.exec("uname -r")
|
let kernel = Process.exec("uname -r")
|
||||||
Console.print(" kernel: " + String.trim(kernel))
|
Console.print(" kernel: " + String.trim(kernel))
|
||||||
|
|
||||||
let files = Process.exec("ls examples/*.lux | wc -l")
|
let files = Process.exec("ls examples/*.lux | wc -l")
|
||||||
Console.print(" .lux files in examples/: " + String.trim(files))
|
Console.print(" .lux files in examples/: " + String.trim(files))
|
||||||
Console.print("")
|
Console.print("")
|
||||||
|
|
||||||
// Command line arguments
|
|
||||||
Console.print("Command line arguments:")
|
Console.print("Command line arguments:")
|
||||||
let args = Process.args()
|
let args = Process.args()
|
||||||
let argCount = List.length(args)
|
let argCount = List.length(args)
|
||||||
if argCount == 0 then {
|
if argCount == 0 then {
|
||||||
Console.print(" (no arguments)")
|
Console.print(" (no arguments)")
|
||||||
} else {
|
} else {
|
||||||
Console.print(" Count: " + toString(argCount))
|
Console.print(" Count: " + toString(argCount))
|
||||||
match List.head(args) {
|
match List.head(args) {
|
||||||
Some(first) => Console.print(" First: " + first),
|
Some(first) => Console.print(" First: " + first),
|
||||||
None => Console.print(" First: (empty)")
|
None => Console.print(" First: (empty)"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = run main() with {}
|
let result = run main() with {}
|
||||||
|
|||||||
107
examples/showcase/README.md
Normal file
107
examples/showcase/README.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# Task Manager Showcase
|
||||||
|
|
||||||
|
This example demonstrates Lux's three killer features in a practical, real-world context.
|
||||||
|
|
||||||
|
## Running the Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lux run examples/showcase/task_manager.lux
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features Demonstrated
|
||||||
|
|
||||||
|
### 1. Algebraic Effects
|
||||||
|
|
||||||
|
Every function signature shows exactly what side effects it can perform:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn createTask(title: String, priority: String): Task@latest
|
||||||
|
with {TaskStore, Random} = { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
- `TaskStore` - database operations
|
||||||
|
- `Random` - random number generation
|
||||||
|
- No hidden I/O or surprise calls
|
||||||
|
|
||||||
|
### 2. Behavioral Types
|
||||||
|
|
||||||
|
Compile-time guarantees about function behavior:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn formatTask(task: Task@latest): String
|
||||||
|
is pure // No side effects
|
||||||
|
is deterministic // Same input = same output
|
||||||
|
is total // Always terminates
|
||||||
|
```
|
||||||
|
|
||||||
|
```lux
|
||||||
|
fn completeTask(id: String): Option<Task@latest>
|
||||||
|
is idempotent // Safe to retry
|
||||||
|
with {TaskStore}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Schema Evolution
|
||||||
|
|
||||||
|
Versioned types with automatic migration:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
type Task @v2 {
|
||||||
|
id: String,
|
||||||
|
title: String,
|
||||||
|
done: Bool,
|
||||||
|
priority: String, // New in v2
|
||||||
|
|
||||||
|
from @v1 = { ...old, priority: "medium" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Handler Swapping (Testing)
|
||||||
|
|
||||||
|
Test without mocks by swapping effect handlers:
|
||||||
|
|
||||||
|
```lux
|
||||||
|
// Production
|
||||||
|
run processOrders() with {
|
||||||
|
TaskStore = PostgresTaskStore,
|
||||||
|
Logger = CloudLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Testing
|
||||||
|
run processOrders() with {
|
||||||
|
TaskStore = InMemoryTaskStore,
|
||||||
|
Logger = SilentLogger
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why This Matters
|
||||||
|
|
||||||
|
| Traditional Languages | Lux |
|
||||||
|
|----------------------|-----|
|
||||||
|
| Side effects are implicit | Effects in type signatures |
|
||||||
|
| Runtime crashes | Compile-time verification |
|
||||||
|
| Complex mocking frameworks | Simple handler swapping |
|
||||||
|
| Manual migration code | Automatic schema evolution |
|
||||||
|
| Hope for retry safety | Verified idempotency |
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
showcase/
|
||||||
|
├── README.md # This file
|
||||||
|
└── task_manager.lux # Main example with all features
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Sections in the Code
|
||||||
|
|
||||||
|
1. **Versioned Data Types** - `Task @v1`, `@v2`, `@v3` with migrations
|
||||||
|
2. **Pure Functions** - `is pure`, `is total`, `is deterministic`, `is idempotent`
|
||||||
|
3. **Effects** - `effect TaskStore` and `effect Logger`
|
||||||
|
4. **Effect Handlers** - `InMemoryTaskStore`, `ConsoleLogger`
|
||||||
|
5. **Testing** - `runTestScenario()` with swapped handlers
|
||||||
|
6. **Migration Demo** - `demonstrateMigration()`
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Read the [Behavioral Types Guide](../../docs/guide/12-behavioral-types.md)
|
||||||
|
- Read the [Schema Evolution Guide](../../docs/guide/13-schema-evolution.md)
|
||||||
|
- Explore [more examples](../)
|
||||||
@@ -1,15 +1,3 @@
|
|||||||
// The "Ask" Pattern - Resumable Effects
|
|
||||||
//
|
|
||||||
// Unlike exceptions which unwind the stack, effect handlers can
|
|
||||||
// RESUME with a value. This enables "ask the environment" patterns.
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// Need config: api_url
|
|
||||||
// Got: https://api.example.com
|
|
||||||
// Need config: timeout
|
|
||||||
// Got: 30
|
|
||||||
// Configured with url=https://api.example.com, timeout=30
|
|
||||||
|
|
||||||
effect Config {
|
effect Config {
|
||||||
fn get(key: String): String
|
fn get(key: String): String
|
||||||
}
|
}
|
||||||
@@ -25,14 +13,13 @@ fn configure(): String with {Config, Console} = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handler envConfig: Config {
|
handler envConfig: Config {
|
||||||
fn get(key) =
|
fn get(key) = if key == "api_url" then resume("https://api.example.com") else if key == "timeout" then resume("30") else resume("unknown")
|
||||||
if key == "api_url" then resume("https://api.example.com")
|
|
||||||
else if key == "timeout" then resume("30")
|
|
||||||
else resume("unknown")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
let result = run configure() with { Config = envConfig }
|
let result = run configure() with {
|
||||||
|
Config = envConfig,
|
||||||
|
}
|
||||||
Console.print(result)
|
Console.print(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,3 @@
|
|||||||
// Custom Logging with Effects
|
|
||||||
//
|
|
||||||
// This demonstrates how effects let you abstract side effects.
|
|
||||||
// The same code can be run with different logging implementations.
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// [INFO] Starting computation
|
|
||||||
// [DEBUG] x = 10
|
|
||||||
// [INFO] Processing
|
|
||||||
// [DEBUG] result = 20
|
|
||||||
// Final: 20
|
|
||||||
|
|
||||||
effect Log {
|
effect Log {
|
||||||
fn info(msg: String): Unit
|
fn info(msg: String): Unit
|
||||||
fn debug(msg: String): Unit
|
fn debug(msg: String): Unit
|
||||||
@@ -26,18 +14,22 @@ fn computation(): Int with {Log} = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handler consoleLogger: Log {
|
handler consoleLogger: Log {
|
||||||
fn info(msg) = {
|
fn info(msg) =
|
||||||
Console.print("[INFO] " + msg)
|
{
|
||||||
resume(())
|
Console.print("[INFO] " + msg)
|
||||||
}
|
resume(())
|
||||||
fn debug(msg) = {
|
}
|
||||||
Console.print("[DEBUG] " + msg)
|
fn debug(msg) =
|
||||||
resume(())
|
{
|
||||||
}
|
Console.print("[DEBUG] " + msg)
|
||||||
|
resume(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
let result = run computation() with { Log = consoleLogger }
|
let result = run computation() with {
|
||||||
|
Log = consoleLogger,
|
||||||
|
}
|
||||||
Console.print("Final: " + toString(result))
|
Console.print("Final: " + toString(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,18 @@
|
|||||||
// Early Return with Fail Effect
|
|
||||||
//
|
|
||||||
// The Fail effect provides clean early termination.
|
|
||||||
// Functions declare their failure modes in the type signature.
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// Parsing "42"...
|
|
||||||
// Result: 42
|
|
||||||
// Parsing "100"...
|
|
||||||
// Result: 100
|
|
||||||
// Dividing 100 by 4...
|
|
||||||
// Result: 25
|
|
||||||
|
|
||||||
fn parsePositive(s: String): Int with {Fail, Console} = {
|
fn parsePositive(s: String): Int with {Fail, Console} = {
|
||||||
Console.print("Parsing \"" + s + "\"...")
|
Console.print("Parsing \"" + s + "\"...")
|
||||||
if s == "42" then 42
|
if s == "42" then 42 else if s == "100" then 100 else Fail.fail("Invalid number: " + s)
|
||||||
else if s == "100" then 100
|
|
||||||
else Fail.fail("Invalid number: " + s)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn safeDivide(a: Int, b: Int): Int with {Fail, Console} = {
|
fn safeDivide(a: Int, b: Int): Int with {Fail, Console} = {
|
||||||
Console.print("Dividing " + toString(a) + " by " + toString(b) + "...")
|
Console.print("Dividing " + toString(a) + " by " + toString(b) + "...")
|
||||||
if b == 0 then Fail.fail("Division by zero")
|
if b == 0 then Fail.fail("Division by zero") else a / b
|
||||||
else a / b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
// These succeed
|
|
||||||
let n1 = run parsePositive("42") with {}
|
let n1 = run parsePositive("42") with {}
|
||||||
Console.print("Result: " + toString(n1))
|
Console.print("Result: " + toString(n1))
|
||||||
|
|
||||||
let n2 = run parsePositive("100") with {}
|
let n2 = run parsePositive("100") with {}
|
||||||
Console.print("Result: " + toString(n2))
|
Console.print("Result: " + toString(n2))
|
||||||
|
|
||||||
let n3 = run safeDivide(100, 4) with {}
|
let n3 = run safeDivide(100, 4) with {}
|
||||||
Console.print("Result: " + toString(n3))
|
Console.print("Result: " + toString(n3))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,3 @@
|
|||||||
// Effect Composition - Combine multiple effects cleanly
|
|
||||||
//
|
|
||||||
// Unlike monad transformers (which have ordering issues),
|
|
||||||
// effects can be freely combined without boilerplate.
|
|
||||||
// Each handler handles its own effect, ignoring others.
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// [LOG] Starting computation
|
|
||||||
// Generated: 7
|
|
||||||
// [LOG] Processing value
|
|
||||||
// [LOG] Done
|
|
||||||
// Result: 14
|
|
||||||
|
|
||||||
effect Log {
|
effect Log {
|
||||||
fn log(msg: String): Unit
|
fn log(msg: String): Unit
|
||||||
}
|
}
|
||||||
@@ -30,8 +17,8 @@ handler consoleLog: Log {
|
|||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
let result = run computation() with {
|
let result = run computation() with {
|
||||||
Log = consoleLog
|
Log = consoleLog,
|
||||||
}
|
}
|
||||||
Console.print("Generated: " + toString(result / 2))
|
Console.print("Generated: " + toString(result / 2))
|
||||||
Console.print("Result: " + toString(result))
|
Console.print("Result: " + toString(result))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,19 @@
|
|||||||
// Higher-Order Functions and Closures
|
|
||||||
//
|
|
||||||
// Functions are first-class values in Lux.
|
|
||||||
// Closures capture their environment.
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// Square of 5: 25
|
|
||||||
// Cube of 3: 27
|
|
||||||
// Add 10 to 5: 15
|
|
||||||
// Add 10 to 20: 30
|
|
||||||
// Composed: 15625 (cube(square(5)) = cube(25) = 15625)
|
|
||||||
|
|
||||||
fn apply(f: fn(Int): Int, x: Int): Int = f(x)
|
fn apply(f: fn(Int): Int, x: Int): Int = f(x)
|
||||||
|
|
||||||
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int =
|
fn compose(f: fn(Int): Int, g: fn(Int): Int): fn(Int): Int = fn(x: Int): Int => f(g(x))
|
||||||
fn(x: Int): Int => f(g(x))
|
|
||||||
|
|
||||||
fn square(n: Int): Int = n * n
|
fn square(n: Int): Int = n * n
|
||||||
|
|
||||||
fn cube(n: Int): Int = n * n * n
|
fn cube(n: Int): Int = n * n * n
|
||||||
|
|
||||||
fn makeAdder(n: Int): fn(Int): Int =
|
fn makeAdder(n: Int): fn(Int): Int = fn(x: Int): Int => x + n
|
||||||
fn(x: Int): Int => x + n
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
// Apply functions
|
|
||||||
Console.print("Square of 5: " + toString(apply(square, 5)))
|
Console.print("Square of 5: " + toString(apply(square, 5)))
|
||||||
Console.print("Cube of 3: " + toString(apply(cube, 3)))
|
Console.print("Cube of 3: " + toString(apply(cube, 3)))
|
||||||
|
|
||||||
// Closures
|
|
||||||
let add10 = makeAdder(10)
|
let add10 = makeAdder(10)
|
||||||
Console.print("Add 10 to 5: " + toString(add10(5)))
|
Console.print("Add 10 to 5: " + toString(add10(5)))
|
||||||
Console.print("Add 10 to 20: " + toString(add10(20)))
|
Console.print("Add 10 to 20: " + toString(add10(20)))
|
||||||
|
|
||||||
// Function composition
|
|
||||||
let squareThenCube = compose(cube, square)
|
let squareThenCube = compose(cube, square)
|
||||||
Console.print("Composed: " + toString(squareThenCube(5)))
|
Console.print("Composed: " + toString(squareThenCube(5)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,3 @@
|
|||||||
// Algebraic Data Types and Pattern Matching
|
|
||||||
//
|
|
||||||
// Lux has powerful ADTs with exhaustive pattern matching.
|
|
||||||
// The type system ensures all cases are handled.
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// Evaluating: (2 + 3)
|
|
||||||
// Result: 5
|
|
||||||
// Evaluating: ((1 + 2) * (3 + 4))
|
|
||||||
// Result: 21
|
|
||||||
// Evaluating: (10 - (2 * 3))
|
|
||||||
// Result: 4
|
|
||||||
|
|
||||||
type Expr =
|
type Expr =
|
||||||
| Num(Int)
|
| Num(Int)
|
||||||
| Add(Expr, Expr)
|
| Add(Expr, Expr)
|
||||||
@@ -19,19 +6,19 @@ type Expr =
|
|||||||
|
|
||||||
fn eval(e: Expr): Int =
|
fn eval(e: Expr): Int =
|
||||||
match e {
|
match e {
|
||||||
Num(n) => n,
|
Num(n) => n,
|
||||||
Add(a, b) => eval(a) + eval(b),
|
Add(a, b) => eval(a) + eval(b),
|
||||||
Sub(a, b) => eval(a) - eval(b),
|
Sub(a, b) => eval(a) - eval(b),
|
||||||
Mul(a, b) => eval(a) * eval(b)
|
Mul(a, b) => eval(a) * eval(b),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn showExpr(e: Expr): String =
|
fn showExpr(e: Expr): String =
|
||||||
match e {
|
match e {
|
||||||
Num(n) => toString(n),
|
Num(n) => toString(n),
|
||||||
Add(a, b) => "(" + showExpr(a) + " + " + showExpr(b) + ")",
|
Add(a, b) => "(" + showExpr(a) + " + " + showExpr(b) + ")",
|
||||||
Sub(a, b) => "(" + showExpr(a) + " - " + showExpr(b) + ")",
|
Sub(a, b) => "(" + showExpr(a) + " - " + showExpr(b) + ")",
|
||||||
Mul(a, b) => "(" + showExpr(a) + " * " + showExpr(b) + ")"
|
Mul(a, b) => "(" + showExpr(a) + " * " + showExpr(b) + ")",
|
||||||
}
|
}
|
||||||
|
|
||||||
fn evalAndPrint(e: Expr): Unit with {Console} = {
|
fn evalAndPrint(e: Expr): Unit with {Console} = {
|
||||||
Console.print("Evaluating: " + showExpr(e))
|
Console.print("Evaluating: " + showExpr(e))
|
||||||
@@ -39,15 +26,10 @@ fn evalAndPrint(e: Expr): Unit with {Console} = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
// (2 + 3)
|
|
||||||
let e1 = Add(Num(2), Num(3))
|
let e1 = Add(Num(2), Num(3))
|
||||||
evalAndPrint(e1)
|
evalAndPrint(e1)
|
||||||
|
|
||||||
// ((1 + 2) * (3 + 4))
|
|
||||||
let e2 = Mul(Add(Num(1), Num(2)), Add(Num(3), Num(4)))
|
let e2 = Mul(Add(Num(1), Num(2)), Add(Num(3), Num(4)))
|
||||||
evalAndPrint(e2)
|
evalAndPrint(e2)
|
||||||
|
|
||||||
// (10 - (2 * 3))
|
|
||||||
let e3 = Sub(Num(10), Mul(Num(2), Num(3)))
|
let e3 = Sub(Num(10), Mul(Num(2), Num(3)))
|
||||||
evalAndPrint(e3)
|
evalAndPrint(e3)
|
||||||
}
|
}
|
||||||
|
|||||||
419
examples/showcase/task_manager.lux
Normal file
419
examples/showcase/task_manager.lux
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// Task Manager API - A Showcase of Lux's Unique Features
|
||||||
|
// =============================================================================
|
||||||
|
//
|
||||||
|
// This example demonstrates Lux's three killer features:
|
||||||
|
//
|
||||||
|
// 1. ALGEBRAIC EFFECTS - Every side effect is explicit in function signatures
|
||||||
|
// - No hidden I/O, no surprise database calls
|
||||||
|
// - Testing is trivial: just swap handlers
|
||||||
|
//
|
||||||
|
// 2. BEHAVIORAL TYPES - Compile-time guarantees about function behavior
|
||||||
|
// - `is pure` - no side effects, safe to cache
|
||||||
|
// - `is total` - always terminates, never fails
|
||||||
|
// - `is idempotent` - safe to retry without side effects
|
||||||
|
// - `is deterministic` - same input = same output
|
||||||
|
//
|
||||||
|
// 3. SCHEMA EVOLUTION - Versioned types with automatic migration
|
||||||
|
// - Data structures evolve safely over time
|
||||||
|
// - Old data automatically upgrades
|
||||||
|
//
|
||||||
|
// To run: lux run examples/showcase/task_manager.lux
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PART 1: VERSIONED DATA TYPES (Schema Evolution)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Task v1: Our original data model (simple)
|
||||||
|
type Task @v1 {
|
||||||
|
id: String,
|
||||||
|
title: String,
|
||||||
|
done: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task v2: Added priority field
|
||||||
|
// The `from @v1` clause defines how to migrate old data automatically
|
||||||
|
type Task @v2 {
|
||||||
|
id: String,
|
||||||
|
title: String,
|
||||||
|
done: Bool,
|
||||||
|
priority: String, // New field: "low", "medium", "high"
|
||||||
|
|
||||||
|
// Migration: old tasks get "medium" priority by default
|
||||||
|
from @v1 = {
|
||||||
|
id: old.id,
|
||||||
|
title: old.title,
|
||||||
|
done: old.done,
|
||||||
|
priority: "medium"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task v3: Added due date and tags
|
||||||
|
// Migrations chain automatically: v1 → v2 → v3
|
||||||
|
type Task @v3 {
|
||||||
|
id: String,
|
||||||
|
title: String,
|
||||||
|
done: Bool,
|
||||||
|
priority: String,
|
||||||
|
dueDate: Option<Int>, // Unix timestamp, optional
|
||||||
|
tags: List<String>, // New: categorization
|
||||||
|
|
||||||
|
from @v2 = {
|
||||||
|
id: old.id,
|
||||||
|
title: old.title,
|
||||||
|
done: old.done,
|
||||||
|
priority: old.priority,
|
||||||
|
dueDate: None, // No due date for migrated tasks
|
||||||
|
tags: [] // Empty tags for migrated tasks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use @latest to always refer to the newest version
|
||||||
|
type TaskList = List<Task@latest>
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PART 2: PURE FUNCTIONS WITH BEHAVIORAL TYPES
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Pure function: no side effects, safe to cache, parallelize, eliminate if unused
|
||||||
|
// The compiler verifies `is pure` - if you try to call an effect, it errors.
|
||||||
|
fn formatTask(task: Task@latest): String
|
||||||
|
is pure
|
||||||
|
is deterministic
|
||||||
|
is total = {
|
||||||
|
let status = if task.done then "[x]" else "[ ]"
|
||||||
|
let priority = match task.priority {
|
||||||
|
"high" => "!!",
|
||||||
|
"medium" => "!",
|
||||||
|
_ => ""
|
||||||
|
}
|
||||||
|
status + " " + priority + task.title
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idempotent function: f(f(x)) = f(x)
|
||||||
|
// Safe to apply multiple times without changing the result
|
||||||
|
// Critical for retry logic - the compiler verifies this property
|
||||||
|
fn normalizeTitle(title: String): String
|
||||||
|
is pure
|
||||||
|
is idempotent = {
|
||||||
|
title
|
||||||
|
|> String.trim
|
||||||
|
|> String.toLower
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total function: always terminates, never throws
|
||||||
|
// No Fail effect allowed, recursion must be structurally decreasing
|
||||||
|
fn countCompleted(tasks: TaskList): Int
|
||||||
|
is pure
|
||||||
|
is total = {
|
||||||
|
match tasks {
|
||||||
|
[] => 0,
|
||||||
|
[task, ...rest] =>
|
||||||
|
(if task.done then 1 else 0) + countCompleted(rest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commutative function: f(a, b) = f(b, a)
|
||||||
|
// Enables parallel reduction and argument reordering optimizations
|
||||||
|
fn maxPriority(a: String, b: String): String
|
||||||
|
is pure
|
||||||
|
is commutative = {
|
||||||
|
let priorityValue = fn(p: String): Int =>
|
||||||
|
match p {
|
||||||
|
"high" => 3,
|
||||||
|
"medium" => 2,
|
||||||
|
"low" => 1,
|
||||||
|
_ => 0
|
||||||
|
}
|
||||||
|
if priorityValue(a) > priorityValue(b) then a else b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter tasks by criteria - pure, can be cached and parallelized
|
||||||
|
fn filterByPriority(tasks: TaskList, priority: String): TaskList
|
||||||
|
is pure
|
||||||
|
is deterministic = {
|
||||||
|
List.filter(tasks, fn(t: Task@latest): Bool => t.priority == priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filterPending(tasks: TaskList): TaskList
|
||||||
|
is pure
|
||||||
|
is deterministic = {
|
||||||
|
List.filter(tasks, fn(t: Task@latest): Bool => !t.done)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filterCompleted(tasks: TaskList): TaskList
|
||||||
|
is pure
|
||||||
|
is deterministic = {
|
||||||
|
List.filter(tasks, fn(t: Task@latest): Bool => t.done)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PART 3: EFFECTS - EXPLICIT SIDE EFFECTS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Custom effect for task storage
|
||||||
|
// This declares WHAT operations are available, not HOW they work
|
||||||
|
effect TaskStore {
|
||||||
|
fn save(task: Task@latest): Result<Task@latest, String>
|
||||||
|
fn getById(id: String): Option<Task@latest>
|
||||||
|
fn getAll(): TaskList
|
||||||
|
fn delete(id: String): Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service functions declare their effects in the type signature
|
||||||
|
// Anyone reading the signature knows exactly what side effects can occur
|
||||||
|
|
||||||
|
// Create a new task - requires TaskStore and Random effects
|
||||||
|
fn createTask(title: String, priority: String): Task@latest
|
||||||
|
with {TaskStore, Random} = {
|
||||||
|
let id = "task_" + toString(Random.int(10000, 99999))
|
||||||
|
let task = {
|
||||||
|
id: id,
|
||||||
|
title: normalizeTitle(title), // Uses our idempotent normalizer
|
||||||
|
done: false,
|
||||||
|
priority: priority,
|
||||||
|
dueDate: None,
|
||||||
|
tags: []
|
||||||
|
}
|
||||||
|
match TaskStore.save(task) {
|
||||||
|
Ok(saved) => saved,
|
||||||
|
Err(_) => task // Return unsaved if storage fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete a task - idempotent, safe to retry
|
||||||
|
// If the network fails mid-request, retry is safe
|
||||||
|
fn completeTask(id: String): Option<Task@latest>
|
||||||
|
is idempotent // Compiler verifies this is safe to retry
|
||||||
|
with {TaskStore} = {
|
||||||
|
match TaskStore.getById(id) {
|
||||||
|
None => None,
|
||||||
|
Some(task) => {
|
||||||
|
// Setting done = true is idempotent: already done? stays done
|
||||||
|
let updated = { ...task, done: true }
|
||||||
|
match TaskStore.save(updated) {
|
||||||
|
Ok(saved) => Some(saved),
|
||||||
|
Err(_) => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get task summary - logging effect, but computation is pure
|
||||||
|
fn getTaskSummary(): { total: Int, completed: Int, pending: Int, highPriority: Int }
|
||||||
|
with {TaskStore, Logger} = {
|
||||||
|
let tasks = TaskStore.getAll()
|
||||||
|
Logger.log("Fetched " + toString(List.length(tasks)) + " tasks")
|
||||||
|
|
||||||
|
// These computations are pure - could be parallelized
|
||||||
|
let completed = countCompleted(tasks)
|
||||||
|
let pending = List.length(tasks) - completed
|
||||||
|
let highPriority = List.length(filterByPriority(tasks, "high"))
|
||||||
|
|
||||||
|
{ total: List.length(tasks), completed: completed, pending: pending, highPriority: highPriority }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PART 4: EFFECT HANDLERS - SWAP IMPLEMENTATIONS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// In-memory handler for testing
|
||||||
|
// This handler stores tasks in a mutable list - perfect for unit tests
|
||||||
|
handler InMemoryTaskStore: TaskStore {
|
||||||
|
let tasks: List<Task@latest> = []
|
||||||
|
|
||||||
|
fn save(task: Task@latest): Result<Task@latest, String> = {
|
||||||
|
// Remove existing task with same ID (if any), then add new
|
||||||
|
tasks = List.filter(tasks, fn(t: Task@latest): Bool => t.id != task.id)
|
||||||
|
tasks = List.concat(tasks, [task])
|
||||||
|
Ok(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getById(id: String): Option<Task@latest> = {
|
||||||
|
List.find(tasks, fn(t: Task@latest): Bool => t.id == id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getAll(): TaskList = tasks
|
||||||
|
|
||||||
|
fn delete(id: String): Bool = {
|
||||||
|
let before = List.length(tasks)
|
||||||
|
tasks = List.filter(tasks, fn(t: Task@latest): Bool => t.id != task.id)
|
||||||
|
List.length(tasks) < before
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logging handler - wraps another handler with logging
|
||||||
|
handler LoggingTaskStore(inner: TaskStore): TaskStore with {Logger} {
|
||||||
|
fn save(task: Task@latest): Result<Task@latest, String> = {
|
||||||
|
Logger.log("Saving task: " + task.id)
|
||||||
|
inner.save(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getById(id: String): Option<Task@latest> = {
|
||||||
|
Logger.log("Getting task: " + id)
|
||||||
|
inner.getById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getAll(): TaskList = {
|
||||||
|
Logger.log("Getting all tasks")
|
||||||
|
inner.getAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete(id: String): Bool = {
|
||||||
|
Logger.log("Deleting task: " + id)
|
||||||
|
inner.delete(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple logger effect and handler
|
||||||
|
effect Logger {
|
||||||
|
fn log(message: String): Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
handler ConsoleLogger: Logger with {Console} {
|
||||||
|
fn log(message: String): Unit = {
|
||||||
|
Console.print("[LOG] " + message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handler SilentLogger: Logger {
|
||||||
|
fn log(message: String): Unit = {
|
||||||
|
// Do nothing - useful for tests
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PART 5: TESTING - SWAP HANDLERS, NO MOCKS NEEDED
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Test helper: creates a controlled environment
|
||||||
|
fn runTestScenario(): Unit with {Console} = {
|
||||||
|
Console.print("=== Running Test Scenario ===")
|
||||||
|
Console.print("")
|
||||||
|
|
||||||
|
// Use in-memory storage and silent logging for tests
|
||||||
|
// No database, no file I/O, no network - pure in-memory testing
|
||||||
|
let result = run {
|
||||||
|
// Create some tasks
|
||||||
|
let task1 = createTask("Write documentation", "high")
|
||||||
|
let task2 = createTask("Fix bug #123", "medium")
|
||||||
|
let task3 = createTask("Review PR", "low")
|
||||||
|
|
||||||
|
// Complete one task
|
||||||
|
completeTask(task1.id)
|
||||||
|
|
||||||
|
// Get summary
|
||||||
|
getTaskSummary()
|
||||||
|
} with {
|
||||||
|
TaskStore = InMemoryTaskStore,
|
||||||
|
Logger = SilentLogger,
|
||||||
|
Random = {
|
||||||
|
// Deterministic "random" for tests
|
||||||
|
let counter = 0
|
||||||
|
fn int(min: Int, max: Int): Int = {
|
||||||
|
counter = counter + 1
|
||||||
|
min + (counter * 12345) % (max - min)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.print("Test Results:")
|
||||||
|
Console.print(" Total tasks: " + toString(result.total))
|
||||||
|
Console.print(" Completed: " + toString(result.completed))
|
||||||
|
Console.print(" Pending: " + toString(result.pending))
|
||||||
|
Console.print(" High priority: " + toString(result.highPriority))
|
||||||
|
Console.print("")
|
||||||
|
|
||||||
|
// Verify results
|
||||||
|
if result.total == 3 &&
|
||||||
|
result.completed == 1 &&
|
||||||
|
result.pending == 2 &&
|
||||||
|
result.highPriority == 1 {
|
||||||
|
Console.print("All tests passed!")
|
||||||
|
} else {
|
||||||
|
Console.print("Test failed!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PART 6: SCHEMA MIGRATION DEMO
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
fn demonstrateMigration(): Unit with {Console} = {
|
||||||
|
Console.print("=== Schema Evolution Demo ===")
|
||||||
|
Console.print("")
|
||||||
|
|
||||||
|
// Simulate loading a v1 task (from old database/API)
|
||||||
|
let oldTask = Schema.versioned("Task", 1, {
|
||||||
|
id: "legacy_001",
|
||||||
|
title: "Old task from v1",
|
||||||
|
done: false
|
||||||
|
})
|
||||||
|
|
||||||
|
Console.print("Loaded v1 task:")
|
||||||
|
Console.print(" Version: " + toString(Schema.getVersion(oldTask)))
|
||||||
|
Console.print("")
|
||||||
|
|
||||||
|
// Migrate to latest version automatically
|
||||||
|
let migratedTask = Schema.migrate(oldTask, 3)
|
||||||
|
|
||||||
|
Console.print("After migration to v3:")
|
||||||
|
Console.print(" Version: " + toString(Schema.getVersion(migratedTask)))
|
||||||
|
Console.print(" Has priority: " + migratedTask.priority) // Added by v2 migration
|
||||||
|
Console.print(" Has tags: " + toString(List.length(migratedTask.tags)) + " tags") // Added by v3
|
||||||
|
Console.print("")
|
||||||
|
Console.print("Old data seamlessly upgraded!")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PART 7: MAIN - PUTTING IT ALL TOGETHER
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
fn main(): Unit with {Console} = {
|
||||||
|
Console.print("╔═══════════════════════════════════════════════════════════╗")
|
||||||
|
Console.print("║ Lux Task Manager - Feature Showcase ║")
|
||||||
|
Console.print("╚═══════════════════════════════════════════════════════════╝")
|
||||||
|
Console.print("")
|
||||||
|
|
||||||
|
// Demonstrate pure functions
|
||||||
|
Console.print("--- Pure Functions (Behavioral Types) ---")
|
||||||
|
let sampleTask = {
|
||||||
|
id: "demo",
|
||||||
|
title: "Learn Lux",
|
||||||
|
done: false,
|
||||||
|
priority: "high",
|
||||||
|
dueDate: None,
|
||||||
|
tags: ["learning", "programming"]
|
||||||
|
}
|
||||||
|
Console.print("Formatted task: " + formatTask(sampleTask))
|
||||||
|
Console.print("Normalized title: " + normalizeTitle(" HELLO WORLD "))
|
||||||
|
Console.print("")
|
||||||
|
|
||||||
|
// Demonstrate schema evolution
|
||||||
|
demonstrateMigration()
|
||||||
|
Console.print("")
|
||||||
|
|
||||||
|
// Run tests with swapped handlers
|
||||||
|
runTestScenario()
|
||||||
|
Console.print("")
|
||||||
|
|
||||||
|
Console.print("╔═══════════════════════════════════════════════════════════╗")
|
||||||
|
Console.print("║ Key Takeaways: ║")
|
||||||
|
Console.print("║ ║")
|
||||||
|
Console.print("║ 1. Effects in signatures = no hidden side effects ║")
|
||||||
|
Console.print("║ 2. Behavioral types = compile-time guarantees ║")
|
||||||
|
Console.print("║ 3. Handler swapping = easy testing without mocks ║")
|
||||||
|
Console.print("║ 4. Schema evolution = safe data migrations ║")
|
||||||
|
Console.print("╚═══════════════════════════════════════════════════════════╝")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the showcase
|
||||||
|
let _ = run main() with {}
|
||||||
@@ -1,14 +1,6 @@
|
|||||||
// Factorial - compute n!
|
fn factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)
|
||||||
|
|
||||||
// Recursive version
|
fn factorialTail(n: Int, acc: Int): Int = if n <= 1 then acc else factorialTail(n - 1, n * acc)
|
||||||
fn factorial(n: Int): Int =
|
|
||||||
if n <= 1 then 1
|
|
||||||
else n * factorial(n - 1)
|
|
||||||
|
|
||||||
// Tail-recursive version (optimized)
|
|
||||||
fn factorialTail(n: Int, acc: Int): Int =
|
|
||||||
if n <= 1 then acc
|
|
||||||
else factorialTail(n - 1, n * acc)
|
|
||||||
|
|
||||||
fn factorial2(n: Int): Int = factorialTail(n, 1)
|
fn factorial2(n: Int): Int = factorialTail(n, 1)
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,11 @@
|
|||||||
// FizzBuzz - print numbers 1-100, but:
|
fn fizzbuzz(n: Int): String = if n % 15 == 0 then "FizzBuzz" else if n % 3 == 0 then "Fizz" else if n % 5 == 0 then "Buzz" else toString(n)
|
||||||
// - multiples of 3: print "Fizz"
|
|
||||||
// - multiples of 5: print "Buzz"
|
|
||||||
// - multiples of both: print "FizzBuzz"
|
|
||||||
|
|
||||||
fn fizzbuzz(n: Int): String =
|
|
||||||
if n % 15 == 0 then "FizzBuzz"
|
|
||||||
else if n % 3 == 0 then "Fizz"
|
|
||||||
else if n % 5 == 0 then "Buzz"
|
|
||||||
else toString(n)
|
|
||||||
|
|
||||||
fn printFizzbuzz(i: Int, max: Int): Unit with {Console} =
|
fn printFizzbuzz(i: Int, max: Int): Unit with {Console} =
|
||||||
if i > max then ()
|
if i > max then () else {
|
||||||
else {
|
Console.print(fizzbuzz(i))
|
||||||
Console.print(fizzbuzz(i))
|
printFizzbuzz(i + 1, max)
|
||||||
printFizzbuzz(i + 1, max)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} =
|
fn main(): Unit with {Console} = printFizzbuzz(1, 100)
|
||||||
printFizzbuzz(1, 100)
|
|
||||||
|
|
||||||
let output = run main() with {}
|
let output = run main() with {}
|
||||||
|
|||||||
@@ -1,42 +1,17 @@
|
|||||||
// Number guessing game - demonstrates Random and Console effects
|
fn checkGuess(guess: Int, secret: Int): String = if guess == secret then "Correct" else if guess < secret then "Too low" else "Too high"
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// Welcome to the Guessing Game!
|
|
||||||
// Target number: 42
|
|
||||||
// Simulating guesses...
|
|
||||||
// Guess 50: Too high!
|
|
||||||
// Guess 25: Too low!
|
|
||||||
// Guess 37: Too low!
|
|
||||||
// Guess 43: Too high!
|
|
||||||
// Guess 40: Too low!
|
|
||||||
// Guess 41: Too low!
|
|
||||||
// Guess 42: Correct!
|
|
||||||
// Found in 7 attempts!
|
|
||||||
|
|
||||||
// Game logic - check a guess against the secret
|
|
||||||
fn checkGuess(guess: Int, secret: Int): String =
|
|
||||||
if guess == secret then "Correct"
|
|
||||||
else if guess < secret then "Too low"
|
|
||||||
else "Too high"
|
|
||||||
|
|
||||||
// Binary search simulation to find the number
|
|
||||||
fn binarySearch(low: Int, high: Int, secret: Int, attempts: Int): Int with {Console} = {
|
fn binarySearch(low: Int, high: Int, secret: Int, attempts: Int): Int with {Console} = {
|
||||||
let mid = (low + high) / 2
|
let mid = low + high / 2
|
||||||
let result = checkGuess(mid, secret)
|
let result = checkGuess(mid, secret)
|
||||||
Console.print("Guess " + toString(mid) + ": " + result + "!")
|
Console.print("Guess " + toString(mid) + ": " + result + "!")
|
||||||
|
if result == "Correct" then attempts else if result == "Too low" then binarySearch(mid + 1, high, secret, attempts + 1) else binarySearch(low, mid - 1, secret, attempts + 1)
|
||||||
if result == "Correct" then attempts
|
|
||||||
else if result == "Too low" then binarySearch(mid + 1, high, secret, attempts + 1)
|
|
||||||
else binarySearch(low, mid - 1, secret, attempts + 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
Console.print("Welcome to the Guessing Game!")
|
Console.print("Welcome to the Guessing Game!")
|
||||||
// Use a fixed "secret" for reproducible output
|
|
||||||
let secret = 42
|
let secret = 42
|
||||||
Console.print("Target number: " + toString(secret))
|
Console.print("Target number: " + toString(secret))
|
||||||
Console.print("Simulating guesses...")
|
Console.print("Simulating guesses...")
|
||||||
|
|
||||||
let attempts = binarySearch(1, 100, secret, 1)
|
let attempts = binarySearch(1, 100, secret, 1)
|
||||||
Console.print("Found in " + toString(attempts) + " attempts!")
|
Console.print("Found in " + toString(attempts) + " attempts!")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// The classic first program
|
fn main(): Unit with {Console} = Console.print("Hello, World!")
|
||||||
// Expected output: Hello, World!
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} =
|
|
||||||
Console.print("Hello, World!")
|
|
||||||
|
|
||||||
let output = run main() with {}
|
let output = run main() with {}
|
||||||
|
|||||||
@@ -1,25 +1,14 @@
|
|||||||
// Prime number utilities
|
fn isPrime(n: Int): Bool = if n < 2 then false else isPrimeHelper(n, 2)
|
||||||
|
|
||||||
fn isPrime(n: Int): Bool =
|
fn isPrimeHelper(n: Int, i: Int): Bool = if i * i > n then true else if n % i == 0 then false else isPrimeHelper(n, i + 1)
|
||||||
if n < 2 then false
|
|
||||||
else isPrimeHelper(n, 2)
|
|
||||||
|
|
||||||
fn isPrimeHelper(n: Int, i: Int): Bool =
|
fn findPrimes(count: Int): Unit with {Console} = findPrimesHelper(2, count)
|
||||||
if i * i > n then true
|
|
||||||
else if n % i == 0 then false
|
|
||||||
else isPrimeHelper(n, i + 1)
|
|
||||||
|
|
||||||
// Find first n primes
|
|
||||||
fn findPrimes(count: Int): Unit with {Console} =
|
|
||||||
findPrimesHelper(2, count)
|
|
||||||
|
|
||||||
fn findPrimesHelper(current: Int, remaining: Int): Unit with {Console} =
|
fn findPrimesHelper(current: Int, remaining: Int): Unit with {Console} =
|
||||||
if remaining <= 0 then ()
|
if remaining <= 0 then () else if isPrime(current) then {
|
||||||
else if isPrime(current) then {
|
Console.print(toString(current))
|
||||||
Console.print(toString(current))
|
findPrimesHelper(current + 1, remaining - 1)
|
||||||
findPrimesHelper(current + 1, remaining - 1)
|
} else findPrimesHelper(current + 1, remaining)
|
||||||
}
|
|
||||||
else findPrimesHelper(current + 1, remaining)
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
Console.print("First 20 prime numbers:")
|
Console.print("First 20 prime numbers:")
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// Standard Library Demo
|
|
||||||
// Demonstrates the built-in modules: List, String, Option, Math
|
|
||||||
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
Console.print("=== List Operations ===")
|
Console.print("=== List Operations ===")
|
||||||
let nums = [1, 2, 3, 4, 5]
|
let nums = [1, 2, 3, 4, 5]
|
||||||
@@ -11,7 +8,6 @@ fn main(): Unit with {Console} = {
|
|||||||
Console.print("Length: " + toString(List.length(nums)))
|
Console.print("Length: " + toString(List.length(nums)))
|
||||||
Console.print("Reversed: " + toString(List.reverse(nums)))
|
Console.print("Reversed: " + toString(List.reverse(nums)))
|
||||||
Console.print("Range 1-5: " + toString(List.range(1, 6)))
|
Console.print("Range 1-5: " + toString(List.range(1, 6)))
|
||||||
|
|
||||||
Console.print("")
|
Console.print("")
|
||||||
Console.print("=== String Operations ===")
|
Console.print("=== String Operations ===")
|
||||||
let text = " Hello, World! "
|
let text = " Hello, World! "
|
||||||
@@ -22,7 +18,6 @@ fn main(): Unit with {Console} = {
|
|||||||
Console.print("Contains 'World': " + toString(String.contains(text, "World")))
|
Console.print("Contains 'World': " + toString(String.contains(text, "World")))
|
||||||
Console.print("Split by comma: " + toString(String.split("a,b,c", ",")))
|
Console.print("Split by comma: " + toString(String.split("a,b,c", ",")))
|
||||||
Console.print("Join with dash: " + String.join(["x", "y", "z"], "-"))
|
Console.print("Join with dash: " + String.join(["x", "y", "z"], "-"))
|
||||||
|
|
||||||
Console.print("")
|
Console.print("")
|
||||||
Console.print("=== Option Operations ===")
|
Console.print("=== Option Operations ===")
|
||||||
let some_val = Some(42)
|
let some_val = Some(42)
|
||||||
@@ -31,7 +26,6 @@ fn main(): Unit with {Console} = {
|
|||||||
Console.print("None mapped: " + toString(Option.map(none_val, fn(x: Int): Int => x * 2)))
|
Console.print("None mapped: " + toString(Option.map(none_val, fn(x: Int): Int => x * 2)))
|
||||||
Console.print("Some(42) getOrElse(0): " + toString(Option.getOrElse(some_val, 0)))
|
Console.print("Some(42) getOrElse(0): " + toString(Option.getOrElse(some_val, 0)))
|
||||||
Console.print("None getOrElse(0): " + toString(Option.getOrElse(none_val, 0)))
|
Console.print("None getOrElse(0): " + toString(Option.getOrElse(none_val, 0)))
|
||||||
|
|
||||||
Console.print("")
|
Console.print("")
|
||||||
Console.print("=== Math Operations ===")
|
Console.print("=== Math Operations ===")
|
||||||
Console.print("abs(-5): " + toString(Math.abs(-5)))
|
Console.print("abs(-5): " + toString(Math.abs(-5)))
|
||||||
|
|||||||
@@ -1,13 +1,3 @@
|
|||||||
// State machine example using algebraic data types
|
|
||||||
// Demonstrates pattern matching for state transitions
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// Initial light: red
|
|
||||||
// After transition: green
|
|
||||||
// After two transitions: yellow
|
|
||||||
// Door: Closed -> Open -> Closed -> Locked
|
|
||||||
|
|
||||||
// Traffic light state machine
|
|
||||||
type TrafficLight =
|
type TrafficLight =
|
||||||
| Red
|
| Red
|
||||||
| Yellow
|
| Yellow
|
||||||
@@ -15,26 +5,25 @@ type TrafficLight =
|
|||||||
|
|
||||||
fn nextLight(light: TrafficLight): TrafficLight =
|
fn nextLight(light: TrafficLight): TrafficLight =
|
||||||
match light {
|
match light {
|
||||||
Red => Green,
|
Red => Green,
|
||||||
Green => Yellow,
|
Green => Yellow,
|
||||||
Yellow => Red
|
Yellow => Red,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn canGo(light: TrafficLight): Bool =
|
fn canGo(light: TrafficLight): Bool =
|
||||||
match light {
|
match light {
|
||||||
Green => true,
|
Green => true,
|
||||||
Yellow => false,
|
Yellow => false,
|
||||||
Red => false
|
Red => false,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lightColor(light: TrafficLight): String =
|
fn lightColor(light: TrafficLight): String =
|
||||||
match light {
|
match light {
|
||||||
Red => "red",
|
Red => "red",
|
||||||
Yellow => "yellow",
|
Yellow => "yellow",
|
||||||
Green => "green"
|
Green => "green",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Door state machine
|
|
||||||
type DoorState =
|
type DoorState =
|
||||||
| Open
|
| Open
|
||||||
| Closed
|
| Closed
|
||||||
@@ -48,31 +37,34 @@ type DoorAction =
|
|||||||
|
|
||||||
fn applyAction(state: DoorState, action: DoorAction): DoorState =
|
fn applyAction(state: DoorState, action: DoorAction): DoorState =
|
||||||
match (state, action) {
|
match (state, action) {
|
||||||
(Closed, OpenDoor) => Open,
|
(Closed, OpenDoor) => Open,
|
||||||
(Open, CloseDoor) => Closed,
|
(Open, CloseDoor) => Closed,
|
||||||
(Closed, LockDoor) => Locked,
|
(Closed, LockDoor) => Locked,
|
||||||
(Locked, UnlockDoor) => Closed,
|
(Locked, UnlockDoor) => Closed,
|
||||||
_ => state
|
_ => state,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn doorStateName(state: DoorState): String =
|
fn doorStateName(state: DoorState): String =
|
||||||
match state {
|
match state {
|
||||||
Open => "Open",
|
Open => "Open",
|
||||||
Closed => "Closed",
|
Closed => "Closed",
|
||||||
Locked => "Locked"
|
Locked => "Locked",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test the state machines
|
|
||||||
let light1 = Red
|
let light1 = Red
|
||||||
|
|
||||||
let light2 = nextLight(light1)
|
let light2 = nextLight(light1)
|
||||||
|
|
||||||
let light3 = nextLight(light2)
|
let light3 = nextLight(light2)
|
||||||
|
|
||||||
let door1 = Closed
|
let door1 = Closed
|
||||||
|
|
||||||
let door2 = applyAction(door1, OpenDoor)
|
let door2 = applyAction(door1, OpenDoor)
|
||||||
|
|
||||||
let door3 = applyAction(door2, CloseDoor)
|
let door3 = applyAction(door2, CloseDoor)
|
||||||
|
|
||||||
let door4 = applyAction(door3, LockDoor)
|
let door4 = applyAction(door3, LockDoor)
|
||||||
|
|
||||||
// Print results
|
|
||||||
fn printResults(): Unit with {Console} = {
|
fn printResults(): Unit with {Console} = {
|
||||||
Console.print("Initial light: " + lightColor(light1))
|
Console.print("Initial light: " + lightColor(light1))
|
||||||
Console.print("After transition: " + lightColor(light2))
|
Console.print("After transition: " + lightColor(light2))
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
// Stress test for RC system with large lists
|
|
||||||
// Tests FBIP optimization with single-owner chains
|
|
||||||
|
|
||||||
fn processChain(n: Int): Int = {
|
fn processChain(n: Int): Int = {
|
||||||
// Single owner chain - FBIP should reuse lists
|
|
||||||
let nums = List.range(1, n)
|
let nums = List.range(1, n)
|
||||||
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
||||||
let filtered = List.filter(doubled, fn(x: Int): Bool => x > n)
|
let filtered = List.filter(doubled, fn(x: Int): Bool => x > n)
|
||||||
@@ -12,13 +8,10 @@ fn processChain(n: Int): Int = {
|
|||||||
|
|
||||||
fn main(): Unit = {
|
fn main(): Unit = {
|
||||||
Console.print("=== RC Stress Test ===")
|
Console.print("=== RC Stress Test ===")
|
||||||
|
|
||||||
// Run multiple iterations of list operations
|
|
||||||
let result1 = processChain(100)
|
let result1 = processChain(100)
|
||||||
let result2 = processChain(200)
|
let result2 = processChain(200)
|
||||||
let result3 = processChain(500)
|
let result3 = processChain(500)
|
||||||
let result4 = processChain(1000)
|
let result4 = processChain(1000)
|
||||||
|
|
||||||
Console.print("Completed 4 chains")
|
Console.print("Completed 4 chains")
|
||||||
Console.print("Sizes: 100, 200, 500, 1000")
|
Console.print("Sizes: 100, 200, 500, 1000")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
// Stress test for RC system WITH shared references
|
|
||||||
// Forces rc>1 path by keeping aliases
|
|
||||||
|
|
||||||
fn processWithAlias(n: Int): Int = {
|
fn processWithAlias(n: Int): Int = {
|
||||||
let nums = List.range(1, n)
|
let nums = List.range(1, n)
|
||||||
let alias = nums // This increments rc, forcing copy path
|
let alias = nums
|
||||||
let _len = List.length(alias) // Use the alias
|
let _len = List.length(alias)
|
||||||
|
|
||||||
// Now nums has rc>1, so map must allocate new
|
|
||||||
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
||||||
let filtered = List.filter(doubled, fn(x: Int): Bool => x > n)
|
let filtered = List.filter(doubled, fn(x: Int): Bool => x > n)
|
||||||
let reversed = List.reverse(filtered)
|
let reversed = List.reverse(filtered)
|
||||||
@@ -15,12 +10,9 @@ fn processWithAlias(n: Int): Int = {
|
|||||||
|
|
||||||
fn main(): Unit = {
|
fn main(): Unit = {
|
||||||
Console.print("=== RC Stress Test (Shared Refs) ===")
|
Console.print("=== RC Stress Test (Shared Refs) ===")
|
||||||
|
|
||||||
// Run multiple iterations with shared references
|
|
||||||
let result1 = processWithAlias(100)
|
let result1 = processWithAlias(100)
|
||||||
let result2 = processWithAlias(200)
|
let result2 = processWithAlias(200)
|
||||||
let result3 = processWithAlias(500)
|
let result3 = processWithAlias(500)
|
||||||
let result4 = processWithAlias(1000)
|
let result4 = processWithAlias(1000)
|
||||||
|
|
||||||
Console.print("Completed 4 chains with shared refs")
|
Console.print("Completed 4 chains with shared refs")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,25 @@
|
|||||||
// Demonstrating tail call optimization (TCO) in Lux
|
fn factorialTCO(n: Int, acc: Int): Int = if n <= 1 then acc else factorialTCO(n - 1, n * acc)
|
||||||
// TCO allows recursive functions to run in constant stack space
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// factorial(20) = 2432902008176640000
|
|
||||||
// fib(30) = 832040
|
|
||||||
// sumTo(1000) = 500500
|
|
||||||
// countdown(10000) completed
|
|
||||||
|
|
||||||
// Factorial with accumulator - tail recursive
|
|
||||||
fn factorialTCO(n: Int, acc: Int): Int =
|
|
||||||
if n <= 1 then acc
|
|
||||||
else factorialTCO(n - 1, n * acc)
|
|
||||||
|
|
||||||
fn factorial(n: Int): Int = factorialTCO(n, 1)
|
fn factorial(n: Int): Int = factorialTCO(n, 1)
|
||||||
|
|
||||||
// Fibonacci with accumulator - tail recursive
|
fn fibTCO(n: Int, a: Int, b: Int): Int = if n <= 0 then a else fibTCO(n - 1, b, a + b)
|
||||||
fn fibTCO(n: Int, a: Int, b: Int): Int =
|
|
||||||
if n <= 0 then a
|
|
||||||
else fibTCO(n - 1, b, a + b)
|
|
||||||
|
|
||||||
fn fib(n: Int): Int = fibTCO(n, 0, 1)
|
fn fib(n: Int): Int = fibTCO(n, 0, 1)
|
||||||
|
|
||||||
// Count down - simple tail recursion
|
fn countdown(n: Int): Int = if n <= 0 then 0 else countdown(n - 1)
|
||||||
fn countdown(n: Int): Int =
|
|
||||||
if n <= 0 then 0
|
|
||||||
else countdown(n - 1)
|
|
||||||
|
|
||||||
// Sum with accumulator - tail recursive
|
fn sumToTCO(n: Int, acc: Int): Int = if n <= 0 then acc else sumToTCO(n - 1, acc + n)
|
||||||
fn sumToTCO(n: Int, acc: Int): Int =
|
|
||||||
if n <= 0 then acc
|
|
||||||
else sumToTCO(n - 1, acc + n)
|
|
||||||
|
|
||||||
fn sumTo(n: Int): Int = sumToTCO(n, 0)
|
fn sumTo(n: Int): Int = sumToTCO(n, 0)
|
||||||
|
|
||||||
// Test the functions
|
|
||||||
let fact20 = factorial(20)
|
let fact20 = factorial(20)
|
||||||
|
|
||||||
let fib30 = fib(30)
|
let fib30 = fib(30)
|
||||||
|
|
||||||
let sum1000 = sumTo(1000)
|
let sum1000 = sumTo(1000)
|
||||||
|
|
||||||
let countResult = countdown(10000)
|
let countResult = countdown(10000)
|
||||||
|
|
||||||
// Print results
|
|
||||||
fn printResults(): Unit with {Console} = {
|
fn printResults(): Unit with {Console} = {
|
||||||
Console.print("factorial(20) = " + toString(fact20))
|
Console.print("factorial(20) = " + toString(fact20))
|
||||||
Console.print("fib(30) = " + toString(fib30))
|
Console.print("fib(30) = " + toString(fib30))
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
// This test shows FBIP optimization by comparing allocation counts
|
|
||||||
// With FBIP (rc=1): lists are reused in-place
|
|
||||||
// Without FBIP (rc>1): new lists are allocated
|
|
||||||
|
|
||||||
fn main(): Unit = {
|
fn main(): Unit = {
|
||||||
Console.print("=== FBIP Allocation Test ===")
|
Console.print("=== FBIP Allocation Test ===")
|
||||||
|
|
||||||
// Case 1: Single owner (FBIP active) - should reuse list
|
|
||||||
let a = List.range(1, 100)
|
let a = List.range(1, 100)
|
||||||
let b = List.map(a, fn(x: Int): Int => x * 2)
|
let b = List.map(a, fn(x: Int): Int => x * 2)
|
||||||
let c = List.filter(b, fn(x: Int): Bool => x > 50)
|
let c = List.filter(b, fn(x: Int): Bool => x > 50)
|
||||||
let d = List.reverse(c)
|
let d = List.reverse(c)
|
||||||
Console.print("Single owner chain done")
|
Console.print("Single owner chain done")
|
||||||
|
|
||||||
// The allocation count will show FBIP is working
|
|
||||||
// if allocations are low relative to operations performed
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
fn main(): Unit = {
|
fn main(): Unit = {
|
||||||
// Test FBIP without string operations
|
|
||||||
let nums = [1, 2, 3, 4, 5]
|
let nums = [1, 2, 3, 4, 5]
|
||||||
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
let doubled = List.map(nums, fn(x: Int): Int => x * 2)
|
||||||
let filtered = List.filter(doubled, fn(x: Int): Bool => x > 4)
|
let filtered = List.filter(doubled, fn(x: Int): Bool => x > 4)
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// List Operations Test Suite
|
|
||||||
// Run with: lux test examples/test_lists.lux
|
|
||||||
|
|
||||||
fn test_list_length(): Unit with {Test} = {
|
fn test_list_length(): Unit with {Test} = {
|
||||||
Test.assertEqual(0, List.length([]))
|
Test.assertEqual(0, List.length([]))
|
||||||
Test.assertEqual(1, List.length([1]))
|
Test.assertEqual(1, List.length([1]))
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// Math Test Suite
|
|
||||||
// Run with: lux test examples/test_math.lux
|
|
||||||
|
|
||||||
fn test_addition(): Unit with {Test} = {
|
fn test_addition(): Unit with {Test} = {
|
||||||
Test.assertEqual(4, 2 + 2)
|
Test.assertEqual(4, 2 + 2)
|
||||||
Test.assertEqual(0, 0 + 0)
|
Test.assertEqual(0, 0 + 0)
|
||||||
|
|||||||
@@ -1,21 +1,10 @@
|
|||||||
// Test demonstrating ownership transfer with aliases
|
|
||||||
// The ownership transfer optimization ensures FBIP still works
|
|
||||||
// even when variables are aliased, because ownership is transferred
|
|
||||||
// rather than reference count being incremented.
|
|
||||||
|
|
||||||
fn main(): Unit = {
|
fn main(): Unit = {
|
||||||
Console.print("=== Ownership Transfer Test ===")
|
Console.print("=== Ownership Transfer Test ===")
|
||||||
|
|
||||||
let a = List.range(1, 100)
|
let a = List.range(1, 100)
|
||||||
// Ownership transfers from 'a' to 'alias', keeping rc=1
|
|
||||||
let alias = a
|
let alias = a
|
||||||
let len1 = List.length(alias)
|
let len1 = List.length(alias)
|
||||||
|
|
||||||
// Since ownership transferred, 'a' still has rc=1
|
|
||||||
// FBIP can still optimize map/filter/reverse
|
|
||||||
let b = List.map(a, fn(x: Int): Int => x * 2)
|
let b = List.map(a, fn(x: Int): Int => x * 2)
|
||||||
let c = List.filter(b, fn(x: Int): Bool => x > 50)
|
let c = List.filter(b, fn(x: Int): Bool => x > 50)
|
||||||
let d = List.reverse(c)
|
let d = List.reverse(c)
|
||||||
|
|
||||||
Console.print("Ownership transfer chain done")
|
Console.print("Ownership transfer chain done")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
fn main(): Unit = {
|
fn main(): Unit = {
|
||||||
Console.print("=== Allocation Comparison ===")
|
Console.print("=== Allocation Comparison ===")
|
||||||
|
|
||||||
// FBIP path (rc=1): list is reused
|
|
||||||
Console.print("Test 1: FBIP path")
|
Console.print("Test 1: FBIP path")
|
||||||
let a1 = List.range(1, 50)
|
let a1 = List.range(1, 50)
|
||||||
let b1 = List.map(a1, fn(x: Int): Int => x * 2)
|
let b1 = List.map(a1, fn(x: Int): Int => x * 2)
|
||||||
let c1 = List.reverse(b1)
|
let c1 = List.reverse(b1)
|
||||||
Console.print("FBIP done")
|
Console.print("FBIP done")
|
||||||
|
|
||||||
// To show non-FBIP, we need concat which doesn't have FBIP
|
|
||||||
Console.print("Test 2: Non-FBIP path (concat)")
|
Console.print("Test 2: Non-FBIP path (concat)")
|
||||||
let x = List.range(1, 25)
|
let x = List.range(1, 25)
|
||||||
let y = List.range(26, 50)
|
let y = List.range(26, 50)
|
||||||
let z = List.concat(x, y) // concat always allocates new
|
let z = List.concat(x, y)
|
||||||
Console.print("Concat done")
|
Console.print("Concat done")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
// Demonstrating type classes (traits) in Lux
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// RGB color: rgb(255,128,0)
|
|
||||||
// Red color: red
|
|
||||||
// Green color: green
|
|
||||||
|
|
||||||
// Define a simple Printable trait
|
|
||||||
trait Printable {
|
trait Printable {
|
||||||
fn format(value: Int): String
|
fn format(value: Int): String
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement Printable
|
|
||||||
impl Printable for Int {
|
impl Printable for Int {
|
||||||
fn format(value: Int): String = "Number: " + toString(value)
|
fn format(value: Int): String = "Number: " + toString(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Color type with pattern matching
|
|
||||||
type Color =
|
type Color =
|
||||||
| Red
|
| Red
|
||||||
| Green
|
| Green
|
||||||
@@ -24,18 +14,18 @@ type Color =
|
|||||||
|
|
||||||
fn colorName(c: Color): String =
|
fn colorName(c: Color): String =
|
||||||
match c {
|
match c {
|
||||||
Red => "red",
|
Red => "red",
|
||||||
Green => "green",
|
Green => "green",
|
||||||
Blue => "blue",
|
Blue => "blue",
|
||||||
RGB(r, g, b) => "rgb(" + toString(r) + "," + toString(g) + "," + toString(b) + ")"
|
RGB(r, g, b) => "rgb(" + toString(r) + "," + toString(g) + "," + toString(b) + ")",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test
|
|
||||||
let myColor = RGB(255, 128, 0)
|
let myColor = RGB(255, 128, 0)
|
||||||
|
|
||||||
let redColor = Red
|
let redColor = Red
|
||||||
|
|
||||||
let greenColor = Green
|
let greenColor = Green
|
||||||
|
|
||||||
// Print results
|
|
||||||
fn printResults(): Unit with {Console} = {
|
fn printResults(): Unit with {Console} = {
|
||||||
Console.print("RGB color: " + colorName(myColor))
|
Console.print("RGB color: " + colorName(myColor))
|
||||||
Console.print("Red color: " + colorName(redColor))
|
Console.print("Red color: " + colorName(redColor))
|
||||||
|
|||||||
@@ -1,15 +1,3 @@
|
|||||||
// Demonstrating Schema Evolution in Lux
|
|
||||||
//
|
|
||||||
// Lux provides versioned types to help manage data evolution over time.
|
|
||||||
// The Schema module provides functions for creating and migrating versioned values.
|
|
||||||
//
|
|
||||||
// Expected output:
|
|
||||||
// Created user v1: Alice (age unknown)
|
|
||||||
// User version: 1
|
|
||||||
// Migrated to v2: Alice (age unknown)
|
|
||||||
// User version after migration: 2
|
|
||||||
|
|
||||||
// Create a versioned User value at v1
|
|
||||||
fn createUserV1(name: String): Unit with {Console} = {
|
fn createUserV1(name: String): Unit with {Console} = {
|
||||||
let user = Schema.versioned("User", 1, { name: name })
|
let user = Schema.versioned("User", 1, { name: name })
|
||||||
let version = Schema.getVersion(user)
|
let version = Schema.getVersion(user)
|
||||||
@@ -17,7 +5,6 @@ fn createUserV1(name: String): Unit with {Console} = {
|
|||||||
Console.print("User version: " + toString(version))
|
Console.print("User version: " + toString(version))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate a user to v2
|
|
||||||
fn migrateUserToV2(name: String): Unit with {Console} = {
|
fn migrateUserToV2(name: String): Unit with {Console} = {
|
||||||
let userV1 = Schema.versioned("User", 1, { name: name })
|
let userV1 = Schema.versioned("User", 1, { name: name })
|
||||||
let userV2 = Schema.migrate(userV1, 2)
|
let userV2 = Schema.migrate(userV1, 2)
|
||||||
@@ -26,7 +13,6 @@ fn migrateUserToV2(name: String): Unit with {Console} = {
|
|||||||
Console.print("User version after migration: " + toString(newVersion))
|
Console.print("User version after migration: " + toString(newVersion))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main
|
|
||||||
fn main(): Unit with {Console} = {
|
fn main(): Unit with {Console} = {
|
||||||
createUserV1("Alice")
|
createUserV1("Alice")
|
||||||
migrateUserToV2("Alice")
|
migrateUserToV2("Alice")
|
||||||
|
|||||||
@@ -1,62 +1,38 @@
|
|||||||
// Simple Counter for Browser
|
type Model =
|
||||||
// Compile with: lux compile examples/web/counter.lux --target js -o examples/web/counter.js
|
| Counter(Int)
|
||||||
|
|
||||||
// ============================================================================
|
fn getCount(m: Model): Int =
|
||||||
// Model
|
match m {
|
||||||
// ============================================================================
|
Counter(n) => n,
|
||||||
|
}
|
||||||
type Model = | Counter(Int)
|
|
||||||
|
|
||||||
fn getCount(m: Model): Int = match m { Counter(n) => n }
|
|
||||||
|
|
||||||
fn init(): Model = Counter(0)
|
fn init(): Model = Counter(0)
|
||||||
|
|
||||||
// ============================================================================
|
type Msg =
|
||||||
// Messages
|
| Increment
|
||||||
// ============================================================================
|
| Decrement
|
||||||
|
| Reset
|
||||||
type Msg = | Increment | Decrement | Reset
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Update
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
fn update(model: Model, msg: Msg): Model =
|
fn update(model: Model, msg: Msg): Model =
|
||||||
match msg {
|
match msg {
|
||||||
Increment => Counter(getCount(model) + 1),
|
Increment => Counter(getCount(model) + 1),
|
||||||
Decrement => Counter(getCount(model) - 1),
|
Decrement => Counter(getCount(model) - 1),
|
||||||
Reset => Counter(0)
|
Reset => Counter(0),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// View - Returns HTML string for simplicity
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
fn view(model: Model): String = {
|
fn view(model: Model): String = {
|
||||||
let count = getCount(model)
|
let count = getCount(model)
|
||||||
"<div class=\"counter\">" +
|
"<div class=\"counter\">" + "<h1>Lux Counter</h1>" + "<div class=\"display\">" + toString(count) + "</div>" + "<div class=\"buttons\">" + "<button onclick=\"dispatch('Decrement')\">-</button>" + "<button onclick=\"dispatch('Reset')\">Reset</button>" + "<button onclick=\"dispatch('Increment')\">+</button>" + "</div>" + "</div>"
|
||||||
"<h1>Lux Counter</h1>" +
|
|
||||||
"<div class=\"display\">" + toString(count) + "</div>" +
|
|
||||||
"<div class=\"buttons\">" +
|
|
||||||
"<button onclick=\"dispatch('Decrement')\">-</button>" +
|
|
||||||
"<button onclick=\"dispatch('Reset')\">Reset</button>" +
|
|
||||||
"<button onclick=\"dispatch('Increment')\">+</button>" +
|
|
||||||
"</div>" +
|
|
||||||
"</div>"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Export for browser runtime
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
fn luxInit(): Model = init()
|
fn luxInit(): Model = init()
|
||||||
|
|
||||||
fn luxUpdate(model: Model, msgName: String): Model =
|
fn luxUpdate(model: Model, msgName: String): Model =
|
||||||
match msgName {
|
match msgName {
|
||||||
"Increment" => update(model, Increment),
|
"Increment" => update(model, Increment),
|
||||||
"Decrement" => update(model, Decrement),
|
"Decrement" => update(model, Decrement),
|
||||||
"Reset" => update(model, Reset),
|
"Reset" => update(model, Reset),
|
||||||
_ => model
|
_ => model,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn luxView(model: Model): String = view(model)
|
fn luxView(model: Model): String = view(model)
|
||||||
|
|||||||
125
flake.nix
125
flake.nix
@@ -14,6 +14,7 @@
|
|||||||
pkgs = import nixpkgs { inherit system overlays; };
|
pkgs = import nixpkgs { inherit system overlays; };
|
||||||
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
||||||
extensions = [ "rust-src" "rust-analyzer" ];
|
extensions = [ "rust-src" "rust-analyzer" ];
|
||||||
|
targets = [ "x86_64-unknown-linux-musl" ];
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -22,8 +23,11 @@
|
|||||||
rustToolchain
|
rustToolchain
|
||||||
cargo-watch
|
cargo-watch
|
||||||
cargo-edit
|
cargo-edit
|
||||||
pkg-config
|
# Static builds
|
||||||
openssl
|
pkgsStatic.stdenv.cc
|
||||||
|
# Benchmark tools
|
||||||
|
hyperfine
|
||||||
|
poop
|
||||||
];
|
];
|
||||||
|
|
||||||
RUST_BACKTRACE = "1";
|
RUST_BACKTRACE = "1";
|
||||||
@@ -62,8 +66,121 @@
|
|||||||
src = ./.;
|
src = ./.;
|
||||||
cargoLock.lockFile = ./Cargo.lock;
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
|
|
||||||
nativeBuildInputs = [ pkgs.pkg-config ];
|
doCheck = false;
|
||||||
buildInputs = [ pkgs.openssl ];
|
};
|
||||||
|
|
||||||
|
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.0";
|
||||||
|
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'
|
||||||
|
'');
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
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 {}
|
||||||
175
scripts/release.sh
Executable file
175
scripts/release.sh
Executable file
@@ -0,0 +1,175 @@
|
|||||||
|
#!/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 [version]
|
||||||
|
#
|
||||||
|
# Environment:
|
||||||
|
# GITEA_TOKEN - API token for git.qrty.ink (prompted if not set)
|
||||||
|
# GITEA_URL - Gitea instance URL (default: https://git.qrty.ink)
|
||||||
|
|
||||||
|
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 ---
|
||||||
|
VERSION="${1:-}"
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||||
|
info "Version from Cargo.toml: v$VERSION"
|
||||||
|
fi
|
||||||
|
# Ensure v prefix
|
||||||
|
[[ "$VERSION" == v* ]] || VERSION="v$VERSION"
|
||||||
|
TAG="$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. Bump version in Cargo.toml or choose a different version."
|
||||||
|
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"
|
||||||
@@ -499,6 +499,12 @@ pub enum Expr {
|
|||||||
field: Ident,
|
field: Ident,
|
||||||
span: Span,
|
span: Span,
|
||||||
},
|
},
|
||||||
|
/// Tuple index access: tuple.0, tuple.1
|
||||||
|
TupleIndex {
|
||||||
|
object: Box<Expr>,
|
||||||
|
index: usize,
|
||||||
|
span: Span,
|
||||||
|
},
|
||||||
/// Lambda: fn(x, y) => x + y or fn(x: Int): Int => x + 1
|
/// Lambda: fn(x, y) => x + y or fn(x: Int): Int => x + 1
|
||||||
Lambda {
|
Lambda {
|
||||||
params: Vec<Parameter>,
|
params: Vec<Parameter>,
|
||||||
@@ -563,6 +569,7 @@ impl Expr {
|
|||||||
Expr::Call { span, .. } => *span,
|
Expr::Call { span, .. } => *span,
|
||||||
Expr::EffectOp { span, .. } => *span,
|
Expr::EffectOp { span, .. } => *span,
|
||||||
Expr::Field { span, .. } => *span,
|
Expr::Field { span, .. } => *span,
|
||||||
|
Expr::TupleIndex { span, .. } => *span,
|
||||||
Expr::Lambda { span, .. } => *span,
|
Expr::Lambda { span, .. } => *span,
|
||||||
Expr::Let { span, .. } => *span,
|
Expr::Let { span, .. } => *span,
|
||||||
Expr::If { span, .. } => *span,
|
Expr::If { span, .. } => *span,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1268,6 +1268,11 @@ impl JsBackend {
|
|||||||
Ok(format!("{}.{}", obj, field.name))
|
Ok(format!("{}.{}", obj, field.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Expr::TupleIndex { object, index, .. } => {
|
||||||
|
let obj = self.emit_expr(object)?;
|
||||||
|
Ok(format!("{}[{}]", obj, index))
|
||||||
|
}
|
||||||
|
|
||||||
Expr::Run {
|
Expr::Run {
|
||||||
expr, handlers, ..
|
expr, handlers, ..
|
||||||
} => {
|
} => {
|
||||||
|
|||||||
@@ -1,19 +1,254 @@
|
|||||||
//! Elm-style diagnostic messages for beautiful error reporting
|
//! Elm-style diagnostic messages for beautiful error reporting
|
||||||
|
//!
|
||||||
|
//! This module provides Elm-quality error messages with:
|
||||||
|
//! - Error codes (E0001, E0002, etc.) for easy searchability
|
||||||
|
//! - Visual type diffs showing expected vs actual
|
||||||
|
//! - "Did you mean?" suggestions using Levenshtein distance
|
||||||
|
//! - Context-aware hints based on error type
|
||||||
|
//! - Colored output with syntax highlighting
|
||||||
|
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use crate::ast::Span;
|
use crate::ast::Span;
|
||||||
|
|
||||||
|
/// Error codes for all Lux compiler errors
|
||||||
|
/// These follow the pattern E{category}{number}:
|
||||||
|
/// - E01xx: Parse errors
|
||||||
|
/// - E02xx: Type errors
|
||||||
|
/// - E03xx: Name resolution errors
|
||||||
|
/// - E04xx: Effect errors
|
||||||
|
/// - E05xx: Pattern matching errors
|
||||||
|
/// - E06xx: Import/module errors
|
||||||
|
/// - E07xx: Behavioral type errors
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ErrorCode {
|
||||||
|
// Parse errors (E01xx)
|
||||||
|
E0100, // Generic parse error
|
||||||
|
E0101, // Unexpected token
|
||||||
|
E0102, // Unclosed delimiter
|
||||||
|
E0103, // Invalid literal
|
||||||
|
E0104, // Missing semicolon or delimiter
|
||||||
|
E0105, // Invalid operator
|
||||||
|
|
||||||
|
// Type errors (E02xx)
|
||||||
|
E0200, // Generic type error
|
||||||
|
E0201, // Type mismatch
|
||||||
|
E0202, // Cannot unify types
|
||||||
|
E0203, // Missing type annotation
|
||||||
|
E0204, // Invalid type application
|
||||||
|
E0205, // Recursive type without indirection
|
||||||
|
E0206, // Field type mismatch
|
||||||
|
E0207, // Missing record field
|
||||||
|
E0208, // Extra record field
|
||||||
|
E0209, // Wrong number of type arguments
|
||||||
|
E0210, // Type not found
|
||||||
|
E0211, // Return type mismatch
|
||||||
|
|
||||||
|
// Name resolution errors (E03xx)
|
||||||
|
E0300, // Generic name error
|
||||||
|
E0301, // Undefined variable
|
||||||
|
E0302, // Undefined function
|
||||||
|
E0303, // Undefined type
|
||||||
|
E0304, // Undefined module
|
||||||
|
E0305, // Duplicate definition
|
||||||
|
E0306, // Shadowing warning
|
||||||
|
E0307, // Unused variable warning
|
||||||
|
E0308, // Undefined field
|
||||||
|
|
||||||
|
// Effect errors (E04xx)
|
||||||
|
E0400, // Generic effect error
|
||||||
|
E0401, // Unhandled effect
|
||||||
|
E0402, // Effect not in scope
|
||||||
|
E0403, // Missing effect handler
|
||||||
|
E0404, // Invalid effect operation
|
||||||
|
E0405, // Effect mismatch
|
||||||
|
|
||||||
|
// Pattern matching errors (E05xx)
|
||||||
|
E0500, // Generic pattern error
|
||||||
|
E0501, // Non-exhaustive patterns
|
||||||
|
E0502, // Unreachable pattern
|
||||||
|
E0503, // Invalid pattern
|
||||||
|
E0504, // Duplicate pattern binding
|
||||||
|
|
||||||
|
// Import/module errors (E06xx)
|
||||||
|
E0600, // Generic module error
|
||||||
|
E0601, // Module not found
|
||||||
|
E0602, // Circular import
|
||||||
|
E0603, // Export not found
|
||||||
|
E0604, // Private item accessed
|
||||||
|
|
||||||
|
// Behavioral type errors (E07xx)
|
||||||
|
E0700, // Generic behavioral error
|
||||||
|
E0701, // Purity violation
|
||||||
|
E0702, // Totality violation
|
||||||
|
E0703, // Idempotency violation
|
||||||
|
E0704, // Commutativity violation
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorCode {
|
||||||
|
pub fn code(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
// Parse errors
|
||||||
|
ErrorCode::E0100 => "E0100",
|
||||||
|
ErrorCode::E0101 => "E0101",
|
||||||
|
ErrorCode::E0102 => "E0102",
|
||||||
|
ErrorCode::E0103 => "E0103",
|
||||||
|
ErrorCode::E0104 => "E0104",
|
||||||
|
ErrorCode::E0105 => "E0105",
|
||||||
|
// Type errors
|
||||||
|
ErrorCode::E0200 => "E0200",
|
||||||
|
ErrorCode::E0201 => "E0201",
|
||||||
|
ErrorCode::E0202 => "E0202",
|
||||||
|
ErrorCode::E0203 => "E0203",
|
||||||
|
ErrorCode::E0204 => "E0204",
|
||||||
|
ErrorCode::E0205 => "E0205",
|
||||||
|
ErrorCode::E0206 => "E0206",
|
||||||
|
ErrorCode::E0207 => "E0207",
|
||||||
|
ErrorCode::E0208 => "E0208",
|
||||||
|
ErrorCode::E0209 => "E0209",
|
||||||
|
ErrorCode::E0210 => "E0210",
|
||||||
|
ErrorCode::E0211 => "E0211",
|
||||||
|
// Name errors
|
||||||
|
ErrorCode::E0300 => "E0300",
|
||||||
|
ErrorCode::E0301 => "E0301",
|
||||||
|
ErrorCode::E0302 => "E0302",
|
||||||
|
ErrorCode::E0303 => "E0303",
|
||||||
|
ErrorCode::E0304 => "E0304",
|
||||||
|
ErrorCode::E0305 => "E0305",
|
||||||
|
ErrorCode::E0306 => "E0306",
|
||||||
|
ErrorCode::E0307 => "E0307",
|
||||||
|
ErrorCode::E0308 => "E0308",
|
||||||
|
// Effect errors
|
||||||
|
ErrorCode::E0400 => "E0400",
|
||||||
|
ErrorCode::E0401 => "E0401",
|
||||||
|
ErrorCode::E0402 => "E0402",
|
||||||
|
ErrorCode::E0403 => "E0403",
|
||||||
|
ErrorCode::E0404 => "E0404",
|
||||||
|
ErrorCode::E0405 => "E0405",
|
||||||
|
// Pattern errors
|
||||||
|
ErrorCode::E0500 => "E0500",
|
||||||
|
ErrorCode::E0501 => "E0501",
|
||||||
|
ErrorCode::E0502 => "E0502",
|
||||||
|
ErrorCode::E0503 => "E0503",
|
||||||
|
ErrorCode::E0504 => "E0504",
|
||||||
|
// Module errors
|
||||||
|
ErrorCode::E0600 => "E0600",
|
||||||
|
ErrorCode::E0601 => "E0601",
|
||||||
|
ErrorCode::E0602 => "E0602",
|
||||||
|
ErrorCode::E0603 => "E0603",
|
||||||
|
ErrorCode::E0604 => "E0604",
|
||||||
|
// Behavioral errors
|
||||||
|
ErrorCode::E0700 => "E0700",
|
||||||
|
ErrorCode::E0701 => "E0701",
|
||||||
|
ErrorCode::E0702 => "E0702",
|
||||||
|
ErrorCode::E0703 => "E0703",
|
||||||
|
ErrorCode::E0704 => "E0704",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
// Parse errors
|
||||||
|
ErrorCode::E0100 => "Parse Error",
|
||||||
|
ErrorCode::E0101 => "Unexpected Token",
|
||||||
|
ErrorCode::E0102 => "Unclosed Delimiter",
|
||||||
|
ErrorCode::E0103 => "Invalid Literal",
|
||||||
|
ErrorCode::E0104 => "Missing Delimiter",
|
||||||
|
ErrorCode::E0105 => "Invalid Operator",
|
||||||
|
// Type errors
|
||||||
|
ErrorCode::E0200 => "Type Error",
|
||||||
|
ErrorCode::E0201 => "Type Mismatch",
|
||||||
|
ErrorCode::E0202 => "Cannot Unify Types",
|
||||||
|
ErrorCode::E0203 => "Missing Type Annotation",
|
||||||
|
ErrorCode::E0204 => "Invalid Type Application",
|
||||||
|
ErrorCode::E0205 => "Recursive Type Error",
|
||||||
|
ErrorCode::E0206 => "Field Type Mismatch",
|
||||||
|
ErrorCode::E0207 => "Missing Record Field",
|
||||||
|
ErrorCode::E0208 => "Extra Record Field",
|
||||||
|
ErrorCode::E0209 => "Wrong Type Arity",
|
||||||
|
ErrorCode::E0210 => "Type Not Found",
|
||||||
|
ErrorCode::E0211 => "Return Type Mismatch",
|
||||||
|
// Name errors
|
||||||
|
ErrorCode::E0300 => "Name Error",
|
||||||
|
ErrorCode::E0301 => "Undefined Variable",
|
||||||
|
ErrorCode::E0302 => "Undefined Function",
|
||||||
|
ErrorCode::E0303 => "Undefined Type",
|
||||||
|
ErrorCode::E0304 => "Undefined Module",
|
||||||
|
ErrorCode::E0305 => "Duplicate Definition",
|
||||||
|
ErrorCode::E0306 => "Variable Shadowing",
|
||||||
|
ErrorCode::E0307 => "Unused Variable",
|
||||||
|
ErrorCode::E0308 => "Undefined Field",
|
||||||
|
// Effect errors
|
||||||
|
ErrorCode::E0400 => "Effect Error",
|
||||||
|
ErrorCode::E0401 => "Unhandled Effect",
|
||||||
|
ErrorCode::E0402 => "Effect Not In Scope",
|
||||||
|
ErrorCode::E0403 => "Missing Effect Handler",
|
||||||
|
ErrorCode::E0404 => "Invalid Effect Operation",
|
||||||
|
ErrorCode::E0405 => "Effect Mismatch",
|
||||||
|
// Pattern errors
|
||||||
|
ErrorCode::E0500 => "Pattern Error",
|
||||||
|
ErrorCode::E0501 => "Non-Exhaustive Patterns",
|
||||||
|
ErrorCode::E0502 => "Unreachable Pattern",
|
||||||
|
ErrorCode::E0503 => "Invalid Pattern",
|
||||||
|
ErrorCode::E0504 => "Duplicate Pattern Binding",
|
||||||
|
// Module errors
|
||||||
|
ErrorCode::E0600 => "Module Error",
|
||||||
|
ErrorCode::E0601 => "Module Not Found",
|
||||||
|
ErrorCode::E0602 => "Circular Import",
|
||||||
|
ErrorCode::E0603 => "Export Not Found",
|
||||||
|
ErrorCode::E0604 => "Private Item Access",
|
||||||
|
// Behavioral errors
|
||||||
|
ErrorCode::E0700 => "Behavioral Type Error",
|
||||||
|
ErrorCode::E0701 => "Purity Violation",
|
||||||
|
ErrorCode::E0702 => "Totality Violation",
|
||||||
|
ErrorCode::E0703 => "Idempotency Violation",
|
||||||
|
ErrorCode::E0704 => "Commutativity Violation",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a URL to documentation about this error
|
||||||
|
pub fn help_url(&self) -> String {
|
||||||
|
format!("https://lux-lang.dev/errors/{}", self.code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ErrorCode {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// ANSI color codes for terminal output
|
/// ANSI color codes for terminal output
|
||||||
pub mod colors {
|
pub mod colors {
|
||||||
pub const RESET: &str = "\x1b[0m";
|
pub const RESET: &str = "\x1b[0m";
|
||||||
pub const BOLD: &str = "\x1b[1m";
|
pub const BOLD: &str = "\x1b[1m";
|
||||||
pub const DIM: &str = "\x1b[2m";
|
pub const DIM: &str = "\x1b[2m";
|
||||||
pub const RED: &str = "\x1b[31m";
|
pub const RED: &str = "\x1b[31m";
|
||||||
|
pub const GREEN: &str = "\x1b[32m";
|
||||||
pub const YELLOW: &str = "\x1b[33m";
|
pub const YELLOW: &str = "\x1b[33m";
|
||||||
pub const BLUE: &str = "\x1b[34m";
|
pub const BLUE: &str = "\x1b[34m";
|
||||||
|
pub const MAGENTA: &str = "\x1b[35m";
|
||||||
pub const CYAN: &str = "\x1b[36m";
|
pub const CYAN: &str = "\x1b[36m";
|
||||||
pub const WHITE: &str = "\x1b[37m";
|
pub const WHITE: &str = "\x1b[37m";
|
||||||
|
pub const GRAY: &str = "\x1b[90m";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply color to text, respecting NO_COLOR / TERM=dumb
|
||||||
|
pub fn c(color: &str, text: &str) -> String {
|
||||||
|
if supports_color() {
|
||||||
|
format!("{}{}{}", color, text, colors::RESET)
|
||||||
|
} else {
|
||||||
|
text.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply bold + color to text
|
||||||
|
pub fn bc(color: &str, text: &str) -> String {
|
||||||
|
if supports_color() {
|
||||||
|
format!("{}{}{}{}", colors::BOLD, color, text, colors::RESET)
|
||||||
|
} else {
|
||||||
|
text.to_string()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Severity level for diagnostics
|
/// Severity level for diagnostics
|
||||||
@@ -46,30 +281,57 @@ impl Severity {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Diagnostic {
|
pub struct Diagnostic {
|
||||||
pub severity: Severity,
|
pub severity: Severity,
|
||||||
|
pub code: Option<ErrorCode>,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
pub hints: Vec<String>,
|
pub hints: Vec<String>,
|
||||||
|
pub expected_type: Option<String>,
|
||||||
|
pub actual_type: Option<String>,
|
||||||
|
pub secondary_spans: Vec<(Span, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Diagnostic {
|
impl Diagnostic {
|
||||||
pub fn error(title: impl Into<String>, message: impl Into<String>, span: Span) -> Self {
|
pub fn error(title: impl Into<String>, message: impl Into<String>, span: Span) -> Self {
|
||||||
Self {
|
Self {
|
||||||
severity: Severity::Error,
|
severity: Severity::Error,
|
||||||
|
code: None,
|
||||||
title: title.into(),
|
title: title.into(),
|
||||||
message: message.into(),
|
message: message.into(),
|
||||||
span,
|
span,
|
||||||
hints: Vec::new(),
|
hints: Vec::new(),
|
||||||
|
expected_type: None,
|
||||||
|
actual_type: None,
|
||||||
|
secondary_spans: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn warning(title: impl Into<String>, message: impl Into<String>, span: Span) -> Self {
|
pub fn warning(title: impl Into<String>, message: impl Into<String>, span: Span) -> Self {
|
||||||
Self {
|
Self {
|
||||||
severity: Severity::Warning,
|
severity: Severity::Warning,
|
||||||
|
code: None,
|
||||||
title: title.into(),
|
title: title.into(),
|
||||||
message: message.into(),
|
message: message.into(),
|
||||||
span,
|
span,
|
||||||
hints: Vec::new(),
|
hints: Vec::new(),
|
||||||
|
expected_type: None,
|
||||||
|
actual_type: None,
|
||||||
|
secondary_spans: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an error with a specific error code
|
||||||
|
pub fn with_code(code: ErrorCode, message: impl Into<String>, span: Span) -> Self {
|
||||||
|
Self {
|
||||||
|
severity: Severity::Error,
|
||||||
|
code: Some(code),
|
||||||
|
title: code.title().to_string(),
|
||||||
|
message: message.into(),
|
||||||
|
span,
|
||||||
|
hints: Vec::new(),
|
||||||
|
expected_type: None,
|
||||||
|
actual_type: None,
|
||||||
|
secondary_spans: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +344,111 @@ impl Diagnostic {
|
|||||||
self.hints.extend(hints);
|
self.hints.extend(hints);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add expected and actual types for type mismatch visualization
|
||||||
|
pub fn with_types(mut self, expected: impl Into<String>, actual: impl Into<String>) -> Self {
|
||||||
|
self.expected_type = Some(expected.into());
|
||||||
|
self.actual_type = Some(actual.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a secondary span with a label (e.g., "defined here")
|
||||||
|
pub fn with_secondary(mut self, span: Span, label: impl Into<String>) -> Self {
|
||||||
|
self.secondary_spans.push((span, label.into()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the error code
|
||||||
|
pub fn code(mut self, code: ErrorCode) -> Self {
|
||||||
|
self.code = Some(code);
|
||||||
|
self.title = code.title().to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a visual type diff between expected and actual types
|
||||||
|
/// Highlights the differences in red
|
||||||
|
pub fn format_type_diff(expected: &str, actual: &str) -> String {
|
||||||
|
let mut output = String::new();
|
||||||
|
|
||||||
|
output.push_str(&format!(
|
||||||
|
"\n {}{}Expected:{} {}{}{}\n",
|
||||||
|
colors::BOLD,
|
||||||
|
colors::CYAN,
|
||||||
|
colors::RESET,
|
||||||
|
colors::BOLD,
|
||||||
|
expected,
|
||||||
|
colors::RESET
|
||||||
|
));
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {}{}Found: {} {}{}{}\n",
|
||||||
|
colors::BOLD,
|
||||||
|
colors::RED,
|
||||||
|
colors::RESET,
|
||||||
|
colors::BOLD,
|
||||||
|
actual,
|
||||||
|
colors::RESET
|
||||||
|
));
|
||||||
|
|
||||||
|
// If types are structurally similar, show where they differ
|
||||||
|
if let Some(diff) = compute_type_diff(expected, actual) {
|
||||||
|
output.push_str(&format!("\n {}\n", diff));
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute a simple diff between two type strings
|
||||||
|
fn compute_type_diff(expected: &str, actual: &str) -> Option<String> {
|
||||||
|
// Simple case: find the first difference
|
||||||
|
let exp_chars: Vec<char> = expected.chars().collect();
|
||||||
|
let act_chars: Vec<char> = actual.chars().collect();
|
||||||
|
|
||||||
|
let mut diff_start = None;
|
||||||
|
|
||||||
|
for (i, (e, a)) in exp_chars.iter().zip(act_chars.iter()).enumerate() {
|
||||||
|
if e != a && diff_start.is_none() {
|
||||||
|
diff_start = Some(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the types are identical, no diff needed
|
||||||
|
if diff_start.is_none() && exp_chars.len() == act_chars.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the difference location with an arrow
|
||||||
|
if let Some(start) = diff_start {
|
||||||
|
let pointer = format!(
|
||||||
|
"{}{}^-- difference starts here{}",
|
||||||
|
" ".repeat(start + 9), // Account for "Found: " prefix
|
||||||
|
colors::YELLOW,
|
||||||
|
colors::RESET
|
||||||
|
);
|
||||||
|
return Some(pointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length difference
|
||||||
|
if exp_chars.len() != act_chars.len() {
|
||||||
|
return Some(format!(
|
||||||
|
"{}Note:{} Types have different lengths ({} vs {} characters)",
|
||||||
|
colors::YELLOW,
|
||||||
|
colors::RESET,
|
||||||
|
exp_chars.len(),
|
||||||
|
act_chars.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a plain type diff (no colors)
|
||||||
|
pub fn format_type_diff_plain(expected: &str, actual: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"\n Expected: {}\n Found: {}\n",
|
||||||
|
expected, actual
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate the Levenshtein edit distance between two strings
|
/// Calculate the Levenshtein edit distance between two strings
|
||||||
@@ -211,18 +578,23 @@ pub fn render_diagnostic(
|
|||||||
let severity_color = diagnostic.severity.color();
|
let severity_color = diagnostic.severity.color();
|
||||||
let severity_label = diagnostic.severity.label();
|
let severity_label = diagnostic.severity.label();
|
||||||
|
|
||||||
// Header: -- ERROR ----------- filename.lux
|
// Header with error code: ── ERROR[E0201] ── filename.lux
|
||||||
let filename_str = filename.unwrap_or("<input>");
|
let filename_str = filename.unwrap_or("<input>");
|
||||||
|
let code_str = diagnostic
|
||||||
|
.code
|
||||||
|
.map(|c| format!("[{}]", c.code()))
|
||||||
|
.unwrap_or_default();
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"{}{}{} ── {} ──────────────────────────────────\n",
|
"{}{}── {}{} ──────────────────── {}{}\n",
|
||||||
colors::BOLD,
|
colors::BOLD,
|
||||||
severity_color,
|
severity_color,
|
||||||
severity_label,
|
severity_label,
|
||||||
filename_str
|
code_str,
|
||||||
|
filename_str,
|
||||||
|
colors::RESET
|
||||||
));
|
));
|
||||||
output.push_str(colors::RESET);
|
|
||||||
|
|
||||||
// Title
|
// Location
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"\n{}{}{}:{}{}\n\n",
|
"\n{}{}{}:{}{}\n\n",
|
||||||
colors::BOLD,
|
colors::BOLD,
|
||||||
@@ -309,6 +681,11 @@ pub fn render_diagnostic(
|
|||||||
// Error message
|
// Error message
|
||||||
output.push_str(&format!("\n{}\n", diagnostic.message));
|
output.push_str(&format!("\n{}\n", diagnostic.message));
|
||||||
|
|
||||||
|
// Type diff visualization (if present)
|
||||||
|
if let (Some(expected), Some(actual)) = (&diagnostic.expected_type, &diagnostic.actual_type) {
|
||||||
|
output.push_str(&format_type_diff(expected, actual));
|
||||||
|
}
|
||||||
|
|
||||||
// Hints
|
// Hints
|
||||||
if !diagnostic.hints.is_empty() {
|
if !diagnostic.hints.is_empty() {
|
||||||
output.push('\n');
|
output.push('\n');
|
||||||
@@ -323,6 +700,17 @@ pub fn render_diagnostic(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Help URL for error codes
|
||||||
|
if let Some(code) = diagnostic.code {
|
||||||
|
output.push_str(&format!(
|
||||||
|
"\n{}{}Learn more:{} {}\n",
|
||||||
|
colors::DIM,
|
||||||
|
colors::BLUE,
|
||||||
|
colors::RESET,
|
||||||
|
code.help_url()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
output.push('\n');
|
output.push('\n');
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
@@ -338,9 +726,14 @@ pub fn render_diagnostic_plain(
|
|||||||
let (end_line, end_col) = offset_to_line_col(source, diagnostic.span.end);
|
let (end_line, end_col) = offset_to_line_col(source, diagnostic.span.end);
|
||||||
|
|
||||||
let filename_str = filename.unwrap_or("<input>");
|
let filename_str = filename.unwrap_or("<input>");
|
||||||
|
let code_str = diagnostic
|
||||||
|
.code
|
||||||
|
.map(|c| format!("[{}]", c.code()))
|
||||||
|
.unwrap_or_default();
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"-- {} ── {} ──────────────────────────────────\n",
|
"-- {}{} ── {} ──────────────────────────────────\n",
|
||||||
diagnostic.severity.label(),
|
diagnostic.severity.label(),
|
||||||
|
code_str,
|
||||||
filename_str
|
filename_str
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -381,6 +774,11 @@ pub fn render_diagnostic_plain(
|
|||||||
|
|
||||||
output.push_str(&format!("\n{}\n", diagnostic.message));
|
output.push_str(&format!("\n{}\n", diagnostic.message));
|
||||||
|
|
||||||
|
// Type diff visualization (plain)
|
||||||
|
if let (Some(expected), Some(actual)) = (&diagnostic.expected_type, &diagnostic.actual_type) {
|
||||||
|
output.push_str(&format_type_diff_plain(expected, actual));
|
||||||
|
}
|
||||||
|
|
||||||
if !diagnostic.hints.is_empty() {
|
if !diagnostic.hints.is_empty() {
|
||||||
output.push('\n');
|
output.push('\n');
|
||||||
for hint in &diagnostic.hints {
|
for hint in &diagnostic.hints {
|
||||||
@@ -388,6 +786,11 @@ pub fn render_diagnostic_plain(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Help URL for error codes
|
||||||
|
if let Some(code) = diagnostic.code {
|
||||||
|
output.push_str(&format!("\nLearn more: {}\n", code.help_url()));
|
||||||
|
}
|
||||||
|
|
||||||
output.push('\n');
|
output.push('\n');
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
@@ -478,25 +881,28 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_type_error_categorization() {
|
fn test_type_error_categorization() {
|
||||||
use super::Diagnostic;
|
use super::{Diagnostic, ErrorCode};
|
||||||
|
|
||||||
// Test that errors are properly categorized
|
// Test that errors are properly categorized
|
||||||
let source = "let x: Int = \"hello\"";
|
let source = "let x: Int = \"hello\"";
|
||||||
|
|
||||||
// Simulate a type mismatch diagnostic
|
// Simulate a type mismatch diagnostic with error code
|
||||||
let diag = Diagnostic {
|
let diag = Diagnostic::with_code(
|
||||||
severity: Severity::Error,
|
ErrorCode::E0201,
|
||||||
title: "Type Mismatch".to_string(),
|
"Type mismatch: expected Int, got String",
|
||||||
message: "Type mismatch: expected Int, got String".to_string(),
|
Span { start: 13, end: 20 },
|
||||||
span: Span { start: 13, end: 20 },
|
)
|
||||||
hints: vec!["Check that the types on both sides of the expression are compatible.".to_string()],
|
.with_types("Int", "String")
|
||||||
};
|
.with_hint("Check that the types on both sides of the expression are compatible.");
|
||||||
|
|
||||||
let output = render_diagnostic_plain(&diag, source, Some("test.lux"));
|
let output = render_diagnostic_plain(&diag, source, Some("test.lux"));
|
||||||
|
|
||||||
assert!(output.contains("Type Mismatch"));
|
assert!(output.contains("Type Mismatch"));
|
||||||
assert!(output.contains("\"hello\""));
|
assert!(output.contains("\"hello\""));
|
||||||
assert!(output.contains("Hint:"));
|
assert!(output.contains("Hint:"));
|
||||||
|
assert!(output.contains("[E0201]"));
|
||||||
|
assert!(output.contains("Expected: Int"));
|
||||||
|
assert!(output.contains("Found: String"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -589,4 +995,60 @@ mod tests {
|
|||||||
let hint = super::format_did_you_mean(&["a".to_string(), "b".to_string(), "c".to_string()]);
|
let hint = super::format_did_you_mean(&["a".to_string(), "b".to_string(), "c".to_string()]);
|
||||||
assert_eq!(hint, Some("Did you mean 'a', 'b', or 'c'?".to_string()));
|
assert_eq!(hint, Some("Did you mean 'a', 'b', or 'c'?".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_error_code_display() {
|
||||||
|
assert_eq!(super::ErrorCode::E0201.code(), "E0201");
|
||||||
|
assert_eq!(super::ErrorCode::E0201.title(), "Type Mismatch");
|
||||||
|
assert!(super::ErrorCode::E0201.help_url().contains("E0201"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_diagnostic_with_code() {
|
||||||
|
let diag = super::Diagnostic::with_code(
|
||||||
|
super::ErrorCode::E0301,
|
||||||
|
"Variable 'x' not found",
|
||||||
|
Span { start: 0, end: 1 },
|
||||||
|
);
|
||||||
|
assert_eq!(diag.code, Some(super::ErrorCode::E0301));
|
||||||
|
assert_eq!(diag.title, "Undefined Variable");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_diagnostic_with_types() {
|
||||||
|
let diag = super::Diagnostic::error("Test", "Msg", Span { start: 0, end: 1 })
|
||||||
|
.with_types("Int", "String");
|
||||||
|
assert_eq!(diag.expected_type, Some("Int".to_string()));
|
||||||
|
assert_eq!(diag.actual_type, Some("String".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_type_diff_plain() {
|
||||||
|
let diff = super::format_type_diff_plain("Int", "String");
|
||||||
|
assert!(diff.contains("Expected: Int"));
|
||||||
|
assert!(diff.contains("Found: String"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_diagnostic_render_with_all_features() {
|
||||||
|
let source = "let x: Int = \"hello\"";
|
||||||
|
let diag = super::Diagnostic::with_code(
|
||||||
|
super::ErrorCode::E0201,
|
||||||
|
"This value should be an Int but is a String",
|
||||||
|
Span { start: 13, end: 20 },
|
||||||
|
)
|
||||||
|
.with_types("Int", "String")
|
||||||
|
.with_hint("Try using String.parseInt to convert the string to an integer");
|
||||||
|
|
||||||
|
let output = super::render_diagnostic_plain(&diag, source, Some("test.lux"));
|
||||||
|
|
||||||
|
// Check all features are present
|
||||||
|
assert!(output.contains("[E0201]"));
|
||||||
|
assert!(output.contains("Type Mismatch"));
|
||||||
|
assert!(output.contains("Expected: Int"));
|
||||||
|
assert!(output.contains("Found: String"));
|
||||||
|
assert!(output.contains("Hint:"));
|
||||||
|
assert!(output.contains("parseInt"));
|
||||||
|
assert!(output.contains("lux-lang.dev/errors/E0201"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user