49 Commits

Author SHA1 Message Date
52dcc88051 chore: bump version to 0.1.5 2026-02-19 03:47:28 -05:00
1842b668e5 chore: sync Cargo.lock with version 0.1.4
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 03:47:11 -05:00
c67e3f31c3 feat: add and/or keywords, handle alias, --watch flag, JS tree-shaking
- WISH-008: `and`/`or` as aliases for `&&`/`||` boolean operators
- WISH-006: `handle` as alias for `run ... with` (same AST output)
- WISH-005: `--watch` flag for `lux compile` recompiles on file change
- WISH-009: Tree-shake unused runtime sections from JS output based on
  which effects are actually used (Console, Random, Time, Http, Dom)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 03:35:47 -05:00
b0ccde749c chore: bump version to 0.1.4 2026-02-19 02:48:56 -05:00
4ba7a23ae3 feat: add comprehensive compilation checks to validate.sh
Adds interpreter, JS compilation, and C compilation checks for all
examples, showcase programs, standard examples, and projects (113 total
checks). Skip lists exclude programs requiring unsupported effects or
interactive I/O.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 02:43:46 -05:00
89741b4a32 fix: move top-level let initialization into main() in C backend
Top-level let bindings with function calls (e.g., `let result = factorial(10)`)
were emitted as static initializers, which is invalid C since function calls
aren't compile-time constants. Now globals are declared with zero-init and
initialized inside main() before any run expressions execute.

Also fixes validate.sh to use exit codes instead of grep for cargo check/build.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 02:31:49 -05:00
3a2376cd49 feat: port AST definitions to Lux (self-hosting)
Translate all 30+ type definitions from src/ast.rs (727 lines Rust)
into Lux ADTs in projects/lux-compiler/ast.lux.

Types ported: Span, Ident, Visibility, Version, VersionConstraint,
BehavioralProperty, WhereClause, ModulePath, ImportDecl, Program,
Declaration, FunctionDecl, Parameter, EffectDecl, EffectOp, TypeDecl,
TypeDef, RecordField, Variant, VariantFields, Migration, HandlerDecl,
HandlerImpl, LetDecl, TraitDecl, TraitMethod, TraitBound, ImplDecl,
TraitConstraint, ImplMethod, TypeExpr, Expr (19 variants), Literal,
LiteralKind, BinaryOp, UnaryOp, Statement, MatchArm, Pattern.

Passes `lux check` and `lux run`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 02:07:30 -05:00
4dfb04a1b6 chore: sync Cargo.lock with version 0.1.3
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 02:05:51 -05:00
3cdde02eb2 feat: add Int.toFloat/Float.toInt JS backend support and fix Map C codegen
- JS backend: Add Int/Float module dispatch in both Call and EffectOp paths
  for toFloat, toInt, and toString operations
- C backend: Fix lux_strdup → lux_string_dup in Map module codegen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 02:05:40 -05:00
a5762d0397 feat: add built-in Map type with String keys
Add Map<String, V> as a first-class built-in type for key-value storage,
needed for self-hosting the compiler (parser/typechecker/interpreter all
rely heavily on hashmaps).

- types.rs: Type::Map(K,V) variant, all match arms (unify, apply, etc.)
- interpreter.rs: Value::Map, 12 BuiltinFn variants (new/set/get/contains/
  remove/keys/values/size/isEmpty/fromList/toList/merge), immutable semantics
- typechecker.rs: Map<K,V> resolution in resolve_type
- js_backend.rs: Map as JS Map with emit_map_operation()
- c_backend.rs: LuxMap struct (linear-scan), runtime fns, emit_map_operation()
- main.rs: 12 tests covering all Map operations
- validate.sh: now checks all projects/ directories too

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 01:45:13 -05:00
1132c621c6 fix: allow newlines before then in if/then/else expressions
The parser now skips newlines between the condition and `then` keyword,
enabling multiline if expressions like:
  if long_condition
    then expr1
    else expr2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 01:38:05 -05:00
a0fff1814e fix: JS backend scoping for let/match/if inside closures
Three related bugs fixed:
- BUG-009: let bindings inside lambdas hoisted to top-level
- BUG-011: match expressions inside lambdas hoisted to top-level
- BUG-012: variable name deduplication leaked across function scopes

Root cause: emit_expr() uses writeln() for statements, but lambdas
captured only the return value, not the emitted statements. Also,
var_substitutions from emit_function() leaked to subsequent code.

Fix: Lambda handler now captures all output emitted during body
evaluation and places it inside the function body. Both emit_function
and Lambda save/restore var_substitutions to prevent cross-scope leaks.
Lambda params are registered as identity substitutions to override any
outer bindings with the same name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 01:10:55 -05:00
4e9e823246 fix: record spread works with named type aliases
Resolve type aliases (e.g. Player -> { pos: Vec2, speed: Float })
before checking if spread expression is a record type. Previously
{ ...p, field: val } failed with "must be a record type, got Player"
when the variable had a named type annotation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 00:01:20 -05:00
6a2e4a7ac1 chore: bump version to 0.1.3 2026-02-18 23:06:10 -05:00
3d706cb32b feat: add record spread syntax { ...base, field: val }
Adds spread operator for records, allowing concise record updates:
  let p2 = { ...p, x: 5.0 }

Changes across the full pipeline:
- Lexer: new DotDotDot (...) token
- AST: optional spread field on Record variant
- Parser: detect ... at start of record expression
- Typechecker: merge spread record fields with explicit overrides
- Interpreter: evaluate spread, overlay explicit fields
- JS backend: emit native JS spread syntax
- C backend: copy spread into temp, assign overrides
- Formatter, linter, LSP, symbol table: propagate spread

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 23:05:27 -05:00
7c3bfa9301 feat: add Math.sin, Math.cos, Math.atan2 trig functions
Adds trigonometric functions to the Math module across interpreter,
type system, and C backend. JS backend already supported them.
Also adds #include <math.h> to C preamble and handles Math module
calls through both Call and EffectOp paths in C backend.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 23:05:12 -05:00
b56c5461f1 fix: JS const _ duplication and hardcoded version string
- JS backend now emits wildcard let bindings as side-effect statements
  instead of const _ declarations, fixing SyntaxError on multiple let _ = ...
- Version string now uses env!("CARGO_PKG_VERSION") to auto-sync with Cargo.toml
- Add -lm linker flag for math library support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 23:05:03 -05:00
61e1469845 feat: add ++ concat operator and auto-invoke main
BUG-004: Add ++ operator for string and list concatenation across all
backends (interpreter, C, JS) with type checking and formatting support.

BUG-001: Auto-invoke top-level `let main = fn () => ...` when main is
a zero-parameter function, instead of just printing the function value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 22:01:41 -05:00
bb0a288210 chore: bump version to 0.1.2 2026-02-18 21:16:44 -05:00
5d7f4633e1 docs: add explicit commit instructions to CLAUDE.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 21:09:27 -05:00
d05b13d840 fix: JS backend compiles print() to console.log()
Bare `print()` calls in Lux now emit `console.log()` in JS output
instead of undefined `print()`. Fixes BUG-006.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 21:09:07 -05:00
0ee3050704 chore: bump version to 0.1.1 2026-02-18 20:41:43 -05:00
80b1276f9f fix: release script auto-bumps patch by default
Release script now supports: patch (default), minor, major, or explicit
version. Auto-updates Cargo.toml and flake.nix before building.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 20:41:29 -05:00
bd843d2219 fix: record type aliases now work for unification and field access
Expand type aliases via unify_with_env() everywhere in the type checker,
not just in a few places. This fixes named record types like
`type Vec2 = { x: Float, y: Float }` — they now properly unify with
anonymous records and support field access (v.x, v.y).

Also adds scripts/validate.sh for automated full-suite regression
testing (Rust tests + all 5 package test suites + type checking).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 20:21:29 -05:00
d76aa17b38 feat: static binary builds and automated release script
Switch reqwest from native-tls (openssl) to rustls-tls for a pure-Rust
TLS stack, enabling fully static musl builds. Add `nix build .#static`
for portable Linux binaries and `scripts/release.sh` for automated
Gitea releases with changelog generation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 19:09:32 -05:00
c23d9c7078 fix: test runner now supports module imports
The `lux test` command used Parser::parse_source() and
check_program() directly, which meant test files with `import`
statements would fail with type errors. Now uses ModuleLoader
and check_program_with_modules() to properly resolve imports,
and run_with_modules() for execution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 17:11:16 -05:00
fffacd2467 feat: C backend module import support, Int/Float.toString, Test.assertEqualMsg
The C backend can now compile programs that import user-defined modules.
Module-qualified calls like `mymodule.func(args)` are resolved to prefixed
C functions (e.g., `mymodule_func_lux`), with full support for transitive
imports and effect-passing. Also adds Int.toString/Float.toString to type
system, interpreter, and C backend, and Test.assertEqualMsg for labeled
test assertions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 16:35:24 -05:00
2ae2c132e5 docs: add language philosophy document and compiler integration
Write comprehensive PHILOSOPHY.md covering Lux's six core principles
(explicit over implicit, composition over configuration, safety without
ceremony, practical over academic, one right way, tools are the language)
with detailed comparisons against JS/TS, Python, Rust, Go, Java/C#,
Haskell/Elm, and Gleam/Elixir. Includes tooling audit and improvement
suggestions.

Add `lux philosophy` command to the compiler, update help screen with
abbreviated philosophy, and link from README.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:19:29 -05:00
4909ff9fff docs: add package ecosystem plan and error documentation workflow
Add PACKAGES.md analyzing the Lux package ecosystem gaps vs stdlib,
with prioritized implementation plans for markdown, xml, rss, frontmatter,
path, and sitemap packages. Add CLAUDE.md instructions for documenting
Lux language errors in ISSUES.md during every major task.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:01:56 -05:00
8e788c8a9f fix: embed C compiler path at build time for self-contained binary
build.rs captures the absolute path to cc/gcc/clang during compilation
and bakes it into the binary. On Nix systems this embeds the full
/nix/store path so `lux compile` works without cc on PATH.

Lookup order: $CC env var > embedded build-time path > PATH search.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 08:12:18 -05:00
dbdd3cca57 chore: move blu-site to its own repo at ~/src/blu-site
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 07:57:55 -05:00
3ac022c04a chore: gitignore build output (_site/, docs/)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 07:48:51 -05:00
6bedd37ac7 fix: show help menu when running lux with no arguments
Previously `lux` with no args entered the REPL. Now it shows the help
menu. Use `lux repl` to start the REPL explicitly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 07:34:09 -05:00
2909bf14b6 fix: eliminate all non-json C backend errors (79→0)
Second round of C backend fixes, building on d8871ac which reduced
errors from 286 to 111. This eliminates all 79 non-json errors:

- Fix function references as values (wrap in LuxClosure*)
- Fix fold/map/filter with type-aware calling conventions
- Add String.indexOf/lastIndexOf emission and C runtime functions
- Add File.readDir with dirent.h implementation
- Fix string concat in closure bodies
- Exclude ADT constructors from closure free variable capture
- Fix match result type inference (prioritize pattern binding types)
- Fix Option inner type inference (usage-based for List.head)
- Fix void* to struct cast (dereference through pointer)
- Handle constructors in emit_expr_with_env

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 05:56:21 -05:00
d8871acf7e fix: improve C backend robustness, reduce compilation errors by 61%
- Fix closure captured variable types: look up actual types from var_types
  instead of hardcoding LuxInt for all captured variables
- Register function parameters in var_types so closures can find their types
- Replace is_string_expr() with infer_expr_type() for more accurate string
  detection in binary ops (concat, comparison)
- Add missing String operations to infer_expr_type (substring, indexOf, etc.)
- Add module method call type inference (String.*, List.*, Int.*, Float.*)
- Add built-in Result type (Ok/Err) to C prelude alongside Option
- Register Ok/Err/Some/None in variant_to_type and variant_field_types
- Fix variable scoping: use if-statement pattern instead of ternary when
  branches emit statements (prevents redefinition of h2/h3 etc.)
- Add RC scope management for if-else branches and match arms to prevent
  undeclared variable errors from cleanup code
- Add infer_pattern_binding_type for better match result type inference
- Add expr_emits_statements helper to detect statement-emitting expressions
- Add infer_option_inner_type for String.indexOf (returns Option<Int>)

Reduces blu-site compilation errors from 286 to 111 (remaining are mostly
unsupported json effect and function-as-value references).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 17:56:27 -05:00
73b5eee664 docs: add commit-after-every-piece-of-work instruction to CLAUDE.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 16:21:54 -05:00
542255780d feat: add tuple index access, multiline args, and effect unification fix
- Tuple index: `pair.0`, `pair.1` syntax across parser, typechecker,
  interpreter, C/JS backends, formatter, linter, and symbol table
- Multi-line function args: allow newlines inside argument lists
- Fix effect unification for callback parameters (empty expected
  effects means "no constraint", not "must be pure")

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 16:21:48 -05:00
bac63bab2a feat: add blu-site static site generator and fix language issues
Build a complete static site generator in Lux that faithfully clones
blu.cx (elmstatic). Generates 14 post pages, section indexes, tag pages,
and a home page with snippets grid from markdown content.

Language fixes discovered during development:
- Add \{ and \} escape sequences in string literals (lexer)
- Register String.indexOf and String.lastIndexOf in type checker
- Fix formatter to preserve brace escapes in string literals
- Improve LSP hover to show documentation for let bindings and functions

ISSUES.md documents 15 Lux language limitations found during the project.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 15:43:05 -05:00
db82ca1a1c fix: improve LSP hover to show function info when cursor is on fn keyword
When hovering on declaration keywords (fn, type, effect, let, trait),
look ahead to find the declaration name and show that symbol's full
info from the symbol table instead of generic keyword documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 08:32:01 -05:00
98605d2b70 feat: add self-hosted Lux lexer as first step toward bootstrapping
The lexer tokenizes Lux source code written entirely in Lux itself.
Supports all token types: keywords, operators, literals, behavioral
properties, doc comments, and delimiters.

This is the first component of the Lux-in-Lux compiler, demonstrating
that Lux's pattern matching, recursion, and string handling are
sufficient for compiler construction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 08:25:22 -05:00
e3b6f4322a fix: add Char pattern matching and Char comparison operators
- Parser: support Char literals in match patterns (e.g., 'x' => ...)
- Interpreter: add Char comparison for <, <=, >, >= operators
  Previously only Int, Float, and String supported ordering comparisons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 08:25:15 -05:00
d26fd975d1 feat: enhance LSP with inlay hints, parameter hints, and improved hover
Add inlay type hints for let bindings, parameter name hints at call sites,
behavioral property documentation in hover, and long signature wrapping.

- Inlay hints: show inferred types for let bindings without annotations
- Parameter hints: show param names at call sites for multi-arg functions
- Hover: wrap long signatures, show behavioral property docs (pure, total, etc.)
- Rich docs: detailed hover for keywords like pure, total, idempotent, run, with
- TypeChecker: expose get_inferred_type() for LSP consumption
- Symbol table: include behavioral properties in function type signatures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 08:06:36 -05:00
1fa599f856 fix: support comma-separated behavioral properties without repeating 'is'
Allows `is pure, commutative` syntax in addition to `is pure is commutative`.
After the initial `is`, comma-separated properties no longer require repeating
the `is` keyword (though it's still accepted for compatibility).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 07:44:18 -05:00
c2404a5ec1 docs: update CLAUDE.md with post-work checklist and CLI aliases table
Adds the post-work checklist (cargo check, cargo test, lux check, lux fmt,
lux lint) and documents all CLI command aliases. Updates test count to 381.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 07:36:09 -05:00
19068ead96 feat: add lux lint command with Lux-specific static analysis
Implements a linter with 21 lint rules across 6 categories (correctness,
suspicious, idiom, performance, style, pedantic). Lux-specific lints include
could-be-pure, could-be-total, unnecessary-effect-decl, and single-arm-match.
Integrates lints into `lux check` for unified type+lint checking. Available
standalone via `lux lint` (alias: `lux l`) with --explain for detailed help.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 07:35:36 -05:00
44ea1eebb0 style: auto-format example files with lux fmt
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 06:52:44 -05:00
8c90d5a8dc feat: CLI UX overhaul with colored output, timing, shorthands, and fuzzy suggestions
Add polished CLI output across all commands: colored help text, green/red
pass/fail indicators (✓/✗), elapsed timing on compile/check/test/fmt,
command shorthands (c/t/f/s/k), fuzzy "did you mean?" on typos, and
smart port-in-use suggestions for serve. Respects NO_COLOR/TERM=dumb.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 06:52:36 -05:00
bc60f1c8f1 fix: improve error message for bare 'run' expressions at top level
When users write `run main() with {}` at top level instead of
`let _ = run main() with {}`, provide a helpful error message
explaining the correct syntax instead of the generic "Expected
declaration" error.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 23:40:15 -05:00
52e3876b81 feat: add projects showcase and Lux-powered static file server
- Add website/serve.lux: static file server using HttpServer effect
  - Demonstrates serving the Lux website with Lux itself
  - Handles index files, clean URLs, and 404 responses

- Add website/projects/index.html: example projects showcase
  - Features 6 real project cards (REST API, Todo App, JSON Parser, etc.)
  - Highlights Task Manager API demonstrating all 3 killer features
  - Links to full source code in the repository

- Update examples sidebar with Projects section

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 23:34:17 -05:00
85 changed files with 9054 additions and 2074 deletions

5
.gitignore vendored
View File

@@ -4,6 +4,11 @@
# Claude Code project instructions # Claude Code project instructions
CLAUDE.md CLAUDE.md
# Build output
_site/
docs/*.html
docs/*.css
# Test binaries # Test binaries
hello hello
test_rc test_rc

177
CLAUDE.md Normal file
View File

@@ -0,0 +1,177 @@
# Lux Project Notes
## Development Environment
This is a **Nix environment**. Tools like `cargo`, `rustc`, `clippy`, etc. are not available in the base shell.
To run Rust/Cargo commands, use one of:
```bash
nix develop --command cargo test
nix develop --command cargo build
nix develop --command cargo clippy
nix develop --command cargo fmt
```
Or enter the development shell first:
```bash
nix develop
# then run commands normally
cargo test
```
The `lux` binary can be run directly if already built:
```bash
./target/debug/lux test
./target/release/lux <file.lux>
```
For additional tools not in the dev shell:
```bash
nix-shell -p <program>
```
## Development Workflow
When making changes:
1. **Always run tests**: `cargo check && cargo test` - fix all errors and warnings
2. **Lint the Lux code**: `./target/release/lux lint` - fix warnings
3. **Check Lux code**: `./target/release/lux check` - type check + lint in one pass
4. **Format Lux code**: `./target/release/lux fmt` - auto-format all .lux files
5. **Write tests**: Add tests to cover new code
6. **Document features**: Provide documentation and tutorials for new features/frameworks
7. **Fix language limitations**: If you encounter parser/type system limitations, fix them (without regressions on guarantees or speed)
8. **Git commits**: Always use `--no-gpg-sign` flag
### Post-work checklist (run after each committable change)
**MANDATORY: Run the full validation script after every committable change:**
```bash
./scripts/validate.sh
```
This script runs ALL of the following checks and will fail if any regress:
1. `cargo check` — no Rust compilation errors
2. `cargo test` — all Rust tests pass (currently 387)
3. `cargo build --release` — release binary builds
4. `lux test` on every package (path, frontmatter, xml, rss, markdown) — all 286 package tests pass
5. `lux check` on every package — type checking + lint passes
If `validate.sh` is not available or you need to run manually:
```bash
nix develop --command cargo check # No Rust errors
nix develop --command cargo test # All Rust tests pass
nix develop --command cargo build --release # Build release binary
cd ../packages/path && ../../lang/target/release/lux test # Package tests
cd ../packages/frontmatter && ../../lang/target/release/lux test
cd ../packages/xml && ../../lang/target/release/lux test
cd ../packages/rss && ../../lang/target/release/lux test
cd ../packages/markdown && ../../lang/target/release/lux test
```
**Do NOT commit if any check fails.** Fix the issue first.
### Commit after every piece of work
**After completing each logical unit of work, commit immediately.** This is NOT optional — every fix, feature, or change MUST be committed right away. Do not let changes accumulate uncommitted across multiple features. Each commit should be a single logical change (one feature, one bugfix, etc.). Use `--no-gpg-sign` flag for all commits.
**Commit workflow:**
1. Make the change
2. Run `./scripts/validate.sh` (all 13 checks must pass)
3. `git add` the relevant files
4. `git commit --no-gpg-sign -m "type: description"` (use conventional commits: fix/feat/chore/docs)
5. Move on to the next task
**Never skip committing.** If you fixed a bug, commit it. If you added a feature, commit it. If you updated docs, commit it. Do not batch unrelated changes into one commit.
**IMPORTANT: Always verify Lux code you write:**
- Run with interpreter: `./target/release/lux file.lux`
- Compile to binary: `./target/release/lux compile file.lux`
- Both must work before claiming code is functional
- The C backend has limited effect support (Console, File only - no HttpServer, Http, etc.)
## CLI Commands & Aliases
| Command | Alias | Description |
|---------|-------|-------------|
| `lux fmt` | `lux f` | Format .lux files |
| `lux test` | `lux t` | Run test suite |
| `lux check` | `lux k` | Type check + lint |
| `lux lint` | `lux l` | Lint only (with `--explain` for detailed help) |
| `lux serve` | `lux s` | Static file server |
| `lux compile` | `lux c` | Compile to binary |
## Documenting Lux Language Errors
When working on any major task that involves writing Lux code, **document every language error, limitation, or surprising behavior** you encounter. This log is optimized for LLM consumption so future sessions can avoid repeating mistakes.
**File:** Maintain an `ISSUES.md` in the relevant project directory (e.g., `~/src/blu-site/ISSUES.md`).
**Format for each entry:**
```markdown
## Issue N: <Short descriptive title>
**Category**: Parser limitation | Type checker gap | Missing feature | Runtime error | Documentation gap
**Severity**: High | Medium | Low
**Status**: Open | **Fixed** (commit hash or version)
<1-2 sentence description of the problem>
**Reproduction:**
```lux
// Minimal code that triggers the issue
```
**Error message:** `<exact error text>`
**Workaround:** <how to accomplish the goal despite the limitation>
**Fix:** <if fixed, what was changed and where>
```
**Rules:**
- Add new issues as you encounter them during any task
- When a previously documented issue gets fixed, update its status to **Fixed** and note the commit/version
- Remove entries that are no longer relevant (e.g., the feature was redesigned entirely)
- Keep the summary table at the bottom of ISSUES.md in sync with the entries
- Do NOT duplicate issues already documented -- check existing entries first
## Code Quality
- Fix all compiler warnings before committing
- Ensure all tests pass (currently 387 tests)
- Add new tests when adding features
- Keep examples and documentation in sync
## Lux Language Notes
### Top-level expressions
Bare `run` expressions are not allowed at top-level. You must wrap them in a `let` binding:
```lux
// WRONG: parse error
run main() with {}
// CORRECT
let output = run main() with {}
```
### String methods
Lux uses module-qualified function calls, not method syntax on primitives:
```lux
// WRONG: not valid syntax
path.endsWith(".html")
// CORRECT
String.endsWith(path, ".html")
```
### Available String functions
Key string functions (all in `String.` namespace):
- `String.length(s)` - get length
- `String.startsWith(s, prefix)` - check prefix
- `String.endsWith(s, suffix)` - check suffix
- `String.split(s, delimiter)` - split into list
- `String.join(list, delimiter)` - join list
- `String.substring(s, start, end)` - extract substring
- `String.indexOf(s, needle)` - find position (returns Option)
- `String.replace(s, old, new)` - replace occurrences
- `String.trim(s)` - trim whitespace
- `String.toLower(s)` / `String.toUpper(s)` - case conversion

216
Cargo.lock generated
View File

@@ -135,16 +135,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.7" version = "0.8.7"
@@ -235,7 +225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -297,21 +287,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.2" version = "1.2.2"
@@ -552,16 +527,17 @@ dependencies = [
] ]
[[package]] [[package]]
name = "hyper-tls" name = "hyper-rustls"
version = "0.5.0" version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [ dependencies = [
"bytes", "futures-util",
"http",
"hyper", "hyper",
"native-tls", "rustls",
"tokio", "tokio",
"tokio-native-tls", "tokio-rustls",
] ]
[[package]] [[package]]
@@ -794,7 +770,7 @@ dependencies = [
[[package]] [[package]]
name = "lux" name = "lux"
version = "0.1.0" version = "0.1.4"
dependencies = [ dependencies = [
"lsp-server", "lsp-server",
"lsp-types", "lsp-types",
@@ -843,23 +819,6 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "native-tls"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5d26952a508f321b4d3d2e80e78fc2603eaefcdf0c30783867f19586518bdc"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]] [[package]]
name = "nibble_vec" name = "nibble_vec"
version = "0.1.0" version = "0.1.0"
@@ -905,50 +864,6 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openssl"
version = "0.10.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
dependencies = [
"bitflags 2.10.0",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "openssl-sys"
version = "0.9.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.5" version = "0.12.5"
@@ -1203,15 +1118,15 @@ dependencies = [
"http", "http",
"http-body", "http-body",
"hyper", "hyper",
"hyper-tls", "hyper-rustls",
"ipnet", "ipnet",
"js-sys", "js-sys",
"log", "log",
"mime", "mime",
"native-tls",
"once_cell", "once_cell",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls",
"rustls-pemfile", "rustls-pemfile",
"serde", "serde",
"serde_json", "serde_json",
@@ -1219,15 +1134,30 @@ dependencies = [
"sync_wrapper", "sync_wrapper",
"system-configuration", "system-configuration",
"tokio", "tokio",
"tokio-native-tls", "tokio-rustls",
"tower-service", "tower-service",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"webpki-roots",
"winreg", "winreg",
] ]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "rusqlite" name = "rusqlite"
version = "0.31.0" version = "0.31.0"
@@ -1252,7 +1182,19 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.61.2", "windows-sys 0.59.0",
]
[[package]]
name = "rustls"
version = "0.21.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [
"log",
"ring",
"rustls-webpki",
"sct",
] ]
[[package]] [[package]]
@@ -1264,6 +1206,16 @@ dependencies = [
"base64 0.21.7", "base64 0.21.7",
] ]
[[package]]
name = "rustls-webpki"
version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
"ring",
"untrusted",
]
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.22" version = "1.0.22"
@@ -1298,15 +1250,6 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "schannel"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
dependencies = [
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@@ -1314,26 +1257,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "security-framework" name = "sct"
version = "3.6.0" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [ dependencies = [
"bitflags 2.10.0", "ring",
"core-foundation 0.10.1", "untrusted",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a"
dependencies = [
"core-foundation-sys",
"libc",
] ]
[[package]] [[package]]
@@ -1521,7 +1451,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation 0.9.4", "core-foundation",
"system-configuration-sys", "system-configuration-sys",
] ]
@@ -1545,7 +1475,7 @@ dependencies = [
"getrandom 0.4.1", "getrandom 0.4.1",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -1619,16 +1549,6 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]] [[package]]
name = "tokio-postgres" name = "tokio-postgres"
version = "0.7.16" version = "0.7.16"
@@ -1655,6 +1575,16 @@ dependencies = [
"whoami", "whoami",
] ]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [
"rustls",
"tokio",
]
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.18" version = "0.7.18"
@@ -1750,6 +1680,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.8" version = "2.5.8"
@@ -1941,6 +1877,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "webpki-roots"
version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]] [[package]]
name = "whoami" name = "whoami"
version = "2.1.1" version = "2.1.1"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lux" name = "lux"
version = "0.1.0" version = "0.1.5"
edition = "2021" edition = "2021"
description = "A functional programming language with first-class effects, schema evolution, and behavioral types" description = "A functional programming language with first-class effects, schema evolution, and behavioral types"
license = "MIT" license = "MIT"
@@ -13,7 +13,7 @@ lsp-types = "0.94"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
rand = "0.8" rand = "0.8"
reqwest = { version = "0.11", features = ["blocking", "json"] } reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls"] }
tiny_http = "0.12" tiny_http = "0.12"
rusqlite = { version = "0.31", features = ["bundled"] } rusqlite = { version = "0.31", features = ["bundled"] }
postgres = "0.19" postgres = "0.19"

367
PACKAGES.md Normal file
View 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.

View File

@@ -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

38
build.rs Normal file
View File

@@ -0,0 +1,38 @@
use std::path::PathBuf;
fn main() {
// Capture the absolute C compiler path at build time so the binary is self-contained.
// This is critical for Nix builds where cc/gcc live in /nix/store paths.
let cc_path = std::env::var("CC").ok()
.filter(|s| !s.is_empty())
.and_then(|s| resolve_absolute(&s))
.or_else(|| find_in_path("cc"))
.or_else(|| find_in_path("gcc"))
.or_else(|| find_in_path("clang"))
.unwrap_or_default();
println!("cargo:rustc-env=LUX_CC_PATH={}", cc_path);
println!("cargo:rerun-if-env-changed=CC");
println!("cargo:rerun-if-env-changed=PATH");
}
/// Resolve a command name to its absolute path by searching PATH.
fn find_in_path(cmd: &str) -> Option<String> {
let path_var = std::env::var("PATH").ok()?;
for dir in path_var.split(':') {
let candidate = PathBuf::from(dir).join(cmd);
if candidate.is_file() {
return Some(candidate.to_string_lossy().into_owned());
}
}
None
}
/// If the path is already absolute and exists, return it. Otherwise search PATH.
fn resolve_absolute(cmd: &str) -> Option<String> {
let p = PathBuf::from(cmd);
if p.is_absolute() && p.is_file() {
return Some(cmd.to_string());
}
find_in_path(cmd)
}

449
docs/PHILOSOPHY.md Normal file
View 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.

View File

@@ -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))

View File

@@ -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)))

View File

@@ -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!")

View File

@@ -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 {}

View File

@@ -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))

View File

@@ -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))
} }

View File

@@ -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 {}

View File

@@ -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")
} }

View File

@@ -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))

View File

@@ -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))

View File

@@ -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))
} }

View File

@@ -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 {}

View File

@@ -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 {}

View File

@@ -1,85 +1,48 @@
// HTTP API Example fn httpOk(body: String): { status: Int, body: String } = { status: 200, body: body }
//
// A complete REST API demonstrating:
// - Route matching with path parameters
// - Response builders
// - JSON construction
//
// Run with: lux examples/http_api.lux
// Test with:
// curl http://localhost:8080/
// curl http://localhost:8080/users
// curl http://localhost:8080/users/42
// ============================================================ fn httpCreated(body: String): { status: Int, body: String } = { status: 201, body: body }
// Response Helpers
// ============================================================
fn httpOk(body: String): { status: Int, body: String } = fn httpNotFound(body: String): { status: Int, body: String } = { status: 404, body: body }
{ status: 200, body: body }
fn httpCreated(body: String): { status: Int, body: String } = fn httpBadRequest(body: String): { status: Int, body: String } = { status: 400, body: body }
{ status: 201, body: body }
fn httpNotFound(body: String): { status: Int, body: String } = fn jsonEscape(s: String): String = String.replace(String.replace(s, "\\", "\\\\"), "\"", "\\\"")
{ status: 404, body: body }
fn httpBadRequest(body: String): { status: Int, body: String } = fn jsonStr(key: String, value: String): String = "\"" + jsonEscape(key) + "\":\"" + jsonEscape(value) + "\""
{ status: 400, body: body }
// ============================================================ fn jsonNum(key: String, value: Int): String = "\"" + jsonEscape(key) + "\":" + toString(value)
// JSON Helpers
// ============================================================
fn jsonEscape(s: String): String = fn jsonObj(content: String): String = toString(" + content + ")
String.replace(String.replace(s, "\\", "\\\\"), "\"", "\\\"")
fn jsonStr(key: String, value: String): String = fn jsonArr(content: String): String = "[" + content + "]"
"\"" + jsonEscape(key) + "\":\"" + jsonEscape(value) + "\""
fn jsonNum(key: String, value: Int): String = fn jsonError(message: String): String = jsonObj(jsonStr("error", message))
"\"" + jsonEscape(key) + "\":" + toString(value)
fn jsonObj(content: String): String =
"{" + content + "}"
fn jsonArr(content: String): String =
"[" + content + "]"
fn jsonError(message: String): String =
jsonObj(jsonStr("error", message))
// ============================================================
// Path Matching
// ============================================================
fn pathMatches(path: String, pattern: String): Bool = { fn pathMatches(path: String, pattern: String): Bool = {
let pathParts = String.split(path, "/") let pathParts = String.split(path, "/")
let patternParts = String.split(pattern, "/") let patternParts = String.split(pattern, "/")
if List.length(pathParts) != List.length(patternParts) then false if List.length(pathParts) != List.length(patternParts) then false else matchParts(pathParts, patternParts)
else matchParts(pathParts, patternParts)
} }
fn matchParts(pathParts: List<String>, patternParts: List<String>): Bool = { fn matchParts(pathParts: List<String>, patternParts: List<String>): Bool = {
if List.length(pathParts) == 0 then true if List.length(pathParts) == 0 then true else {
else { match List.head(pathParts) {
match List.head(pathParts) { None => true,
None => true, Some(pathPart) => {
Some(pathPart) => { match List.head(patternParts) {
match List.head(patternParts) { None => true,
None => true, Some(patternPart) => {
Some(patternPart) => { let isMatch = if String.startsWith(patternPart, ":") then true else pathPart == patternPart
let isMatch = if String.startsWith(patternPart, ":") then true else pathPart == patternPart if isMatch then {
if isMatch then { let restPath = Option.getOrElse(List.tail(pathParts), [])
let restPath = Option.getOrElse(List.tail(pathParts), []) let restPattern = Option.getOrElse(List.tail(patternParts), [])
let restPattern = Option.getOrElse(List.tail(patternParts), []) matchParts(restPath, restPattern)
matchParts(restPath, restPattern) } else false
} else false },
} }
} },
} }
} }
}
} }
fn getPathSegment(path: String, index: Int): Option<String> = { fn getPathSegment(path: String, index: Int): Option<String> = {
@@ -87,15 +50,9 @@ fn getPathSegment(path: String, index: Int): Option<String> = {
List.get(parts, index + 1) List.get(parts, index + 1)
} }
// ============================================================ fn indexHandler(): { status: Int, body: String } = httpOk(jsonObj(jsonStr("message", "Welcome to Lux HTTP API")))
// Handlers
// ============================================================
fn indexHandler(): { status: Int, body: String } = fn healthHandler(): { status: Int, body: String } = httpOk(jsonObj(jsonStr("status", "healthy")))
httpOk(jsonObj(jsonStr("message", "Welcome to Lux HTTP API")))
fn healthHandler(): { status: Int, body: String } =
httpOk(jsonObj(jsonStr("status", "healthy")))
fn listUsersHandler(): { status: Int, body: String } = { fn listUsersHandler(): { status: Int, body: String } = {
let user1 = jsonObj(jsonNum("id", 1) + "," + jsonStr("name", "Alice")) let user1 = jsonObj(jsonNum("id", 1) + "," + jsonStr("name", "Alice"))
@@ -105,12 +62,12 @@ fn listUsersHandler(): { status: Int, body: String } = {
fn getUserHandler(path: String): { status: Int, body: String } = { fn getUserHandler(path: String): { status: Int, body: String } = {
match getPathSegment(path, 1) { match getPathSegment(path, 1) {
Some(id) => { Some(id) => {
let body = jsonObj(jsonStr("id", id) + "," + jsonStr("name", "User " + id)) let body = jsonObj(jsonStr("id", id) + "," + jsonStr("name", "User " + id))
httpOk(body) httpOk(body)
}, },
None => httpNotFound(jsonError("User not found")) None => httpNotFound(jsonError("User not found")),
} }
} }
fn createUserHandler(body: String): { status: Int, body: String } = { fn createUserHandler(body: String): { status: Int, body: String } = {
@@ -118,34 +75,21 @@ fn createUserHandler(body: String): { status: Int, body: String } = {
httpCreated(newUser) httpCreated(newUser)
} }
// ============================================================
// Router
// ============================================================
fn router(method: String, path: String, body: String): { status: Int, body: String } = { fn router(method: String, path: String, body: String): { status: Int, body: String } = {
if method == "GET" && path == "/" then indexHandler() if method == "GET" && path == "/" then indexHandler() else if method == "GET" && path == "/health" then healthHandler() else if method == "GET" && path == "/users" then listUsersHandler() else if method == "GET" && pathMatches(path, "/users/:id") then getUserHandler(path) else if method == "POST" && path == "/users" then createUserHandler(body) else httpNotFound(jsonError("Not found: " + path))
else if method == "GET" && path == "/health" then healthHandler()
else if method == "GET" && path == "/users" then listUsersHandler()
else if method == "GET" && pathMatches(path, "/users/:id") then getUserHandler(path)
else if method == "POST" && path == "/users" then createUserHandler(body)
else httpNotFound(jsonError("Not found: " + path))
} }
// ============================================================
// Server
// ============================================================
fn serveLoop(remaining: Int): Unit with {Console, HttpServer} = { fn serveLoop(remaining: Int): Unit with {Console, HttpServer} = {
if remaining <= 0 then { if remaining <= 0 then {
Console.print("Max requests reached, stopping server.") Console.print("Max requests reached, stopping server.")
HttpServer.stop() HttpServer.stop()
} else { } else {
let req = HttpServer.accept() let req = HttpServer.accept()
Console.print(req.method + " " + req.path) Console.print(req.method + " " + req.path)
let resp = router(req.method, req.path, req.body) let resp = router(req.method, req.path, req.body)
HttpServer.respond(resp.status, resp.body) HttpServer.respond(resp.status, resp.body)
serveLoop(remaining - 1) serveLoop(remaining - 1)
} }
} }
fn main(): Unit with {Console, HttpServer} = { fn main(): Unit with {Console, HttpServer} = {

View File

@@ -1,24 +1,4 @@
// HTTP Router Example fn indexHandler(): { status: Int, body: String } = httpOk("Welcome to Lux HTTP Framework!")
//
// Demonstrates the HTTP helper library with:
// - Path pattern matching
// - Response builders
// - JSON helpers
//
// Run with: lux examples/http_router.lux
// Test with:
// curl http://localhost:8080/
// curl http://localhost:8080/users
// curl http://localhost:8080/users/42
import stdlib/http
// ============================================================
// Route Handlers
// ============================================================
fn indexHandler(): { status: Int, body: String } =
httpOk("Welcome to Lux HTTP Framework!")
fn listUsersHandler(): { status: Int, body: String } = { fn listUsersHandler(): { status: Int, body: String } = {
let user1 = jsonObject(jsonJoin([jsonNumber("id", 1), jsonString("name", "Alice")])) let user1 = jsonObject(jsonJoin([jsonNumber("id", 1), jsonString("name", "Alice")]))
@@ -29,44 +9,31 @@ fn listUsersHandler(): { status: Int, body: String } = {
fn getUserHandler(path: String): { status: Int, body: String } = { fn getUserHandler(path: String): { status: Int, body: String } = {
match getPathSegment(path, 1) { match getPathSegment(path, 1) {
Some(id) => { Some(id) => {
let body = jsonObject(jsonJoin([jsonString("id", id), jsonString("name", "User " + id)])) let body = jsonObject(jsonJoin([jsonString("id", id), jsonString("name", "User " + id)]))
httpOk(body) httpOk(body)
}, },
None => httpNotFound(jsonErrorMsg("User ID required")) None => httpNotFound(jsonErrorMsg("User ID required")),
} }
} }
fn healthHandler(): { status: Int, body: String } = fn healthHandler(): { status: Int, body: String } = httpOk(jsonObject(jsonString("status", "healthy")))
httpOk(jsonObject(jsonString("status", "healthy")))
// ============================================================
// Router
// ============================================================
fn router(method: String, path: String, body: String): { status: Int, body: String } = { fn router(method: String, path: String, body: String): { status: Int, body: String } = {
if method == "GET" && path == "/" then indexHandler() if method == "GET" && path == "/" then indexHandler() else if method == "GET" && path == "/health" then healthHandler() else if method == "GET" && path == "/users" then listUsersHandler() else if method == "GET" && pathMatches(path, "/users/:id") then getUserHandler(path) else httpNotFound(jsonErrorMsg("Not found: " + path))
else if method == "GET" && path == "/health" then healthHandler()
else if method == "GET" && path == "/users" then listUsersHandler()
else if method == "GET" && pathMatches(path, "/users/:id") then getUserHandler(path)
else httpNotFound(jsonErrorMsg("Not found: " + path))
} }
// ============================================================
// Server
// ============================================================
fn serveLoop(remaining: Int): Unit with {Console, HttpServer} = { fn serveLoop(remaining: Int): Unit with {Console, HttpServer} = {
if remaining <= 0 then { if remaining <= 0 then {
Console.print("Max requests reached, stopping server.") Console.print("Max requests reached, stopping server.")
HttpServer.stop() HttpServer.stop()
} else { } else {
let req = HttpServer.accept() let req = HttpServer.accept()
Console.print(req.method + " " + req.path) Console.print(req.method + " " + req.path)
let resp = router(req.method, req.path, req.body) let resp = router(req.method, req.path, req.body)
HttpServer.respond(resp.status, resp.body) HttpServer.respond(resp.status, resp.body)
serveLoop(remaining - 1) serveLoop(remaining - 1)
} }
} }
fn main(): Unit with {Console, HttpServer} = { fn main(): Unit with {Console, HttpServer} = {

View File

@@ -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)

View File

@@ -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()

View File

@@ -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))

View File

@@ -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"))
} }

View File

@@ -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)))

View File

@@ -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)

View File

@@ -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 + "!"

View File

@@ -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)))

View File

@@ -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))

View File

@@ -1,72 +1,42 @@
// PostgreSQL Database Example fn jsonStr(key: String, value: String): String = "\"" + key + "\":\"" + value + "\""
//
// Demonstrates the Postgres effect for database operations.
//
// Prerequisites:
// - PostgreSQL server running locally
// - Database 'testdb' created
// - User 'testuser' with password 'testpass'
//
// To set up:
// createdb testdb
// psql testdb -c "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT, email TEXT);"
//
// Run with: lux examples/postgres_demo.lux
// ============================================================ fn jsonNum(key: String, value: Int): String = "\"" + key + "\":" + toString(value)
// Helper Functions
// ============================================================
fn jsonStr(key: String, value: String): String = fn jsonObj(content: String): String = toString(" + content + ")
"\"" + key + "\":\"" + value + "\""
fn jsonNum(key: String, value: Int): String =
"\"" + key + "\":" + toString(value)
fn jsonObj(content: String): String =
"{" + content + "}"
// ============================================================
// Database Operations
// ============================================================
// Insert a user
fn insertUser(connId: Int, name: String, email: String): Int with {Console, Postgres} = { fn insertUser(connId: Int, name: String, email: String): Int with {Console, Postgres} = {
let sql = "INSERT INTO users (name, email) VALUES ('" + name + "', '" + email + "') RETURNING id" let sql = "INSERT INTO users (name, email) VALUES ('" + name + "', '" + email + "') RETURNING id"
Console.print("Inserting user: " + name) Console.print("Inserting user: " + name)
match Postgres.queryOne(connId, sql) { match Postgres.queryOne(connId, sql) {
Some(row) => { Some(row) => {
Console.print(" Inserted with ID: " + toString(row.id)) Console.print(" Inserted with ID: " + toString(row.id))
row.id row.id
}, },
None => { None => {
Console.print(" Insert failed") Console.print(" Insert failed")
-1 -1
} },
} }
} }
// Get all users
fn getUsers(connId: Int): Unit with {Console, Postgres} = { fn getUsers(connId: Int): Unit with {Console, Postgres} = {
Console.print("Fetching all users...") Console.print("Fetching all users...")
let rows = Postgres.query(connId, "SELECT id, name, email FROM users ORDER BY id") let rows = Postgres.query(connId, "SELECT id, name, email FROM users ORDER BY id")
Console.print(" Found " + toString(List.length(rows)) + " users:") Console.print(" Found " + toString(List.length(rows)) + " users:")
List.forEach(rows, fn(row: { id: Int, name: String, email: String }): Unit with {Console} => { List.forEach(rows, fn(row: { id: Int, name: String, email: String }): Unit => {
Console.print(" - " + toString(row.id) + ": " + row.name + " <" + row.email + ">") Console.print(" - " + toString(row.id) + ": " + row.name + " <" + row.email + ">")
}) })
} }
// Get user by ID
fn getUserById(connId: Int, id: Int): Unit with {Console, Postgres} = { fn getUserById(connId: Int, id: Int): Unit with {Console, Postgres} = {
let sql = "SELECT id, name, email FROM users WHERE id = " + toString(id) let sql = "SELECT id, name, email FROM users WHERE id = " + toString(id)
Console.print("Looking up user " + toString(id) + "...") Console.print("Looking up user " + toString(id) + "...")
match Postgres.queryOne(connId, sql) { match Postgres.queryOne(connId, sql) {
Some(row) => Console.print(" Found: " + row.name + " <" + row.email + ">"), Some(row) => Console.print(" Found: " + row.name + " <" + row.email + ">"),
None => Console.print(" User not found") None => Console.print(" User not found"),
} }
} }
// Update user email
fn updateUserEmail(connId: Int, id: Int, newEmail: String): Unit with {Console, Postgres} = { fn updateUserEmail(connId: Int, id: Int, newEmail: String): Unit with {Console, Postgres} = {
let sql = "UPDATE users SET email = '" + newEmail + "' WHERE id = " + toString(id) let sql = "UPDATE users SET email = '" + newEmail + "' WHERE id = " + toString(id)
Console.print("Updating user " + toString(id) + " email to " + newEmail) Console.print("Updating user " + toString(id) + " email to " + newEmail)
@@ -74,7 +44,6 @@ fn updateUserEmail(connId: Int, id: Int, newEmail: String): Unit with {Console,
Console.print(" Rows affected: " + toString(affected)) Console.print(" Rows affected: " + toString(affected))
} }
// Delete user
fn deleteUser(connId: Int, id: Int): Unit with {Console, Postgres} = { fn deleteUser(connId: Int, id: Int): Unit with {Console, Postgres} = {
let sql = "DELETE FROM users WHERE id = " + toString(id) let sql = "DELETE FROM users WHERE id = " + toString(id)
Console.print("Deleting user " + toString(id)) Console.print("Deleting user " + toString(id))
@@ -82,104 +51,63 @@ fn deleteUser(connId: Int, id: Int): Unit with {Console, Postgres} = {
Console.print(" Rows affected: " + toString(affected)) Console.print(" Rows affected: " + toString(affected))
} }
// ============================================================
// Transaction Example
// ============================================================
fn transactionDemo(connId: Int): Unit with {Console, Postgres} = { fn transactionDemo(connId: Int): Unit with {Console, Postgres} = {
Console.print("") Console.print("")
Console.print("=== Transaction Demo ===") Console.print("=== Transaction Demo ===")
// Start transaction
Console.print("Beginning transaction...") Console.print("Beginning transaction...")
Postgres.beginTx(connId) Postgres.beginTx(connId)
// Make some changes
insertUser(connId, "TxUser1", "tx1@example.com") insertUser(connId, "TxUser1", "tx1@example.com")
insertUser(connId, "TxUser2", "tx2@example.com") insertUser(connId, "TxUser2", "tx2@example.com")
// Show users before commit
Console.print("Users before commit:") Console.print("Users before commit:")
getUsers(connId) getUsers(connId)
// Commit the transaction
Console.print("Committing transaction...") Console.print("Committing transaction...")
Postgres.commit(connId) Postgres.commit(connId)
Console.print("Transaction committed!") Console.print("Transaction committed!")
} }
// ============================================================
// Main
// ============================================================
fn main(): Unit with {Console, Postgres} = { fn main(): Unit with {Console, Postgres} = {
Console.print("========================================") Console.print("========================================")
Console.print(" PostgreSQL Demo") Console.print(" PostgreSQL Demo")
Console.print("========================================") Console.print("========================================")
Console.print("") Console.print("")
// Connect to database
Console.print("Connecting to PostgreSQL...") Console.print("Connecting to PostgreSQL...")
let connStr = "host=localhost user=testuser password=testpass dbname=testdb" let connStr = "host=localhost user=testuser password=testpass dbname=testdb"
let connId = Postgres.connect(connStr) let connId = Postgres.connect(connStr)
Console.print("Connected! Connection ID: " + toString(connId)) Console.print("Connected! Connection ID: " + toString(connId))
Console.print("") Console.print("")
// Create table if not exists
Console.print("Creating users table...") Console.print("Creating users table...")
Postgres.execute(connId, "CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL)") Postgres.execute(connId, "CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL)")
Console.print("") Console.print("")
// Clear table for demo
Console.print("Clearing existing data...") Console.print("Clearing existing data...")
Postgres.execute(connId, "DELETE FROM users") Postgres.execute(connId, "DELETE FROM users")
Console.print("") Console.print("")
// Insert some users
Console.print("=== Inserting Users ===") Console.print("=== Inserting Users ===")
let id1 = insertUser(connId, "Alice", "alice@example.com") let id1 = insertUser(connId, "Alice", "alice@example.com")
let id2 = insertUser(connId, "Bob", "bob@example.com") let id2 = insertUser(connId, "Bob", "bob@example.com")
let id3 = insertUser(connId, "Charlie", "charlie@example.com") let id3 = insertUser(connId, "Charlie", "charlie@example.com")
Console.print("") Console.print("")
// Query all users
Console.print("=== All Users ===") Console.print("=== All Users ===")
getUsers(connId) getUsers(connId)
Console.print("") Console.print("")
// Query single user
Console.print("=== Single User Lookup ===") Console.print("=== Single User Lookup ===")
getUserById(connId, id2) getUserById(connId, id2)
Console.print("") Console.print("")
// Update user
Console.print("=== Update User ===") Console.print("=== Update User ===")
updateUserEmail(connId, id2, "bob.new@example.com") updateUserEmail(connId, id2, "bob.new@example.com")
getUserById(connId, id2) getUserById(connId, id2)
Console.print("") Console.print("")
// Delete user
Console.print("=== Delete User ===") Console.print("=== Delete User ===")
deleteUser(connId, id3) deleteUser(connId, id3)
getUsers(connId) getUsers(connId)
Console.print("") Console.print("")
// Transaction demo
transactionDemo(connId) transactionDemo(connId)
Console.print("") Console.print("")
// Final state
Console.print("=== Final State ===") Console.print("=== Final State ===")
getUsers(connId) getUsers(connId)
Console.print("") Console.print("")
// Close connection
Console.print("Closing connection...") Console.print("Closing connection...")
Postgres.close(connId) Postgres.close(connId)
Console.print("Done!") Console.print("Done!")
} }
// Note: This will fail if PostgreSQL is not running
// To test the syntax only, you can comment out the last line
let output = run main() with {} let output = run main() with {}

View File

@@ -1,18 +1,6 @@
// Property-Based Testing Example
//
// This example demonstrates property-based testing in Lux,
// where we verify properties hold for randomly generated inputs.
//
// Run with: lux examples/property_testing.lux
// ============================================================
// Generator Functions (using Random effect)
// ============================================================
let CHARS = "abcdefghijklmnopqrstuvwxyz" let CHARS = "abcdefghijklmnopqrstuvwxyz"
fn genInt(min: Int, max: Int): Int with {Random} = fn genInt(min: Int, max: Int): Int with {Random} = Random.int(min, max)
Random.int(min, max)
fn genIntList(min: Int, max: Int, maxLen: Int): List<Int> with {Random} = { fn genIntList(min: Int, max: Int, maxLen: Int): List<Int> with {Random} = {
let len = Random.int(0, maxLen) let len = Random.int(0, maxLen)
@@ -20,10 +8,7 @@ fn genIntList(min: Int, max: Int, maxLen: Int): List<Int> with {Random} = {
} }
fn genIntListHelper(min: Int, max: Int, len: Int): List<Int> with {Random} = { fn genIntListHelper(min: Int, max: Int, len: Int): List<Int> with {Random} = {
if len <= 0 then if len <= 0 then [] else List.concat([Random.int(min, max)], genIntListHelper(min, max, len - 1))
[]
else
List.concat([Random.int(min, max)], genIntListHelper(min, max, len - 1))
} }
fn genChar(): String with {Random} = { fn genChar(): String with {Random} = {
@@ -37,195 +22,147 @@ fn genString(maxLen: Int): String with {Random} = {
} }
fn genStringHelper(len: Int): String with {Random} = { fn genStringHelper(len: Int): String with {Random} = {
if len <= 0 then if len <= 0 then "" else genChar() + genStringHelper(len - 1)
""
else
genChar() + genStringHelper(len - 1)
} }
// ============================================================
// Test Runner State
// ============================================================
fn printResult(name: String, passed: Bool, count: Int): Unit with {Console} = { fn printResult(name: String, passed: Bool, count: Int): Unit with {Console} = {
if passed then if passed then Console.print(" PASS " + name + " (" + toString(count) + " tests)") else Console.print(" FAIL " + name)
Console.print(" PASS " + name + " (" + toString(count) + " tests)")
else
Console.print(" FAIL " + name)
} }
// ============================================================
// Property Tests
// ============================================================
// Test: List reverse is involutive
fn testReverseInvolutive(n: Int, count: Int): Bool with {Console, Random} = { fn testReverseInvolutive(n: Int, count: Int): Bool with {Console, Random} = {
if n <= 0 then { if n <= 0 then {
printResult("reverse(reverse(xs)) == xs", true, count) printResult("reverse(reverse(xs)) == xs", true, count)
true true
} else { } else {
let xs = genIntList(0, 100, 20) let xs = genIntList(0, 100, 20)
if List.reverse(List.reverse(xs)) == xs then if List.reverse(List.reverse(xs)) == xs then testReverseInvolutive(n - 1, count) else {
testReverseInvolutive(n - 1, count) printResult("reverse(reverse(xs)) == xs", false, count - n + 1)
else { false
printResult("reverse(reverse(xs)) == xs", false, count - n + 1) }
false }
}
}
} }
// Test: List reverse preserves length
fn testReverseLength(n: Int, count: Int): Bool with {Console, Random} = { fn testReverseLength(n: Int, count: Int): Bool with {Console, Random} = {
if n <= 0 then { if n <= 0 then {
printResult("length(reverse(xs)) == length(xs)", true, count) printResult("length(reverse(xs)) == length(xs)", true, count)
true true
} else { } else {
let xs = genIntList(0, 100, 20) let xs = genIntList(0, 100, 20)
if List.length(List.reverse(xs)) == List.length(xs) then if List.length(List.reverse(xs)) == List.length(xs) then testReverseLength(n - 1, count) else {
testReverseLength(n - 1, count) printResult("length(reverse(xs)) == length(xs)", false, count - n + 1)
else { false
printResult("length(reverse(xs)) == length(xs)", false, count - n + 1) }
false }
}
}
} }
// Test: List map preserves length
fn testMapLength(n: Int, count: Int): Bool with {Console, Random} = { fn testMapLength(n: Int, count: Int): Bool with {Console, Random} = {
if n <= 0 then { if n <= 0 then {
printResult("length(map(xs, f)) == length(xs)", true, count) printResult("length(map(xs, f)) == length(xs)", true, count)
true true
} else { } else {
let xs = genIntList(0, 100, 20) let xs = genIntList(0, 100, 20)
if List.length(List.map(xs, fn(x) => x * 2)) == List.length(xs) then if List.length(List.map(xs, fn(x: _) => x * 2)) == List.length(xs) then testMapLength(n - 1, count) else {
testMapLength(n - 1, count) printResult("length(map(xs, f)) == length(xs)", false, count - n + 1)
else { false
printResult("length(map(xs, f)) == length(xs)", false, count - n + 1) }
false }
}
}
} }
// Test: List concat length is sum
fn testConcatLength(n: Int, count: Int): Bool with {Console, Random} = { fn testConcatLength(n: Int, count: Int): Bool with {Console, Random} = {
if n <= 0 then { if n <= 0 then {
printResult("length(xs ++ ys) == length(xs) + length(ys)", true, count) printResult("length(xs ++ ys) == length(xs) + length(ys)", true, count)
true true
} else { } else {
let xs = genIntList(0, 50, 10) let xs = genIntList(0, 50, 10)
let ys = genIntList(0, 50, 10) let ys = genIntList(0, 50, 10)
if List.length(List.concat(xs, ys)) == List.length(xs) + List.length(ys) then if List.length(List.concat(xs, ys)) == List.length(xs) + List.length(ys) then testConcatLength(n - 1, count) else {
testConcatLength(n - 1, count) printResult("length(xs ++ ys) == length(xs) + length(ys)", false, count - n + 1)
else { false
printResult("length(xs ++ ys) == length(xs) + length(ys)", false, count - n + 1) }
false }
}
}
} }
// Test: Addition is commutative
fn testAddCommutative(n: Int, count: Int): Bool with {Console, Random} = { fn testAddCommutative(n: Int, count: Int): Bool with {Console, Random} = {
if n <= 0 then { if n <= 0 then {
printResult("a + b == b + a", true, count) printResult("a + b == b + a", true, count)
true true
} else { } else {
let a = genInt(-1000, 1000) let a = genInt(-1000, 1000)
let b = genInt(-1000, 1000) let b = genInt(-1000, 1000)
if a + b == b + a then if a + b == b + a then testAddCommutative(n - 1, count) else {
testAddCommutative(n - 1, count) printResult("a + b == b + a", false, count - n + 1)
else { false
printResult("a + b == b + a", false, count - n + 1) }
false }
}
}
} }
// Test: Multiplication is associative
fn testMulAssociative(n: Int, count: Int): Bool with {Console, Random} = { fn testMulAssociative(n: Int, count: Int): Bool with {Console, Random} = {
if n <= 0 then { if n <= 0 then {
printResult("(a * b) * c == a * (b * c)", true, count) printResult("(a * b) * c == a * (b * c)", true, count)
true true
} else { } else {
let a = genInt(-100, 100) let a = genInt(-100, 100)
let b = genInt(-100, 100) let b = genInt(-100, 100)
let c = genInt(-100, 100) let c = genInt(-100, 100)
if (a * b) * c == a * (b * c) then if a * b * c == a * b * c then testMulAssociative(n - 1, count) else {
testMulAssociative(n - 1, count) printResult("(a * b) * c == a * (b * c)", false, count - n + 1)
else { false
printResult("(a * b) * c == a * (b * c)", false, count - n + 1) }
false }
}
}
} }
// Test: String concat length is sum
fn testStringConcatLength(n: Int, count: Int): Bool with {Console, Random} = { fn testStringConcatLength(n: Int, count: Int): Bool with {Console, Random} = {
if n <= 0 then { if n <= 0 then {
printResult("length(s1 + s2) == length(s1) + length(s2)", true, count) printResult("length(s1 + s2) == length(s1) + length(s2)", true, count)
true true
} else { } else {
let s1 = genString(10) let s1 = genString(10)
let s2 = genString(10) let s2 = genString(10)
if String.length(s1 + s2) == String.length(s1) + String.length(s2) then if String.length(s1 + s2) == String.length(s1) + String.length(s2) then testStringConcatLength(n - 1, count) else {
testStringConcatLength(n - 1, count) printResult("length(s1 + s2) == length(s1) + length(s2)", false, count - n + 1)
else { false
printResult("length(s1 + s2) == length(s1) + length(s2)", false, count - n + 1) }
false }
}
}
} }
// Test: Zero is identity for addition
fn testAddIdentity(n: Int, count: Int): Bool with {Console, Random} = { fn testAddIdentity(n: Int, count: Int): Bool with {Console, Random} = {
if n <= 0 then { if n <= 0 then {
printResult("x + 0 == x && 0 + x == x", true, count) printResult("x + 0 == x && 0 + x == x", true, count)
true true
} else { } else {
let x = genInt(-10000, 10000) let x = genInt(-10000, 10000)
if x + 0 == x && 0 + x == x then if x + 0 == x && 0 + x == x then testAddIdentity(n - 1, count) else {
testAddIdentity(n - 1, count) printResult("x + 0 == x && 0 + x == x", false, count - n + 1)
else { false
printResult("x + 0 == x && 0 + x == x", false, count - n + 1) }
false }
}
}
} }
// Test: Filter reduces or maintains length
fn testFilterLength(n: Int, count: Int): Bool with {Console, Random} = { fn testFilterLength(n: Int, count: Int): Bool with {Console, Random} = {
if n <= 0 then { if n <= 0 then {
printResult("length(filter(xs, p)) <= length(xs)", true, count) printResult("length(filter(xs, p)) <= length(xs)", true, count)
true true
} else { } else {
let xs = genIntList(0, 100, 20) let xs = genIntList(0, 100, 20)
if List.length(List.filter(xs, fn(x) => x > 50)) <= List.length(xs) then if List.length(List.filter(xs, fn(x: _) => x > 50)) <= List.length(xs) then testFilterLength(n - 1, count) else {
testFilterLength(n - 1, count) printResult("length(filter(xs, p)) <= length(xs)", false, count - n + 1)
else { false
printResult("length(filter(xs, p)) <= length(xs)", false, count - n + 1) }
false }
}
}
} }
// Test: Empty list is identity for concat
fn testConcatIdentity(n: Int, count: Int): Bool with {Console, Random} = { fn testConcatIdentity(n: Int, count: Int): Bool with {Console, Random} = {
if n <= 0 then { if n <= 0 then {
printResult("concat(xs, []) == xs && concat([], xs) == xs", true, count) printResult("concat(xs, []) == xs && concat([], xs) == xs", true, count)
true true
} else { } else {
let xs = genIntList(0, 100, 10) let xs = genIntList(0, 100, 10)
if List.concat(xs, []) == xs && List.concat([], xs) == xs then if List.concat(xs, []) == xs && List.concat([], xs) == xs then testConcatIdentity(n - 1, count) else {
testConcatIdentity(n - 1, count) printResult("concat(xs, []) == xs && concat([], xs) == xs", false, count - n + 1)
else { false
printResult("concat(xs, []) == xs && concat([], xs) == xs", false, count - n + 1) }
false }
}
}
} }
// ============================================================
// Main
// ============================================================
fn main(): Unit with {Console, Random} = { fn main(): Unit with {Console, Random} = {
Console.print("========================================") Console.print("========================================")
@@ -234,7 +171,6 @@ fn main(): Unit with {Console, Random} = {
Console.print("") Console.print("")
Console.print("Running 100 iterations per property...") Console.print("Running 100 iterations per property...")
Console.print("") Console.print("")
testReverseInvolutive(100, 100) testReverseInvolutive(100, 100)
testReverseLength(100, 100) testReverseLength(100, 100)
testMapLength(100, 100) testMapLength(100, 100)
@@ -245,7 +181,6 @@ fn main(): Unit with {Console, Random} = {
testAddIdentity(100, 100) testAddIdentity(100, 100)
testFilterLength(100, 100) testFilterLength(100, 100)
testConcatIdentity(100, 100) testConcatIdentity(100, 100)
Console.print("") Console.print("")
Console.print("========================================") Console.print("========================================")
Console.print(" All property tests completed!") Console.print(" All property tests completed!")

View File

@@ -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))
} }

View File

@@ -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 ===")

View File

@@ -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 {}

View File

@@ -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)
} }

View File

@@ -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))
} }

View File

@@ -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))
} }

View File

@@ -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))
} }

View File

@@ -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)))
} }

View File

@@ -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)
} }

View File

@@ -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)

View File

@@ -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 {}

View File

@@ -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!")
} }

View File

@@ -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 {}

View File

@@ -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:")

View File

@@ -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)))

View File

@@ -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))

View File

@@ -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")
} }

View File

@@ -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")
} }

View File

@@ -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))

View File

@@ -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
} }

View File

@@ -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)

View File

@@ -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]))

View File

@@ -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)

View File

@@ -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")
} }

View File

@@ -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")
} }

View File

@@ -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))

View File

@@ -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")

View File

@@ -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)

View File

@@ -14,6 +14,7 @@
pkgs = import nixpkgs { inherit system overlays; }; pkgs = import nixpkgs { inherit system overlays; };
rustToolchain = pkgs.rust-bin.stable.latest.default.override { rustToolchain = pkgs.rust-bin.stable.latest.default.override {
extensions = [ "rust-src" "rust-analyzer" ]; extensions = [ "rust-src" "rust-analyzer" ];
targets = [ "x86_64-unknown-linux-musl" ];
}; };
in in
{ {
@@ -22,8 +23,8 @@
rustToolchain rustToolchain
cargo-watch cargo-watch
cargo-edit cargo-edit
pkg-config # Static builds
openssl pkgsStatic.stdenv.cc
# Benchmark tools # Benchmark tools
hyperfine hyperfine
poop poop
@@ -43,7 +44,7 @@
printf "\n" printf "\n"
printf " \033[1;35m \033[0m\n" printf " \033[1;35m \033[0m\n"
printf " \033[1;35m \033[0m\n" printf " \033[1;35m \033[0m\n"
printf " \033[1;35m \033[0m v0.1.0\n" printf " \033[1;35m \033[0m v0.1.5\n"
printf "\n" printf "\n"
printf " Functional language with first-class effects\n" printf " Functional language with first-class effects\n"
printf "\n" printf "\n"
@@ -61,18 +62,47 @@
packages.default = pkgs.rustPlatform.buildRustPackage { packages.default = pkgs.rustPlatform.buildRustPackage {
pname = "lux"; pname = "lux";
version = "0.1.0"; version = "0.1.5";
src = ./.; src = ./.;
cargoLock.lockFile = ./Cargo.lock; cargoLock.lockFile = ./Cargo.lock;
nativeBuildInputs = [ pkgs.pkg-config ];
buildInputs = [ pkgs.openssl ];
doCheck = false; doCheck = false;
}; };
# Benchmark scripts packages.static = let
muslPkgs = import nixpkgs {
inherit system;
crossSystem = {
config = "x86_64-unknown-linux-musl";
isStatic = true;
};
};
in muslPkgs.rustPlatform.buildRustPackage {
pname = "lux";
version = "0.1.5";
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
CARGO_BUILD_TARGET = "x86_64-unknown-linux-musl";
CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static";
doCheck = false;
postInstall = ''
$STRIP $out/bin/lux 2>/dev/null || true
'';
};
apps = { apps = {
# Release automation
release = {
type = "app";
program = toString (pkgs.writeShellScript "lux-release" ''
exec ${self}/scripts/release.sh "$@"
'');
};
# Benchmark scripts
# Run hyperfine benchmark comparison # Run hyperfine benchmark comparison
bench = { bench = {
type = "app"; type = "app";

View File

@@ -0,0 +1,225 @@
// Lux AST — Self-hosted Abstract Syntax Tree definitions
//
// Direct translation of src/ast.rs into Lux ADTs.
// These types represent the parsed structure of a Lux program.
//
// Naming conventions to avoid collisions:
// Ex = Expr variant, Pat = Pattern, Te = TypeExpr
// Td = TypeDef, Vf = VariantFields, Op = Operator
// Decl = Declaration, St = Statement
// === Source Location ===
type Span = | Span(Int, Int)
// === Identifiers ===
type Ident = | Ident(String, Span)
// === Visibility ===
type Visibility = | Public | Private
// === Schema Evolution ===
type Version = | Version(Int, Span)
type VersionConstraint =
| VcExact(Version)
| VcAtLeast(Version)
| VcLatest(Span)
// === Behavioral Types ===
type BehavioralProperty =
| BpPure
| BpTotal
| BpIdempotent
| BpDeterministic
| BpCommutative
// === Trait Bound (needed before WhereClause) ===
type TraitBound = | TraitBound(Ident, List<TypeExpr>, Span)
// === Trait Constraint (needed before WhereClause) ===
type TraitConstraint = | TraitConstraint(Ident, List<TraitBound>, Span)
// === Where Clauses ===
type WhereClause =
| WcProperty(Ident, BehavioralProperty, Span)
| WcResult(Expr, Span)
| WcTrait(TraitConstraint)
// === Module Path ===
type ModulePath = | ModulePath(List<Ident>, Span)
// === Import ===
// path, alias, items, wildcard, span
type ImportDecl = | ImportDecl(ModulePath, Option<Ident>, Option<List<Ident>>, Bool, Span)
// === Program ===
type Program = | Program(List<ImportDecl>, List<Declaration>)
// === Declarations ===
type Declaration =
| DeclFunction(FunctionDecl)
| DeclEffect(EffectDecl)
| DeclType(TypeDecl)
| DeclHandler(HandlerDecl)
| DeclLet(LetDecl)
| DeclTrait(TraitDecl)
| DeclImpl(ImplDecl)
// === Parameter ===
type Parameter = | Parameter(Ident, TypeExpr, Span)
// === Effect Operation ===
type EffectOp = | EffectOp(Ident, List<Parameter>, TypeExpr, Span)
// === Record Field ===
type RecordField = | RecordField(Ident, TypeExpr, Span)
// === Variant Fields ===
type VariantFields =
| VfUnit
| VfTuple(List<TypeExpr>)
| VfRecord(List<RecordField>)
// === Variant ===
type Variant = | Variant(Ident, VariantFields, Span)
// === Migration ===
type Migration = | Migration(Version, Expr, Span)
// === Handler Impl ===
// op_name, params, resume, body, span
type HandlerImpl = | HandlerImpl(Ident, List<Ident>, Option<Ident>, Expr, Span)
// === Impl Method ===
// name, params, return_type, body, span
type ImplMethod = | ImplMethod(Ident, List<Parameter>, Option<TypeExpr>, Expr, Span)
// === Trait Method ===
// name, type_params, params, return_type, default_impl, span
type TraitMethod = | TraitMethod(Ident, List<Ident>, List<Parameter>, TypeExpr, Option<Expr>, Span)
// === Type Expressions ===
type TypeExpr =
| TeNamed(Ident)
| TeApp(TypeExpr, List<TypeExpr>)
| TeFunction(List<TypeExpr>, TypeExpr, List<Ident>)
| TeTuple(List<TypeExpr>)
| TeRecord(List<RecordField>)
| TeUnit
| TeVersioned(TypeExpr, VersionConstraint)
// === Literal ===
type LiteralKind =
| LitInt(Int)
| LitFloat(String)
| LitString(String)
| LitChar(Char)
| LitBool(Bool)
| LitUnit
type Literal = | Literal(LiteralKind, Span)
// === Binary Operators ===
type BinaryOp =
| OpAdd | OpSub | OpMul | OpDiv | OpMod
| OpEq | OpNe | OpLt | OpLe | OpGt | OpGe
| OpAnd | OpOr
| OpPipe | OpConcat
// === Unary Operators ===
type UnaryOp = | OpNeg | OpNot
// === Statements ===
type Statement =
| StExpr(Expr)
| StLet(Ident, Option<TypeExpr>, Expr, Span)
// === Match Arms ===
type MatchArm = | MatchArm(Pattern, Option<Expr>, Expr, Span)
// === Patterns ===
type Pattern =
| PatWildcard(Span)
| PatVar(Ident)
| PatLiteral(Literal)
| PatConstructor(Ident, List<Pattern>, Span)
| PatRecord(List<(Ident, Pattern)>, Span)
| PatTuple(List<Pattern>, Span)
// === Function Declaration ===
// visibility, doc, name, type_params, params, return_type, effects, properties, where_clauses, body, span
type FunctionDecl = | FunctionDecl(Visibility, Option<String>, Ident, List<Ident>, List<Parameter>, TypeExpr, List<Ident>, List<BehavioralProperty>, List<WhereClause>, Expr, Span)
// === Effect Declaration ===
// doc, name, type_params, operations, span
type EffectDecl = | EffectDecl(Option<String>, Ident, List<Ident>, List<EffectOp>, Span)
// === Type Declaration ===
// visibility, doc, name, type_params, version, definition, migrations, span
type TypeDecl = | TypeDecl(Visibility, Option<String>, Ident, List<Ident>, Option<Version>, TypeDef, List<Migration>, Span)
// === Handler Declaration ===
// name, params, effect, implementations, span
type HandlerDecl = | HandlerDecl(Ident, List<Parameter>, Ident, List<HandlerImpl>, Span)
// === Let Declaration ===
// visibility, doc, name, typ, value, span
type LetDecl = | LetDecl(Visibility, Option<String>, Ident, Option<TypeExpr>, Expr, Span)
// === Trait Declaration ===
// visibility, doc, name, type_params, super_traits, methods, span
type TraitDecl = | TraitDecl(Visibility, Option<String>, Ident, List<Ident>, List<TraitBound>, List<TraitMethod>, Span)
// === Impl Declaration ===
// type_params, constraints, trait_name, trait_args, target_type, methods, span
type ImplDecl = | ImplDecl(List<Ident>, List<TraitConstraint>, Ident, List<TypeExpr>, TypeExpr, List<ImplMethod>, Span)
// === Expressions ===
type Expr =
| ExLiteral(Literal)
| ExVar(Ident)
| ExBinaryOp(BinaryOp, Expr, Expr, Span)
| ExUnaryOp(UnaryOp, Expr, Span)
| ExCall(Expr, List<Expr>, Span)
| ExEffectOp(Ident, Ident, List<Expr>, Span)
| ExField(Expr, Ident, Span)
| ExTupleIndex(Expr, Int, Span)
| ExLambda(List<Parameter>, Option<TypeExpr>, List<Ident>, Expr, Span)
| ExLet(Ident, Option<TypeExpr>, Expr, Expr, Span)
| ExIf(Expr, Expr, Expr, Span)
| ExMatch(Expr, List<MatchArm>, Span)
| ExBlock(List<Statement>, Expr, Span)
| ExRecord(Option<Expr>, List<(Ident, Expr)>, Span)
| ExTuple(List<Expr>, Span)
| ExList(List<Expr>, Span)
| ExRun(Expr, List<(Ident, Expr)>, Span)
| ExResume(Expr, Span)

View File

@@ -0,0 +1,512 @@
// Lux Lexer — Self-hosted lexer for the Lux language
//
// This is the first component of the Lux-in-Lux compiler.
// It tokenizes Lux source code into a list of tokens.
//
// Design:
// - Recursive descent character scanning
// - Immutable state (ParseState tracks chars + position)
// - Pattern matching for all token types
// === Token types ===
type TokenKind =
// Literals
| TkInt(Int)
| TkFloat(String)
| TkString(String)
| TkChar(Char)
| TkBool(Bool)
// Identifiers
| TkIdent(String)
// Keywords
| TkFn | TkLet | TkIf | TkThen | TkElse | TkMatch
| TkWith | TkEffect | TkHandler | TkRun | TkResume
| TkType | TkImport | TkPub | TkAs | TkFrom
| TkTrait | TkImpl | TkFor
// Behavioral
| TkIs | TkPure | TkTotal | TkIdempotent
| TkDeterministic | TkCommutative
| TkWhere | TkAssume
// Operators
| TkPlus | TkMinus | TkStar | TkSlash | TkPercent
| TkEq | TkEqEq | TkNe | TkLt | TkLe | TkGt | TkGe
| TkAnd | TkOr | TkNot
| TkPipe | TkPipeGt | TkArrow | TkThinArrow
| TkDot | TkColon | TkColonColon | TkComma | TkSemi | TkAt
// Delimiters
| TkLParen | TkRParen | TkLBrace | TkRBrace
| TkLBracket | TkRBracket
// Special
| TkUnderscore | TkNewline | TkEof
// Doc comment
| TkDocComment(String)
type Token =
| Token(TokenKind, Int, Int) // kind, start, end
type LexState =
| LexState(List<Char>, Int) // chars, position
type LexResult =
| LexOk(Token, LexState)
| LexErr(String, Int)
// === Character utilities ===
fn peek(state: LexState): Option<Char> =
match state {
LexState(chars, pos) => List.get(chars, pos)
}
fn peekAt(state: LexState, offset: Int): Option<Char> =
match state {
LexState(chars, pos) => List.get(chars, pos + offset)
}
fn advance(state: LexState): LexState =
match state {
LexState(chars, pos) => LexState(chars, pos + 1)
}
fn position(state: LexState): Int =
match state { LexState(_, pos) => pos }
fn isDigit(c: Char): Bool =
c == '0' || c == '1' || c == '2' || c == '3' || c == '4' ||
c == '5' || c == '6' || c == '7' || c == '8' || c == '9'
fn isAlpha(c: Char): Bool =
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'
fn isAlphaNumeric(c: Char): Bool =
isAlpha(c) || isDigit(c)
fn isWhitespace(c: Char): Bool =
c == ' ' || c == '\t' || c == '\r'
// === Core lexing ===
fn skipLineComment(state: LexState): LexState =
match peek(state) {
None => state,
Some(c) =>
if c == '\n' then state
else skipLineComment(advance(state))
}
fn skipWhitespaceAndComments(state: LexState): LexState =
match peek(state) {
None => state,
Some(c) =>
if isWhitespace(c) then
skipWhitespaceAndComments(advance(state))
else if c == '/' then
match peekAt(state, 1) {
Some('/') =>
// Check for doc comment (///)
match peekAt(state, 2) {
Some('/') => state, // Don't skip doc comments
_ => skipWhitespaceAndComments(skipLineComment(advance(advance(state))))
},
_ => state
}
else state
}
// Collect identifier characters
fn collectIdent(state: LexState, acc: List<Char>): (List<Char>, LexState) =
match peek(state) {
None => (acc, state),
Some(c) =>
if isAlphaNumeric(c) then
collectIdent(advance(state), List.concat(acc, [c]))
else (acc, state)
}
// Collect number characters (digits only)
fn collectDigits(state: LexState, acc: List<Char>): (List<Char>, LexState) =
match peek(state) {
None => (acc, state),
Some(c) =>
if isDigit(c) then
collectDigits(advance(state), List.concat(acc, [c]))
else (acc, state)
}
// Convert list of digit chars to int
fn charsToInt(chars: List<Char>): Int =
List.fold(chars, 0, fn(acc, c) => acc * 10 + charToDigit(c))
fn charToDigit(c: Char): Int =
if c == '0' then 0
else if c == '1' then 1
else if c == '2' then 2
else if c == '3' then 3
else if c == '4' then 4
else if c == '5' then 5
else if c == '6' then 6
else if c == '7' then 7
else if c == '8' then 8
else 9
// Map identifier string to keyword token or ident
fn identToToken(name: String): TokenKind =
if name == "fn" then TkFn
else if name == "let" then TkLet
else if name == "if" then TkIf
else if name == "then" then TkThen
else if name == "else" then TkElse
else if name == "match" then TkMatch
else if name == "with" then TkWith
else if name == "effect" then TkEffect
else if name == "handler" then TkHandler
else if name == "run" then TkRun
else if name == "resume" then TkResume
else if name == "type" then TkType
else if name == "true" then TkBool(true)
else if name == "false" then TkBool(false)
else if name == "import" then TkImport
else if name == "pub" then TkPub
else if name == "as" then TkAs
else if name == "from" then TkFrom
else if name == "trait" then TkTrait
else if name == "impl" then TkImpl
else if name == "for" then TkFor
else if name == "is" then TkIs
else if name == "pure" then TkPure
else if name == "total" then TkTotal
else if name == "idempotent" then TkIdempotent
else if name == "deterministic" then TkDeterministic
else if name == "commutative" then TkCommutative
else if name == "where" then TkWhere
else if name == "assume" then TkAssume
else TkIdent(name)
// Lex a string literal (after opening quote consumed)
fn lexStringBody(state: LexState, acc: List<Char>): (List<Char>, LexState) =
match peek(state) {
None => (acc, state),
Some(c) =>
if c == '"' then (acc, advance(state))
else if c == '\\' then
match peekAt(state, 1) {
Some('n') => lexStringBody(advance(advance(state)), List.concat(acc, ['\n'])),
Some('t') => lexStringBody(advance(advance(state)), List.concat(acc, ['\t'])),
Some('\\') => lexStringBody(advance(advance(state)), List.concat(acc, ['\\'])),
Some('"') => lexStringBody(advance(advance(state)), List.concat(acc, ['"'])),
_ => lexStringBody(advance(state), List.concat(acc, [c]))
}
else lexStringBody(advance(state), List.concat(acc, [c]))
}
// Lex a char literal (after opening quote consumed)
fn lexCharLiteral(state: LexState): LexResult =
let start = position(state) - 1;
match peek(state) {
None => LexErr("Unexpected end of input in char literal", start),
Some(c) =>
if c == '\\' then
match peekAt(state, 1) {
Some('n') =>
match peekAt(state, 2) {
Some('\'') => LexOk(Token(TkChar('\n'), start, position(state) + 3), advance(advance(advance(state)))),
_ => LexErr("Expected closing quote", position(state))
},
Some('t') =>
match peekAt(state, 2) {
Some('\'') => LexOk(Token(TkChar('\t'), start, position(state) + 3), advance(advance(advance(state)))),
_ => LexErr("Expected closing quote", position(state))
},
Some('\\') =>
match peekAt(state, 2) {
Some('\'') => LexOk(Token(TkChar('\\'), start, position(state) + 3), advance(advance(advance(state)))),
_ => LexErr("Expected closing quote", position(state))
},
_ => LexErr("Unknown escape sequence", position(state))
}
else
match peekAt(state, 1) {
Some('\'') => LexOk(Token(TkChar(c), start, position(state) + 2), advance(advance(state))),
_ => LexErr("Expected closing quote", position(state))
}
}
// Collect doc comment text (after /// consumed)
fn collectDocComment(state: LexState, acc: List<Char>): (List<Char>, LexState) =
match peek(state) {
None => (acc, state),
Some(c) =>
if c == '\n' then (acc, state)
else collectDocComment(advance(state), List.concat(acc, [c]))
}
// Lex a single token
fn lexToken(state: LexState): LexResult =
let state = skipWhitespaceAndComments(state);
let start = position(state);
match peek(state) {
None => LexOk(Token(TkEof, start, start), state),
Some(c) =>
if c == '\n' then
LexOk(Token(TkNewline, start, start + 1), advance(state))
// Numbers
else if isDigit(c) then
let result = collectDigits(state, []);
match result {
(digits, nextState) =>
// Check for float
match peek(nextState) {
Some('.') =>
match peekAt(nextState, 1) {
Some(d) =>
if isDigit(d) then
let fracResult = collectDigits(advance(nextState), []);
match fracResult {
(fracDigits, finalState) =>
let intPart = String.join(List.map(digits, fn(ch) => String.fromChar(ch)), "");
let fracPart = String.join(List.map(fracDigits, fn(ch) => String.fromChar(ch)), "");
LexOk(Token(TkFloat(intPart + "." + fracPart), start, position(finalState)), finalState)
}
else
LexOk(Token(TkInt(charsToInt(digits)), start, position(nextState)), nextState),
None =>
LexOk(Token(TkInt(charsToInt(digits)), start, position(nextState)), nextState)
},
_ => LexOk(Token(TkInt(charsToInt(digits)), start, position(nextState)), nextState)
}
}
// Identifiers and keywords
else if isAlpha(c) then
let result = collectIdent(state, []);
match result {
(chars, nextState) =>
let name = String.join(List.map(chars, fn(ch) => String.fromChar(ch)), "");
LexOk(Token(identToToken(name), start, position(nextState)), nextState)
}
// String literals
else if c == '"' then
let result = lexStringBody(advance(state), []);
match result {
(chars, nextState) =>
let str = String.join(List.map(chars, fn(ch) => String.fromChar(ch)), "");
LexOk(Token(TkString(str), start, position(nextState)), nextState)
}
// Char literals
else if c == '\'' then
lexCharLiteral(advance(state))
// Doc comments (///)
else if c == '/' then
match peekAt(state, 1) {
Some('/') =>
match peekAt(state, 2) {
Some('/') =>
// Skip the "/// " prefix
let docState = advance(advance(advance(state)));
let docState = match peek(docState) {
Some(' ') => advance(docState),
_ => docState
};
let result = collectDocComment(docState, []);
match result {
(chars, nextState) =>
let text = String.join(List.map(chars, fn(ch) => String.fromChar(ch)), "");
LexOk(Token(TkDocComment(text), start, position(nextState)), nextState)
},
_ => LexOk(Token(TkSlash, start, start + 1), advance(state))
},
_ => LexOk(Token(TkSlash, start, start + 1), advance(state))
}
// Two-character operators
else if c == '=' then
match peekAt(state, 1) {
Some('=') => LexOk(Token(TkEqEq, start, start + 2), advance(advance(state))),
Some('>') => LexOk(Token(TkArrow, start, start + 2), advance(advance(state))),
_ => LexOk(Token(TkEq, start, start + 1), advance(state))
}
else if c == '!' then
match peekAt(state, 1) {
Some('=') => LexOk(Token(TkNe, start, start + 2), advance(advance(state))),
_ => LexOk(Token(TkNot, start, start + 1), advance(state))
}
else if c == '<' then
match peekAt(state, 1) {
Some('=') => LexOk(Token(TkLe, start, start + 2), advance(advance(state))),
_ => LexOk(Token(TkLt, start, start + 1), advance(state))
}
else if c == '>' then
match peekAt(state, 1) {
Some('=') => LexOk(Token(TkGe, start, start + 2), advance(advance(state))),
_ => LexOk(Token(TkGt, start, start + 1), advance(state))
}
else if c == '&' then
match peekAt(state, 1) {
Some('&') => LexOk(Token(TkAnd, start, start + 2), advance(advance(state))),
_ => LexErr("Expected '&&'", start)
}
else if c == '|' then
match peekAt(state, 1) {
Some('|') => LexOk(Token(TkOr, start, start + 2), advance(advance(state))),
Some('>') => LexOk(Token(TkPipeGt, start, start + 2), advance(advance(state))),
_ => LexOk(Token(TkPipe, start, start + 1), advance(state))
}
else if c == '-' then
match peekAt(state, 1) {
Some('>') => LexOk(Token(TkThinArrow, start, start + 2), advance(advance(state))),
_ => LexOk(Token(TkMinus, start, start + 1), advance(state))
}
else if c == ':' then
match peekAt(state, 1) {
Some(':') => LexOk(Token(TkColonColon, start, start + 2), advance(advance(state))),
_ => LexOk(Token(TkColon, start, start + 1), advance(state))
}
// Single-character tokens
else if c == '+' then LexOk(Token(TkPlus, start, start + 1), advance(state))
else if c == '*' then LexOk(Token(TkStar, start, start + 1), advance(state))
else if c == '%' then LexOk(Token(TkPercent, start, start + 1), advance(state))
else if c == '.' then LexOk(Token(TkDot, start, start + 1), advance(state))
else if c == ',' then LexOk(Token(TkComma, start, start + 1), advance(state))
else if c == ';' then LexOk(Token(TkSemi, start, start + 1), advance(state))
else if c == '@' then LexOk(Token(TkAt, start, start + 1), advance(state))
else if c == '(' then LexOk(Token(TkLParen, start, start + 1), advance(state))
else if c == ')' then LexOk(Token(TkRParen, start, start + 1), advance(state))
else if c == '{' then LexOk(Token(TkLBrace, start, start + 1), advance(state))
else if c == '}' then LexOk(Token(TkRBrace, start, start + 1), advance(state))
else if c == '[' then LexOk(Token(TkLBracket, start, start + 1), advance(state))
else if c == ']' then LexOk(Token(TkRBracket, start, start + 1), advance(state))
else if c == '_' then
// Check if it's just underscore or start of ident
match peekAt(state, 1) {
Some(next) =>
if isAlphaNumeric(next) then
let result = collectIdent(state, []);
match result {
(chars, nextState) =>
let name = String.join(List.map(chars, fn(ch) => String.fromChar(ch)), "");
LexOk(Token(TkIdent(name), start, position(nextState)), nextState)
}
else LexOk(Token(TkUnderscore, start, start + 1), advance(state)),
None => LexOk(Token(TkUnderscore, start, start + 1), advance(state))
}
else LexErr("Unexpected character: " + String.fromChar(c), start)
}
// Lex all tokens from source
fn lexAll(state: LexState, acc: List<Token>): List<Token> =
match lexToken(state) {
LexErr(msg, pos) =>
// On error, skip the character and continue
List.concat(acc, [Token(TkEof, pos, pos)]),
LexOk(token, nextState) =>
match token {
Token(TkEof, _, _) => List.concat(acc, [token]),
Token(TkNewline, _, _) =>
// Skip consecutive newlines
lexAll(nextState, List.concat(acc, [token])),
_ => lexAll(nextState, List.concat(acc, [token]))
}
}
// Public API: tokenize a source string
fn tokenize(source: String): List<Token> =
let chars = String.chars(source);
let state = LexState(chars, 0);
lexAll(state, [])
// === Token display ===
fn tokenKindToString(kind: TokenKind): String =
match kind {
TkInt(n) => "Int(" + toString(n) + ")",
TkFloat(s) => "Float(" + s + ")",
TkString(s) => "String(\"" + s + "\")",
TkChar(c) => "Char('" + String.fromChar(c) + "')",
TkBool(b) => if b then "true" else "false",
TkIdent(name) => "Ident(" + name + ")",
TkFn => "fn", TkLet => "let", TkIf => "if",
TkThen => "then", TkElse => "else", TkMatch => "match",
TkWith => "with", TkEffect => "effect", TkHandler => "handler",
TkRun => "run", TkResume => "resume", TkType => "type",
TkImport => "import", TkPub => "pub", TkAs => "as",
TkFrom => "from", TkTrait => "trait", TkImpl => "impl", TkFor => "for",
TkIs => "is", TkPure => "pure", TkTotal => "total",
TkIdempotent => "idempotent", TkDeterministic => "deterministic",
TkCommutative => "commutative", TkWhere => "where", TkAssume => "assume",
TkPlus => "+", TkMinus => "-", TkStar => "*", TkSlash => "/",
TkPercent => "%", TkEq => "=", TkEqEq => "==", TkNe => "!=",
TkLt => "<", TkLe => "<=", TkGt => ">", TkGe => ">=",
TkAnd => "&&", TkOr => "||", TkNot => "!",
TkPipe => "|", TkPipeGt => "|>",
TkArrow => "=>", TkThinArrow => "->",
TkDot => ".", TkColon => ":", TkColonColon => "::",
TkComma => ",", TkSemi => ";", TkAt => "@",
TkLParen => "(", TkRParen => ")", TkLBrace => "{", TkRBrace => "}",
TkLBracket => "[", TkRBracket => "]",
TkUnderscore => "_", TkNewline => "\\n", TkEof => "EOF",
TkDocComment(text) => "DocComment(\"" + text + "\")",
_ => "?"
}
fn tokenToString(token: Token): String =
match token {
Token(kind, start, end) =>
tokenKindToString(kind) + " [" + toString(start) + ".." + toString(end) + "]"
}
// === Tests ===
fn printTokens(tokens: List<Token>): Unit with {Console} =
match List.head(tokens) {
None => Console.print(""),
Some(t) => {
Console.print(" " + tokenToString(t));
match List.tail(tokens) {
Some(rest) => printTokens(rest),
None => Console.print("")
}
}
}
fn testLexer(label: String, source: String): Unit with {Console} = {
Console.print("--- " + label + " ---");
Console.print(" Input: \"" + source + "\"");
let tokens = tokenize(source);
printTokens(tokens)
}
fn main(): Unit with {Console} = {
Console.print("=== Lux Self-Hosted Lexer ===");
Console.print("");
// Basic tokens
testLexer("numbers", "42 3");
Console.print("");
// Identifiers and keywords
testLexer("keywords", "fn main let x");
Console.print("");
// Operators
testLexer("operators", "a + b == c");
Console.print("");
// String literal
testLexer("string", "\"hello world\"");
Console.print("");
// Function declaration
testLexer("function", "fn add(a: Int, b: Int): Int = a + b");
Console.print("");
// Behavioral properties
testLexer("behavioral", "fn add(a: Int): Int is pure = a");
Console.print("");
// Complex expression
testLexer("complex", "let result = if x > 0 then x else 0 - x");
Console.print("");
Console.print("=== Lexer test complete ===")
}
let _ = run main() with {}

213
scripts/release.sh Executable file
View File

@@ -0,0 +1,213 @@
#!/usr/bin/env bash
set -euo pipefail
# Lux Release Script
# Builds a static binary, generates changelog, and creates a Gitea release.
#
# Usage:
# ./scripts/release.sh # auto-bump patch (0.2.0 → 0.2.1)
# ./scripts/release.sh patch # same as above
# ./scripts/release.sh minor # bump minor (0.2.0 → 0.3.0)
# ./scripts/release.sh major # bump major (0.2.0 → 1.0.0)
# ./scripts/release.sh v1.2.3 # explicit version
#
# Environment:
# GITEA_TOKEN - API token for git.qrty.ink (prompted if not set)
# GITEA_URL - Gitea instance URL (default: https://git.qrty.ink)
# cd to repo root (directory containing this script's parent)
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR/.."
GITEA_URL="${GITEA_URL:-https://git.qrty.ink}"
REPO_OWNER="blu"
REPO_NAME="lux"
API_BASE="$GITEA_URL/api/v1"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
info() { printf "${CYAN}::${NC} %s\n" "$1"; }
ok() { printf "${GREEN}ok${NC} %s\n" "$1"; }
warn() { printf "${YELLOW}!!${NC} %s\n" "$1"; }
err() { printf "${RED}error:${NC} %s\n" "$1" >&2; exit 1; }
# --- Determine version ---
CURRENT=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
BUMP="${1:-patch}"
bump_version() {
local ver="$1" part="$2"
IFS='.' read -r major minor patch <<< "$ver"
case "$part" in
major) echo "$((major + 1)).0.0" ;;
minor) echo "$major.$((minor + 1)).0" ;;
patch) echo "$major.$minor.$((patch + 1))" ;;
*) echo "$part" ;; # treat as explicit version
esac
}
case "$BUMP" in
major|minor|patch)
VERSION=$(bump_version "$CURRENT" "$BUMP")
info "Bumping $BUMP: $CURRENT$VERSION"
;;
*)
# Explicit version — strip v prefix if present
VERSION="${BUMP#v}"
info "Explicit version: $VERSION"
;;
esac
TAG="v$VERSION"
# --- Check for clean working tree ---
if [ -n "$(git status --porcelain)" ]; then
warn "Working tree has uncommitted changes:"
git status --short
printf "\n"
read -rp "Continue anyway? [y/N] " confirm
[[ "$confirm" =~ ^[Yy]$ ]] || exit 1
fi
# --- Check if tag already exists ---
if git rev-parse "$TAG" >/dev/null 2>&1; then
err "Tag $TAG already exists. Choose a different version."
fi
# --- Update version in source files ---
if [ "$VERSION" != "$CURRENT" ]; then
info "Updating version in Cargo.toml and flake.nix..."
sed -i "0,/^version = \"$CURRENT\"/s//version = \"$VERSION\"/" Cargo.toml
sed -i "s/version = \"$CURRENT\";/version = \"$VERSION\";/g" flake.nix
sed -i "s/v$CURRENT/v$VERSION/g" flake.nix
git add Cargo.toml flake.nix
git commit --no-gpg-sign -m "chore: bump version to $VERSION"
ok "Version updated and committed"
fi
# --- Generate changelog ---
info "Generating changelog..."
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -n "$LAST_TAG" ]; then
RANGE="$LAST_TAG..HEAD"
info "Changes since $LAST_TAG:"
else
RANGE="HEAD"
info "First release — summarizing recent commits:"
fi
CHANGELOG=$(git log "$RANGE" --pretty=format:"- %s" --no-merges 2>/dev/null | head -50 || true)
if [ -z "$CHANGELOG" ]; then
CHANGELOG="- Initial release"
fi
# --- Build static binary ---
info "Building static binary (nix build .#static)..."
nix build .#static
BINARY="result/bin/lux"
if [ ! -f "$BINARY" ]; then
err "Static binary not found at $BINARY"
fi
BINARY_SIZE=$(ls -lh "$BINARY" | awk '{print $5}')
BINARY_TYPE=$(file "$BINARY" | sed 's/.*: //')
ok "Binary: $BINARY_SIZE, $BINARY_TYPE"
# --- Prepare release artifact ---
ARTIFACT="/tmp/lux-${TAG}-linux-x86_64"
cp "$BINARY" "$ARTIFACT"
chmod +x "$ARTIFACT"
# --- Show release summary ---
printf "\n"
printf "${BOLD}═══ Release Summary ═══${NC}\n"
printf "\n"
printf " ${BOLD}Tag:${NC} %s\n" "$TAG"
printf " ${BOLD}Binary:${NC} %s (%s)\n" "lux-${TAG}-linux-x86_64" "$BINARY_SIZE"
printf " ${BOLD}Commit:${NC} %s\n" "$(git rev-parse --short HEAD)"
printf "\n"
printf "${BOLD}Changelog:${NC}\n"
printf "%s\n" "$CHANGELOG"
printf "\n"
# --- Confirm ---
read -rp "Create release $TAG? [y/N] " confirm
[[ "$confirm" =~ ^[Yy]$ ]] || { info "Aborted."; exit 0; }
# --- Get Gitea token ---
if [ -z "${GITEA_TOKEN:-}" ]; then
printf "\n"
info "Gitea API token required (create at $GITEA_URL/user/settings/applications)"
read -rsp "Token: " GITEA_TOKEN
printf "\n"
fi
if [ -z "$GITEA_TOKEN" ]; then
err "No token provided"
fi
# --- Create and push tag ---
info "Creating tag $TAG..."
git tag -a "$TAG" -m "Release $TAG" --no-sign
ok "Tag created"
info "Pushing tag to origin..."
git push origin "$TAG"
ok "Tag pushed"
# --- Create Gitea release ---
info "Creating release on Gitea..."
RELEASE_BODY=$(printf "## Lux %s\n\n### Changes\n\n%s\n\n### Installation\n\n\`\`\`bash\ncurl -Lo lux %s/%s/%s/releases/download/%s/lux-linux-x86_64\nchmod +x lux\n./lux --version\n\`\`\`" \
"$TAG" "$CHANGELOG" "$GITEA_URL" "$REPO_OWNER" "$REPO_NAME" "$TAG")
RELEASE_JSON=$(jq -n \
--arg tag "$TAG" \
--arg name "Lux $TAG" \
--arg body "$RELEASE_BODY" \
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')
RELEASE_RESPONSE=$(curl -s -X POST \
"$API_BASE/repos/$REPO_OWNER/$REPO_NAME/releases" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "$RELEASE_JSON")
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id // empty')
if [ -z "$RELEASE_ID" ]; then
echo "$RELEASE_RESPONSE" | jq . 2>/dev/null || echo "$RELEASE_RESPONSE"
err "Failed to create release"
fi
ok "Release created (id: $RELEASE_ID)"
# --- Upload binary ---
info "Uploading binary..."
UPLOAD_RESPONSE=$(curl -s -X POST \
"$API_BASE/repos/$REPO_OWNER/$REPO_NAME/releases/$RELEASE_ID/assets?name=lux-linux-x86_64" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$ARTIFACT")
ASSET_NAME=$(echo "$UPLOAD_RESPONSE" | jq -r '.name // empty')
if [ -z "$ASSET_NAME" ]; then
echo "$UPLOAD_RESPONSE" | jq . 2>/dev/null || echo "$UPLOAD_RESPONSE"
err "Failed to upload binary"
fi
ok "Binary uploaded: $ASSET_NAME"
# --- Done ---
printf "\n"
printf "${GREEN}${BOLD}Release $TAG published!${NC}\n"
printf "\n"
printf " ${BOLD}URL:${NC} %s/%s/%s/releases/tag/%s\n" "$GITEA_URL" "$REPO_OWNER" "$REPO_NAME" "$TAG"
printf " ${BOLD}Download:${NC} %s/%s/%s/releases/download/%s/lux-linux-x86_64\n" "$GITEA_URL" "$REPO_OWNER" "$REPO_NAME" "$TAG"
printf "\n"
# Cleanup
rm -f "$ARTIFACT"

211
scripts/validate.sh Executable file
View File

@@ -0,0 +1,211 @@
#!/usr/bin/env bash
set -euo pipefail
# Lux Full Validation Script
# Runs all checks: Rust tests, package tests, type checking, example compilation.
# Run after every committable change to ensure no regressions.
# cd to repo root (directory containing this script's parent)
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR/.."
LUX="$(pwd)/target/release/lux"
PACKAGES_DIR="$(pwd)/../packages"
PROJECTS_DIR="$(pwd)/projects"
EXAMPLES_DIR="$(pwd)/examples"
RED='\033[0;31m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
FAILED=0
TOTAL=0
step() {
TOTAL=$((TOTAL + 1))
printf "${CYAN}[%d]${NC} %s... " "$TOTAL" "$1"
}
ok() { printf "${GREEN}ok${NC} %s\n" "${1:-}"; }
fail() { printf "${RED}FAIL${NC} %s\n" "${1:-}"; FAILED=$((FAILED + 1)); }
# --- Rust checks ---
step "cargo check"
if nix develop --command cargo check 2>/dev/null; then ok; else fail; fi
step "cargo test"
OUTPUT=$(nix develop --command cargo test 2>&1 || true)
RESULT=$(echo "$OUTPUT" | grep "test result:" || echo "no result")
if echo "$RESULT" | grep -q "0 failed"; then ok "$RESULT"; else fail "$RESULT"; fi
# --- Build release binary ---
step "cargo build --release"
if nix develop --command cargo build --release 2>/dev/null; then ok; else fail; fi
# --- Package tests ---
for pkg in path frontmatter xml rss markdown; do
PKG_DIR="$PACKAGES_DIR/$pkg"
if [ -d "$PKG_DIR" ]; then
step "lux test ($pkg)"
OUTPUT=$(cd "$PKG_DIR" && "$LUX" test 2>&1 || true)
RESULT=$(echo "$OUTPUT" | grep "passed" | tail -1 || echo "no result")
if echo "$RESULT" | grep -q "passed"; then ok "$RESULT"; else fail "$RESULT"; fi
fi
done
# --- Lux check on packages ---
for pkg in path frontmatter xml rss markdown; do
PKG_DIR="$PACKAGES_DIR/$pkg"
if [ -d "$PKG_DIR" ]; then
step "lux check ($pkg)"
OUTPUT=$(cd "$PKG_DIR" && "$LUX" check 2>&1 || true)
RESULT=$(echo "$OUTPUT" | grep "passed" | tail -1 || echo "no result")
if echo "$RESULT" | grep -q "passed"; then ok; else fail "$RESULT"; fi
fi
done
# --- Project checks ---
for proj_dir in "$PROJECTS_DIR"/*/; do
proj=$(basename "$proj_dir")
if [ -f "$proj_dir/main.lux" ]; then
step "lux check (project: $proj)"
OUTPUT=$("$LUX" check "$proj_dir/main.lux" 2>&1 || true)
if echo "$OUTPUT" | grep -qi "error"; then fail; else ok; fi
fi
# Check any standalone .lux files in the project
for lux_file in "$proj_dir"/*.lux; do
[ -f "$lux_file" ] || continue
fname=$(basename "$lux_file")
[ "$fname" = "main.lux" ] && continue
step "lux check (project: $proj/$fname)"
OUTPUT=$("$LUX" check "$lux_file" 2>&1 || true)
if echo "$OUTPUT" | grep -qi "error"; then fail; else ok; fi
done
done
# === Compilation & Interpreter Checks ===
# --- Interpreter: examples ---
# Skip: http_api, http, http_router, http_server (network), postgres_demo (db),
# random, property_testing (Random effect), shell (Process), json (File I/O),
# file_io (File I/O), test_math, test_lists (Test effect), stress_shared_rc,
# test_rc_comparison (internal tests), modules/* (need cwd)
INTERP_SKIP="http_api http http_router http_server postgres_demo random property_testing shell json file_io test_math test_lists stress_shared_rc test_rc_comparison"
for f in "$EXAMPLES_DIR"/*.lux; do
name=$(basename "$f" .lux)
skip=false
for s in $INTERP_SKIP; do [ "$name" = "$s" ] && skip=true; done
$skip && continue
step "interpreter (examples/$name)"
if timeout 10 "$LUX" "$f" >/dev/null 2>&1; then ok; else fail; fi
done
# --- Interpreter: examples/standard ---
# Skip: guessing_game (reads stdin)
for f in "$EXAMPLES_DIR"/standard/*.lux; do
[ -f "$f" ] || continue
name=$(basename "$f" .lux)
[ "$name" = "guessing_game" ] && continue
step "interpreter (standard/$name)"
if timeout 10 "$LUX" "$f" >/dev/null 2>&1; then ok; else fail; fi
done
# --- Interpreter: examples/showcase ---
# Skip: task_manager (parse error in current version)
for f in "$EXAMPLES_DIR"/showcase/*.lux; do
[ -f "$f" ] || continue
name=$(basename "$f" .lux)
[ "$name" = "task_manager" ] && continue
step "interpreter (showcase/$name)"
if timeout 10 "$LUX" "$f" >/dev/null 2>&1; then ok; else fail; fi
done
# --- Interpreter: projects ---
# Skip: guessing-game (Random), rest-api (HttpServer)
PROJ_INTERP_SKIP="guessing-game rest-api"
for proj_dir in "$PROJECTS_DIR"/*/; do
proj=$(basename "$proj_dir")
[ -f "$proj_dir/main.lux" ] || continue
skip=false
for s in $PROJ_INTERP_SKIP; do [ "$proj" = "$s" ] && skip=true; done
$skip && continue
step "interpreter (project: $proj)"
if timeout 10 "$LUX" "$proj_dir/main.lux" >/dev/null 2>&1; then ok; else fail; fi
done
# --- JS compilation: examples ---
# Skip files that fail JS compilation (unsupported features)
JS_SKIP="http_api http http_router postgres_demo property_testing json test_lists test_rc_comparison"
for f in "$EXAMPLES_DIR"/*.lux; do
name=$(basename "$f" .lux)
skip=false
for s in $JS_SKIP; do [ "$name" = "$s" ] && skip=true; done
$skip && continue
step "compile JS (examples/$name)"
if "$LUX" compile "$f" --target js -o /tmp/lux_validate.js >/dev/null 2>&1; then ok; else fail; fi
done
# --- JS compilation: examples/standard ---
# Skip: stdlib_demo (uses String.toUpper not in JS backend)
for f in "$EXAMPLES_DIR"/standard/*.lux; do
[ -f "$f" ] || continue
name=$(basename "$f" .lux)
[ "$name" = "stdlib_demo" ] && continue
step "compile JS (standard/$name)"
if "$LUX" compile "$f" --target js -o /tmp/lux_validate.js >/dev/null 2>&1; then ok; else fail; fi
done
# --- JS compilation: examples/showcase ---
# Skip: task_manager (unsupported features)
for f in "$EXAMPLES_DIR"/showcase/*.lux; do
[ -f "$f" ] || continue
name=$(basename "$f" .lux)
[ "$name" = "task_manager" ] && continue
step "compile JS (showcase/$name)"
if "$LUX" compile "$f" --target js -o /tmp/lux_validate.js >/dev/null 2>&1; then ok; else fail; fi
done
# --- JS compilation: projects ---
# Skip: json-parser, rest-api (unsupported features)
JS_PROJ_SKIP="json-parser rest-api"
for proj_dir in "$PROJECTS_DIR"/*/; do
proj=$(basename "$proj_dir")
[ -f "$proj_dir/main.lux" ] || continue
skip=false
for s in $JS_PROJ_SKIP; do [ "$proj" = "$s" ] && skip=true; done
$skip && continue
step "compile JS (project: $proj)"
if "$LUX" compile "$proj_dir/main.lux" --target js -o /tmp/lux_validate.js >/dev/null 2>&1; then ok; else fail; fi
done
# --- C compilation: examples ---
# Only compile examples known to work with C backend
C_EXAMPLES="hello factorial pipelines tailcall jit_test"
for name in $C_EXAMPLES; do
f="$EXAMPLES_DIR/$name.lux"
[ -f "$f" ] || continue
step "compile C (examples/$name)"
if "$LUX" compile "$f" -o /tmp/lux_validate_bin >/dev/null 2>&1; then ok; else fail; fi
done
# --- C compilation: examples/standard ---
C_STD_EXAMPLES="hello_world factorial fizzbuzz primes guessing_game"
for name in $C_STD_EXAMPLES; do
f="$EXAMPLES_DIR/standard/$name.lux"
[ -f "$f" ] || continue
step "compile C (standard/$name)"
if "$LUX" compile "$f" -o /tmp/lux_validate_bin >/dev/null 2>&1; then ok; else fail; fi
done
# --- Cleanup ---
rm -f /tmp/lux_validate.js /tmp/lux_validate_bin
# --- Summary ---
printf "\n${BOLD}═══ Validation Summary ═══${NC}\n"
if [ $FAILED -eq 0 ]; then
printf "${GREEN}All %d checks passed.${NC}\n" "$TOTAL"
else
printf "${RED}%d/%d checks failed.${NC}\n" "$FAILED" "$TOTAL"
exit 1
fi

View File

@@ -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>,
@@ -535,7 +541,9 @@ pub enum Expr {
span: Span, span: Span,
}, },
/// Record literal: { name: "Alice", age: 30 } /// Record literal: { name: "Alice", age: 30 }
/// With optional spread: { ...base, name: "Bob" }
Record { Record {
spread: Option<Box<Expr>>,
fields: Vec<(Ident, Expr)>, fields: Vec<(Ident, Expr)>,
span: Span, span: Span,
}, },
@@ -563,6 +571,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,
@@ -614,7 +623,8 @@ pub enum BinaryOp {
And, And,
Or, Or,
// Other // Other
Pipe, // |> Pipe, // |>
Concat, // ++
} }
impl fmt::Display for BinaryOp { impl fmt::Display for BinaryOp {
@@ -634,6 +644,7 @@ impl fmt::Display for BinaryOp {
BinaryOp::And => write!(f, "&&"), BinaryOp::And => write!(f, "&&"),
BinaryOp::Or => write!(f, "||"), BinaryOp::Or => write!(f, "||"),
BinaryOp::Pipe => write!(f, "|>"), BinaryOp::Pipe => write!(f, "|>"),
BinaryOp::Concat => write!(f, "++"),
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -69,6 +69,8 @@ pub struct JsBackend {
has_handlers: bool, has_handlers: bool,
/// Variable substitutions for let binding /// Variable substitutions for let binding
var_substitutions: HashMap<String, String>, var_substitutions: HashMap<String, String>,
/// Effects actually used in the program (for tree-shaking runtime)
used_effects: HashSet<String>,
} }
impl JsBackend { impl JsBackend {
@@ -90,6 +92,7 @@ impl JsBackend {
effectful_functions: HashSet::new(), effectful_functions: HashSet::new(),
has_handlers: false, has_handlers: false,
var_substitutions: HashMap::new(), var_substitutions: HashMap::new(),
used_effects: HashSet::new(),
} }
} }
@@ -97,9 +100,6 @@ impl JsBackend {
pub fn generate(&mut self, program: &Program) -> Result<String, JsGenError> { pub fn generate(&mut self, program: &Program) -> Result<String, JsGenError> {
self.output.clear(); self.output.clear();
// Emit runtime helpers
self.emit_runtime();
// First pass: collect all function names, types, and effects // First pass: collect all function names, types, and effects
for decl in &program.declarations { for decl in &program.declarations {
match decl { match decl {
@@ -116,6 +116,12 @@ impl JsBackend {
} }
} }
// Collect used effects for tree-shaking
self.collect_used_effects(program);
// Emit runtime helpers (tree-shaken based on used effects)
self.emit_runtime();
// Emit type constructors // Emit type constructors
for decl in &program.declarations { for decl in &program.declarations {
if let Declaration::Type(t) = decl { if let Declaration::Type(t) = decl {
@@ -163,32 +169,181 @@ impl JsBackend {
Ok(self.output.clone()) Ok(self.output.clone())
} }
/// Emit the minimal Lux runtime /// Collect all effects used in the program for runtime tree-shaking
fn collect_used_effects(&mut self, program: &Program) {
for decl in &program.declarations {
match decl {
Declaration::Function(f) => {
for effect in &f.effects {
self.used_effects.insert(effect.name.clone());
}
self.collect_effects_from_expr(&f.body);
}
Declaration::Let(l) => {
self.collect_effects_from_expr(&l.value);
}
Declaration::Handler(h) => {
self.used_effects.insert(h.effect.name.clone());
for imp in &h.implementations {
self.collect_effects_from_expr(&imp.body);
}
}
_ => {}
}
}
}
/// Recursively collect effect names from an expression
fn collect_effects_from_expr(&mut self, expr: &Expr) {
match expr {
Expr::EffectOp { effect, args, .. } => {
self.used_effects.insert(effect.name.clone());
for arg in args {
self.collect_effects_from_expr(arg);
}
}
Expr::Run { expr, handlers, .. } => {
self.collect_effects_from_expr(expr);
for (effect, handler) in handlers {
self.used_effects.insert(effect.name.clone());
self.collect_effects_from_expr(handler);
}
}
Expr::Call { func, args, .. } => {
self.collect_effects_from_expr(func);
for arg in args {
self.collect_effects_from_expr(arg);
}
}
Expr::Lambda { body, effects, .. } => {
for effect in effects {
self.used_effects.insert(effect.name.clone());
}
self.collect_effects_from_expr(body);
}
Expr::Let { value, body, .. } => {
self.collect_effects_from_expr(value);
self.collect_effects_from_expr(body);
}
Expr::If { condition, then_branch, else_branch, .. } => {
self.collect_effects_from_expr(condition);
self.collect_effects_from_expr(then_branch);
self.collect_effects_from_expr(else_branch);
}
Expr::Match { scrutinee, arms, .. } => {
self.collect_effects_from_expr(scrutinee);
for arm in arms {
self.collect_effects_from_expr(&arm.body);
if let Some(guard) = &arm.guard {
self.collect_effects_from_expr(guard);
}
}
}
Expr::Block { statements, result, .. } => {
for stmt in statements {
match stmt {
Statement::Expr(e) => self.collect_effects_from_expr(e),
Statement::Let { value, .. } => self.collect_effects_from_expr(value),
}
}
self.collect_effects_from_expr(result);
}
Expr::BinaryOp { left, right, .. } => {
self.collect_effects_from_expr(left);
self.collect_effects_from_expr(right);
}
Expr::UnaryOp { operand, .. } => {
self.collect_effects_from_expr(operand);
}
Expr::Field { object, .. } => {
self.collect_effects_from_expr(object);
}
Expr::TupleIndex { object, .. } => {
self.collect_effects_from_expr(object);
}
Expr::Record { spread, fields, .. } => {
if let Some(s) = spread {
self.collect_effects_from_expr(s);
}
for (_, expr) in fields {
self.collect_effects_from_expr(expr);
}
}
Expr::Tuple { elements, .. } | Expr::List { elements, .. } => {
for el in elements {
self.collect_effects_from_expr(el);
}
}
Expr::Resume { value, .. } => {
self.collect_effects_from_expr(value);
}
Expr::Literal(_) | Expr::Var(_) => {}
}
}
/// Emit the Lux runtime, tree-shaken based on used effects
fn emit_runtime(&mut self) { fn emit_runtime(&mut self) {
let uses_console = self.used_effects.contains("Console");
let uses_random = self.used_effects.contains("Random");
let uses_time = self.used_effects.contains("Time");
let uses_http = self.used_effects.contains("Http");
let uses_dom = self.used_effects.contains("Dom");
let uses_html = self.used_effects.contains("Html") || uses_dom;
self.writeln("// Lux Runtime"); self.writeln("// Lux Runtime");
self.writeln("const Lux = {"); self.writeln("const Lux = {");
self.indent += 1; self.indent += 1;
// Option helpers // Core helpers — always emitted
self.writeln("Some: (value) => ({ tag: \"Some\", value }),"); self.writeln("Some: (value) => ({ tag: \"Some\", value }),");
self.writeln("None: () => ({ tag: \"None\" }),"); self.writeln("None: () => ({ tag: \"None\" }),");
self.writeln(""); self.writeln("");
// Result helpers
self.writeln("Ok: (value) => ({ tag: \"Ok\", value }),"); self.writeln("Ok: (value) => ({ tag: \"Ok\", value }),");
self.writeln("Err: (error) => ({ tag: \"Err\", error }),"); self.writeln("Err: (error) => ({ tag: \"Err\", error }),");
self.writeln(""); self.writeln("");
// List helpers
self.writeln("Cons: (head, tail) => [head, ...tail],"); self.writeln("Cons: (head, tail) => [head, ...tail],");
self.writeln("Nil: () => [],"); self.writeln("Nil: () => [],");
self.writeln(""); self.writeln("");
// Default handlers for effects // Default handlers — only include effects that are used
self.writeln("defaultHandlers: {"); self.writeln("defaultHandlers: {");
self.indent += 1; self.indent += 1;
// Console effect if uses_console {
self.emit_console_handler();
}
if uses_random {
self.emit_random_handler();
}
if uses_time {
self.emit_time_handler();
}
if uses_http {
self.emit_http_handler();
}
if uses_dom {
self.emit_dom_handler();
}
self.indent -= 1;
self.writeln("},");
// HTML rendering — only if Html or Dom effects are used
if uses_html {
self.emit_html_helpers();
}
// TEA runtime — only if Dom is used
if uses_dom {
self.emit_tea_runtime();
}
self.indent -= 1;
self.writeln("};");
self.writeln("");
}
fn emit_console_handler(&mut self) {
self.writeln("Console: {"); self.writeln("Console: {");
self.indent += 1; self.indent += 1;
self.writeln("print: (msg) => console.log(msg),"); self.writeln("print: (msg) => console.log(msg),");
@@ -207,8 +362,9 @@ impl JsBackend {
self.writeln("readInt: () => parseInt(Lux.defaultHandlers.Console.readLine(), 10)"); self.writeln("readInt: () => parseInt(Lux.defaultHandlers.Console.readLine(), 10)");
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
}
// Random effect fn emit_random_handler(&mut self) {
self.writeln("Random: {"); self.writeln("Random: {");
self.indent += 1; self.indent += 1;
self.writeln("int: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min,"); self.writeln("int: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min,");
@@ -216,16 +372,18 @@ impl JsBackend {
self.writeln("float: () => Math.random()"); self.writeln("float: () => Math.random()");
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
}
// Time effect fn emit_time_handler(&mut self) {
self.writeln("Time: {"); self.writeln("Time: {");
self.indent += 1; self.indent += 1;
self.writeln("now: () => Date.now(),"); self.writeln("now: () => Date.now(),");
self.writeln("sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms))"); self.writeln("sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms))");
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
}
// Http effect (browser/Node compatible) fn emit_http_handler(&mut self) {
self.writeln("Http: {"); self.writeln("Http: {");
self.indent += 1; self.indent += 1;
self.writeln("get: async (url) => {"); self.writeln("get: async (url) => {");
@@ -287,8 +445,9 @@ impl JsBackend {
self.writeln("}"); self.writeln("}");
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
}
// Dom effect (browser only - stubs for Node.js) fn emit_dom_handler(&mut self) {
self.writeln("Dom: {"); self.writeln("Dom: {");
self.indent += 1; self.indent += 1;
@@ -316,7 +475,6 @@ impl JsBackend {
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
// Element creation
self.writeln("createElement: (tag) => {"); self.writeln("createElement: (tag) => {");
self.indent += 1; self.indent += 1;
self.writeln("if (typeof document === 'undefined') return null;"); self.writeln("if (typeof document === 'undefined') return null;");
@@ -331,7 +489,6 @@ impl JsBackend {
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
// DOM manipulation
self.writeln("appendChild: (parent, child) => {"); self.writeln("appendChild: (parent, child) => {");
self.indent += 1; self.indent += 1;
self.writeln("if (parent && child) parent.appendChild(child);"); self.writeln("if (parent && child) parent.appendChild(child);");
@@ -356,7 +513,6 @@ impl JsBackend {
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
// Content
self.writeln("setTextContent: (el, text) => {"); self.writeln("setTextContent: (el, text) => {");
self.indent += 1; self.indent += 1;
self.writeln("if (el) el.textContent = text;"); self.writeln("if (el) el.textContent = text;");
@@ -381,7 +537,6 @@ impl JsBackend {
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
// Attributes
self.writeln("setAttribute: (el, name, value) => {"); self.writeln("setAttribute: (el, name, value) => {");
self.indent += 1; self.indent += 1;
self.writeln("if (el) el.setAttribute(name, value);"); self.writeln("if (el) el.setAttribute(name, value);");
@@ -408,7 +563,6 @@ impl JsBackend {
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
// Classes
self.writeln("addClass: (el, className) => {"); self.writeln("addClass: (el, className) => {");
self.indent += 1; self.indent += 1;
self.writeln("if (el) el.classList.add(className);"); self.writeln("if (el) el.classList.add(className);");
@@ -433,7 +587,6 @@ impl JsBackend {
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
// Styles
self.writeln("setStyle: (el, property, value) => {"); self.writeln("setStyle: (el, property, value) => {");
self.indent += 1; self.indent += 1;
self.writeln("if (el) el.style[property] = value;"); self.writeln("if (el) el.style[property] = value;");
@@ -446,7 +599,6 @@ impl JsBackend {
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
// Form elements
self.writeln("getValue: (el) => {"); self.writeln("getValue: (el) => {");
self.indent += 1; self.indent += 1;
self.writeln("return el ? el.value : '';"); self.writeln("return el ? el.value : '';");
@@ -471,7 +623,6 @@ impl JsBackend {
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
// Events
self.writeln("addEventListener: (el, event, handler) => {"); self.writeln("addEventListener: (el, event, handler) => {");
self.indent += 1; self.indent += 1;
self.writeln("if (el) el.addEventListener(event, handler);"); self.writeln("if (el) el.addEventListener(event, handler);");
@@ -484,7 +635,6 @@ impl JsBackend {
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
// Focus
self.writeln("focus: (el) => {"); self.writeln("focus: (el) => {");
self.indent += 1; self.indent += 1;
self.writeln("if (el && el.focus) el.focus();"); self.writeln("if (el && el.focus) el.focus();");
@@ -497,7 +647,6 @@ impl JsBackend {
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
// Document
self.writeln("getBody: () => {"); self.writeln("getBody: () => {");
self.indent += 1; self.indent += 1;
self.writeln("if (typeof document === 'undefined') return null;"); self.writeln("if (typeof document === 'undefined') return null;");
@@ -512,7 +661,6 @@ impl JsBackend {
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
// Window
self.writeln("getWindow: () => {"); self.writeln("getWindow: () => {");
self.indent += 1; self.indent += 1;
self.writeln("if (typeof window === 'undefined') return null;"); self.writeln("if (typeof window === 'undefined') return null;");
@@ -545,7 +693,6 @@ impl JsBackend {
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
// Scroll
self.writeln("scrollTo: (x, y) => {"); self.writeln("scrollTo: (x, y) => {");
self.indent += 1; self.indent += 1;
self.writeln("if (typeof window !== 'undefined') window.scrollTo(x, y);"); self.writeln("if (typeof window !== 'undefined') window.scrollTo(x, y);");
@@ -558,7 +705,6 @@ impl JsBackend {
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
// Dimensions
self.writeln("getBoundingClientRect: (el) => {"); self.writeln("getBoundingClientRect: (el) => {");
self.indent += 1; self.indent += 1;
self.writeln("if (!el) return { top: 0, left: 0, width: 0, height: 0, right: 0, bottom: 0 };"); self.writeln("if (!el) return { top: 0, left: 0, width: 0, height: 0, right: 0, bottom: 0 };");
@@ -574,13 +720,11 @@ impl JsBackend {
self.indent -= 1; self.indent -= 1;
self.writeln("}"); self.writeln("}");
self.indent -= 1;
self.writeln("}");
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
}
// HTML rendering helpers fn emit_html_helpers(&mut self) {
self.writeln(""); self.writeln("");
self.writeln("// HTML rendering"); self.writeln("// HTML rendering");
self.writeln("renderHtml: (node) => {"); self.writeln("renderHtml: (node) => {");
@@ -682,8 +826,9 @@ impl JsBackend {
self.writeln("return el;"); self.writeln("return el;");
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
}
// TEA (The Elm Architecture) runtime fn emit_tea_runtime(&mut self) {
self.writeln(""); self.writeln("");
self.writeln("// The Elm Architecture (TEA) runtime"); self.writeln("// The Elm Architecture (TEA) runtime");
self.writeln("app: (config) => {"); self.writeln("app: (config) => {");
@@ -727,7 +872,6 @@ impl JsBackend {
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
// Simple app (for string-based views like the counter example)
self.writeln(""); self.writeln("");
self.writeln("// Simple TEA app (string-based view)"); self.writeln("// Simple TEA app (string-based view)");
self.writeln("simpleApp: (config) => {"); self.writeln("simpleApp: (config) => {");
@@ -757,7 +901,6 @@ impl JsBackend {
self.indent -= 1; self.indent -= 1;
self.writeln("},"); self.writeln("},");
// Diff and patch (basic implementation for view_deps optimization)
self.writeln(""); self.writeln("");
self.writeln("// Basic diff - checks if model fields changed"); self.writeln("// Basic diff - checks if model fields changed");
self.writeln("hasChanged: (oldModel, newModel, ...paths) => {"); self.writeln("hasChanged: (oldModel, newModel, ...paths) => {");
@@ -777,11 +920,7 @@ impl JsBackend {
self.writeln("}"); self.writeln("}");
self.writeln("return false;"); self.writeln("return false;");
self.indent -= 1; self.indent -= 1;
self.writeln("}"); self.writeln("},");
self.indent -= 1;
self.writeln("};");
self.writeln("");
} }
/// Collect type information from a type declaration /// Collect type information from a type declaration
@@ -888,7 +1027,8 @@ impl JsBackend {
let prev_has_handlers = self.has_handlers; let prev_has_handlers = self.has_handlers;
self.has_handlers = is_effectful; self.has_handlers = is_effectful;
// Clear var substitutions for this function // Save and clear var substitutions for this function scope
let saved_substitutions = self.var_substitutions.clone();
self.var_substitutions.clear(); self.var_substitutions.clear();
// Emit function body // Emit function body
@@ -896,6 +1036,7 @@ impl JsBackend {
self.writeln(&format!("return {};", body_code)); self.writeln(&format!("return {};", body_code));
self.has_handlers = prev_has_handlers; self.has_handlers = prev_has_handlers;
self.var_substitutions = saved_substitutions;
self.indent -= 1; self.indent -= 1;
self.writeln("}"); self.writeln("}");
@@ -909,13 +1050,16 @@ impl JsBackend {
let val = self.emit_expr(&let_decl.value)?; let val = self.emit_expr(&let_decl.value)?;
let var_name = &let_decl.name.name; let var_name = &let_decl.name.name;
// Check if this is a run expression (often results in undefined) if var_name == "_" {
// We still want to execute it for its side effects // Wildcard binding: just execute for side effects
self.writeln(&format!("const {} = {};", var_name, val)); self.writeln(&format!("{};", val));
} else {
self.writeln(&format!("const {} = {};", var_name, val));
// Register the variable for future use // Register the variable for future use
self.var_substitutions self.var_substitutions
.insert(var_name.clone(), var_name.clone()); .insert(var_name.clone(), var_name.clone());
}
Ok(()) Ok(())
} }
@@ -954,12 +1098,17 @@ impl JsBackend {
let r = self.emit_expr(right)?; let r = self.emit_expr(right)?;
// Check for string concatenation // Check for string concatenation
if matches!(op, BinaryOp::Add) { if matches!(op, BinaryOp::Add | BinaryOp::Concat) {
if self.is_string_expr(left) || self.is_string_expr(right) { if self.is_string_expr(left) || self.is_string_expr(right) {
return Ok(format!("({} + {})", l, r)); return Ok(format!("({} + {})", l, r));
} }
} }
// ++ on lists: use .concat()
if matches!(op, BinaryOp::Concat) {
return Ok(format!("{}.concat({})", l, r));
}
let op_str = match op { let op_str = match op {
BinaryOp::Add => "+", BinaryOp::Add => "+",
BinaryOp::Sub => "-", BinaryOp::Sub => "-",
@@ -974,6 +1123,7 @@ impl JsBackend {
BinaryOp::Ge => ">=", BinaryOp::Ge => ">=",
BinaryOp::And => "&&", BinaryOp::And => "&&",
BinaryOp::Or => "||", BinaryOp::Or => "||",
BinaryOp::Concat => unreachable!("handled above"),
BinaryOp::Pipe => { BinaryOp::Pipe => {
// Pipe operator: x |> f becomes f(x) // Pipe operator: x |> f becomes f(x)
return Ok(format!("{}({})", r, l)); return Ok(format!("{}({})", r, l));
@@ -1034,18 +1184,26 @@ impl JsBackend {
name, value, body, .. name, value, body, ..
} => { } => {
let val = self.emit_expr(value)?; let val = self.emit_expr(value)?;
let var_name = format!("{}_{}", name.name, self.fresh_name());
self.writeln(&format!("const {} = {};", var_name, val)); if name.name == "_" {
// Wildcard binding: just execute for side effects
self.writeln(&format!("{};", val));
} else {
let var_name = format!("{}_{}", name.name, self.fresh_name());
// Add substitution self.writeln(&format!("const {} = {};", var_name, val));
self.var_substitutions
.insert(name.name.clone(), var_name.clone()); // Add substitution
self.var_substitutions
.insert(name.name.clone(), var_name.clone());
}
let body_result = self.emit_expr(body)?; let body_result = self.emit_expr(body)?;
// Remove substitution // Remove substitution
self.var_substitutions.remove(&name.name); if name.name != "_" {
self.var_substitutions.remove(&name.name);
}
Ok(body_result) Ok(body_result)
} }
@@ -1057,6 +1215,31 @@ impl JsBackend {
if module_name.name == "List" { if module_name.name == "List" {
return self.emit_list_operation(&field.name, args); return self.emit_list_operation(&field.name, args);
} }
if module_name.name == "Map" {
return self.emit_map_operation(&field.name, args);
}
}
}
// Int/Float module operations
if let Expr::Field { object, field, .. } = func.as_ref() {
if let Expr::Var(module_name) = object.as_ref() {
if module_name.name == "Int" {
let arg = self.emit_expr(&args[0])?;
match field.name.as_str() {
"toFloat" => return Ok(arg),
"toString" => return Ok(format!("String({})", arg)),
_ => {}
}
}
if module_name.name == "Float" {
let arg = self.emit_expr(&args[0])?;
match field.name.as_str() {
"toInt" => return Ok(format!("Math.trunc({})", arg)),
"toString" => return Ok(format!("String({})", arg)),
_ => {}
}
}
} }
} }
@@ -1066,6 +1249,10 @@ impl JsBackend {
let arg = self.emit_expr(&args[0])?; let arg = self.emit_expr(&args[0])?;
return Ok(format!("String({})", arg)); return Ok(format!("String({})", arg));
} }
if ident.name == "print" {
let arg = self.emit_expr(&args[0])?;
return Ok(format!("console.log({})", arg));
}
} }
let arg_strs: Result<Vec<_>, _> = args.iter().map(|a| self.emit_expr(a)).collect(); let arg_strs: Result<Vec<_>, _> = args.iter().map(|a| self.emit_expr(a)).collect();
@@ -1142,6 +1329,26 @@ impl JsBackend {
return self.emit_math_operation(&operation.name, args); return self.emit_math_operation(&operation.name, args);
} }
// Special case: Int module operations
if effect.name == "Int" {
let arg = self.emit_expr(&args[0])?;
match operation.name.as_str() {
"toFloat" => return Ok(arg), // JS numbers are already floats
"toString" => return Ok(format!("String({})", arg)),
_ => {}
}
}
// Special case: Float module operations
if effect.name == "Float" {
let arg = self.emit_expr(&args[0])?;
match operation.name.as_str() {
"toInt" => return Ok(format!("Math.trunc({})", arg)),
"toString" => return Ok(format!("String({})", arg)),
_ => {}
}
}
// Special case: Result module operations (not an effect) // Special case: Result module operations (not an effect)
if effect.name == "Result" { if effect.name == "Result" {
return self.emit_result_operation(&operation.name, args); return self.emit_result_operation(&operation.name, args);
@@ -1152,6 +1359,11 @@ impl JsBackend {
return self.emit_json_operation(&operation.name, args); return self.emit_json_operation(&operation.name, args);
} }
// Special case: Map module operations (not an effect)
if effect.name == "Map" {
return self.emit_map_operation(&operation.name, args);
}
// Special case: Html module operations (not an effect) // Special case: Html module operations (not an effect)
if effect.name == "Html" { if effect.name == "Html" {
return self.emit_html_operation(&operation.name, args); return self.emit_html_operation(&operation.name, args);
@@ -1197,18 +1409,39 @@ impl JsBackend {
param_names param_names
}; };
// Save handler state // Save state
let prev_has_handlers = self.has_handlers; let prev_has_handlers = self.has_handlers;
let saved_substitutions = self.var_substitutions.clone();
self.has_handlers = !effects.is_empty(); self.has_handlers = !effects.is_empty();
// Register lambda params as themselves (override any outer substitutions)
for p in &all_params {
self.var_substitutions.insert(p.clone(), p.clone());
}
// Capture any statements emitted during body evaluation
let output_start = self.output.len();
let prev_indent = self.indent;
self.indent += 1;
let body_code = self.emit_expr(body)?; let body_code = self.emit_expr(body)?;
self.writeln(&format!("return {};", body_code));
// Extract body statements and restore output
let body_statements = self.output[output_start..].to_string();
self.output.truncate(output_start);
self.indent = prev_indent;
// Restore state
self.has_handlers = prev_has_handlers; self.has_handlers = prev_has_handlers;
self.var_substitutions = saved_substitutions;
let indent_str = " ".repeat(self.indent);
Ok(format!( Ok(format!(
"(function({}) {{ return {}; }})", "(function({}) {{\n{}{}}})",
all_params.join(", "), all_params.join(", "),
body_code body_statements,
indent_str,
)) ))
} }
@@ -1228,10 +1461,15 @@ impl JsBackend {
} }
Statement::Let { name, value, .. } => { Statement::Let { name, value, .. } => {
let val = self.emit_expr(value)?; let val = self.emit_expr(value)?;
let var_name = format!("{}_{}", name.name, self.fresh_name()); if name.name == "_" {
self.writeln(&format!("const {} = {};", var_name, val)); self.writeln(&format!("{};", val));
self.var_substitutions } else {
.insert(name.name.clone(), var_name.clone()); let var_name =
format!("{}_{}", name.name, self.fresh_name());
self.writeln(&format!("const {} = {};", var_name, val));
self.var_substitutions
.insert(name.name.clone(), var_name.clone());
}
} }
} }
} }
@@ -1240,15 +1478,19 @@ impl JsBackend {
self.emit_expr(result) self.emit_expr(result)
} }
Expr::Record { fields, .. } => { Expr::Record {
let field_strs: Result<Vec<_>, _> = fields spread, fields, ..
.iter() } => {
.map(|(name, expr)| { let mut parts = Vec::new();
let val = self.emit_expr(expr)?; if let Some(spread_expr) = spread {
Ok(format!("{}: {}", name.name, val)) let spread_code = self.emit_expr(spread_expr)?;
}) parts.push(format!("...{}", spread_code));
.collect(); }
Ok(format!("{{ {} }}", field_strs?.join(", "))) for (name, expr) in fields {
let val = self.emit_expr(expr)?;
parts.push(format!("{}: {}", name.name, val));
}
Ok(format!("{{ {} }}", parts.join(", ")))
} }
Expr::Tuple { elements, .. } => { Expr::Tuple { elements, .. } => {
@@ -1268,6 +1510,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, ..
} => { } => {
@@ -2062,6 +2309,86 @@ impl JsBackend {
} }
} }
/// Emit Map module operations using JS Map
fn emit_map_operation(
&mut self,
operation: &str,
args: &[Expr],
) -> Result<String, JsGenError> {
match operation {
"new" => Ok("new Map()".to_string()),
"set" => {
let map = self.emit_expr(&args[0])?;
let key = self.emit_expr(&args[1])?;
let val = self.emit_expr(&args[2])?;
Ok(format!(
"(function() {{ var m = new Map({}); m.set({}, {}); return m; }})()",
map, key, val
))
}
"get" => {
let map = self.emit_expr(&args[0])?;
let key = self.emit_expr(&args[1])?;
Ok(format!(
"({0}.has({1}) ? Lux.Some({0}.get({1})) : Lux.None())",
map, key
))
}
"contains" => {
let map = self.emit_expr(&args[0])?;
let key = self.emit_expr(&args[1])?;
Ok(format!("{}.has({})", map, key))
}
"remove" => {
let map = self.emit_expr(&args[0])?;
let key = self.emit_expr(&args[1])?;
Ok(format!(
"(function() {{ var m = new Map({}); m.delete({}); return m; }})()",
map, key
))
}
"keys" => {
let map = self.emit_expr(&args[0])?;
Ok(format!("Array.from({}.keys()).sort()", map))
}
"values" => {
let map = self.emit_expr(&args[0])?;
Ok(format!(
"Array.from({0}.entries()).sort(function(a,b) {{ return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0; }}).map(function(e) {{ return e[1]; }})",
map
))
}
"size" => {
let map = self.emit_expr(&args[0])?;
Ok(format!("{}.size", map))
}
"isEmpty" => {
let map = self.emit_expr(&args[0])?;
Ok(format!("({}.size === 0)", map))
}
"fromList" => {
let list = self.emit_expr(&args[0])?;
Ok(format!("new Map({}.map(function(t) {{ return [t[0], t[1]]; }}))", list))
}
"toList" => {
let map = self.emit_expr(&args[0])?;
Ok(format!(
"Array.from({}.entries()).sort(function(a,b) {{ return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0; }})",
map
))
}
"merge" => {
let m1 = self.emit_expr(&args[0])?;
let m2 = self.emit_expr(&args[1])?;
Ok(format!("new Map([...{}, ...{}])", m1, m2))
}
_ => Err(JsGenError {
message: format!("Unknown Map operation: {}", operation),
span: None,
}),
}
}
/// Emit Html module operations for type-safe HTML construction /// Emit Html module operations for type-safe HTML construction
fn emit_html_operation( fn emit_html_operation(
&mut self, &mut self,
@@ -2333,7 +2660,7 @@ impl JsBackend {
} }
} }
Expr::BinaryOp { op, left, right, .. } => { Expr::BinaryOp { op, left, right, .. } => {
matches!(op, BinaryOp::Add) matches!(op, BinaryOp::Add | BinaryOp::Concat)
&& (self.is_string_expr(left) || self.is_string_expr(right)) && (self.is_string_expr(left) || self.is_string_expr(right))
} }
_ => false, _ => false,
@@ -3732,7 +4059,7 @@ line3"
#[test] #[test]
fn test_js_runtime_generated() { fn test_js_runtime_generated() {
// Test that the Lux runtime is properly generated // Test that the Lux runtime core is always generated
use crate::parser::Parser; use crate::parser::Parser;
let source = r#" let source = r#"
@@ -3743,21 +4070,51 @@ line3"
let mut backend = JsBackend::new(); let mut backend = JsBackend::new();
let js_code = backend.generate(&program).expect("Should generate"); let js_code = backend.generate(&program).expect("Should generate");
// Check that Lux runtime includes key functions // Core runtime is always present
assert!(js_code.contains("const Lux = {"), "Lux object should be defined"); assert!(js_code.contains("const Lux = {"), "Lux object should be defined");
assert!(js_code.contains("Some:"), "Option Some should be defined"); assert!(js_code.contains("Some:"), "Option Some should be defined");
assert!(js_code.contains("None:"), "Option None should be defined"); assert!(js_code.contains("None:"), "Option None should be defined");
assert!(js_code.contains("renderHtml:"), "renderHtml should be defined");
assert!(js_code.contains("renderToDom:"), "renderToDom should be defined"); // Console-only program should NOT include Dom, Html, or TEA sections
assert!(js_code.contains("escapeHtml:"), "escapeHtml should be defined"); assert!(!js_code.contains("Dom:"), "Dom handler should not be in Console-only program");
assert!(js_code.contains("app:"), "TEA app should be defined"); assert!(!js_code.contains("renderHtml:"), "renderHtml should not be in Console-only program");
assert!(js_code.contains("simpleApp:"), "simpleApp should be defined"); assert!(!js_code.contains("app:"), "TEA app should not be in Console-only program");
assert!(js_code.contains("hasChanged:"), "hasChanged should be defined"); assert!(!js_code.contains("Http:"), "Http should not be in Console-only program");
// Console should be present
assert!(js_code.contains("Console:"), "Console handler should exist");
}
#[test]
fn test_js_runtime_tree_shaking_all_effects() {
// Test that all effects are included when all are used
use crate::parser::Parser;
let source = r#"
fn main(): Unit with {Console, Dom} = {
Console.print("Hello")
let _ = Dom.getElementById("app")
()
}
"#;
let program = Parser::parse_source(source).expect("Should parse");
let mut backend = JsBackend::new();
let js_code = backend.generate(&program).expect("Should generate");
assert!(js_code.contains("Console:"), "Console handler should exist");
assert!(js_code.contains("Dom:"), "Dom handler should exist");
assert!(js_code.contains("renderHtml:"), "renderHtml should be defined when Dom is used");
assert!(js_code.contains("renderToDom:"), "renderToDom should be defined when Dom is used");
assert!(js_code.contains("escapeHtml:"), "escapeHtml should be defined when Dom is used");
assert!(js_code.contains("app:"), "TEA app should be defined when Dom is used");
assert!(js_code.contains("simpleApp:"), "simpleApp should be defined when Dom is used");
assert!(js_code.contains("hasChanged:"), "hasChanged should be defined when Dom is used");
} }
#[test] #[test]
fn test_js_runtime_default_handlers() { fn test_js_runtime_default_handlers() {
// Test that default handlers are properly generated // Test that only used effect handlers are generated
use crate::parser::Parser; use crate::parser::Parser;
let source = r#" let source = r#"
@@ -3768,12 +4125,12 @@ line3"
let mut backend = JsBackend::new(); let mut backend = JsBackend::new();
let js_code = backend.generate(&program).expect("Should generate"); let js_code = backend.generate(&program).expect("Should generate");
// Check that default handlers include all effects // Only Console should be present
assert!(js_code.contains("Console:"), "Console handler should exist"); assert!(js_code.contains("Console:"), "Console handler should exist");
assert!(js_code.contains("Random:"), "Random handler should exist"); assert!(!js_code.contains("Random:"), "Random handler should not exist in Console-only program");
assert!(js_code.contains("Time:"), "Time handler should exist"); assert!(!js_code.contains("Time:"), "Time handler should not exist in Console-only program");
assert!(js_code.contains("Http:"), "Http handler should exist"); assert!(!js_code.contains("Http:"), "Http handler should not exist in Console-only program");
assert!(js_code.contains("Dom:"), "Dom handler should exist"); assert!(!js_code.contains("Dom:"), "Dom handler should not exist in Console-only program");
} }
#[test] #[test]

View File

@@ -224,10 +224,31 @@ pub mod colors {
pub const BOLD: &str = "\x1b[1m"; pub const BOLD: &str = "\x1b[1m";
pub const DIM: &str = "\x1b[2m"; pub const DIM: &str = "\x1b[2m";
pub const RED: &str = "\x1b[31m"; pub const RED: &str = "\x1b[31m";
pub const GREEN: &str = "\x1b[32m";
pub const YELLOW: &str = "\x1b[33m"; pub const YELLOW: &str = "\x1b[33m";
pub const BLUE: &str = "\x1b[34m"; pub const BLUE: &str = "\x1b[34m";
pub const MAGENTA: &str = "\x1b[35m";
pub const CYAN: &str = "\x1b[36m"; pub const CYAN: &str = "\x1b[36m";
pub const WHITE: &str = "\x1b[37m"; pub const WHITE: &str = "\x1b[37m";
pub const GRAY: &str = "\x1b[90m";
}
/// Apply color to text, respecting NO_COLOR / TERM=dumb
pub fn c(color: &str, text: &str) -> String {
if supports_color() {
format!("{}{}{}", color, text, colors::RESET)
} else {
text.to_string()
}
}
/// Apply bold + color to text
pub fn bc(color: &str, text: &str) -> String {
if supports_color() {
format!("{}{}{}{}", colors::BOLD, color, text, colors::RESET)
} else {
text.to_string()
}
} }
/// Severity level for diagnostics /// Severity level for diagnostics

View File

@@ -598,6 +598,9 @@ impl Formatter {
Expr::Field { object, field, .. } => { Expr::Field { object, field, .. } => {
format!("{}.{}", self.format_expr(object), field.name) format!("{}.{}", self.format_expr(object), field.name)
} }
Expr::TupleIndex { object, index, .. } => {
format!("{}.{}", self.format_expr(object), index)
}
Expr::If { condition, then_branch, else_branch, .. } => { Expr::If { condition, then_branch, else_branch, .. } => {
format!( format!(
"if {} then {} else {}", "if {} then {} else {}",
@@ -685,15 +688,17 @@ impl Formatter {
.join(", ") .join(", ")
) )
} }
Expr::Record { fields, .. } => { Expr::Record {
format!( spread, fields, ..
"{{ {} }}", } => {
fields let mut parts = Vec::new();
.iter() if let Some(spread_expr) = spread {
.map(|(name, val)| format!("{}: {}", name.name, self.format_expr(val))) parts.push(format!("...{}", self.format_expr(spread_expr)));
.collect::<Vec<_>>() }
.join(", ") for (name, val) in fields {
) parts.push(format!("{}: {}", name.name, self.format_expr(val)));
}
format!("{{ {} }}", parts.join(", "))
} }
Expr::EffectOp { effect, operation, args, .. } => { Expr::EffectOp { effect, operation, args, .. } => {
format!( format!(
@@ -728,7 +733,7 @@ impl Formatter {
match &lit.kind { match &lit.kind {
LiteralKind::Int(n) => n.to_string(), LiteralKind::Int(n) => n.to_string(),
LiteralKind::Float(f) => format!("{}", f), LiteralKind::Float(f) => format!("{}", f),
LiteralKind::String(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")), LiteralKind::String(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"").replace('{', "\\{").replace('}', "\\}")),
LiteralKind::Char(c) => format!("'{}'", c), LiteralKind::Char(c) => format!("'{}'", c),
LiteralKind::Bool(b) => b.to_string(), LiteralKind::Bool(b) => b.to_string(),
LiteralKind::Unit => "()".to_string(), LiteralKind::Unit => "()".to_string(),
@@ -750,6 +755,7 @@ impl Formatter {
BinaryOp::Ge => ">=", BinaryOp::Ge => ">=",
BinaryOp::And => "&&", BinaryOp::And => "&&",
BinaryOp::Or => "||", BinaryOp::Or => "||",
BinaryOp::Concat => "++",
BinaryOp::Pipe => "|>", BinaryOp::Pipe => "|>",
} }
} }

View File

@@ -74,6 +74,9 @@ pub enum BuiltinFn {
MathFloor, MathFloor,
MathCeil, MathCeil,
MathRound, MathRound,
MathSin,
MathCos,
MathAtan2,
// Additional List operations // Additional List operations
ListIsEmpty, ListIsEmpty,
@@ -95,6 +98,12 @@ pub enum BuiltinFn {
StringLastIndexOf, StringLastIndexOf,
StringRepeat, StringRepeat,
// Int/Float operations
IntToString,
IntToFloat,
FloatToString,
FloatToInt,
// JSON operations // JSON operations
JsonParse, JsonParse,
JsonStringify, JsonStringify,
@@ -115,6 +124,20 @@ pub enum BuiltinFn {
JsonString, JsonString,
JsonArray, JsonArray,
JsonObject, JsonObject,
// Map operations
MapNew,
MapSet,
MapGet,
MapContains,
MapRemove,
MapKeys,
MapValues,
MapSize,
MapIsEmpty,
MapFromList,
MapToList,
MapMerge,
} }
/// Runtime value /// Runtime value
@@ -129,6 +152,7 @@ pub enum Value {
List(Vec<Value>), List(Vec<Value>),
Tuple(Vec<Value>), Tuple(Vec<Value>),
Record(HashMap<String, Value>), Record(HashMap<String, Value>),
Map(HashMap<String, Value>),
Function(Rc<Closure>), Function(Rc<Closure>),
Handler(Rc<HandlerValue>), Handler(Rc<HandlerValue>),
/// Built-in function /// Built-in function
@@ -160,6 +184,7 @@ impl Value {
Value::List(_) => "List", Value::List(_) => "List",
Value::Tuple(_) => "Tuple", Value::Tuple(_) => "Tuple",
Value::Record(_) => "Record", Value::Record(_) => "Record",
Value::Map(_) => "Map",
Value::Function(_) => "Function", Value::Function(_) => "Function",
Value::Handler(_) => "Handler", Value::Handler(_) => "Handler",
Value::Builtin(_) => "Function", Value::Builtin(_) => "Function",
@@ -208,6 +233,11 @@ impl Value {
ys.get(k).map(|yv| Value::values_equal(v, yv)).unwrap_or(false) ys.get(k).map(|yv| Value::values_equal(v, yv)).unwrap_or(false)
}) })
} }
(Value::Map(xs), Value::Map(ys)) => {
xs.len() == ys.len() && xs.iter().all(|(k, v)| {
ys.get(k).map(|yv| Value::values_equal(v, yv)).unwrap_or(false)
})
}
(Value::Constructor { name: n1, fields: f1 }, Value::Constructor { name: n2, fields: f2 }) => { (Value::Constructor { name: n1, fields: f1 }, Value::Constructor { name: n2, fields: f2 }) => {
n1 == n2 && f1.len() == f2.len() && f1.iter().zip(f2.iter()).all(|(x, y)| Value::values_equal(x, y)) n1 == n2 && f1.len() == f2.len() && f1.iter().zip(f2.iter()).all(|(x, y)| Value::values_equal(x, y))
} }
@@ -278,6 +308,16 @@ impl TryFromValue for Vec<Value> {
} }
} }
impl TryFromValue for HashMap<String, Value> {
const TYPE_NAME: &'static str = "Map";
fn try_from_value(value: &Value) -> Option<Self> {
match value {
Value::Map(m) => Some(m.clone()),
_ => None,
}
}
}
impl TryFromValue for Value { impl TryFromValue for Value {
const TYPE_NAME: &'static str = "any"; const TYPE_NAME: &'static str = "any";
fn try_from_value(value: &Value) -> Option<Self> { fn try_from_value(value: &Value) -> Option<Self> {
@@ -324,6 +364,18 @@ impl fmt::Display for Value {
} }
write!(f, " }}") write!(f, " }}")
} }
Value::Map(entries) => {
write!(f, "Map {{")?;
let mut sorted: Vec<_> = entries.iter().collect();
sorted.sort_by_key(|(k, _)| (*k).clone());
for (i, (key, value)) in sorted.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "\"{}\": {}", key, value)?;
}
write!(f, "}}")
}
Value::Function(_) => write!(f, "<function>"), Value::Function(_) => write!(f, "<function>"),
Value::Builtin(b) => write!(f, "<builtin:{:?}>", b), Value::Builtin(b) => write!(f, "<builtin:{:?}>", b),
Value::Handler(_) => write!(f, "<handler>"), Value::Handler(_) => write!(f, "<handler>"),
@@ -1068,9 +1120,26 @@ impl Interpreter {
("floor".to_string(), Value::Builtin(BuiltinFn::MathFloor)), ("floor".to_string(), Value::Builtin(BuiltinFn::MathFloor)),
("ceil".to_string(), Value::Builtin(BuiltinFn::MathCeil)), ("ceil".to_string(), Value::Builtin(BuiltinFn::MathCeil)),
("round".to_string(), Value::Builtin(BuiltinFn::MathRound)), ("round".to_string(), Value::Builtin(BuiltinFn::MathRound)),
("sin".to_string(), Value::Builtin(BuiltinFn::MathSin)),
("cos".to_string(), Value::Builtin(BuiltinFn::MathCos)),
("atan2".to_string(), Value::Builtin(BuiltinFn::MathAtan2)),
])); ]));
env.define("Math", math_module); env.define("Math", math_module);
// Int module
let int_module = Value::Record(HashMap::from([
("toString".to_string(), Value::Builtin(BuiltinFn::IntToString)),
("toFloat".to_string(), Value::Builtin(BuiltinFn::IntToFloat)),
]));
env.define("Int", int_module);
// Float module
let float_module = Value::Record(HashMap::from([
("toString".to_string(), Value::Builtin(BuiltinFn::FloatToString)),
("toInt".to_string(), Value::Builtin(BuiltinFn::FloatToInt)),
]));
env.define("Float", float_module);
// JSON module // JSON module
let json_module = Value::Record(HashMap::from([ let json_module = Value::Record(HashMap::from([
("parse".to_string(), Value::Builtin(BuiltinFn::JsonParse)), ("parse".to_string(), Value::Builtin(BuiltinFn::JsonParse)),
@@ -1094,16 +1163,72 @@ impl Interpreter {
("object".to_string(), Value::Builtin(BuiltinFn::JsonObject)), ("object".to_string(), Value::Builtin(BuiltinFn::JsonObject)),
])); ]));
env.define("Json", json_module); env.define("Json", json_module);
// Map module
let map_module = Value::Record(HashMap::from([
("new".to_string(), Value::Builtin(BuiltinFn::MapNew)),
("set".to_string(), Value::Builtin(BuiltinFn::MapSet)),
("get".to_string(), Value::Builtin(BuiltinFn::MapGet)),
("contains".to_string(), Value::Builtin(BuiltinFn::MapContains)),
("remove".to_string(), Value::Builtin(BuiltinFn::MapRemove)),
("keys".to_string(), Value::Builtin(BuiltinFn::MapKeys)),
("values".to_string(), Value::Builtin(BuiltinFn::MapValues)),
("size".to_string(), Value::Builtin(BuiltinFn::MapSize)),
("isEmpty".to_string(), Value::Builtin(BuiltinFn::MapIsEmpty)),
("fromList".to_string(), Value::Builtin(BuiltinFn::MapFromList)),
("toList".to_string(), Value::Builtin(BuiltinFn::MapToList)),
("merge".to_string(), Value::Builtin(BuiltinFn::MapMerge)),
]));
env.define("Map", map_module);
} }
/// Execute a program /// Execute a program
pub fn run(&mut self, program: &Program) -> Result<Value, RuntimeError> { pub fn run(&mut self, program: &Program) -> Result<Value, RuntimeError> {
let mut last_value = Value::Unit; let mut last_value = Value::Unit;
let mut has_main_let = false;
for decl in &program.declarations { for decl in &program.declarations {
// Track if there's a top-level `let main = ...`
if let Declaration::Let(let_decl) = decl {
if let_decl.name.name == "main" {
has_main_let = true;
}
}
last_value = self.eval_declaration(decl)?; last_value = self.eval_declaration(decl)?;
} }
// Auto-invoke main if it was defined as a let binding with a function value
if has_main_let {
if let Some(main_val) = self.global_env.get("main") {
if let Value::Function(ref closure) = main_val {
if closure.params.is_empty() {
let span = Span { start: 0, end: 0 };
let mut result = self.eval_call(main_val.clone(), vec![], span)?;
// Trampoline loop
loop {
match result {
EvalResult::Value(v) => {
last_value = v;
break;
}
EvalResult::Effect(req) => {
last_value = self.handle_effect(req)?;
break;
}
EvalResult::TailCall { func, args, span } => {
result = self.eval_call(func, args, span)?;
}
EvalResult::Resume(v) => {
last_value = v;
break;
}
}
}
}
}
}
}
Ok(last_value) Ok(last_value)
} }
@@ -1415,6 +1540,34 @@ impl Interpreter {
} }
} }
Expr::TupleIndex {
object,
index,
span,
} => {
let obj_val = self.eval_expr(object, env)?;
match obj_val {
Value::Tuple(elements) => {
if *index < elements.len() {
Ok(EvalResult::Value(elements[*index].clone()))
} else {
Err(RuntimeError {
message: format!(
"Tuple index {} out of bounds for tuple with {} elements",
index,
elements.len()
),
span: Some(*span),
})
}
}
_ => Err(RuntimeError {
message: format!("Cannot use tuple index on {}", obj_val.type_name()),
span: Some(*span),
}),
}
}
Expr::Lambda { params, body, .. } => { Expr::Lambda { params, body, .. } => {
let closure = Closure { let closure = Closure {
params: params.iter().map(|p| p.name.name.clone()).collect(), params: params.iter().map(|p| p.name.name.clone()).collect(),
@@ -1481,8 +1634,28 @@ impl Interpreter {
self.eval_expr_tail(result, &block_env, tail) self.eval_expr_tail(result, &block_env, tail)
} }
Expr::Record { fields, .. } => { Expr::Record {
spread, fields, ..
} => {
let mut record = HashMap::new(); let mut record = HashMap::new();
// If there's a spread, evaluate it and start with its fields
if let Some(spread_expr) = spread {
let spread_val = self.eval_expr(spread_expr, env)?;
if let Value::Record(spread_fields) = spread_val {
record = spread_fields;
} else {
return Err(RuntimeError {
message: format!(
"Spread expression must evaluate to a record, got {}",
spread_val.type_name()
),
span: Some(expr.span()),
});
}
}
// Override with explicit fields
for (name, expr) in fields { for (name, expr) in fields {
let val = self.eval_expr(expr, env)?; let val = self.eval_expr(expr, env)?;
record.insert(name.name.clone(), val); record.insert(name.name.clone(), val);
@@ -1555,6 +1728,18 @@ impl Interpreter {
span: Some(span), span: Some(span),
}), }),
}, },
BinaryOp::Concat => match (left, right) {
(Value::String(a), Value::String(b)) => Ok(Value::String(a + &b)),
(Value::List(a), Value::List(b)) => {
let mut result = a;
result.extend(b);
Ok(Value::List(result))
}
(l, r) => Err(RuntimeError {
message: format!("Cannot concatenate {} and {}", l.type_name(), r.type_name()),
span: Some(span),
}),
},
BinaryOp::Sub => match (left, right) { BinaryOp::Sub => match (left, right) {
(Value::Int(a), Value::Int(b)) => Ok(Value::Int(a - b)), (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a - b)),
(Value::Float(a), Value::Float(b)) => Ok(Value::Float(a - b)), (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a - b)),
@@ -1610,6 +1795,7 @@ impl Interpreter {
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a < b)), (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a < b)),
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a < b)), (Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a < b)),
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a < b)), (Value::String(a), Value::String(b)) => Ok(Value::Bool(a < b)),
(Value::Char(a), Value::Char(b)) => Ok(Value::Bool(a < b)),
(l, r) => Err(RuntimeError { (l, r) => Err(RuntimeError {
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()), message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
span: Some(span), span: Some(span),
@@ -1619,6 +1805,7 @@ impl Interpreter {
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a <= b)), (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a <= b)),
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a <= b)), (Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a <= b)),
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a <= b)), (Value::String(a), Value::String(b)) => Ok(Value::Bool(a <= b)),
(Value::Char(a), Value::Char(b)) => Ok(Value::Bool(a <= b)),
(l, r) => Err(RuntimeError { (l, r) => Err(RuntimeError {
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()), message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
span: Some(span), span: Some(span),
@@ -1628,6 +1815,7 @@ impl Interpreter {
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a > b)), (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a > b)),
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a > b)), (Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a > b)),
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a > b)), (Value::String(a), Value::String(b)) => Ok(Value::Bool(a > b)),
(Value::Char(a), Value::Char(b)) => Ok(Value::Bool(a > b)),
(l, r) => Err(RuntimeError { (l, r) => Err(RuntimeError {
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()), message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
span: Some(span), span: Some(span),
@@ -1637,6 +1825,7 @@ impl Interpreter {
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a >= b)), (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a >= b)),
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a >= b)), (Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a >= b)),
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a >= b)), (Value::String(a), Value::String(b)) => Ok(Value::Bool(a >= b)),
(Value::Char(a), Value::Char(b)) => Ok(Value::Bool(a >= b)),
(l, r) => Err(RuntimeError { (l, r) => Err(RuntimeError {
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()), message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
span: Some(span), span: Some(span),
@@ -2219,6 +2408,46 @@ impl Interpreter {
Ok(EvalResult::Value(Value::String(result))) Ok(EvalResult::Value(Value::String(result)))
} }
BuiltinFn::IntToString => {
if args.len() != 1 {
return Err(err("Int.toString requires 1 argument"));
}
match &args[0] {
Value::Int(n) => Ok(EvalResult::Value(Value::String(format!("{}", n)))),
v => Ok(EvalResult::Value(Value::String(format!("{}", v)))),
}
}
BuiltinFn::FloatToString => {
if args.len() != 1 {
return Err(err("Float.toString requires 1 argument"));
}
match &args[0] {
Value::Float(f) => Ok(EvalResult::Value(Value::String(format!("{}", f)))),
v => Ok(EvalResult::Value(Value::String(format!("{}", v)))),
}
}
BuiltinFn::IntToFloat => {
if args.len() != 1 {
return Err(err("Int.toFloat requires 1 argument"));
}
match &args[0] {
Value::Int(n) => Ok(EvalResult::Value(Value::Float(*n as f64))),
v => Err(err(&format!("Int.toFloat expects Int, got {}", v.type_name()))),
}
}
BuiltinFn::FloatToInt => {
if args.len() != 1 {
return Err(err("Float.toInt requires 1 argument"));
}
match &args[0] {
Value::Float(f) => Ok(EvalResult::Value(Value::Int(*f as i64))),
v => Err(err(&format!("Float.toInt expects Float, got {}", v.type_name()))),
}
}
BuiltinFn::TypeOf => { BuiltinFn::TypeOf => {
if args.len() != 1 { if args.len() != 1 {
return Err(err("typeOf requires 1 argument")); return Err(err("typeOf requires 1 argument"));
@@ -2395,6 +2624,45 @@ impl Interpreter {
} }
} }
BuiltinFn::MathSin => {
if args.len() != 1 {
return Err(err("Math.sin requires 1 argument"));
}
match &args[0] {
Value::Float(n) => Ok(EvalResult::Value(Value::Float(n.sin()))),
Value::Int(n) => Ok(EvalResult::Value(Value::Float((*n as f64).sin()))),
v => Err(err(&format!("Math.sin expects number, got {}", v.type_name()))),
}
}
BuiltinFn::MathCos => {
if args.len() != 1 {
return Err(err("Math.cos requires 1 argument"));
}
match &args[0] {
Value::Float(n) => Ok(EvalResult::Value(Value::Float(n.cos()))),
Value::Int(n) => Ok(EvalResult::Value(Value::Float((*n as f64).cos()))),
v => Err(err(&format!("Math.cos expects number, got {}", v.type_name()))),
}
}
BuiltinFn::MathAtan2 => {
if args.len() != 2 {
return Err(err("Math.atan2 requires 2 arguments: y, x"));
}
let y = match &args[0] {
Value::Float(n) => *n,
Value::Int(n) => *n as f64,
v => return Err(err(&format!("Math.atan2 expects number, got {}", v.type_name()))),
};
let x = match &args[1] {
Value::Float(n) => *n,
Value::Int(n) => *n as f64,
v => return Err(err(&format!("Math.atan2 expects number, got {}", v.type_name()))),
};
Ok(EvalResult::Value(Value::Float(y.atan2(x))))
}
// Additional List operations // Additional List operations
BuiltinFn::ListIsEmpty => { BuiltinFn::ListIsEmpty => {
let list = Self::expect_arg_1::<Vec<Value>>(&args, "List.isEmpty", span)?; let list = Self::expect_arg_1::<Vec<Value>>(&args, "List.isEmpty", span)?;
@@ -2884,6 +3152,128 @@ impl Interpreter {
} }
Ok(EvalResult::Value(Value::Json(serde_json::Value::Object(map)))) Ok(EvalResult::Value(Value::Json(serde_json::Value::Object(map))))
} }
// Map operations
BuiltinFn::MapNew => {
Ok(EvalResult::Value(Value::Map(HashMap::new())))
}
BuiltinFn::MapSet => {
if args.len() != 3 {
return Err(err("Map.set requires 3 arguments: map, key, value"));
}
let mut map = match &args[0] {
Value::Map(m) => m.clone(),
v => return Err(err(&format!("Map.set expects Map as first argument, got {}", v.type_name()))),
};
let key = match &args[1] {
Value::String(s) => s.clone(),
v => return Err(err(&format!("Map.set expects String key, got {}", v.type_name()))),
};
map.insert(key, args[2].clone());
Ok(EvalResult::Value(Value::Map(map)))
}
BuiltinFn::MapGet => {
let (map, key) = Self::expect_args_2::<HashMap<String, Value>, String>(&args, "Map.get", span)?;
match map.get(&key) {
Some(v) => Ok(EvalResult::Value(Value::Constructor {
name: "Some".to_string(),
fields: vec![v.clone()],
})),
None => Ok(EvalResult::Value(Value::Constructor {
name: "None".to_string(),
fields: vec![],
})),
}
}
BuiltinFn::MapContains => {
let (map, key) = Self::expect_args_2::<HashMap<String, Value>, String>(&args, "Map.contains", span)?;
Ok(EvalResult::Value(Value::Bool(map.contains_key(&key))))
}
BuiltinFn::MapRemove => {
let (mut map, key) = Self::expect_args_2::<HashMap<String, Value>, String>(&args, "Map.remove", span)?;
map.remove(&key);
Ok(EvalResult::Value(Value::Map(map)))
}
BuiltinFn::MapKeys => {
let map = Self::expect_arg_1::<HashMap<String, Value>>(&args, "Map.keys", span)?;
let mut keys: Vec<String> = map.keys().cloned().collect();
keys.sort();
Ok(EvalResult::Value(Value::List(
keys.into_iter().map(Value::String).collect(),
)))
}
BuiltinFn::MapValues => {
let map = Self::expect_arg_1::<HashMap<String, Value>>(&args, "Map.values", span)?;
let mut entries: Vec<(String, Value)> = map.into_iter().collect();
entries.sort_by(|(a, _), (b, _)| a.cmp(b));
Ok(EvalResult::Value(Value::List(
entries.into_iter().map(|(_, v)| v).collect(),
)))
}
BuiltinFn::MapSize => {
let map = Self::expect_arg_1::<HashMap<String, Value>>(&args, "Map.size", span)?;
Ok(EvalResult::Value(Value::Int(map.len() as i64)))
}
BuiltinFn::MapIsEmpty => {
let map = Self::expect_arg_1::<HashMap<String, Value>>(&args, "Map.isEmpty", span)?;
Ok(EvalResult::Value(Value::Bool(map.is_empty())))
}
BuiltinFn::MapFromList => {
let list = Self::expect_arg_1::<Vec<Value>>(&args, "Map.fromList", span)?;
let mut map = HashMap::new();
for item in list {
match item {
Value::Tuple(fields) if fields.len() == 2 => {
let key = match &fields[0] {
Value::String(s) => s.clone(),
v => return Err(err(&format!("Map.fromList expects (String, V) tuples, got {} key", v.type_name()))),
};
map.insert(key, fields[1].clone());
}
_ => return Err(err("Map.fromList expects List<(String, V)>")),
}
}
Ok(EvalResult::Value(Value::Map(map)))
}
BuiltinFn::MapToList => {
let map = Self::expect_arg_1::<HashMap<String, Value>>(&args, "Map.toList", span)?;
let mut entries: Vec<(String, Value)> = map.into_iter().collect();
entries.sort_by(|(a, _), (b, _)| a.cmp(b));
Ok(EvalResult::Value(Value::List(
entries
.into_iter()
.map(|(k, v)| Value::Tuple(vec![Value::String(k), v]))
.collect(),
)))
}
BuiltinFn::MapMerge => {
if args.len() != 2 {
return Err(err("Map.merge requires 2 arguments: map1, map2"));
}
let mut map1 = match &args[0] {
Value::Map(m) => m.clone(),
v => return Err(err(&format!("Map.merge expects Map as first argument, got {}", v.type_name()))),
};
let map2 = match &args[1] {
Value::Map(m) => m.clone(),
v => return Err(err(&format!("Map.merge expects Map as second argument, got {}", v.type_name()))),
};
for (k, v) in map2 {
map1.insert(k, v);
}
Ok(EvalResult::Value(Value::Map(map1)))
}
} }
} }
@@ -3049,6 +3439,11 @@ impl Interpreter {
b.get(k).map(|bv| self.values_equal(v, bv)).unwrap_or(false) b.get(k).map(|bv| self.values_equal(v, bv)).unwrap_or(false)
}) })
} }
(Value::Map(a), Value::Map(b)) => {
a.len() == b.len() && a.iter().all(|(k, v)| {
b.get(k).map(|bv| self.values_equal(v, bv)).unwrap_or(false)
})
}
( (
Value::Constructor { Value::Constructor {
name: n1, name: n1,
@@ -3824,6 +4219,26 @@ impl Interpreter {
} }
Ok(Value::Unit) Ok(Value::Unit)
} }
("Test", "assertEqualMsg") => {
let expected = request.args.first().cloned().unwrap_or(Value::Unit);
let actual = request.args.get(1).cloned().unwrap_or(Value::Unit);
let label = match request.args.get(2) {
Some(Value::String(s)) => s.clone(),
_ => "Values not equal".to_string(),
};
if Value::values_equal(&expected, &actual) {
self.test_results.borrow_mut().passed += 1;
} else {
self.test_results.borrow_mut().failed += 1;
self.test_results.borrow_mut().failures.push(TestFailure {
message: label,
expected: Some(format!("{}", expected)),
actual: Some(format!("{}", actual)),
});
}
Ok(Value::Unit)
}
("Test", "assertNotEqual") => { ("Test", "assertNotEqual") => {
let a = request.args.first().cloned().unwrap_or(Value::Unit); let a = request.args.first().cloned().unwrap_or(Value::Unit);
let b = request.args.get(1).cloned().unwrap_or(Value::Unit); let b = request.args.get(1).cloned().unwrap_or(Value::Unit);
@@ -4956,6 +5371,7 @@ mod tests {
// Create a simple migration that adds a field // Create a simple migration that adds a field
// Migration: old.name -> { name: old.name, email: "unknown" } // Migration: old.name -> { name: old.name, email: "unknown" }
let migration_body = Expr::Record { let migration_body = Expr::Record {
spread: None,
fields: vec![ fields: vec![
( (
Ident::new("name", Span::default()), Ident::new("name", Span::default()),

View File

@@ -42,6 +42,7 @@ pub enum TokenKind {
Effect, Effect,
Handler, Handler,
Run, Run,
Handle,
Resume, Resume,
Type, Type,
True, True,
@@ -70,6 +71,7 @@ pub enum TokenKind {
// Operators // Operators
Plus, // + Plus, // +
PlusPlus, // ++
Minus, // - Minus, // -
Star, // * Star, // *
Slash, // / Slash, // /
@@ -89,6 +91,7 @@ pub enum TokenKind {
Arrow, // => Arrow, // =>
ThinArrow, // -> ThinArrow, // ->
Dot, // . Dot, // .
DotDotDot, // ...
Colon, // : Colon, // :
ColonColon, // :: ColonColon, // ::
Comma, // , Comma, // ,
@@ -138,6 +141,7 @@ impl fmt::Display for TokenKind {
TokenKind::Effect => write!(f, "effect"), TokenKind::Effect => write!(f, "effect"),
TokenKind::Handler => write!(f, "handler"), TokenKind::Handler => write!(f, "handler"),
TokenKind::Run => write!(f, "run"), TokenKind::Run => write!(f, "run"),
TokenKind::Handle => write!(f, "handle"),
TokenKind::Resume => write!(f, "resume"), TokenKind::Resume => write!(f, "resume"),
TokenKind::Type => write!(f, "type"), TokenKind::Type => write!(f, "type"),
TokenKind::Import => write!(f, "import"), TokenKind::Import => write!(f, "import"),
@@ -160,6 +164,7 @@ impl fmt::Display for TokenKind {
TokenKind::True => write!(f, "true"), TokenKind::True => write!(f, "true"),
TokenKind::False => write!(f, "false"), TokenKind::False => write!(f, "false"),
TokenKind::Plus => write!(f, "+"), TokenKind::Plus => write!(f, "+"),
TokenKind::PlusPlus => write!(f, "++"),
TokenKind::Minus => write!(f, "-"), TokenKind::Minus => write!(f, "-"),
TokenKind::Star => write!(f, "*"), TokenKind::Star => write!(f, "*"),
TokenKind::Slash => write!(f, "/"), TokenKind::Slash => write!(f, "/"),
@@ -179,6 +184,7 @@ impl fmt::Display for TokenKind {
TokenKind::Arrow => write!(f, "=>"), TokenKind::Arrow => write!(f, "=>"),
TokenKind::ThinArrow => write!(f, "->"), TokenKind::ThinArrow => write!(f, "->"),
TokenKind::Dot => write!(f, "."), TokenKind::Dot => write!(f, "."),
TokenKind::DotDotDot => write!(f, "..."),
TokenKind::Colon => write!(f, ":"), TokenKind::Colon => write!(f, ":"),
TokenKind::ColonColon => write!(f, "::"), TokenKind::ColonColon => write!(f, "::"),
TokenKind::Comma => write!(f, ","), TokenKind::Comma => write!(f, ","),
@@ -268,7 +274,14 @@ impl<'a> Lexer<'a> {
let kind = match c { let kind = match c {
// Single-character tokens // Single-character tokens
'+' => TokenKind::Plus, '+' => {
if self.peek() == Some('+') {
self.advance();
TokenKind::PlusPlus
} else {
TokenKind::Plus
}
}
'*' => TokenKind::Star, '*' => TokenKind::Star,
'%' => TokenKind::Percent, '%' => TokenKind::Percent,
'(' => TokenKind::LParen, '(' => TokenKind::LParen,
@@ -364,7 +377,22 @@ impl<'a> Lexer<'a> {
TokenKind::Pipe TokenKind::Pipe
} }
} }
'.' => TokenKind::Dot, '.' => {
if self.peek() == Some('.') {
// Check for ... (need to peek past second dot)
// We look at source directly since we can only peek one ahead
let next_next = self.source[self.pos..].chars().nth(1);
if next_next == Some('.') {
self.advance(); // consume second '.'
self.advance(); // consume third '.'
TokenKind::DotDotDot
} else {
TokenKind::Dot
}
} else {
TokenKind::Dot
}
}
':' => { ':' => {
if self.peek() == Some(':') { if self.peek() == Some(':') {
self.advance(); self.advance();
@@ -493,6 +521,8 @@ impl<'a> Lexer<'a> {
Some('"') => '"', Some('"') => '"',
Some('0') => '\0', Some('0') => '\0',
Some('\'') => '\'', Some('\'') => '\'',
Some('{') => '{',
Some('}') => '}',
Some('x') => { Some('x') => {
// Hex escape \xNN // Hex escape \xNN
let h1 = self.advance().and_then(|c| c.to_digit(16)); let h1 = self.advance().and_then(|c| c.to_digit(16));
@@ -743,6 +773,7 @@ impl<'a> Lexer<'a> {
"effect" => TokenKind::Effect, "effect" => TokenKind::Effect,
"handler" => TokenKind::Handler, "handler" => TokenKind::Handler,
"run" => TokenKind::Run, "run" => TokenKind::Run,
"handle" => TokenKind::Handle,
"resume" => TokenKind::Resume, "resume" => TokenKind::Resume,
"type" => TokenKind::Type, "type" => TokenKind::Type,
"import" => TokenKind::Import, "import" => TokenKind::Import,
@@ -761,6 +792,8 @@ impl<'a> Lexer<'a> {
"commutative" => TokenKind::Commutative, "commutative" => TokenKind::Commutative,
"where" => TokenKind::Where, "where" => TokenKind::Where,
"assume" => TokenKind::Assume, "assume" => TokenKind::Assume,
"and" => TokenKind::And,
"or" => TokenKind::Or,
"true" => TokenKind::Bool(true), "true" => TokenKind::Bool(true),
"false" => TokenKind::Bool(false), "false" => TokenKind::Bool(false),
_ => TokenKind::Ident(ident.to_string()), _ => TokenKind::Ident(ident.to_string()),

1146
src/linter.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@ use crate::formatter::{format as format_source, FormatConfig};
use lsp_server::{Connection, ExtractError, Message, Request, RequestId, Response}; use lsp_server::{Connection, ExtractError, Message, Request, RequestId, Response};
use lsp_types::{ use lsp_types::{
notification::{DidChangeTextDocument, DidOpenTextDocument, Notification}, notification::{DidChangeTextDocument, DidOpenTextDocument, Notification},
request::{Completion, GotoDefinition, HoverRequest, References, DocumentSymbolRequest, Rename, SignatureHelpRequest, Formatting}, request::{Completion, GotoDefinition, HoverRequest, References, DocumentSymbolRequest, Rename, SignatureHelpRequest, Formatting, InlayHintRequest},
CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse, CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse,
Diagnostic, DiagnosticSeverity, DidChangeTextDocumentParams, DidOpenTextDocumentParams, Diagnostic, DiagnosticSeverity, DidChangeTextDocumentParams, DidOpenTextDocumentParams,
GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams,
@@ -28,7 +28,8 @@ use lsp_types::{
TextDocumentSyncKind, Url, ReferenceParams, Location, DocumentSymbolParams, TextDocumentSyncKind, Url, ReferenceParams, Location, DocumentSymbolParams,
DocumentSymbolResponse, SymbolInformation, RenameParams, WorkspaceEdit, TextEdit, DocumentSymbolResponse, SymbolInformation, RenameParams, WorkspaceEdit, TextEdit,
SignatureHelpParams, SignatureHelp, SignatureInformation, ParameterInformation, SignatureHelpParams, SignatureHelp, SignatureInformation, ParameterInformation,
SignatureHelpOptions, DocumentFormattingParams, TextDocumentIdentifier, SignatureHelpOptions, DocumentFormattingParams,
InlayHint, InlayHintKind, InlayHintLabel, InlayHintParams,
}; };
use std::collections::HashMap; use std::collections::HashMap;
use std::error::Error; use std::error::Error;
@@ -88,6 +89,7 @@ impl LspServer {
work_done_progress_options: Default::default(), work_done_progress_options: Default::default(),
}), }),
document_formatting_provider: Some(lsp_types::OneOf::Left(true)), document_formatting_provider: Some(lsp_types::OneOf::Left(true)),
inlay_hint_provider: Some(lsp_types::OneOf::Left(true)),
..Default::default() ..Default::default()
})?; })?;
@@ -191,7 +193,7 @@ impl LspServer {
Err(req) => req, Err(req) => req,
}; };
let _req = match cast_request::<Formatting>(req) { let req = match cast_request::<Formatting>(req) {
Ok((id, params)) => { Ok((id, params)) => {
let result = self.handle_formatting(params); let result = self.handle_formatting(params);
let resp = Response::new_ok(id, result); let resp = Response::new_ok(id, result);
@@ -201,6 +203,16 @@ impl LspServer {
Err(req) => req, Err(req) => req,
}; };
let _req = match cast_request::<InlayHintRequest>(req) {
Ok((id, params)) => {
let result = self.handle_inlay_hints(params);
let resp = Response::new_ok(id, result);
self.connection.sender.send(Message::Response(resp))?;
return Ok(());
}
Err(req) => req,
};
Ok(()) Ok(())
} }
@@ -305,59 +317,227 @@ impl LspServer {
let doc = self.documents.get(&uri)?; let doc = self.documents.get(&uri)?;
let source = &doc.text; let source = &doc.text;
// Try to get info from symbol table first // Try to get info from symbol table first (position-based lookup)
if let Some(ref table) = doc.symbol_table { if let Some(ref table) = doc.symbol_table {
let offset = self.position_to_offset(source, position); let offset = self.position_to_offset(source, position);
if let Some(symbol) = table.definition_at_position(offset) { if let Some(symbol) = table.definition_at_position(offset) {
let signature = symbol.type_signature.as_ref() return Some(self.format_symbol_hover(symbol));
.map(|s| s.as_str())
.unwrap_or(&symbol.name);
let kind_str = match symbol.kind {
SymbolKind::Function => "function",
SymbolKind::Variable => "variable",
SymbolKind::Parameter => "parameter",
SymbolKind::Type => "type",
SymbolKind::TypeParameter => "type parameter",
SymbolKind::Variant => "variant",
SymbolKind::Effect => "effect",
SymbolKind::EffectOperation => "effect operation",
SymbolKind::Field => "field",
SymbolKind::Module => "module",
};
let doc_str = symbol.documentation.as_ref()
.map(|d| format!("\n\n{}", d))
.unwrap_or_default();
return Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: format!("```lux\n{}\n```\n\n*{}*{}", signature, kind_str, doc_str),
}),
range: None,
});
} }
} }
// Fall back to hardcoded info // Get the word under cursor
// Extract the word at the cursor position
let word = self.get_word_at_position(source, position)?; let word = self.get_word_at_position(source, position)?;
// Look up documentation for known symbols // When hovering on a keyword like 'fn', 'type', 'effect', 'let', 'trait',
let info = self.get_symbol_info(&word); // look ahead to find the declaration name and show that symbol's info
if let Some(ref table) = doc.symbol_table {
if matches!(word.as_str(), "fn" | "type" | "effect" | "let" | "trait" | "handler" | "impl") {
let offset = self.position_to_offset(source, position);
if let Some(name) = self.find_next_ident(source, offset + word.len()) {
for sym in table.global_symbols() {
if sym.name == name {
return Some(self.format_symbol_hover(sym));
}
}
}
}
if let Some((signature, doc)) = info { // Try name-based lookup in symbol table (for usage sites)
Some(Hover { for sym in table.global_symbols() {
if sym.name == word {
return Some(self.format_symbol_hover(sym));
}
}
}
// Check for module names (Console, List, String, etc.)
if let Some(hover) = self.get_module_hover(&word) {
return Some(hover);
}
// Rich documentation for behavioral property keywords
if let Some((signature, doc_text)) = self.get_rich_symbol_info(&word) {
return Some(Hover {
contents: HoverContents::Markup(MarkupContent { contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown, kind: MarkupKind::Markdown,
value: format!("```lux\n{}\n```\n\n{}", signature, doc), value: format!("```lux\n{}\n```\n\n{}", signature, doc_text),
}), }),
range: None, range: None,
}) });
} else {
// Return generic info for unknown symbols
None
} }
// Builtin keyword/function info
if let Some((signature, doc_text)) = self.get_symbol_info(&word) {
return Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: format!("```lux\n{}\n```\n\n{}", signature, doc_text),
}),
range: None,
});
}
None
}
/// Format a symbol into a hover response
fn format_symbol_hover(&self, symbol: &crate::symbol_table::Symbol) -> Hover {
let signature = symbol.type_signature.as_ref()
.map(|s| s.as_str())
.unwrap_or(&symbol.name);
let kind_str = match symbol.kind {
SymbolKind::Function => "function",
SymbolKind::Variable => "variable",
SymbolKind::Parameter => "parameter",
SymbolKind::Type => "type",
SymbolKind::TypeParameter => "type parameter",
SymbolKind::Variant => "variant",
SymbolKind::Effect => "effect",
SymbolKind::EffectOperation => "effect operation",
SymbolKind::Field => "field",
SymbolKind::Module => "module",
};
let doc_str = symbol.documentation.as_ref()
.map(|d| format!("\n\n{}", d))
.unwrap_or_default();
let formatted_sig = format_signature_for_hover(signature);
let property_docs = extract_property_docs(signature);
Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: format!(
"```lux\n{}\n```\n*{}*{}{}",
formatted_sig, kind_str, property_docs, doc_str
),
}),
range: None,
}
}
/// Get hover info for built-in module names
fn get_module_hover(&self, name: &str) -> Option<Hover> {
let (sig, doc) = match name {
"Console" => (
"effect Console",
"**Console I/O**\n\n\
- `Console.print(msg: String): Unit` — print to stdout\n\
- `Console.readLine(): String` — read a line from stdin\n\
- `Console.readInt(): Int` — read an integer from stdin",
),
"File" => (
"effect File",
"**File System**\n\n\
- `File.read(path: String): String` — read file contents\n\
- `File.write(path: String, content: String): Unit` — write to file\n\
- `File.append(path: String, content: String): Unit` — append to file\n\
- `File.exists(path: String): Bool` — check if file exists\n\
- `File.delete(path: String): Unit` — delete a file\n\
- `File.list(path: String): List<String>` — list directory",
),
"Http" => (
"effect Http",
"**HTTP Client**\n\n\
- `Http.get(url: String): String` — GET request\n\
- `Http.post(url: String, body: String): String` — POST request\n\
- `Http.put(url: String, body: String): String` — PUT request\n\
- `Http.delete(url: String): String` — DELETE request",
),
"Sql" => (
"effect Sql",
"**SQL Database**\n\n\
- `Sql.open(path: String): Connection` — open database\n\
- `Sql.execute(conn: Connection, sql: String): Unit` — execute SQL\n\
- `Sql.query(conn: Connection, sql: String): List<Row>` — query rows\n\
- `Sql.close(conn: Connection): Unit` — close connection",
),
"Random" => (
"effect Random",
"**Random Number Generation**\n\n\
- `Random.int(min: Int, max: Int): Int` — random integer\n\
- `Random.float(): Float` — random float 0.01.0\n\
- `Random.bool(): Bool` — random boolean",
),
"Time" => (
"effect Time",
"**Time**\n\n\
- `Time.now(): Int` — current Unix timestamp (ms)\n\
- `Time.sleep(ms: Int): Unit` — sleep for milliseconds",
),
"Process" => (
"effect Process",
"**Process / System**\n\n\
- `Process.exec(cmd: String): String` — run shell command\n\
- `Process.env(name: String): String` — get env variable\n\
- `Process.args(): List<String>` — command-line arguments\n\
- `Process.exit(code: Int): Unit` — exit with code",
),
"Math" => (
"module Math",
"**Math Functions**\n\n\
- `Math.abs(n: Int): Int` — absolute value\n\
- `Math.min(a: Int, b: Int): Int` — minimum\n\
- `Math.max(a: Int, b: Int): Int` — maximum\n\
- `Math.sqrt(n: Float): Float` — square root\n\
- `Math.pow(base: Float, exp: Float): Float` — power\n\
- `Math.floor(n: Float): Int` — round down\n\
- `Math.ceil(n: Float): Int` — round up",
),
"List" => (
"module List",
"**List Operations**\n\n\
- `List.map(list, f)` — transform each element\n\
- `List.filter(list, p)` — keep matching elements\n\
- `List.fold(list, init, f)` — reduce to single value\n\
- `List.head(list)` — first element (Option)\n\
- `List.tail(list)` — all except first (Option)\n\
- `List.length(list)` — number of elements\n\
- `List.concat(a, b)` — concatenate lists\n\
- `List.range(start, end)` — integer range\n\
- `List.reverse(list)` — reverse order\n\
- `List.get(list, i)` — element at index (Option)",
),
"String" => (
"module String",
"**String Operations**\n\n\
- `String.length(s)` — string length\n\
- `String.split(s, delim)` — split by delimiter\n\
- `String.join(list, delim)` — join with delimiter\n\
- `String.trim(s)` — trim whitespace\n\
- `String.contains(s, sub)` — check substring\n\
- `String.replace(s, from, to)` — replace occurrences\n\
- `String.startsWith(s, prefix)` — check prefix\n\
- `String.endsWith(s, suffix)` — check suffix\n\
- `String.substring(s, start, end)` — extract range\n\
- `String.chars(s)` — list of characters",
),
"Option" => (
"type Option<A> = Some(A) | None",
"**Optional Value**\n\n\
- `Option.isSome(opt)` — has a value?\n\
- `Option.isNone(opt)` — is empty?\n\
- `Option.getOrElse(opt, default)` — unwrap or default\n\
- `Option.map(opt, f)` — transform if present\n\
- `Option.flatMap(opt, f)` — chain operations",
),
"Result" => (
"type Result<A, E> = Ok(A) | Err(E)",
"**Result of Fallible Operation**\n\n\
- `Result.isOk(r)` — succeeded?\n\
- `Result.isErr(r)` — failed?\n\
- `Result.map(r, f)` — transform success value\n\
- `Result.mapErr(r, f)` — transform error value",
),
_ => return None,
};
Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: format!("```lux\n{}\n```\n{}", sig, doc),
}),
range: None,
})
} }
fn get_word_at_position(&self, source: &str, position: Position) -> Option<String> { fn get_word_at_position(&self, source: &str, position: Position) -> Option<String> {
@@ -383,6 +563,26 @@ impl LspServer {
} }
} }
/// Find the next identifier in source after the given offset (skipping whitespace)
fn find_next_ident(&self, source: &str, start: usize) -> Option<String> {
let chars: Vec<char> = source.chars().collect();
let mut pos = start;
// Skip whitespace
while pos < chars.len() && (chars[pos] == ' ' || chars[pos] == '\t' || chars[pos] == '\n' || chars[pos] == '\r') {
pos += 1;
}
// Collect identifier
let ident_start = pos;
while pos < chars.len() && (chars[pos].is_alphanumeric() || chars[pos] == '_') {
pos += 1;
}
if pos > ident_start {
Some(chars[ident_start..pos].iter().collect())
} else {
None
}
}
fn get_symbol_info(&self, word: &str) -> Option<(&'static str, &'static str)> { fn get_symbol_info(&self, word: &str) -> Option<(&'static str, &'static str)> {
match word { match word {
// Keywords // Keywords
@@ -439,6 +639,84 @@ impl LspServer {
} }
} }
/// Rich documentation for behavioral properties and keywords
fn get_rich_symbol_info(&self, word: &str) -> Option<(String, String)> {
match word {
"pure" => Some((
"is pure".to_string(),
"**Behavioral Property: Pure**\n\n\
A pure function has no side effects and always produces the same output for the same inputs. \
The compiler can safely memoize calls, reorder them, or eliminate duplicates.\n\n\
```lux\nfn add(a: Int, b: Int): Int is pure = a + b\n```\n\n\
**Guarantees:**\n\
- No effect operations (Console, File, Http, etc.)\n\
- Referential transparency: `f(x)` can be replaced with its result\n\
- Enables memoization and common subexpression elimination".to_string(),
)),
"total" => Some((
"is total".to_string(),
"**Behavioral Property: Total**\n\n\
A total function always terminates and never throws exceptions. \
The compiler verifies termination through structural recursion analysis.\n\n\
```lux\nfn factorial(n: Int): Int is total =\n if n <= 0 then 1\n else n * factorial(n - 1)\n```\n\n\
**Guarantees:**\n\
- Always produces a result (no infinite loops)\n\
- Cannot use the `Fail` effect\n\
- Recursive calls must be structurally decreasing".to_string(),
)),
"idempotent" => Some((
"is idempotent".to_string(),
"**Behavioral Property: Idempotent**\n\n\
An idempotent function satisfies `f(f(x)) == f(x)` for all inputs. \
Applying it multiple times has the same effect as applying it once.\n\n\
```lux\nfn abs(x: Int): Int is idempotent =\n if x < 0 then 0 - x else x\n\n\
fn clamp(x: Int): Int is idempotent =\n if x < 0 then 0\n else if x > 100 then 100\n else x\n```\n\n\
**Guarantees:**\n\
- `f(f(x)) == f(x)` for all valid inputs\n\
- Safe to retry without changing outcome\n\
- Compiler can deduplicate consecutive calls".to_string(),
)),
"deterministic" => Some((
"is deterministic".to_string(),
"**Behavioral Property: Deterministic**\n\n\
A deterministic function always produces the same output for the same inputs, \
with no dependence on randomness, time, or external state.\n\n\
```lux\nfn multiply(a: Int, b: Int): Int is deterministic = a * b\n```\n\n\
**Guarantees:**\n\
- Cannot use `Random` or `Time` effects\n\
- Same inputs always produce same outputs\n\
- Results can be cached across runs".to_string(),
)),
"commutative" => Some((
"is commutative".to_string(),
"**Behavioral Property: Commutative**\n\n\
A commutative function satisfies `f(a, b) == f(b, a)`. \
The order of arguments doesn't affect the result.\n\n\
```lux\nfn add(a: Int, b: Int): Int is commutative = a + b\nfn max(a: Int, b: Int): Int is commutative =\n if a > b then a else b\n```\n\n\
**Guarantees:**\n\
- Must have exactly 2 parameters\n\
- `f(a, b) == f(b, a)` for all inputs\n\
- Compiler can normalize argument order for optimization".to_string(),
)),
"run" => Some((
"run expr with { handlers }".to_string(),
"**Effect Handler**\n\n\
Execute an effectful expression with explicit effect handlers. \
Must be bound to a variable at top level.\n\n\
```lux\nlet result = run myFunction() with {\n Console = { /* handler */ }\n}\n```\n\n\
Handlers intercept effect operations and provide implementations.".to_string(),
)),
"with" => Some((
"with {Effect1, Effect2}".to_string(),
"**Effect Declaration / Handler Block**\n\n\
Declares which effects a function may perform, or provides handlers in a `run` expression.\n\n\
```lux\n// In function signature:\nfn greet(name: String): Unit with {Console} =\n Console.print(\"Hello, \" + name)\n\n\
// In run expression:\nlet _ = run greet(\"world\") with {}\n```".to_string(),
)),
_ => None,
}
}
fn handle_completion(&self, params: CompletionParams) -> Option<CompletionResponse> { fn handle_completion(&self, params: CompletionParams) -> Option<CompletionResponse> {
let uri = params.text_document_position.text_document.uri; let uri = params.text_document_position.text_document.uri;
let position = params.text_document_position.position; let position = params.text_document_position.position;
@@ -510,17 +788,11 @@ impl LspServer {
fn position_to_offset(&self, source: &str, position: Position) -> usize { fn position_to_offset(&self, source: &str, position: Position) -> usize {
let mut offset = 0; let mut offset = 0;
let mut line = 0u32; for (line_idx, line) in source.lines().enumerate() {
if line_idx == position.line as usize {
for (i, c) in source.char_indices() { return offset + (position.character as usize).min(line.len());
if line == position.line {
let col = i - offset;
return offset + (position.character as usize).min(col + 1);
}
if c == '\n' {
line += 1;
offset = i + 1;
} }
offset += line.len() + 1; // +1 for newline
} }
source.len() source.len()
} }
@@ -1022,6 +1294,90 @@ impl LspServer {
}) })
} }
fn handle_inlay_hints(&self, params: InlayHintParams) -> Option<Vec<InlayHint>> {
let uri = params.text_document.uri;
let doc = self.documents.get(&uri)?;
let source = &doc.text;
// Parse the document to get AST
let program = Parser::parse_source(source).ok()?;
// Type-check to get inferred types
let mut checker = TypeChecker::new();
let _ = checker.check_program(&program);
let mut hints = Vec::new();
// Collect parameter names for known functions (from symbol table)
let param_names = self.collect_function_params(&program);
for decl in &program.declarations {
match decl {
crate::ast::Declaration::Let(l) => {
// Show inferred type for let bindings without explicit type annotations
if l.typ.is_none() {
if let Some(inferred_type) = checker.get_inferred_type(&l.name.name) {
let type_str = format!(": {}", inferred_type);
let pos = offset_to_position(source, l.name.span.end);
hints.push(InlayHint {
position: pos,
label: InlayHintLabel::String(type_str),
kind: Some(InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
padding_left: Some(false),
padding_right: Some(true),
data: None,
});
}
}
// Walk into the value expression for call-site parameter hints
collect_call_site_hints(source, &l.value, &param_names, &mut hints);
}
crate::ast::Declaration::Function(f) => {
// Walk into the function body for call-site parameter hints
collect_call_site_hints(source, &f.body, &param_names, &mut hints);
}
_ => {}
}
}
if hints.is_empty() {
None
} else {
Some(hints)
}
}
/// Collect parameter names for all functions defined in the program
fn collect_function_params(&self, program: &crate::ast::Program) -> HashMap<String, Vec<String>> {
let mut params = HashMap::new();
for decl in &program.declarations {
if let crate::ast::Declaration::Function(f) = decl {
let names: Vec<String> = f.params.iter()
.map(|p| p.name.name.clone())
.collect();
params.insert(f.name.name.clone(), names);
}
}
// Add builtin function parameter names
params.insert("map".into(), vec!["list".into(), "f".into()]);
params.insert("filter".into(), vec!["list".into(), "predicate".into()]);
params.insert("fold".into(), vec!["list".into(), "init".into(), "f".into()]);
params.insert("concat".into(), vec!["a".into(), "b".into()]);
params.insert("range".into(), vec!["start".into(), "end".into()]);
params.insert("get".into(), vec!["list".into(), "index".into()]);
params.insert("take".into(), vec!["list".into(), "n".into()]);
params.insert("drop".into(), vec!["list".into(), "n".into()]);
params.insert("split".into(), vec!["s".into(), "delimiter".into()]);
params.insert("join".into(), vec!["list".into(), "delimiter".into()]);
params.insert("replace".into(), vec!["s".into(), "old".into(), "new".into()]);
params.insert("substring".into(), vec!["s".into(), "start".into(), "end".into()]);
params.insert("contains".into(), vec!["s".into(), "substr".into()]);
params.insert("getOrElse".into(), vec!["opt".into(), "default".into()]);
params
}
fn handle_formatting(&self, params: DocumentFormattingParams) -> Option<Vec<TextEdit>> { fn handle_formatting(&self, params: DocumentFormattingParams) -> Option<Vec<TextEdit>> {
let uri = params.text_document.uri; let uri = params.text_document.uri;
let doc = self.documents.get(&uri)?; let doc = self.documents.get(&uri)?;
@@ -1061,6 +1417,189 @@ impl LspServer {
} }
/// Convert byte offsets to LSP Position /// Convert byte offsets to LSP Position
/// Format a function signature for hover display, wrapping long lines
fn format_signature_for_hover(sig: &str) -> String {
// If it fits in ~60 chars, keep it on one line
if sig.len() <= 60 {
return sig.to_string();
}
// Try to break at parameter list for function signatures
if let Some(paren_start) = sig.find('(') {
if let Some(paren_end) = sig.rfind(')') {
let prefix = &sig[..paren_start + 1];
let params = &sig[paren_start + 1..paren_end];
let suffix = &sig[paren_end..];
// Split parameters and format each on its own line
let param_parts: Vec<&str> = params.split(", ").collect();
if param_parts.len() > 1 {
let indent = " ";
let formatted_params = param_parts.join(&format!(",\n{}", indent));
return format!("{}\n{}{}\n{}", prefix, indent, formatted_params, suffix);
}
}
}
sig.to_string()
}
/// Extract behavioral property documentation from a signature string
fn extract_property_docs(sig: &str) -> String {
let properties = [
("is pure", "**pure** — no side effects, same output for same inputs"),
("is total", "**total** — always terminates, no exceptions"),
("is idempotent", "**idempotent** — `f(f(x)) == f(x)`"),
("is deterministic", "**deterministic** — no randomness or time dependence"),
("is commutative", "**commutative** — `f(a, b) == f(b, a)`"),
];
let mut found = Vec::new();
for (keyword, description) in &properties {
if sig.contains(keyword) {
found.push(*description);
}
}
if found.is_empty() {
String::new()
} else {
format!("\n\n{}", found.join(" \n"))
}
}
/// Recursively collect parameter name hints at call sites
fn collect_call_site_hints(
source: &str,
expr: &crate::ast::Expr,
param_names: &HashMap<String, Vec<String>>,
hints: &mut Vec<InlayHint>,
) {
use crate::ast::Expr;
match expr {
Expr::Call { func, args, .. } => {
// Get the function name for parameter lookup
let func_name = match func.as_ref() {
Expr::Var(ident) => Some(ident.name.clone()),
// Module.method calls like List.map
Expr::Field { object, field, .. } => {
if let Expr::Var(_) = object.as_ref() {
Some(field.name.clone())
} else {
None
}
}
_ => None,
};
if let Some(name) = func_name {
if let Some(names) = param_names.get(&name) {
for (i, arg) in args.iter().enumerate() {
if let Some(param_name) = names.get(i) {
// Skip hint if the argument is already a variable with the same name
if let Expr::Var(ident) = arg {
if &ident.name == param_name {
continue;
}
}
// Skip hints for single-arg functions (obvious)
if args.len() <= 1 {
continue;
}
let pos = offset_to_position(source, arg.span().start);
hints.push(InlayHint {
position: pos,
label: InlayHintLabel::String(format!("{}:", param_name)),
kind: Some(InlayHintKind::PARAMETER),
text_edits: None,
tooltip: None,
padding_left: Some(false),
padding_right: Some(true),
data: None,
});
}
}
}
}
// Recurse into function expression and arguments
collect_call_site_hints(source, func, param_names, hints);
for arg in args {
collect_call_site_hints(source, arg, param_names, hints);
}
}
Expr::BinaryOp { left, right, .. } => {
collect_call_site_hints(source, left, param_names, hints);
collect_call_site_hints(source, right, param_names, hints);
}
Expr::UnaryOp { operand, .. } => {
collect_call_site_hints(source, operand, param_names, hints);
}
Expr::If { condition, then_branch, else_branch, .. } => {
collect_call_site_hints(source, condition, param_names, hints);
collect_call_site_hints(source, then_branch, param_names, hints);
collect_call_site_hints(source, else_branch, param_names, hints);
}
Expr::Let { value, body, .. } => {
collect_call_site_hints(source, value, param_names, hints);
collect_call_site_hints(source, body, param_names, hints);
}
Expr::Block { statements, result, .. } => {
for stmt in statements {
match stmt {
crate::ast::Statement::Expr(e) => {
collect_call_site_hints(source, e, param_names, hints);
}
crate::ast::Statement::Let { value, .. } => {
collect_call_site_hints(source, value, param_names, hints);
}
}
}
collect_call_site_hints(source, result, param_names, hints);
}
Expr::Match { scrutinee, arms, .. } => {
collect_call_site_hints(source, scrutinee, param_names, hints);
for arm in arms {
collect_call_site_hints(source, &arm.body, param_names, hints);
}
}
Expr::Lambda { body, .. } => {
collect_call_site_hints(source, body, param_names, hints);
}
Expr::Tuple { elements, .. } | Expr::List { elements, .. } => {
for e in elements {
collect_call_site_hints(source, e, param_names, hints);
}
}
Expr::Record { spread, fields, .. } => {
if let Some(spread_expr) = spread {
collect_call_site_hints(source, spread_expr, param_names, hints);
}
for (_, e) in fields {
collect_call_site_hints(source, e, param_names, hints);
}
}
Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => {
collect_call_site_hints(source, object, param_names, hints);
}
Expr::Run { expr, handlers, .. } => {
collect_call_site_hints(source, expr, param_names, hints);
for (_, handler_expr) in handlers {
collect_call_site_hints(source, handler_expr, param_names, hints);
}
}
Expr::Resume { value, .. } => {
collect_call_site_hints(source, value, param_names, hints);
}
Expr::EffectOp { args, .. } => {
for arg in args {
collect_call_site_hints(source, arg, param_names, hints);
}
}
Expr::Literal { .. } | Expr::Var(_) => {}
}
}
fn span_to_range(source: &str, start: usize, end: usize) -> Range { fn span_to_range(source: &str, start: usize, end: usize) -> Range {
let start_pos = offset_to_position(source, start); let start_pos = offset_to_position(source, start);
let end_pos = offset_to_position(source, end); let end_pos = offset_to_position(source, end);

File diff suppressed because it is too large Load Diff

View File

@@ -305,6 +305,11 @@ impl ModuleLoader {
self.cache.iter() self.cache.iter()
} }
/// Get the module cache (for passing to C backend)
pub fn module_cache(&self) -> &HashMap<String, Module> {
&self.cache
}
/// Clear the module cache /// Clear the module cache
pub fn clear_cache(&mut self) { pub fn clear_cache(&mut self) {
self.cache.clear(); self.cache.clear();

View File

@@ -244,6 +244,8 @@ impl Parser {
TokenKind::Let => Ok(Declaration::Let(self.parse_let_decl(visibility, doc)?)), TokenKind::Let => Ok(Declaration::Let(self.parse_let_decl(visibility, doc)?)),
TokenKind::Trait => Ok(Declaration::Trait(self.parse_trait_decl(visibility, doc)?)), TokenKind::Trait => Ok(Declaration::Trait(self.parse_trait_decl(visibility, doc)?)),
TokenKind::Impl => Ok(Declaration::Impl(self.parse_impl_decl()?)), TokenKind::Impl => Ok(Declaration::Impl(self.parse_impl_decl()?)),
TokenKind::Run => Err(self.error("Bare 'run' expressions are not allowed at top level. Use 'let _ = run ...' or 'let result = run ...'")),
TokenKind::Handle => Err(self.error("Bare 'handle' expressions are not allowed at top level. Use 'let _ = handle ...' or 'let result = handle ...'")),
_ => Err(self.error("Expected declaration (fn, effect, handler, type, trait, impl, or let)")), _ => Err(self.error("Expected declaration (fn, effect, handler, type, trait, impl, or let)")),
} }
} }
@@ -878,7 +880,8 @@ impl Parser {
Ok(effects) Ok(effects)
} }
/// Parse behavioral properties: is pure, is total, is idempotent, etc. /// Parse behavioral properties: is pure, total, idempotent, etc.
/// Supports: `is pure`, `is pure is total`, `is pure, total`, `is pure, is total`
fn parse_behavioral_properties(&mut self) -> Result<Vec<BehavioralProperty>, ParseError> { fn parse_behavioral_properties(&mut self) -> Result<Vec<BehavioralProperty>, ParseError> {
let mut properties = Vec::new(); let mut properties = Vec::new();
@@ -900,9 +903,15 @@ impl Parser {
let property = self.parse_single_property()?; let property = self.parse_single_property()?;
properties.push(property); properties.push(property);
// Optional comma for multiple properties: is pure, is total // After first property, allow comma-separated list without repeating 'is'
if self.check(TokenKind::Comma) { while self.check(TokenKind::Comma) {
self.advance(); self.advance(); // consume comma
// Allow optional 'is' after comma: `is pure, is total` or `is pure, total`
if self.check(TokenKind::Is) {
self.advance();
}
let property = self.parse_single_property()?;
properties.push(property);
} }
} }
@@ -1550,6 +1559,7 @@ impl Parser {
loop { loop {
let op = match self.peek_kind() { let op = match self.peek_kind() {
TokenKind::Plus => BinaryOp::Add, TokenKind::Plus => BinaryOp::Add,
TokenKind::PlusPlus => BinaryOp::Concat,
TokenKind::Minus => BinaryOp::Sub, TokenKind::Minus => BinaryOp::Sub,
_ => break, _ => break,
}; };
@@ -1638,6 +1648,20 @@ impl Parser {
} else if self.check(TokenKind::Dot) { } else if self.check(TokenKind::Dot) {
let start = expr.span(); let start = expr.span();
self.advance(); self.advance();
// Check for tuple index access: expr.0, expr.1, etc.
if let TokenKind::Int(n) = self.peek_kind() {
let index = n as usize;
self.advance();
let span = start.merge(self.previous_span());
expr = Expr::TupleIndex {
object: Box::new(expr),
index,
span,
};
continue;
}
let field = self.parse_ident()?; let field = self.parse_ident()?;
// Check if this is an effect operation: Effect.operation(args) // Check if this is an effect operation: Effect.operation(args)
@@ -1673,11 +1697,14 @@ impl Parser {
fn parse_args(&mut self) -> Result<Vec<Expr>, ParseError> { fn parse_args(&mut self) -> Result<Vec<Expr>, ParseError> {
let mut args = Vec::new(); let mut args = Vec::new();
self.skip_newlines();
while !self.check(TokenKind::RParen) { while !self.check(TokenKind::RParen) {
args.push(self.parse_expr()?); args.push(self.parse_expr()?);
self.skip_newlines();
if !self.check(TokenKind::RParen) { if !self.check(TokenKind::RParen) {
self.expect(TokenKind::Comma)?; self.expect(TokenKind::Comma)?;
self.skip_newlines();
} }
} }
@@ -1749,6 +1776,7 @@ impl Parser {
TokenKind::Let => self.parse_let_expr(), TokenKind::Let => self.parse_let_expr(),
TokenKind::Fn => self.parse_lambda_expr(), TokenKind::Fn => self.parse_lambda_expr(),
TokenKind::Run => self.parse_run_expr(), TokenKind::Run => self.parse_run_expr(),
TokenKind::Handle => self.parse_handle_expr(),
TokenKind::Resume => self.parse_resume_expr(), TokenKind::Resume => self.parse_resume_expr(),
// Delimiters // Delimiters
@@ -1766,6 +1794,7 @@ impl Parser {
let condition = Box::new(self.parse_expr()?); let condition = Box::new(self.parse_expr()?);
self.skip_newlines();
self.expect(TokenKind::Then)?; self.expect(TokenKind::Then)?;
self.skip_newlines(); self.skip_newlines();
let then_branch = Box::new(self.parse_expr()?); let then_branch = Box::new(self.parse_expr()?);
@@ -1879,6 +1908,14 @@ impl Parser {
span: token.span, span: token.span,
})) }))
} }
TokenKind::Char(c) => {
let c = *c;
self.advance();
Ok(Pattern::Literal(Literal {
kind: LiteralKind::Char(c),
span: token.span,
}))
}
TokenKind::Ident(name) => { TokenKind::Ident(name) => {
// Check if it starts with uppercase (constructor) or lowercase (variable) // Check if it starts with uppercase (constructor) or lowercase (variable)
if name.chars().next().map_or(false, |c| c.is_uppercase()) { if name.chars().next().map_or(false, |c| c.is_uppercase()) {
@@ -2116,6 +2153,40 @@ impl Parser {
}) })
} }
fn parse_handle_expr(&mut self) -> Result<Expr, ParseError> {
let start = self.current_span();
self.expect(TokenKind::Handle)?;
let expr = Box::new(self.parse_call_expr()?);
self.expect(TokenKind::With)?;
self.expect(TokenKind::LBrace)?;
self.skip_newlines();
let mut handlers = Vec::new();
while !self.check(TokenKind::RBrace) {
let effect = self.parse_ident()?;
self.expect(TokenKind::Eq)?;
let handler = self.parse_expr()?;
handlers.push((effect, handler));
self.skip_newlines();
if self.check(TokenKind::Comma) {
self.advance();
}
self.skip_newlines();
}
let end = self.current_span();
self.expect(TokenKind::RBrace)?;
Ok(Expr::Run {
expr,
handlers,
span: start.merge(end),
})
}
fn parse_resume_expr(&mut self) -> Result<Expr, ParseError> { fn parse_resume_expr(&mut self) -> Result<Expr, ParseError> {
let start = self.current_span(); let start = self.current_span();
self.expect(TokenKind::Resume)?; self.expect(TokenKind::Resume)?;
@@ -2174,6 +2245,11 @@ impl Parser {
})); }));
} }
// Check for record spread: { ...expr, field: val }
if matches!(self.peek_kind(), TokenKind::DotDotDot) {
return self.parse_record_expr_rest(start);
}
// Check if it's a record (ident: expr) or block // Check if it's a record (ident: expr) or block
if matches!(self.peek_kind(), TokenKind::Ident(_)) { if matches!(self.peek_kind(), TokenKind::Ident(_)) {
let lookahead = self.tokens.get(self.pos + 1).map(|t| &t.kind); let lookahead = self.tokens.get(self.pos + 1).map(|t| &t.kind);
@@ -2188,6 +2264,20 @@ impl Parser {
fn parse_record_expr_rest(&mut self, start: Span) -> Result<Expr, ParseError> { fn parse_record_expr_rest(&mut self, start: Span) -> Result<Expr, ParseError> {
let mut fields = Vec::new(); let mut fields = Vec::new();
let mut spread = None;
// Check for spread: { ...expr, ... }
if self.check(TokenKind::DotDotDot) {
self.advance(); // consume ...
let spread_expr = self.parse_expr()?;
spread = Some(Box::new(spread_expr));
self.skip_newlines();
if self.check(TokenKind::Comma) {
self.advance();
}
self.skip_newlines();
}
while !self.check(TokenKind::RBrace) { while !self.check(TokenKind::RBrace) {
let name = self.parse_ident()?; let name = self.parse_ident()?;
@@ -2204,7 +2294,11 @@ impl Parser {
self.expect(TokenKind::RBrace)?; self.expect(TokenKind::RBrace)?;
let span = start.merge(self.previous_span()); let span = start.merge(self.previous_span());
Ok(Expr::Record { fields, span }) Ok(Expr::Record {
spread,
fields,
span,
})
} }
fn parse_block_rest(&mut self, start: Span) -> Result<Expr, ParseError> { fn parse_block_rest(&mut self, start: Span) -> Result<Expr, ParseError> {

View File

@@ -228,13 +228,14 @@ impl SymbolTable {
Declaration::Let(let_decl) => { Declaration::Let(let_decl) => {
let is_public = matches!(let_decl.visibility, Visibility::Public); let is_public = matches!(let_decl.visibility, Visibility::Public);
let type_sig = let_decl.typ.as_ref().map(|t| self.type_expr_to_string(t)); let type_sig = let_decl.typ.as_ref().map(|t| self.type_expr_to_string(t));
let symbol = self.new_symbol( let mut symbol = self.new_symbol(
let_decl.name.name.clone(), let_decl.name.name.clone(),
SymbolKind::Variable, SymbolKind::Variable,
let_decl.span, let_decl.span,
type_sig, type_sig,
is_public, is_public,
); );
symbol.documentation = let_decl.doc.clone();
let id = self.add_symbol(scope_idx, symbol); let id = self.add_symbol(scope_idx, symbol);
self.add_reference(id, let_decl.name.span, true, true); self.add_reference(id, let_decl.name.span, true, true);
@@ -263,15 +264,30 @@ impl SymbolTable {
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", ")) .join(", "))
}; };
let type_sig = format!("fn {}({}): {}{}", f.name.name, param_types.join(", "), return_type, effects); let properties = if f.properties.is_empty() {
String::new()
} else {
format!(" is {}", f.properties.iter()
.map(|p| match p {
crate::ast::BehavioralProperty::Pure => "pure",
crate::ast::BehavioralProperty::Total => "total",
crate::ast::BehavioralProperty::Idempotent => "idempotent",
crate::ast::BehavioralProperty::Deterministic => "deterministic",
crate::ast::BehavioralProperty::Commutative => "commutative",
})
.collect::<Vec<_>>()
.join(", "))
};
let type_sig = format!("fn {}({}): {}{}{}", f.name.name, param_types.join(", "), return_type, properties, effects);
let symbol = self.new_symbol( let mut symbol = self.new_symbol(
f.name.name.clone(), f.name.name.clone(),
SymbolKind::Function, SymbolKind::Function,
f.name.span, f.name.span,
Some(type_sig), Some(type_sig),
is_public, is_public,
); );
symbol.documentation = f.doc.clone();
let fn_id = self.add_symbol(scope_idx, symbol); let fn_id = self.add_symbol(scope_idx, symbol);
self.add_reference(fn_id, f.name.span, true, false); self.add_reference(fn_id, f.name.span, true, false);
@@ -312,13 +328,14 @@ impl SymbolTable {
let is_public = matches!(t.visibility, Visibility::Public); let is_public = matches!(t.visibility, Visibility::Public);
let type_sig = format!("type {}", t.name.name); let type_sig = format!("type {}", t.name.name);
let symbol = self.new_symbol( let mut symbol = self.new_symbol(
t.name.name.clone(), t.name.name.clone(),
SymbolKind::Type, SymbolKind::Type,
t.name.span, t.name.span,
Some(type_sig), Some(type_sig),
is_public, is_public,
); );
symbol.documentation = t.doc.clone();
let type_id = self.add_symbol(scope_idx, symbol); let type_id = self.add_symbol(scope_idx, symbol);
self.add_reference(type_id, t.name.span, true, false); self.add_reference(type_id, t.name.span, true, false);
@@ -358,13 +375,14 @@ impl SymbolTable {
let is_public = true; // Effects are typically public let is_public = true; // Effects are typically public
let type_sig = format!("effect {}", e.name.name); let type_sig = format!("effect {}", e.name.name);
let symbol = self.new_symbol( let mut symbol = self.new_symbol(
e.name.name.clone(), e.name.name.clone(),
SymbolKind::Effect, SymbolKind::Effect,
e.name.span, e.name.span,
Some(type_sig), Some(type_sig),
is_public, is_public,
); );
symbol.documentation = e.doc.clone();
let effect_id = self.add_symbol(scope_idx, symbol); let effect_id = self.add_symbol(scope_idx, symbol);
// Add operations // Add operations
@@ -395,13 +413,14 @@ impl SymbolTable {
let is_public = matches!(t.visibility, Visibility::Public); let is_public = matches!(t.visibility, Visibility::Public);
let type_sig = format!("trait {}", t.name.name); let type_sig = format!("trait {}", t.name.name);
let symbol = self.new_symbol( let mut symbol = self.new_symbol(
t.name.name.clone(), t.name.name.clone(),
SymbolKind::Type, // Traits are like types SymbolKind::Type, // Traits are like types
t.name.span, t.name.span,
Some(type_sig), Some(type_sig),
is_public, is_public,
); );
symbol.documentation = t.doc.clone();
self.add_symbol(scope_idx, symbol); self.add_symbol(scope_idx, symbol);
} }
@@ -465,7 +484,7 @@ impl SymbolTable {
self.visit_expr(arg, scope_idx); self.visit_expr(arg, scope_idx);
} }
} }
Expr::Field { object, .. } => { Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => {
self.visit_expr(object, scope_idx); self.visit_expr(object, scope_idx);
} }
Expr::If { condition, then_branch, else_branch, .. } => { Expr::If { condition, then_branch, else_branch, .. } => {
@@ -508,7 +527,10 @@ impl SymbolTable {
self.visit_expr(e, scope_idx); self.visit_expr(e, scope_idx);
} }
} }
Expr::Record { fields, .. } => { Expr::Record { spread, fields, .. } => {
if let Some(spread_expr) = spread {
self.visit_expr(spread_expr, scope_idx);
}
for (_, e) in fields { for (_, e) in fields {
self.visit_expr(e, scope_idx); self.visit_expr(e, scope_idx);
} }

View File

@@ -335,11 +335,14 @@ fn references_params(expr: &Expr, params: &[&str]) -> bool {
Statement::Expr(e) => references_params(e, params), Statement::Expr(e) => references_params(e, params),
}) || references_params(result, params) }) || references_params(result, params)
} }
Expr::Field { object, .. } => references_params(object, params), Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => references_params(object, params),
Expr::Lambda { body, .. } => references_params(body, params), Expr::Lambda { body, .. } => references_params(body, params),
Expr::Tuple { elements, .. } => elements.iter().any(|e| references_params(e, params)), Expr::Tuple { elements, .. } => elements.iter().any(|e| references_params(e, params)),
Expr::List { elements, .. } => elements.iter().any(|e| references_params(e, params)), Expr::List { elements, .. } => elements.iter().any(|e| references_params(e, params)),
Expr::Record { fields, .. } => fields.iter().any(|(_, e)| references_params(e, params)), Expr::Record { spread, fields, .. } => {
spread.as_ref().is_some_and(|s| references_params(s, params))
|| fields.iter().any(|(_, e)| references_params(e, params))
}
Expr::Match { scrutinee, arms, .. } => { Expr::Match { scrutinee, arms, .. } => {
references_params(scrutinee, params) references_params(scrutinee, params)
|| arms.iter().any(|a| references_params(&a.body, params)) || arms.iter().any(|a| references_params(&a.body, params))
@@ -516,10 +519,11 @@ fn has_recursive_calls(func_name: &str, body: &Expr) -> bool {
Expr::Tuple { elements, .. } | Expr::List { elements, .. } => { Expr::Tuple { elements, .. } | Expr::List { elements, .. } => {
elements.iter().any(|e| has_recursive_calls(func_name, e)) elements.iter().any(|e| has_recursive_calls(func_name, e))
} }
Expr::Record { fields, .. } => { Expr::Record { spread, fields, .. } => {
fields.iter().any(|(_, e)| has_recursive_calls(func_name, e)) spread.as_ref().is_some_and(|s| has_recursive_calls(func_name, s))
|| fields.iter().any(|(_, e)| has_recursive_calls(func_name, e))
} }
Expr::Field { object, .. } => has_recursive_calls(func_name, object), Expr::Field { object, .. } | Expr::TupleIndex { object, .. } => has_recursive_calls(func_name, object),
Expr::Let { value, body, .. } => { Expr::Let { value, body, .. } => {
has_recursive_calls(func_name, value) || has_recursive_calls(func_name, body) has_recursive_calls(func_name, value) || has_recursive_calls(func_name, body)
} }
@@ -672,6 +676,7 @@ fn generate_auto_migration_expr(
// Build the record expression // Build the record expression
Some(Expr::Record { Some(Expr::Record {
spread: None,
fields: field_exprs, fields: field_exprs,
span, span,
}) })
@@ -759,6 +764,17 @@ impl TypeChecker {
self.env.bindings.get(name) self.env.bindings.get(name)
} }
/// Get the inferred type of a binding as a display string (for LSP inlay hints)
pub fn get_inferred_type(&self, name: &str) -> Option<String> {
let scheme = self.env.bindings.get(name)?;
let type_str = scheme.typ.to_string();
// Skip unhelpful types
if type_str == "<error>" || type_str.contains('?') {
return None;
}
Some(type_str)
}
/// Get auto-generated migrations from type checking /// Get auto-generated migrations from type checking
/// Returns: type_name -> from_version -> migration_body /// Returns: type_name -> from_version -> migration_body
pub fn get_auto_migrations(&self) -> &HashMap<String, HashMap<u32, Expr>> { pub fn get_auto_migrations(&self) -> &HashMap<String, HashMap<u32, Expr>> {
@@ -1525,7 +1541,7 @@ impl TypeChecker {
// Use the declared type if present, otherwise use inferred // Use the declared type if present, otherwise use inferred
let final_type = if let Some(ref type_expr) = let_decl.typ { let final_type = if let Some(ref type_expr) = let_decl.typ {
let declared = self.resolve_type(type_expr); let declared = self.resolve_type(type_expr);
if let Err(e) = unify(&inferred, &declared) { if let Err(e) = unify_with_env(&inferred, &declared, &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!( message: format!(
"Variable '{}' has type {}, but declared type is {}: {}", "Variable '{}' has type {}, but declared type is {}: {}",
@@ -1662,6 +1678,42 @@ impl TypeChecker {
span, span,
} => self.infer_field(object, field, *span), } => self.infer_field(object, field, *span),
Expr::TupleIndex {
object,
index,
span,
} => {
let object_type = self.infer_expr(object);
match &object_type {
Type::Tuple(types) => {
if *index < types.len() {
types[*index].clone()
} else {
self.errors.push(TypeError {
message: format!(
"Tuple index {} out of bounds for tuple with {} elements",
index,
types.len()
),
span: *span,
});
Type::Error
}
}
Type::Var(_) => Type::var(),
_ => {
self.errors.push(TypeError {
message: format!(
"Cannot use tuple index on non-tuple type {}",
object_type
),
span: *span,
});
Type::Error
}
}
}
Expr::Lambda { Expr::Lambda {
params, params,
return_type, return_type,
@@ -1697,7 +1749,11 @@ impl TypeChecker {
span, span,
} => self.infer_block(statements, result, *span), } => self.infer_block(statements, result, *span),
Expr::Record { fields, span } => self.infer_record(fields, *span), Expr::Record {
spread,
fields,
span,
} => self.infer_record(spread.as_deref(), fields, *span),
Expr::Tuple { elements, span } => self.infer_tuple(elements, *span), Expr::Tuple { elements, span } => self.infer_tuple(elements, *span),
@@ -1736,7 +1792,7 @@ impl TypeChecker {
match op { match op {
BinaryOp::Add => { BinaryOp::Add => {
// Add supports both numeric types and string concatenation // Add supports both numeric types and string concatenation
if let Err(e) = unify(&left_type, &right_type) { if let Err(e) = unify_with_env(&left_type, &right_type, &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!("Operands of '{}' must have same type: {}", op, e), message: format!("Operands of '{}' must have same type: {}", op, e),
span, span,
@@ -1757,9 +1813,32 @@ impl TypeChecker {
} }
} }
BinaryOp::Concat => {
// Concat (++) supports strings and lists
if let Err(e) = unify_with_env(&left_type, &right_type, &self.env) {
self.errors.push(TypeError {
message: format!("Operands of '++' must have same type: {}", e),
span,
});
}
match &left_type {
Type::String | Type::List(_) | Type::Var(_) => left_type,
_ => {
self.errors.push(TypeError {
message: format!(
"Operator '++' requires String or List operands, got {}",
left_type
),
span,
});
Type::Error
}
}
}
BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => { BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => {
// Arithmetic: both operands must be same numeric type // Arithmetic: both operands must be same numeric type
if let Err(e) = unify(&left_type, &right_type) { if let Err(e) = unify_with_env(&left_type, &right_type, &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!("Operands of '{}' must have same type: {}", op, e), message: format!("Operands of '{}' must have same type: {}", op, e),
span, span,
@@ -1783,7 +1862,7 @@ impl TypeChecker {
BinaryOp::Eq | BinaryOp::Ne => { BinaryOp::Eq | BinaryOp::Ne => {
// Equality: operands must have same type // Equality: operands must have same type
if let Err(e) = unify(&left_type, &right_type) { if let Err(e) = unify_with_env(&left_type, &right_type, &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!("Operands of '{}' must have same type: {}", op, e), message: format!("Operands of '{}' must have same type: {}", op, e),
span, span,
@@ -1794,7 +1873,7 @@ impl TypeChecker {
BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge => { BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge => {
// Comparison: operands must be same orderable type // Comparison: operands must be same orderable type
if let Err(e) = unify(&left_type, &right_type) { if let Err(e) = unify_with_env(&left_type, &right_type, &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!("Operands of '{}' must have same type: {}", op, e), message: format!("Operands of '{}' must have same type: {}", op, e),
span, span,
@@ -1805,13 +1884,13 @@ impl TypeChecker {
BinaryOp::And | BinaryOp::Or => { BinaryOp::And | BinaryOp::Or => {
// Logical: both must be Bool // Logical: both must be Bool
if let Err(e) = unify(&left_type, &Type::Bool) { if let Err(e) = unify_with_env(&left_type, &Type::Bool, &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!("Left operand of '{}' must be Bool: {}", op, e), message: format!("Left operand of '{}' must be Bool: {}", op, e),
span: left.span(), span: left.span(),
}); });
} }
if let Err(e) = unify(&right_type, &Type::Bool) { if let Err(e) = unify_with_env(&right_type, &Type::Bool, &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!("Right operand of '{}' must be Bool: {}", op, e), message: format!("Right operand of '{}' must be Bool: {}", op, e),
span: right.span(), span: right.span(),
@@ -1825,7 +1904,7 @@ impl TypeChecker {
// right must be a function that accepts left's type // right must be a function that accepts left's type
let result_type = Type::var(); let result_type = Type::var();
let expected_fn = Type::function(vec![left_type.clone()], result_type.clone()); let expected_fn = Type::function(vec![left_type.clone()], result_type.clone());
if let Err(e) = unify(&right_type, &expected_fn) { if let Err(e) = unify_with_env(&right_type, &expected_fn, &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!( message: format!(
"Pipe target must be a function accepting {}: {}", "Pipe target must be a function accepting {}: {}",
@@ -1857,7 +1936,7 @@ impl TypeChecker {
} }
}, },
UnaryOp::Not => { UnaryOp::Not => {
if let Err(e) = unify(&operand_type, &Type::Bool) { if let Err(e) = unify_with_env(&operand_type, &Type::Bool, &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!("Operator '!' requires Bool operand: {}", e), message: format!("Operator '!' requires Bool operand: {}", e),
span, span,
@@ -1908,7 +1987,7 @@ impl TypeChecker {
self.current_effects.clone(), self.current_effects.clone(),
); );
match unify(&func_type, &expected_fn) { match unify_with_env(&func_type, &expected_fn, &self.env) {
Ok(subst) => result_type.apply(&subst), Ok(subst) => result_type.apply(&subst),
Err(e) => { Err(e) => {
// Provide more detailed error message based on the type of mismatch // Provide more detailed error message based on the type of mismatch
@@ -1985,7 +2064,7 @@ impl TypeChecker {
let result_type = Type::var(); let result_type = Type::var();
let expected_fn = Type::function(arg_types, result_type.clone()); let expected_fn = Type::function(arg_types, result_type.clone());
if let Err(e) = unify(field_type, &expected_fn) { if let Err(e) = unify_with_env(field_type, &expected_fn, &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!( message: format!(
"Type mismatch in {}.{} call: {}", "Type mismatch in {}.{} call: {}",
@@ -2057,7 +2136,7 @@ impl TypeChecker {
for (i, (arg_type, (_, param_type))) in for (i, (arg_type, (_, param_type))) in
arg_types.iter().zip(op.params.iter()).enumerate() arg_types.iter().zip(op.params.iter()).enumerate()
{ {
if let Err(e) = unify(arg_type, param_type) { if let Err(e) = unify_with_env(arg_type, param_type, &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!( message: format!(
"Argument {} of '{}.{}' has type {}, expected {}: {}", "Argument {} of '{}.{}' has type {}, expected {}: {}",
@@ -2090,6 +2169,7 @@ impl TypeChecker {
fn infer_field(&mut self, object: &Expr, field: &Ident, span: Span) -> Type { fn infer_field(&mut self, object: &Expr, field: &Ident, span: Span) -> Type {
let object_type = self.infer_expr(object); let object_type = self.infer_expr(object);
let object_type = self.env.expand_type_alias(&object_type);
match &object_type { match &object_type {
Type::Record(fields) => match fields.iter().find(|(n, _)| n == &field.name) { Type::Record(fields) => match fields.iter().find(|(n, _)| n == &field.name) {
@@ -2170,7 +2250,7 @@ impl TypeChecker {
// Check return type if specified // Check return type if specified
let ret_type = if let Some(rt) = return_type { let ret_type = if let Some(rt) = return_type {
let declared = self.resolve_type(rt); let declared = self.resolve_type(rt);
if let Err(e) = unify(&body_type, &declared) { if let Err(e) = unify_with_env(&body_type, &declared, &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!( message: format!(
"Lambda body type {} doesn't match declared {}: {}", "Lambda body type {} doesn't match declared {}: {}",
@@ -2236,7 +2316,7 @@ impl TypeChecker {
span: Span, span: Span,
) -> Type { ) -> Type {
let cond_type = self.infer_expr(condition); let cond_type = self.infer_expr(condition);
if let Err(e) = unify(&cond_type, &Type::Bool) { if let Err(e) = unify_with_env(&cond_type, &Type::Bool, &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!("If condition must be Bool, got {}: {}", cond_type, e), message: format!("If condition must be Bool, got {}: {}", cond_type, e),
span: condition.span(), span: condition.span(),
@@ -2246,7 +2326,7 @@ impl TypeChecker {
let then_type = self.infer_expr(then_branch); let then_type = self.infer_expr(then_branch);
let else_type = self.infer_expr(else_branch); let else_type = self.infer_expr(else_branch);
match unify(&then_type, &else_type) { match unify_with_env(&then_type, &else_type, &self.env) {
Ok(subst) => then_type.apply(&subst), Ok(subst) => then_type.apply(&subst),
Err(e) => { Err(e) => {
self.errors.push(TypeError { self.errors.push(TypeError {
@@ -2287,7 +2367,7 @@ impl TypeChecker {
// Check guard if present // Check guard if present
if let Some(ref guard) = arm.guard { if let Some(ref guard) = arm.guard {
let guard_type = self.infer_expr(guard); let guard_type = self.infer_expr(guard);
if let Err(e) = unify(&guard_type, &Type::Bool) { if let Err(e) = unify_with_env(&guard_type, &Type::Bool, &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!("Match guard must be Bool: {}", e), message: format!("Match guard must be Bool: {}", e),
span: guard.span(), span: guard.span(),
@@ -2303,7 +2383,7 @@ impl TypeChecker {
match &result_type { match &result_type {
None => result_type = Some(body_type), None => result_type = Some(body_type),
Some(prev) => { Some(prev) => {
if let Err(e) = unify(prev, &body_type) { if let Err(e) = unify_with_env(prev, &body_type, &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!( message: format!(
"Match arm has incompatible type: expected {}, got {}: {}", "Match arm has incompatible type: expected {}, got {}: {}",
@@ -2353,7 +2433,7 @@ impl TypeChecker {
Pattern::Literal(lit) => { Pattern::Literal(lit) => {
let lit_type = self.infer_literal(lit); let lit_type = self.infer_literal(lit);
if let Err(e) = unify(&lit_type, expected) { if let Err(e) = unify_with_env(&lit_type, expected, &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!("Pattern literal type mismatch: {}", e), message: format!("Pattern literal type mismatch: {}", e),
span: lit.span, span: lit.span,
@@ -2367,7 +2447,7 @@ impl TypeChecker {
// For now, handle Option specially // For now, handle Option specially
match name.name.as_str() { match name.name.as_str() {
"None" => { "None" => {
if let Err(e) = unify(expected, &Type::Option(Box::new(Type::var()))) { if let Err(e) = unify_with_env(expected, &Type::Option(Box::new(Type::var())), &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!( message: format!(
"None pattern doesn't match type {}: {}", "None pattern doesn't match type {}: {}",
@@ -2380,7 +2460,7 @@ impl TypeChecker {
} }
"Some" => { "Some" => {
let inner_type = Type::var(); let inner_type = Type::var();
if let Err(e) = unify(expected, &Type::Option(Box::new(inner_type.clone()))) if let Err(e) = unify_with_env(expected, &Type::Option(Box::new(inner_type.clone())), &self.env)
{ {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!( message: format!(
@@ -2409,7 +2489,7 @@ impl TypeChecker {
Pattern::Tuple { elements, span } => { Pattern::Tuple { elements, span } => {
let element_types: Vec<Type> = elements.iter().map(|_| Type::var()).collect(); let element_types: Vec<Type> = elements.iter().map(|_| Type::var()).collect();
if let Err(e) = unify(expected, &Type::Tuple(element_types.clone())) { if let Err(e) = unify_with_env(expected, &Type::Tuple(element_types.clone()), &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!("Tuple pattern doesn't match type {}: {}", expected, e), message: format!("Tuple pattern doesn't match type {}: {}", expected, e),
span: *span, span: *span,
@@ -2459,7 +2539,7 @@ impl TypeChecker {
if let Some(type_expr) = typ { if let Some(type_expr) = typ {
let declared = self.resolve_type(type_expr); let declared = self.resolve_type(type_expr);
if let Err(e) = unify(&value_type, &declared) { if let Err(e) = unify_with_env(&value_type, &declared, &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!( message: format!(
"Variable '{}' has type {}, but declared type is {}: {}", "Variable '{}' has type {}, but declared type is {}: {}",
@@ -2480,12 +2560,47 @@ impl TypeChecker {
self.infer_expr(result) self.infer_expr(result)
} }
fn infer_record(&mut self, fields: &[(Ident, Expr)], _span: Span) -> Type { fn infer_record(
let field_types: Vec<(String, Type)> = fields &mut self,
spread: Option<&Expr>,
fields: &[(Ident, Expr)],
span: Span,
) -> Type {
// Start with spread fields if present
let mut field_types: Vec<(String, Type)> = if let Some(spread_expr) = spread {
let spread_type = self.infer_expr(spread_expr);
let spread_type = self.env.expand_type_alias(&spread_type);
match spread_type {
Type::Record(spread_fields) => spread_fields,
_ => {
self.errors.push(TypeError {
message: format!(
"Spread expression must be a record type, got {}",
spread_type
),
span,
});
Vec::new()
}
}
} else {
Vec::new()
};
// Apply explicit field overrides
let explicit_types: Vec<(String, Type)> = fields
.iter() .iter()
.map(|(name, expr)| (name.name.clone(), self.infer_expr(expr))) .map(|(name, expr)| (name.name.clone(), self.infer_expr(expr)))
.collect(); .collect();
for (name, typ) in explicit_types {
if let Some(existing) = field_types.iter_mut().find(|(n, _)| n == &name) {
existing.1 = typ;
} else {
field_types.push((name, typ));
}
}
Type::Record(field_types) Type::Record(field_types)
} }
@@ -2502,7 +2617,7 @@ impl TypeChecker {
let first_type = self.infer_expr(&elements[0]); let first_type = self.infer_expr(&elements[0]);
for elem in &elements[1..] { for elem in &elements[1..] {
let elem_type = self.infer_expr(elem); let elem_type = self.infer_expr(elem);
if let Err(e) = unify(&first_type, &elem_type) { if let Err(e) = unify_with_env(&first_type, &elem_type, &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!("List elements must have same type: {}", e), message: format!("List elements must have same type: {}", e),
span, span,
@@ -2808,7 +2923,7 @@ impl TypeChecker {
// Check return type matches if specified // Check return type matches if specified
if let Some(ref return_type_expr) = impl_method.return_type { if let Some(ref return_type_expr) = impl_method.return_type {
let return_type = self.resolve_type(return_type_expr); let return_type = self.resolve_type(return_type_expr);
if let Err(e) = unify(&body_type, &return_type) { if let Err(e) = unify_with_env(&body_type, &return_type, &self.env) {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!( message: format!(
"Method '{}' body has type {}, but declared return type is {}: {}", "Method '{}' body has type {}, but declared return type is {}: {}",
@@ -2851,6 +2966,9 @@ impl TypeChecker {
"Option" if resolved_args.len() == 1 => { "Option" if resolved_args.len() == 1 => {
return Type::Option(Box::new(resolved_args[0].clone())); return Type::Option(Box::new(resolved_args[0].clone()));
} }
"Map" if resolved_args.len() == 2 => {
return Type::Map(Box::new(resolved_args[0].clone()), Box::new(resolved_args[1].clone()));
}
_ => {} _ => {}
} }
} }

View File

@@ -47,6 +47,8 @@ pub enum Type {
List(Box<Type>), List(Box<Type>),
/// Option type (sugar for App(Option, [T])) /// Option type (sugar for App(Option, [T]))
Option(Box<Type>), Option(Box<Type>),
/// Map type (sugar for App(Map, [K, V]))
Map(Box<Type>, Box<Type>),
/// Versioned type (e.g., User @v2) /// Versioned type (e.g., User @v2)
Versioned { Versioned {
base: Box<Type>, base: Box<Type>,
@@ -119,6 +121,7 @@ impl Type {
Type::Tuple(elements) => elements.iter().any(|e| e.contains_var(var)), Type::Tuple(elements) => elements.iter().any(|e| e.contains_var(var)),
Type::Record(fields) => fields.iter().any(|(_, t)| t.contains_var(var)), Type::Record(fields) => fields.iter().any(|(_, t)| t.contains_var(var)),
Type::List(inner) | Type::Option(inner) => inner.contains_var(var), Type::List(inner) | Type::Option(inner) => inner.contains_var(var),
Type::Map(k, v) => k.contains_var(var) || v.contains_var(var),
Type::Versioned { base, .. } => base.contains_var(var), Type::Versioned { base, .. } => base.contains_var(var),
_ => false, _ => false,
} }
@@ -158,6 +161,7 @@ impl Type {
), ),
Type::List(inner) => Type::List(Box::new(inner.apply(subst))), Type::List(inner) => Type::List(Box::new(inner.apply(subst))),
Type::Option(inner) => Type::Option(Box::new(inner.apply(subst))), Type::Option(inner) => Type::Option(Box::new(inner.apply(subst))),
Type::Map(k, v) => Type::Map(Box::new(k.apply(subst)), Box::new(v.apply(subst))),
Type::Versioned { base, version } => Type::Versioned { Type::Versioned { base, version } => Type::Versioned {
base: Box::new(base.apply(subst)), base: Box::new(base.apply(subst)),
version: version.clone(), version: version.clone(),
@@ -208,6 +212,11 @@ impl Type {
vars vars
} }
Type::List(inner) | Type::Option(inner) => inner.free_vars(), Type::List(inner) | Type::Option(inner) => inner.free_vars(),
Type::Map(k, v) => {
let mut vars = k.free_vars();
vars.extend(v.free_vars());
vars
}
Type::Versioned { base, .. } => base.free_vars(), Type::Versioned { base, .. } => base.free_vars(),
_ => HashSet::new(), _ => HashSet::new(),
} }
@@ -279,6 +288,7 @@ impl fmt::Display for Type {
} }
Type::List(inner) => write!(f, "List<{}>", inner), Type::List(inner) => write!(f, "List<{}>", inner),
Type::Option(inner) => write!(f, "Option<{}>", inner), Type::Option(inner) => write!(f, "Option<{}>", inner),
Type::Map(k, v) => write!(f, "Map<{}, {}>", k, v),
Type::Versioned { base, version } => { Type::Versioned { base, version } => {
write!(f, "{} {}", base, version) write!(f, "{} {}", base, version)
} }
@@ -1146,6 +1156,15 @@ impl TypeEnv {
], ],
return_type: Type::Unit, return_type: Type::Unit,
}, },
EffectOpDef {
name: "assertEqualMsg".to_string(),
params: vec![
("expected".to_string(), Type::Var(0)),
("actual".to_string(), Type::Var(0)),
("label".to_string(), Type::String),
],
return_type: Type::Unit,
},
EffectOpDef { EffectOpDef {
name: "assertNotEqual".to_string(), name: "assertNotEqual".to_string(),
params: vec![ params: vec![
@@ -1599,6 +1618,14 @@ impl TypeEnv {
"parseFloat".to_string(), "parseFloat".to_string(),
Type::function(vec![Type::String], Type::Option(Box::new(Type::Float))), Type::function(vec![Type::String], Type::Option(Box::new(Type::Float))),
), ),
(
"indexOf".to_string(),
Type::function(vec![Type::String, Type::String], Type::Option(Box::new(Type::Int))),
),
(
"lastIndexOf".to_string(),
Type::function(vec![Type::String, Type::String], Type::Option(Box::new(Type::Int))),
),
]); ]);
env.bind("String", TypeScheme::mono(string_module_type)); env.bind("String", TypeScheme::mono(string_module_type));
@@ -1758,6 +1785,73 @@ impl TypeEnv {
]); ]);
env.bind("Option", TypeScheme::mono(option_module_type)); env.bind("Option", TypeScheme::mono(option_module_type));
// Map module
let map_v = || Type::var();
let map_type = || Type::Map(Box::new(Type::String), Box::new(Type::var()));
let map_module_type = Type::Record(vec![
(
"new".to_string(),
Type::function(vec![], map_type()),
),
(
"set".to_string(),
Type::function(
vec![map_type(), Type::String, map_v()],
map_type(),
),
),
(
"get".to_string(),
Type::function(
vec![map_type(), Type::String],
Type::Option(Box::new(map_v())),
),
),
(
"contains".to_string(),
Type::function(vec![map_type(), Type::String], Type::Bool),
),
(
"remove".to_string(),
Type::function(vec![map_type(), Type::String], map_type()),
),
(
"keys".to_string(),
Type::function(vec![map_type()], Type::List(Box::new(Type::String))),
),
(
"values".to_string(),
Type::function(vec![map_type()], Type::List(Box::new(map_v()))),
),
(
"size".to_string(),
Type::function(vec![map_type()], Type::Int),
),
(
"isEmpty".to_string(),
Type::function(vec![map_type()], Type::Bool),
),
(
"fromList".to_string(),
Type::function(
vec![Type::List(Box::new(Type::Tuple(vec![Type::String, map_v()])))],
map_type(),
),
),
(
"toList".to_string(),
Type::function(
vec![map_type()],
Type::List(Box::new(Type::Tuple(vec![Type::String, map_v()]))),
),
),
(
"merge".to_string(),
Type::function(vec![map_type(), map_type()], map_type()),
),
]);
env.bind("Map", TypeScheme::mono(map_module_type));
// Result module // Result module
let result_type = Type::App { let result_type = Type::App {
constructor: Box::new(Type::Named("Result".to_string())), constructor: Box::new(Type::Named("Result".to_string())),
@@ -1870,9 +1964,47 @@ impl TypeEnv {
"round".to_string(), "round".to_string(),
Type::function(vec![Type::var()], Type::Int), Type::function(vec![Type::var()], Type::Int),
), ),
(
"sin".to_string(),
Type::function(vec![Type::Float], Type::Float),
),
(
"cos".to_string(),
Type::function(vec![Type::Float], Type::Float),
),
(
"atan2".to_string(),
Type::function(vec![Type::Float, Type::Float], Type::Float),
),
]); ]);
env.bind("Math", TypeScheme::mono(math_module_type)); env.bind("Math", TypeScheme::mono(math_module_type));
// Int module
let int_module_type = Type::Record(vec![
(
"toString".to_string(),
Type::function(vec![Type::Int], Type::String),
),
(
"toFloat".to_string(),
Type::function(vec![Type::Int], Type::Float),
),
]);
env.bind("Int", TypeScheme::mono(int_module_type));
// Float module
let float_module_type = Type::Record(vec![
(
"toString".to_string(),
Type::function(vec![Type::Float], Type::String),
),
(
"toInt".to_string(),
Type::function(vec![Type::Float], Type::Int),
),
]);
env.bind("Float", TypeScheme::mono(float_module_type));
env env
} }
@@ -1956,6 +2088,9 @@ impl TypeEnv {
Type::Option(inner) => { Type::Option(inner) => {
Type::Option(Box::new(self.expand_type_alias(inner))) Type::Option(Box::new(self.expand_type_alias(inner)))
} }
Type::Map(k, v) => {
Type::Map(Box::new(self.expand_type_alias(k)), Box::new(self.expand_type_alias(v)))
}
Type::Versioned { base, version } => { Type::Versioned { base, version } => {
Type::Versioned { Type::Versioned {
base: Box::new(self.expand_type_alias(base)), base: Box::new(self.expand_type_alias(base)),
@@ -2032,7 +2167,9 @@ pub fn unify(t1: &Type, t2: &Type) -> Result<Substitution, String> {
// Function's required effects (e1) must be a subset of available effects (e2) // Function's required effects (e1) must be a subset of available effects (e2)
// A pure function (empty effects) can be called anywhere // A pure function (empty effects) can be called anywhere
// A function requiring {Logger} can be called in context with {Logger} or {Logger, Console} // A function requiring {Logger} can be called in context with {Logger} or {Logger, Console}
if !e1.is_subset(&e2) { // When expected effects (e2) are empty, it means "no constraint" (e.g., callback parameter)
// so we allow any actual effects through
if !e2.is_empty() && !e1.is_subset(&e2) {
return Err(format!( return Err(format!(
"Effect mismatch: expected {{{}}}, got {{{}}}", "Effect mismatch: expected {{{}}}, got {{{}}}",
e1, e2 e1, e2
@@ -2114,6 +2251,13 @@ pub fn unify(t1: &Type, t2: &Type) -> Result<Substitution, String> {
// Option // Option
(Type::Option(a), Type::Option(b)) => unify(a, b), (Type::Option(a), Type::Option(b)) => unify(a, b),
// Map
(Type::Map(k1, v1), Type::Map(k2, v2)) => {
let s1 = unify(k1, k2)?;
let s2 = unify(&v1.apply(&s1), &v2.apply(&s1))?;
Ok(s1.compose(&s2))
}
// Versioned types // Versioned types
( (
Type::Versioned { Type::Versioned {

View File

@@ -194,6 +194,14 @@
<li><a href="middleware.html">Middleware</a></li> <li><a href="middleware.html">Middleware</a></li>
<li><a href="routing.html">Routing</a></li> <li><a href="routing.html">Routing</a></li>
</ul> </ul>
<h2>Projects</h2>
<ul>
<li><a href="/projects/">All Projects</a></li>
<li><a href="/projects/#rest-api">REST API</a></li>
<li><a href="/projects/#todo-app">Todo App</a></li>
<li><a href="/projects/#json-parser">JSON Parser</a></li>
</ul>
</aside> </aside>
<div class="examples-content"> <div class="examples-content">

456
website/projects/index.html Normal file
View File

@@ -0,0 +1,456 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Projects - Lux</title>
<meta name="description" content="Real-world example projects demonstrating Lux's capabilities.">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>&#10024;</text></svg>">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Playfair+Display:wght@400;600;700&family=Source+Serif+4:opsz,wght@8..60,400;8..60,500;8..60,600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../static/style.css">
<style>
.projects-container {
max-width: 1100px;
margin: 0 auto;
padding: var(--space-xl) var(--space-lg);
}
.projects-header {
text-align: center;
margin-bottom: var(--space-2xl);
}
.projects-header p {
font-size: 1.1rem;
max-width: 600px;
margin: 0 auto;
}
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: var(--space-lg);
}
.project-card {
background: var(--bg-glass);
border: 1px solid var(--border-subtle);
border-radius: 12px;
padding: var(--space-lg);
transition: border-color 0.3s ease, transform 0.2s ease;
}
.project-card:hover {
border-color: var(--border-gold);
transform: translateY(-2px);
}
.project-card h2 {
font-size: 1.25rem;
margin-bottom: var(--space-sm);
text-align: left;
display: flex;
align-items: center;
gap: var(--space-sm);
}
.project-card h2 .icon {
font-size: 1.5rem;
}
.project-card .description {
color: var(--text-secondary);
margin-bottom: var(--space-md);
line-height: 1.6;
}
.project-card .features {
margin-bottom: var(--space-md);
}
.project-card .features h3 {
font-size: 0.85rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: var(--space-xs);
}
.project-card .features ul {
list-style: none;
padding: 0;
}
.project-card .features li {
color: var(--text-secondary);
font-size: 0.9rem;
padding: 0.2rem 0;
}
.project-card .features li::before {
content: "\2022";
color: var(--gold);
margin-right: var(--space-xs);
}
.project-card .code-preview {
background: var(--code-bg);
border: 1px solid var(--code-border);
border-radius: 6px;
padding: var(--space-md);
margin-bottom: var(--space-md);
overflow-x: auto;
}
.project-card .code-preview pre {
font-family: var(--font-code);
font-size: 0.85rem;
line-height: 1.5;
margin: 0;
}
.project-card .actions {
display: flex;
gap: var(--space-sm);
}
.project-card .actions a {
padding: var(--space-xs) var(--space-md);
font-size: 0.9rem;
}
.showcase-section {
margin-top: var(--space-3xl);
padding-top: var(--space-2xl);
border-top: 1px solid var(--border-subtle);
}
.showcase-section h2 {
text-align: center;
margin-bottom: var(--space-xl);
}
.showcase-card {
background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%);
border: 1px solid var(--border-gold);
border-radius: 16px;
padding: var(--space-xl);
margin-bottom: var(--space-xl);
}
.showcase-card h3 {
font-size: 1.5rem;
color: var(--gold);
margin-bottom: var(--space-md);
text-align: left;
}
.showcase-card p {
color: var(--text-secondary);
margin-bottom: var(--space-lg);
font-size: 1.1rem;
}
.showcase-features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--space-lg);
margin-bottom: var(--space-xl);
}
.showcase-feature {
background: var(--bg-glass);
border-radius: 8px;
padding: var(--space-md);
}
.showcase-feature h4 {
color: var(--gold);
font-size: 1rem;
margin-bottom: var(--space-xs);
}
.showcase-feature p {
color: var(--text-muted);
font-size: 0.9rem;
margin: 0;
}
.showcase-code {
background: var(--code-bg);
border: 1px solid var(--code-border);
border-radius: 8px;
padding: var(--space-lg);
overflow-x: auto;
}
.showcase-code pre {
font-family: var(--font-code);
font-size: 0.9rem;
line-height: 1.6;
margin: 0;
}
</style>
</head>
<body>
<nav>
<a href="/" class="logo">Lux</a>
<ul class="nav-links" id="nav-links">
<li><a href="/install">Install</a></li>
<li><a href="/tour/">Tour</a></li>
<li><a href="/examples/">Examples</a></li>
<li><a href="/docs/">Docs</a></li>
<li><a href="/play">Play</a></li>
<li><a href="https://git.qrty.ink/blu/lux" class="nav-source">Source</a></li>
</ul>
</nav>
<main class="projects-container">
<header class="projects-header">
<h1>Example Projects</h1>
<p>Real-world applications demonstrating Lux's unique capabilities. Clone these to learn by doing.</p>
</header>
<div class="projects-grid">
<div class="project-card">
<h2><span class="icon">API</span> REST API</h2>
<p class="description">
A full REST API for task management with JSON responses, routing, and CRUD operations.
</p>
<div class="features">
<h3>Demonstrates</h3>
<ul>
<li>HttpServer effect</li>
<li>Pattern matching for routing</li>
<li>JSON serialization</li>
<li>Effect tracking</li>
</ul>
</div>
<div class="code-preview">
<pre><code><span class="kw">fn</span> <span class="fn">router</span>(req): Response <span class="kw">with</span> {<span class="ef">Http</span>} =
<span class="kw">match</span> (req.method, req.path) {
(<span class="st">"GET"</span>, <span class="st">"/"</span>) => httpOk(welcome),
(<span class="st">"GET"</span>, <span class="st">"/tasks"</span>) => httpOk(tasks),
_ => httpNotFound(<span class="st">"404"</span>)
}</code></pre>
</div>
<div class="actions">
<a href="https://git.qrty.ink/blu/lux/src/branch/master/projects/rest-api" class="btn btn-primary">View Code</a>
</div>
</div>
<div class="project-card">
<h2><span class="icon">TODO</span> Todo App</h2>
<p class="description">
A command-line todo application showcasing algebraic data types and pattern matching.
</p>
<div class="features">
<h3>Demonstrates</h3>
<ul>
<li>ADTs for data modeling</li>
<li>Pattern matching</li>
<li>Recursive list operations</li>
<li>Console effect</li>
</ul>
</div>
<div class="code-preview">
<pre><code><span class="kw">type</span> <span class="ty">Priority</span> =
| <span class="ty">Low</span>
| <span class="ty">Medium</span>
| <span class="ty">High</span>
<span class="kw">type</span> <span class="ty">TodoItem</span> =
| <span class="ty">TodoItem</span>(Int, String, Bool, Priority)</code></pre>
</div>
<div class="actions">
<a href="https://git.qrty.ink/blu/lux/src/branch/master/projects/todo-app" class="btn btn-primary">View Code</a>
</div>
</div>
<div class="project-card">
<h2><span class="icon">JSON</span> JSON Parser</h2>
<p class="description">
A recursive descent JSON parser demonstrating ADTs for AST representation.
</p>
<div class="features">
<h3>Demonstrates</h3>
<ul>
<li>ADTs for AST</li>
<li>Recursive parsing</li>
<li>String manipulation</li>
<li>Pure functions</li>
</ul>
</div>
<div class="code-preview">
<pre><code><span class="kw">type</span> <span class="ty">JsonValue</span> =
| <span class="ty">JsonNull</span>
| <span class="ty">JsonBool</span>(Bool)
| <span class="ty">JsonNumber</span>(Int)
| <span class="ty">JsonString</span>(String)
| <span class="ty">JsonArray</span>(List&lt;JsonValue&gt;)
| <span class="ty">JsonObject</span>(List&lt;(String, JsonValue)&gt;)</code></pre>
</div>
<div class="actions">
<a href="https://git.qrty.ink/blu/lux/src/branch/master/projects/json-parser" class="btn btn-primary">View Code</a>
</div>
</div>
<div class="project-card">
<h2><span class="icon">GAME</span> Guessing Game</h2>
<p class="description">
A simple number guessing game demonstrating effects for randomness and I/O.
</p>
<div class="features">
<h3>Demonstrates</h3>
<ul>
<li>Random effect</li>
<li>Console I/O</li>
<li>Recursive game loop</li>
<li>User input handling</li>
</ul>
</div>
<div class="code-preview">
<pre><code><span class="kw">fn</span> <span class="fn">playGame</span>(): Unit
<span class="kw">with</span> {<span class="ef">Console</span>, <span class="ef">Random</span>} = {
<span class="kw">let</span> secret = Random.int(<span class="num">1</span>, <span class="num">100</span>)
Console.print(<span class="st">"Guess a number!"</span>)
guessLoop(secret, <span class="num">0</span>)
}</code></pre>
</div>
<div class="actions">
<a href="https://git.qrty.ink/blu/lux/src/branch/master/projects/guessing-game" class="btn btn-primary">View Code</a>
</div>
</div>
<div class="project-card">
<h2><span class="icon">MD</span> Markdown Converter</h2>
<p class="description">
Convert Markdown to HTML using pattern matching and string processing.
</p>
<div class="features">
<h3>Demonstrates</h3>
<ul>
<li>String manipulation</li>
<li>Pattern matching</li>
<li>Pure transformation</li>
<li>List processing</li>
</ul>
</div>
<div class="code-preview">
<pre><code><span class="kw">fn</span> <span class="fn">convertLine</span>(line: String): String =
<span class="kw">if</span> startsWith(line, <span class="st">"# "</span>) <span class="kw">then</span>
<span class="st">"&lt;h1&gt;"</span> + rest(line, <span class="num">2</span>) + <span class="st">"&lt;/h1&gt;"</span>
<span class="kw">else if</span> startsWith(line, <span class="st">"- "</span>) <span class="kw">then</span>
<span class="st">"&lt;li&gt;"</span> + rest(line, <span class="num">2</span>) + <span class="st">"&lt;/li&gt;"</span>
<span class="kw">else</span> <span class="st">"&lt;p&gt;"</span> + line + <span class="st">"&lt;/p&gt;"</span></code></pre>
</div>
<div class="actions">
<a href="https://git.qrty.ink/blu/lux/src/branch/master/projects/markdown-converter" class="btn btn-primary">View Code</a>
</div>
</div>
<div class="project-card">
<h2><span class="icon">CALC</span> Mini Interpreter</h2>
<p class="description">
A tiny expression interpreter demonstrating language implementation patterns.
</p>
<div class="features">
<h3>Demonstrates</h3>
<ul>
<li>ADTs for AST</li>
<li>Recursive evaluation</li>
<li>Environment handling</li>
<li>Interpreter patterns</li>
</ul>
</div>
<div class="code-preview">
<pre><code><span class="kw">type</span> <span class="ty">Expr</span> =
| <span class="ty">Num</span>(Int)
| <span class="ty">Add</span>(Expr, Expr)
| <span class="ty">Mul</span>(Expr, Expr)
| <span class="ty">Var</span>(String)
| <span class="ty">Let</span>(String, Expr, Expr)</code></pre>
</div>
<div class="actions">
<a href="https://git.qrty.ink/blu/lux/src/branch/master/projects/mini-interpreter" class="btn btn-primary">View Code</a>
</div>
</div>
</div>
<!-- Featured Showcase -->
<section class="showcase-section">
<h2>Featured: Task Manager API</h2>
<div class="showcase-card">
<h3>A Complete Showcase of Lux's Unique Features</h3>
<p>This comprehensive example demonstrates all three of Lux's killer features working together.</p>
<div class="showcase-features">
<div class="showcase-feature">
<h4>1. Algebraic Effects</h4>
<p>Every side effect is explicit in function signatures. No hidden I/O.</p>
</div>
<div class="showcase-feature">
<h4>2. Behavioral Types</h4>
<p>Compile-time guarantees: <code>is pure</code>, <code>is total</code>, <code>is idempotent</code>.</p>
</div>
<div class="showcase-feature">
<h4>3. Schema Evolution</h4>
<p>Versioned types with automatic migration. Data structures evolve safely.</p>
</div>
</div>
<div class="showcase-code">
<pre><code><span class="cm">// Task v1: Original data model</span>
<span class="kw">type</span> <span class="ty">Task</span> <span class="cm">@v1</span> {
id: String,
title: String,
done: Bool
}
<span class="cm">// Task v2: Added priority field with migration</span>
<span class="kw">type</span> <span class="ty">Task</span> <span class="cm">@v2</span> {
id: String,
title: String,
done: Bool,
priority: String,
<span class="cm">// Old tasks get "medium" priority by default</span>
<span class="kw">from</span> <span class="cm">@v1</span> = {
id: old.id,
title: old.title,
done: old.done,
priority: <span class="st">"medium"</span>
}
}
<span class="cm">// Pure, total, idempotent business logic</span>
<span class="kw">fn</span> <span class="fn">validateTitle</span>(title: String): <span class="ty">Result</span>&lt;String, String&gt;
<span class="kw">is</span> <span class="ty">pure</span>, <span class="ty">total</span> =
<span class="kw">if</span> String.length(title) == <span class="num">0</span> <span class="kw">then</span>
Err(<span class="st">"Title cannot be empty"</span>)
<span class="kw">else</span>
Ok(title)</code></pre>
</div>
<div class="actions" style="margin-top: var(--space-lg);">
<a href="https://git.qrty.ink/blu/lux/src/branch/master/examples/showcase/task_manager.lux" class="btn btn-primary">View Full Code</a>
<a href="/tour/06-effects-intro.html" class="btn btn-secondary">Learn About Effects</a>
</div>
</div>
</section>
<!-- Get Started -->
<section class="showcase-section" style="text-align: center;">
<h2>Build Your Own</h2>
<p style="margin-bottom: var(--space-lg); color: var(--text-secondary);">Ready to start building with Lux?</p>
<div style="display: flex; gap: var(--space-md); justify-content: center; flex-wrap: wrap;">
<a href="/install" class="btn btn-primary">Install Lux</a>
<a href="/tour/" class="btn btn-secondary">Take the Tour</a>
<a href="/examples/" class="btn btn-tertiary">Browse Examples</a>
</div>
</section>
</main>
</body>
</html>

66
website/serve.lux Normal file
View File

@@ -0,0 +1,66 @@
// Static File Server for Lux Website
//
// Usage: lux website/serve.lux
// Then open http://localhost:8080
//
// This demonstrates serving the Lux website using Lux's own HTTP server!
fn main(): Unit with {HttpServer, Console, File} = {
let port = 8080
let root = "website"
HttpServer.listen(port)
Console.print("Lux website server running at http://localhost:" + toString(port))
Console.print("Serving files from: " + root)
Console.print("Press Ctrl+C to stop")
Console.print("")
serverLoop(root)
}
fn serverLoop(root: String): Unit with {HttpServer, Console, File} = {
let req = HttpServer.accept()
// Log request
Console.print(req.method + " " + req.path)
// Only handle GET requests
if req.method != "GET" then {
HttpServer.respond(405, "Method Not Allowed")
serverLoop(root)
} else {
serveFile(root, req.path)
serverLoop(root)
}
}
fn serveFile(root: String, reqPath: String): Unit with {HttpServer, Console, File} = {
// Determine file path
let path = if reqPath == "/" then "/index.html" else reqPath
let filePath = root + path
// Try to serve the file
if File.exists(filePath) then {
let content = File.read(filePath)
HttpServer.respond(200, content)
} else {
// Try with .html extension for clean URLs
let htmlPath = filePath + ".html"
if File.exists(htmlPath) then {
let content = File.read(htmlPath)
HttpServer.respond(200, content)
} else {
// Try index.html for directory paths
let indexPath = filePath + "/index.html"
if File.exists(indexPath) then {
let content = File.read(indexPath)
HttpServer.respond(200, content)
} else {
Console.print(" -> 404 Not Found: " + filePath)
HttpServer.respond(404, "Not Found: " + reqPath)
}
}
}
}
let output = run main() with {}