45 Commits

Author SHA1 Message Date
81e58cf3d5 Fix Option<String> pattern match double-dereference in C codegen
LuxString is typedef char* but the codegen treated it as a struct type,
generating *(LuxString*)(field0) instead of (LuxString)(field0). This
caused a heap-buffer-overflow on any Option<String> pattern match since
it read the string contents as a memory address.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 23:59:47 -05:00
92d443e475 chore: bump version to 0.1.13 2026-02-20 20:41:01 -05:00
fe30206cd0 add cargo lock 2026-02-20 20:40:55 -05:00
563d62f526 feat: add module import support to JS backend
The JS backend now processes imported modules, emitting their type
constructors and functions with module-prefixed mangled names. Module
function calls (both via Expr::Call with Expr::Field and via
Expr::EffectOp) are resolved to the correct mangled names.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 20:38:36 -05:00
e9ec1bb84d feat: add handler declaration codegen to JS backend
Handler declarations now emit as JavaScript objects with operation
methods. Each operation defines resume as an identity function,
matching the simple handler model used by the interpreter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 20:31:10 -05:00
e46afd98eb feat: auto-invoke let main in JS backend
The JS backend now detects `let main = fn() => ...` patterns and
auto-invokes them at the end of the generated code, matching the
interpreter's behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 20:24:47 -05:00
64f33e4e4b feat: add List.get support to JS backend
List.get(list, index) now correctly compiles to JavaScript, returning
Lux.Some(value) for valid indices and Lux.None() for out-of-bounds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 20:22:15 -05:00
293635f415 chore: bump version to 0.1.12 2026-02-20 20:03:04 -05:00
694e4ec999 feat: add Ref cells for mutable state (Ref.new, Ref.get, Ref.set, Ref.update)
Implements WISH-013 mutable state primitives. Ref<T> is a mutable container
using existing module call syntax. Supported across interpreter, JS, and C backends.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 20:01:29 -05:00
78879ca94e chore: bump version to 0.1.11 2026-02-20 19:36:11 -05:00
01474b401f chore: bump version to 0.1.10 2026-02-20 19:32:56 -05:00
169de0b3c8 chore: update Cargo.lock
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 19:32:27 -05:00
667a94b4dc feat: add extern let declarations for JS FFI
Add support for `extern let name: Type` and `extern let name: Type = "jsName"`
syntax for declaring external JavaScript values. This follows the same pattern
as extern fn across all compiler passes: parser, typechecker, interpreter
(runtime error placeholder), JS backend (emits JS name directly without
mangling), formatter, linter, modules, and symbol table.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 19:29:44 -05:00
1b629aaae4 feat: add 10 missing List operations to JS backend
Add find, findIndex, any, all, zip, flatten, contains, take, drop,
and forEach to the JS backend's emit_list_operation function. These
operations previously worked in the interpreter and C backend but
caused "Unknown List operation" errors when compiled to JS.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 19:21:26 -05:00
0f8babfd8b chore: bump version to 0.1.9 2026-02-20 18:46:51 -05:00
582d603513 chore: update Cargo.lock for v0.1.8
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 18:42:29 -05:00
fbb7ddb6c3 feat: add extern fn declarations for JS FFI
Adds `extern fn` syntax for declaring external JavaScript functions:
  extern fn getElementById(id: String): Element
  extern fn getContext(el: Element, kind: String): CanvasCtx = "getContext"
  pub extern fn alert(msg: String): Unit

Changes across 11 files:
- Lexer: `extern` keyword
- AST: `ExternFnDecl` struct + `Declaration::ExternFn` variant
- Parser: parse `extern fn` with optional `= "jsName"` override
- Typechecker: register extern fn type signatures
- Interpreter: ExternFn value with clear error on call
- JS backend: emit extern fn calls using JS name (no _lux suffix)
- C backend: silently skips extern fns
- Formatter, linter, modules, symbol_table: handle new variant

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 18:38:42 -05:00
400acc3f35 feat: add deep path record update syntax
Adds parser desugaring for `{ ...base, pos.x: val, pos.y: val2 }` which
expands to `{ ...base, pos: { ...base.pos, x: val, y: val2 } }`.
Supports arbitrary nesting depth (e.g. world.physics.gravity.y).
Detects conflicts between flat and deep path fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 18:13:11 -05:00
ea3a7ca2dd chore: bump version to 0.1.8 2026-02-20 16:45:49 -05:00
7b40421a6a feat: add List.findIndex, List.zip, List.flatten, List.contains
Add missing List operations requested by ergon game engine project:
- findIndex(list, predicate) -> Option<Int>
- zip(list1, list2) -> List<(A, B)>
- flatten(listOfLists) -> List<A>
- contains(list, element) -> Bool

Resolves ergon porting blocker #4.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 16:40:32 -05:00
26b94935e9 feat: add File.tryRead, File.tryWrite, File.tryDelete returning Result
Add safe variants of File operations that return Result<T, String> instead
of crashing with RuntimeError. This prevents server crashes when a file
is missing or unwritable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 11:04:33 -05:00
018a799c05 chore: bump version to 0.1.7 2026-02-20 10:38:37 -05:00
ec78286165 feat: enhance Html and Http stdlib modules
Html: add RawHtml, Attribute, meta/link/script/iframe/figure/figcaption
elements, attr() helper, rawHtml() helper, seoDocument() for SEO meta
tags, fix document() to use Attribute instead of DataAttr for standard
HTML attributes.

Http: add serveStaticFile(), parseFormBody(), getFormField(),
sendResponse() convenience helpers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 10:36:56 -05:00
f2688072ac feat: add File.glob for file pattern matching (issue 15)
Add File.glob(pattern) effect operation that returns a list of file
paths matching a glob pattern (e.g., "src/**/*.lux"). Implemented
across interpreter (using glob crate), JS backend (handler-based),
and C backend (using POSIX glob.h).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 10:33:59 -05:00
746643527d feat: add triple-quoted multiline string literals (issue 12)
Support """...""" syntax for multiline strings with:
- Automatic indent stripping (based on minimum indentation)
- Leading newline after opening """ is skipped
- Trailing whitespace-only line before closing """ is stripped
- String interpolation ({expr}) support
- All escape sequences supported
- Formatter outputs multiline strings for strings containing newlines

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 10:22:52 -05:00
091ff1e422 feat: add List.sort and List.sortBy functions (issue 9)
Add sorting support to the List module across all backends:
- List.sort for natural ordering (Int, Float, String, Bool, Char)
- List.sortBy for custom comparator-based sorting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 10:02:21 -05:00
1fc472a54c feat: support module-qualified constructor patterns in match expressions (issue 3)
Added module: Option<Ident> to Pattern::Constructor, updated parser to
handle module.Constructor(args) syntax in patterns, exported ADT
constructors from modules, and copied type definitions during module
import so types like Shape are usable in importing files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 09:46:51 -05:00
caabaeeb9c fix: allow multi-line function params, lambda params, tuples, and patterns
Added skip_newlines() calls throughout the parser so that newlines are
properly handled in parameter lists, tuple expressions, and pattern
matching constructs. Fixes Issue 5 and Issue 6 from ISSUES.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 23:49:47 -05:00
4e43d3d50d fix: C backend String.indexOf/lastIndexOf compilation (issue 8)
Three bugs fixed:
- Global let bindings always typed as LuxInt; now inferred from value
- Option inner type not tracked for function params; added
  var_option_inner_types map so match extraction uses correct type
- indexOf/lastIndexOf stored ints as (void*)(intptr_t) but extraction
  expected boxed pointers; now uses lux_box_int consistently

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:10:52 -05:00
fd5ed53b29 chore: bump version to 0.1.6 2026-02-19 15:22:32 -05:00
2800ce4e2d chore: sync Cargo.lock
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:26:20 -05:00
ec365ebb3f feat: add File.copy and propagate effectful callback effects (WISH-7, WISH-14)
File.copy(source, dest) copies files via interpreter (std::fs::copy) and
C backend (fread/fwrite). Effectful callbacks passed to higher-order
functions like List.map/forEach now propagate their effects to the
enclosing function's inferred effect set.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:24:28 -05:00
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
21 changed files with 4422 additions and 139 deletions

15
Cargo.lock generated
View File

@@ -225,7 +225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -392,6 +392,12 @@ dependencies = [
"wasip3", "wasip3",
] ]
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.27" version = "0.3.27"
@@ -770,8 +776,9 @@ dependencies = [
[[package]] [[package]]
name = "lux" name = "lux"
version = "0.1.2" version = "0.1.12"
dependencies = [ dependencies = [
"glob",
"lsp-server", "lsp-server",
"lsp-types", "lsp-types",
"postgres", "postgres",
@@ -1182,7 +1189,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -1475,7 +1482,7 @@ dependencies = [
"getrandom 0.4.1", "getrandom 0.4.1",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lux" name = "lux"
version = "0.1.3" version = "0.1.13"
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"
@@ -17,6 +17,7 @@ reqwest = { version = "0.11", default-features = false, features = ["blocking",
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"
glob = "0.3"
[dev-dependencies] [dev-dependencies]

View File

@@ -44,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.3\n" printf " \033[1;35m \033[0m v0.1.13\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"
@@ -62,7 +62,7 @@
packages.default = pkgs.rustPlatform.buildRustPackage { packages.default = pkgs.rustPlatform.buildRustPackage {
pname = "lux"; pname = "lux";
version = "0.1.3"; version = "0.1.13";
src = ./.; src = ./.;
cargoLock.lockFile = ./Cargo.lock; cargoLock.lockFile = ./Cargo.lock;
@@ -79,7 +79,7 @@
}; };
in muslPkgs.rustPlatform.buildRustPackage { in muslPkgs.rustPlatform.buildRustPackage {
pname = "lux"; pname = "lux";
version = "0.1.3"; version = "0.1.13";
src = ./.; src = ./.;
cargoLock.lockFile = ./Cargo.lock; cargoLock.lockFile = ./Cargo.lock;

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

@@ -2,7 +2,7 @@
set -euo pipefail set -euo pipefail
# Lux Full Validation Script # Lux Full Validation Script
# Runs all checks: Rust tests, package tests, type checking, formatting, linting. # Runs all checks: Rust tests, package tests, type checking, example compilation.
# Run after every committable change to ensure no regressions. # Run after every committable change to ensure no regressions.
# cd to repo root (directory containing this script's parent) # cd to repo root (directory containing this script's parent)
@@ -11,6 +11,8 @@ cd "$SCRIPT_DIR/.."
LUX="$(pwd)/target/release/lux" LUX="$(pwd)/target/release/lux"
PACKAGES_DIR="$(pwd)/../packages" PACKAGES_DIR="$(pwd)/../packages"
PROJECTS_DIR="$(pwd)/projects"
EXAMPLES_DIR="$(pwd)/examples"
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
CYAN='\033[0;36m' CYAN='\033[0;36m'
@@ -30,7 +32,7 @@ fail() { printf "${RED}FAIL${NC} %s\n" "${1:-}"; FAILED=$((FAILED + 1)); }
# --- Rust checks --- # --- Rust checks ---
step "cargo check" step "cargo check"
if nix develop --command cargo check 2>&1 | grep -q "Finished"; then ok; else fail; fi if nix develop --command cargo check 2>/dev/null; then ok; else fail; fi
step "cargo test" step "cargo test"
OUTPUT=$(nix develop --command cargo test 2>&1 || true) OUTPUT=$(nix develop --command cargo test 2>&1 || true)
@@ -39,7 +41,7 @@ if echo "$RESULT" | grep -q "0 failed"; then ok "$RESULT"; else fail "$RESULT";
# --- Build release binary --- # --- Build release binary ---
step "cargo build --release" step "cargo build --release"
if nix develop --command cargo build --release 2>&1 | grep -q "Finished"; then ok; else fail; fi if nix develop --command cargo build --release 2>/dev/null; then ok; else fail; fi
# --- Package tests --- # --- Package tests ---
for pkg in path frontmatter xml rss markdown; do for pkg in path frontmatter xml rss markdown; do
@@ -63,6 +65,142 @@ for pkg in path frontmatter xml rss markdown; do
fi fi
done 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 --- # --- Summary ---
printf "\n${BOLD}═══ Validation Summary ═══${NC}\n" printf "\n${BOLD}═══ Validation Summary ═══${NC}\n"
if [ $FAILED -eq 0 ]; then if [ $FAILED -eq 0 ]; then

View File

@@ -221,6 +221,10 @@ pub enum Declaration {
Trait(TraitDecl), Trait(TraitDecl),
/// Trait implementation: impl Trait for Type { ... } /// Trait implementation: impl Trait for Type { ... }
Impl(ImplDecl), Impl(ImplDecl),
/// Extern function declaration (FFI): extern fn name(params): ReturnType
ExternFn(ExternFnDecl),
/// Extern let declaration (FFI): extern let name: Type
ExternLet(ExternLetDecl),
} }
/// Function declaration /// Function declaration
@@ -428,6 +432,34 @@ pub struct ImplMethod {
pub span: Span, pub span: Span,
} }
/// Extern function declaration (FFI)
#[derive(Debug, Clone)]
pub struct ExternFnDecl {
pub visibility: Visibility,
/// Documentation comment
pub doc: Option<String>,
pub name: Ident,
pub type_params: Vec<Ident>,
pub params: Vec<Parameter>,
pub return_type: TypeExpr,
/// Optional JS name override: extern fn foo(...): T = "jsFoo"
pub js_name: Option<String>,
pub span: Span,
}
/// Extern let declaration (FFI)
#[derive(Debug, Clone)]
pub struct ExternLetDecl {
pub visibility: Visibility,
/// Documentation comment
pub doc: Option<String>,
pub name: Ident,
pub typ: TypeExpr,
/// Optional JS name override: extern let foo: T = "window.foo"
pub js_name: Option<String>,
pub span: Span,
}
/// Type expressions /// Type expressions
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypeExpr { pub enum TypeExpr {
@@ -697,8 +729,9 @@ pub enum Pattern {
Var(Ident), Var(Ident),
/// Literal: 42, "hello", true /// Literal: 42, "hello", true
Literal(Literal), Literal(Literal),
/// Constructor: Some(x), None, Ok(v) /// Constructor: Some(x), None, Ok(v), module.Constructor(x)
Constructor { Constructor {
module: Option<Ident>,
name: Ident, name: Ident,
fields: Vec<Pattern>, fields: Vec<Pattern>,
span: Span, span: Span,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -333,11 +333,13 @@ mod tests {
fn test_option_exhaustive() { fn test_option_exhaustive() {
let patterns = vec![ let patterns = vec![
Pattern::Constructor { Pattern::Constructor {
module: None,
name: make_ident("None"), name: make_ident("None"),
fields: vec![], fields: vec![],
span: span(), span: span(),
}, },
Pattern::Constructor { Pattern::Constructor {
module: None,
name: make_ident("Some"), name: make_ident("Some"),
fields: vec![Pattern::Wildcard(span())], fields: vec![Pattern::Wildcard(span())],
span: span(), span: span(),
@@ -352,6 +354,7 @@ mod tests {
#[test] #[test]
fn test_option_missing_none() { fn test_option_missing_none() {
let patterns = vec![Pattern::Constructor { let patterns = vec![Pattern::Constructor {
module: None,
name: make_ident("Some"), name: make_ident("Some"),
fields: vec![Pattern::Wildcard(span())], fields: vec![Pattern::Wildcard(span())],
span: span(), span: span(),
@@ -391,11 +394,13 @@ mod tests {
fn test_result_exhaustive() { fn test_result_exhaustive() {
let patterns = vec![ let patterns = vec![
Pattern::Constructor { Pattern::Constructor {
module: None,
name: make_ident("Ok"), name: make_ident("Ok"),
fields: vec![Pattern::Wildcard(span())], fields: vec![Pattern::Wildcard(span())],
span: span(), span: span(),
}, },
Pattern::Constructor { Pattern::Constructor {
module: None,
name: make_ident("Err"), name: make_ident("Err"),
fields: vec![Pattern::Wildcard(span())], fields: vec![Pattern::Wildcard(span())],
span: span(), span: span(),

View File

@@ -3,9 +3,9 @@
//! Formats Lux source code according to standard style guidelines. //! Formats Lux source code according to standard style guidelines.
use crate::ast::{ use crate::ast::{
BehavioralProperty, BinaryOp, Declaration, EffectDecl, Expr, FunctionDecl, HandlerDecl, BehavioralProperty, BinaryOp, Declaration, EffectDecl, ExternFnDecl, ExternLetDecl, Expr, FunctionDecl,
ImplDecl, ImplMethod, LetDecl, Literal, LiteralKind, Pattern, Program, Statement, TraitDecl, HandlerDecl, ImplDecl, ImplMethod, LetDecl, Literal, LiteralKind, Pattern, Program, Statement,
TypeDecl, TypeDef, TypeExpr, UnaryOp, VariantFields, TraitDecl, TypeDecl, TypeDef, TypeExpr, UnaryOp, VariantFields, Visibility,
}; };
use crate::lexer::Lexer; use crate::lexer::Lexer;
use crate::parser::Parser; use crate::parser::Parser;
@@ -103,9 +103,77 @@ impl Formatter {
Declaration::Handler(h) => self.format_handler(h), Declaration::Handler(h) => self.format_handler(h),
Declaration::Trait(t) => self.format_trait(t), Declaration::Trait(t) => self.format_trait(t),
Declaration::Impl(i) => self.format_impl(i), Declaration::Impl(i) => self.format_impl(i),
Declaration::ExternFn(e) => self.format_extern_fn(e),
Declaration::ExternLet(e) => self.format_extern_let(e),
} }
} }
fn format_extern_fn(&mut self, ext: &ExternFnDecl) {
let indent = self.indent();
self.write(&indent);
if ext.visibility == Visibility::Public {
self.write("pub ");
}
self.write("extern fn ");
self.write(&ext.name.name);
// Type parameters
if !ext.type_params.is_empty() {
self.write("<");
self.write(
&ext.type_params
.iter()
.map(|p| p.name.clone())
.collect::<Vec<_>>()
.join(", "),
);
self.write(">");
}
// Parameters
self.write("(");
let params: Vec<String> = ext
.params
.iter()
.map(|p| format!("{}: {}", p.name.name, self.format_type_expr(&p.typ)))
.collect();
self.write(&params.join(", "));
self.write("): ");
// Return type
self.write(&self.format_type_expr(&ext.return_type));
// Optional JS name
if let Some(js_name) = &ext.js_name {
self.write(&format!(" = \"{}\"", js_name));
}
self.newline();
}
fn format_extern_let(&mut self, ext: &ExternLetDecl) {
let indent = self.indent();
self.write(&indent);
if ext.visibility == Visibility::Public {
self.write("pub ");
}
self.write("extern let ");
self.write(&ext.name.name);
self.write(": ");
self.write(&self.format_type_expr(&ext.typ));
// Optional JS name
if let Some(js_name) = &ext.js_name {
self.write(&format!(" = \"{}\"", js_name));
}
self.newline();
}
fn format_function(&mut self, func: &FunctionDecl) { fn format_function(&mut self, func: &FunctionDecl) {
let indent = self.indent(); let indent = self.indent();
self.write(&indent); self.write(&indent);
@@ -733,7 +801,30 @@ 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('"', "\\\"").replace('{', "\\{").replace('}', "\\}")), LiteralKind::String(s) => {
if s.contains('\n') {
// Use triple-quoted multiline string
let tab = " ".repeat(self.config.indent_size);
let base_indent = tab.repeat(self.indent_level);
let content_indent = tab.repeat(self.indent_level + 1);
let lines: Vec<&str> = s.split('\n').collect();
let mut result = String::from("\"\"\"\n");
for line in &lines {
if line.is_empty() {
result.push('\n');
} else {
result.push_str(&content_indent);
result.push_str(&line.replace('{', "\\{").replace('}', "\\}"));
result.push('\n');
}
}
result.push_str(&base_indent);
result.push_str("\"\"\"");
result
} else {
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"").replace('{', "\\{").replace('}', "\\}"))
}
},
LiteralKind::Char(c) => format!("'{}'", c), LiteralKind::Char(c) => format!("'{}'", c),
LiteralKind::Bool(b) => b.to_string(), LiteralKind::Bool(b) => b.to_string(),
LiteralKind::Unit => "()".to_string(), LiteralKind::Unit => "()".to_string(),
@@ -772,12 +863,22 @@ impl Formatter {
Pattern::Wildcard(_) => "_".to_string(), Pattern::Wildcard(_) => "_".to_string(),
Pattern::Var(ident) => ident.name.clone(), Pattern::Var(ident) => ident.name.clone(),
Pattern::Literal(lit) => self.format_literal(lit), Pattern::Literal(lit) => self.format_literal(lit),
Pattern::Constructor { name, fields, .. } => { Pattern::Constructor {
module,
name,
fields,
..
} => {
let prefix = match module {
Some(m) => format!("{}.", m.name),
None => String::new(),
};
if fields.is_empty() { if fields.is_empty() {
name.name.clone() format!("{}{}", prefix, name.name)
} else { } else {
format!( format!(
"{}({})", "{}{}({})",
prefix,
name.name, name.name,
fields fields
.iter() .iter()

View File

@@ -28,6 +28,8 @@ pub enum BuiltinFn {
ListGet, ListGet,
ListRange, ListRange,
ListForEach, ListForEach,
ListSort,
ListSortBy,
// String operations // String operations
StringSplit, StringSplit,
@@ -81,10 +83,14 @@ pub enum BuiltinFn {
// Additional List operations // Additional List operations
ListIsEmpty, ListIsEmpty,
ListFind, ListFind,
ListFindIndex,
ListAny, ListAny,
ListAll, ListAll,
ListTake, ListTake,
ListDrop, ListDrop,
ListZip,
ListFlatten,
ListContains,
// Additional String operations // Additional String operations
StringStartsWith, StringStartsWith,
@@ -100,7 +106,9 @@ pub enum BuiltinFn {
// Int/Float operations // Int/Float operations
IntToString, IntToString,
IntToFloat,
FloatToString, FloatToString,
FloatToInt,
// JSON operations // JSON operations
JsonParse, JsonParse,
@@ -122,6 +130,26 @@ pub enum BuiltinFn {
JsonString, JsonString,
JsonArray, JsonArray,
JsonObject, JsonObject,
// Map operations
MapNew,
MapSet,
MapGet,
MapContains,
MapRemove,
MapKeys,
MapValues,
MapSize,
MapIsEmpty,
MapFromList,
MapToList,
MapMerge,
// Ref operations
RefNew,
RefGet,
RefSet,
RefUpdate,
} }
/// Runtime value /// Runtime value
@@ -136,6 +164,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
@@ -153,6 +182,13 @@ pub enum Value {
}, },
/// JSON value (for JSON parsing/manipulation) /// JSON value (for JSON parsing/manipulation)
Json(serde_json::Value), Json(serde_json::Value),
/// Extern function (FFI — only callable from JS backend)
ExternFn {
name: String,
arity: usize,
},
/// Mutable reference cell
Ref(Rc<RefCell<Value>>),
} }
impl Value { impl Value {
@@ -167,12 +203,15 @@ 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",
Value::Constructor { .. } => "Constructor", Value::Constructor { .. } => "Constructor",
Value::Versioned { .. } => "Versioned", Value::Versioned { .. } => "Versioned",
Value::Json(_) => "Json", Value::Json(_) => "Json",
Value::ExternFn { .. } => "ExternFn",
Value::Ref(_) => "Ref",
} }
} }
@@ -215,6 +254,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))
} }
@@ -223,6 +267,7 @@ impl Value {
t1 == t2 && v1 == v2 && Value::values_equal(val1, val2) t1 == t2 && v1 == v2 && Value::values_equal(val1, val2)
} }
(Value::Json(j1), Value::Json(j2)) => j1 == j2, (Value::Json(j1), Value::Json(j2)) => j1 == j2,
(Value::Ref(r1), Value::Ref(r2)) => Rc::ptr_eq(r1, r2),
// Functions and handlers cannot be compared for equality // Functions and handlers cannot be compared for equality
_ => false, _ => false,
} }
@@ -285,6 +330,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> {
@@ -331,6 +386,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>"),
@@ -356,6 +423,8 @@ impl fmt::Display for Value {
write!(f, "{} @v{}", value, version) write!(f, "{} @v{}", value, version)
} }
Value::Json(json) => write!(f, "{}", json), Value::Json(json) => write!(f, "{}", json),
Value::ExternFn { name, .. } => write!(f, "<extern fn {}>", name),
Value::Ref(cell) => write!(f, "<ref: {}>", cell.borrow()),
} }
} }
} }
@@ -927,14 +996,23 @@ impl Interpreter {
Value::Builtin(BuiltinFn::ListIsEmpty), Value::Builtin(BuiltinFn::ListIsEmpty),
), ),
("find".to_string(), Value::Builtin(BuiltinFn::ListFind)), ("find".to_string(), Value::Builtin(BuiltinFn::ListFind)),
("findIndex".to_string(), Value::Builtin(BuiltinFn::ListFindIndex)),
("any".to_string(), Value::Builtin(BuiltinFn::ListAny)), ("any".to_string(), Value::Builtin(BuiltinFn::ListAny)),
("all".to_string(), Value::Builtin(BuiltinFn::ListAll)), ("all".to_string(), Value::Builtin(BuiltinFn::ListAll)),
("take".to_string(), Value::Builtin(BuiltinFn::ListTake)), ("take".to_string(), Value::Builtin(BuiltinFn::ListTake)),
("drop".to_string(), Value::Builtin(BuiltinFn::ListDrop)), ("drop".to_string(), Value::Builtin(BuiltinFn::ListDrop)),
("zip".to_string(), Value::Builtin(BuiltinFn::ListZip)),
("flatten".to_string(), Value::Builtin(BuiltinFn::ListFlatten)),
("contains".to_string(), Value::Builtin(BuiltinFn::ListContains)),
( (
"forEach".to_string(), "forEach".to_string(),
Value::Builtin(BuiltinFn::ListForEach), Value::Builtin(BuiltinFn::ListForEach),
), ),
("sort".to_string(), Value::Builtin(BuiltinFn::ListSort)),
(
"sortBy".to_string(),
Value::Builtin(BuiltinFn::ListSortBy),
),
])); ]));
env.define("List", list_module); env.define("List", list_module);
@@ -1084,12 +1162,14 @@ impl Interpreter {
// Int module // Int module
let int_module = Value::Record(HashMap::from([ let int_module = Value::Record(HashMap::from([
("toString".to_string(), Value::Builtin(BuiltinFn::IntToString)), ("toString".to_string(), Value::Builtin(BuiltinFn::IntToString)),
("toFloat".to_string(), Value::Builtin(BuiltinFn::IntToFloat)),
])); ]));
env.define("Int", int_module); env.define("Int", int_module);
// Float module // Float module
let float_module = Value::Record(HashMap::from([ let float_module = Value::Record(HashMap::from([
("toString".to_string(), Value::Builtin(BuiltinFn::FloatToString)), ("toString".to_string(), Value::Builtin(BuiltinFn::FloatToString)),
("toInt".to_string(), Value::Builtin(BuiltinFn::FloatToInt)),
])); ]));
env.define("Float", float_module); env.define("Float", float_module);
@@ -1116,6 +1196,32 @@ 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);
// Ref module
let ref_module = Value::Record(HashMap::from([
("new".to_string(), Value::Builtin(BuiltinFn::RefNew)),
("get".to_string(), Value::Builtin(BuiltinFn::RefGet)),
("set".to_string(), Value::Builtin(BuiltinFn::RefSet)),
("update".to_string(), Value::Builtin(BuiltinFn::RefUpdate)),
]));
env.define("Ref", ref_module);
} }
/// Execute a program /// Execute a program
@@ -1326,6 +1432,33 @@ impl Interpreter {
Ok(Value::Unit) Ok(Value::Unit)
} }
Declaration::ExternFn(ext) => {
// Register a placeholder that errors at runtime
let name = ext.name.name.clone();
let arity = ext.params.len();
// Create a closure that produces a clear error
let closure = Closure {
params: ext.params.iter().map(|p| p.name.name.clone()).collect(),
body: Expr::Literal(crate::ast::Literal {
kind: crate::ast::LiteralKind::Unit,
span: ext.span,
}),
env: self.global_env.clone(),
};
// We store an ExternFn marker value
self.global_env
.define(&name, Value::ExternFn { name: name.clone(), arity });
Ok(Value::Unit)
}
Declaration::ExternLet(ext) => {
// Register a placeholder that errors at runtime (extern lets only work in JS)
let name = ext.name.name.clone();
self.global_env
.define(&name, Value::ExternFn { name: name.clone(), arity: 0 });
Ok(Value::Unit)
}
Declaration::Effect(_) | Declaration::Trait(_) | Declaration::Impl(_) => { Declaration::Effect(_) | Declaration::Trait(_) | Declaration::Impl(_) => {
// These are compile-time only // These are compile-time only
Ok(Value::Unit) Ok(Value::Unit)
@@ -1845,6 +1978,13 @@ impl Interpreter {
})) }))
} }
Value::Builtin(builtin) => self.eval_builtin(builtin, args, span), Value::Builtin(builtin) => self.eval_builtin(builtin, args, span),
Value::ExternFn { name, .. } => Err(RuntimeError {
message: format!(
"Extern function '{}' can only be called when compiled to JavaScript (use `lux build --target js`)",
name
),
span: Some(span),
}),
v => Err(RuntimeError { v => Err(RuntimeError {
message: format!("Cannot call {}", v.type_name()), message: format!("Cannot call {}", v.type_name()),
span: Some(span), span: Some(span),
@@ -2364,6 +2504,26 @@ impl Interpreter {
} }
} }
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"));
@@ -2632,6 +2792,55 @@ impl Interpreter {
Ok(EvalResult::Value(Value::Bool(true))) Ok(EvalResult::Value(Value::Bool(true)))
} }
BuiltinFn::ListFindIndex => {
let (list, func) = Self::expect_args_2::<Vec<Value>, Value>(&args, "List.findIndex", span)?;
for (i, item) in list.iter().enumerate() {
let v = self.eval_call_to_value(func.clone(), vec![item.clone()], span)?;
match v {
Value::Bool(true) => {
return Ok(EvalResult::Value(Value::Constructor {
name: "Some".to_string(),
fields: vec![Value::Int(i as i64)],
}));
}
Value::Bool(false) => {}
_ => return Err(err("List.findIndex predicate must return Bool")),
}
}
Ok(EvalResult::Value(Value::Constructor {
name: "None".to_string(),
fields: vec![],
}))
}
BuiltinFn::ListZip => {
let (list1, list2) = Self::expect_args_2::<Vec<Value>, Vec<Value>>(&args, "List.zip", span)?;
let result: Vec<Value> = list1
.into_iter()
.zip(list2.into_iter())
.map(|(a, b)| Value::Tuple(vec![a, b]))
.collect();
Ok(EvalResult::Value(Value::List(result)))
}
BuiltinFn::ListFlatten => {
let list = Self::expect_arg_1::<Vec<Value>>(&args, "List.flatten", span)?;
let mut result = Vec::new();
for item in list {
match item {
Value::List(inner) => result.extend(inner),
other => result.push(other),
}
}
Ok(EvalResult::Value(Value::List(result)))
}
BuiltinFn::ListContains => {
let (list, target) = Self::expect_args_2::<Vec<Value>, Value>(&args, "List.contains", span)?;
let found = list.iter().any(|item| Value::values_equal(item, &target));
Ok(EvalResult::Value(Value::Bool(found)))
}
BuiltinFn::ListTake => { BuiltinFn::ListTake => {
let (list, n) = Self::expect_args_2::<Vec<Value>, i64>(&args, "List.take", span)?; let (list, n) = Self::expect_args_2::<Vec<Value>, i64>(&args, "List.take", span)?;
let n = n.max(0) as usize; let n = n.max(0) as usize;
@@ -2658,6 +2867,67 @@ impl Interpreter {
Ok(EvalResult::Value(Value::Unit)) Ok(EvalResult::Value(Value::Unit))
} }
BuiltinFn::ListSort => {
// List.sort(list) - sort using natural ordering (Int, Float, String, Bool)
let mut list =
Self::expect_arg_1::<Vec<Value>>(&args, "List.sort", span)?;
list.sort_by(|a, b| Self::compare_values(a, b));
Ok(EvalResult::Value(Value::List(list)))
}
BuiltinFn::ListSortBy => {
// List.sortBy(list, fn(a, b) => Int) - sort with custom comparator
// Comparator returns negative (a < b), 0 (a == b), or positive (a > b)
let (list, func) =
Self::expect_args_2::<Vec<Value>, Value>(&args, "List.sortBy", span)?;
let mut indexed: Vec<(usize, Value)> =
list.into_iter().enumerate().collect();
let mut err: Option<RuntimeError> = None;
let func_ref = &func;
let self_ptr = self as *mut Self;
indexed.sort_by(|a, b| {
if err.is_some() {
return std::cmp::Ordering::Equal;
}
// Safety: we're in a single-threaded context and the closure
// needs mutable access to call eval_call_to_value
let interp = unsafe { &mut *self_ptr };
match interp.eval_call_to_value(
func_ref.clone(),
vec![a.1.clone(), b.1.clone()],
span,
) {
Ok(Value::Int(n)) => {
if n < 0 {
std::cmp::Ordering::Less
} else if n > 0 {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
}
Ok(_) => {
err = Some(RuntimeError {
message: "List.sortBy comparator must return Int"
.to_string(),
span: Some(span),
});
std::cmp::Ordering::Equal
}
Err(e) => {
err = Some(e);
std::cmp::Ordering::Equal
}
}
});
if let Some(e) = err {
return Err(e);
}
let result: Vec<Value> =
indexed.into_iter().map(|(_, v)| v).collect();
Ok(EvalResult::Value(Value::List(result)))
}
// Additional String operations // Additional String operations
BuiltinFn::StringStartsWith => { BuiltinFn::StringStartsWith => {
let (s, prefix) = Self::expect_args_2::<String, String>(&args, "String.startsWith", span)?; let (s, prefix) = Self::expect_args_2::<String, String>(&args, "String.startsWith", span)?;
@@ -3068,6 +3338,178 @@ 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)))
}
BuiltinFn::RefNew => {
if args.len() != 1 {
return Err(err("Ref.new requires 1 argument"));
}
Ok(EvalResult::Value(Value::Ref(Rc::new(RefCell::new(args.into_iter().next().unwrap())))))
}
BuiltinFn::RefGet => {
if args.len() != 1 {
return Err(err("Ref.get requires 1 argument"));
}
match &args[0] {
Value::Ref(cell) => Ok(EvalResult::Value(cell.borrow().clone())),
v => Err(err(&format!("Ref.get expects Ref, got {}", v.type_name()))),
}
}
BuiltinFn::RefSet => {
if args.len() != 2 {
return Err(err("Ref.set requires 2 arguments: ref, value"));
}
match &args[0] {
Value::Ref(cell) => {
*cell.borrow_mut() = args[1].clone();
Ok(EvalResult::Value(Value::Unit))
}
v => Err(err(&format!("Ref.set expects Ref as first argument, got {}", v.type_name()))),
}
}
BuiltinFn::RefUpdate => {
if args.len() != 2 {
return Err(err("Ref.update requires 2 arguments: ref, fn"));
}
match &args[0] {
Value::Ref(cell) => {
let old = cell.borrow().clone();
let result = self.eval_call(args[1].clone(), vec![old], span)?;
match result {
EvalResult::Value(new_val) => {
*cell.borrow_mut() = new_val;
}
_ => return Err(err("Ref.update callback must return a value")),
}
Ok(EvalResult::Value(Value::Unit))
}
v => Err(err(&format!("Ref.update expects Ref as first argument, got {}", v.type_name()))),
}
}
} }
} }
@@ -3151,6 +3593,18 @@ impl Interpreter {
}) })
} }
/// Compare two values for natural ordering (used by List.sort)
fn compare_values(a: &Value, b: &Value) -> std::cmp::Ordering {
match (a, b) {
(Value::Int(x), Value::Int(y)) => x.cmp(y),
(Value::Float(x), Value::Float(y)) => x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal),
(Value::String(x), Value::String(y)) => x.cmp(y),
(Value::Bool(x), Value::Bool(y)) => x.cmp(y),
(Value::Char(x), Value::Char(y)) => x.cmp(y),
_ => std::cmp::Ordering::Equal,
}
}
fn match_pattern(&self, pattern: &Pattern, value: &Value) -> Option<Vec<(String, Value)>> { fn match_pattern(&self, pattern: &Pattern, value: &Value) -> Option<Vec<(String, Value)>> {
match pattern { match pattern {
Pattern::Wildcard(_) => Some(Vec::new()), Pattern::Wildcard(_) => Some(Vec::new()),
@@ -3233,6 +3687,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,
@@ -3653,6 +4112,119 @@ impl Interpreter {
} }
} }
("File", "copy") => {
let source = match request.args.first() {
Some(Value::String(s)) => s.clone(),
_ => return Err(RuntimeError {
message: "File.copy requires a string source path".to_string(),
span: None,
}),
};
let dest = match request.args.get(1) {
Some(Value::String(s)) => s.clone(),
_ => return Err(RuntimeError {
message: "File.copy requires a string destination path".to_string(),
span: None,
}),
};
match std::fs::copy(&source, &dest) {
Ok(_) => Ok(Value::Unit),
Err(e) => Err(RuntimeError {
message: format!("Failed to copy '{}' to '{}': {}", source, dest, e),
span: None,
}),
}
}
("File", "glob") => {
let pattern = match request.args.first() {
Some(Value::String(s)) => s.clone(),
_ => return Err(RuntimeError {
message: "File.glob requires a string pattern".to_string(),
span: None,
}),
};
match glob::glob(&pattern) {
Ok(paths) => {
let entries: Vec<Value> = paths
.filter_map(|entry| entry.ok())
.map(|path| Value::String(path.to_string_lossy().to_string()))
.collect();
Ok(Value::List(entries))
}
Err(e) => Err(RuntimeError {
message: format!("Invalid glob pattern '{}': {}", pattern, e),
span: None,
}),
}
}
// ===== File Effect (safe Result-returning variants) =====
("File", "tryRead") => {
let path = match request.args.first() {
Some(Value::String(s)) => s.clone(),
_ => return Err(RuntimeError {
message: "File.tryRead requires a string path".to_string(),
span: None,
}),
};
match std::fs::read_to_string(&path) {
Ok(content) => Ok(Value::Constructor {
name: "Ok".to_string(),
fields: vec![Value::String(content)],
}),
Err(e) => Ok(Value::Constructor {
name: "Err".to_string(),
fields: vec![Value::String(format!("Failed to read file '{}': {}", path, e))],
}),
}
}
("File", "tryWrite") => {
let path = match request.args.first() {
Some(Value::String(s)) => s.clone(),
_ => return Err(RuntimeError {
message: "File.tryWrite requires a string path".to_string(),
span: None,
}),
};
let content = match request.args.get(1) {
Some(Value::String(s)) => s.clone(),
_ => return Err(RuntimeError {
message: "File.tryWrite requires string content".to_string(),
span: None,
}),
};
match std::fs::write(&path, &content) {
Ok(()) => Ok(Value::Constructor {
name: "Ok".to_string(),
fields: vec![Value::Unit],
}),
Err(e) => Ok(Value::Constructor {
name: "Err".to_string(),
fields: vec![Value::String(format!("Failed to write file '{}': {}", path, e))],
}),
}
}
("File", "tryDelete") => {
let path = match request.args.first() {
Some(Value::String(s)) => s.clone(),
_ => return Err(RuntimeError {
message: "File.tryDelete requires a string path".to_string(),
span: None,
}),
};
match std::fs::remove_file(&path) {
Ok(()) => Ok(Value::Constructor {
name: "Ok".to_string(),
fields: vec![Value::Unit],
}),
Err(e) => Ok(Value::Constructor {
name: "Err".to_string(),
fields: vec![Value::String(format!("Failed to delete file '{}': {}", path, e))],
}),
}
}
// ===== Process Effect ===== // ===== Process Effect =====
("Process", "exec") => { ("Process", "exec") => {
use std::process::Command; use std::process::Command;

View File

@@ -42,6 +42,7 @@ pub enum TokenKind {
Effect, Effect,
Handler, Handler,
Run, Run,
Handle,
Resume, Resume,
Type, Type,
True, True,
@@ -54,6 +55,7 @@ pub enum TokenKind {
Trait, // trait (for type classes) Trait, // trait (for type classes)
Impl, // impl (for trait implementations) Impl, // impl (for trait implementations)
For, // for (in impl Trait for Type) For, // for (in impl Trait for Type)
Extern, // extern (for FFI declarations)
// Documentation // Documentation
DocComment(String), // /// doc comment DocComment(String), // /// doc comment
@@ -140,6 +142,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"),
@@ -150,6 +153,7 @@ impl fmt::Display for TokenKind {
TokenKind::Trait => write!(f, "trait"), TokenKind::Trait => write!(f, "trait"),
TokenKind::Impl => write!(f, "impl"), TokenKind::Impl => write!(f, "impl"),
TokenKind::For => write!(f, "for"), TokenKind::For => write!(f, "for"),
TokenKind::Extern => write!(f, "extern"),
TokenKind::DocComment(s) => write!(f, "/// {}", s), TokenKind::DocComment(s) => write!(f, "/// {}", s),
TokenKind::Is => write!(f, "is"), TokenKind::Is => write!(f, "is"),
TokenKind::Pure => write!(f, "pure"), TokenKind::Pure => write!(f, "pure"),
@@ -409,7 +413,26 @@ impl<'a> Lexer<'a> {
} }
// String literals // String literals
'"' => self.scan_string(start)?, '"' => {
// Check for triple-quote multiline string """
if self.peek() == Some('"') {
// Clone to peek at the second char
let mut lookahead = self.chars.clone();
lookahead.next(); // consume first peeked "
if lookahead.peek() == Some(&'"') {
// It's a triple-quote: consume both remaining quotes
self.advance(); // second "
self.advance(); // third "
self.scan_multiline_string(start)?
} else {
// It's an empty string ""
self.advance(); // consume closing "
TokenKind::String(String::new())
}
} else {
self.scan_string(start)?
}
}
// Char literals // Char literals
'\'' => self.scan_char(start)?, '\'' => self.scan_char(start)?,
@@ -667,6 +690,211 @@ impl<'a> Lexer<'a> {
Ok(TokenKind::InterpolatedString(parts)) Ok(TokenKind::InterpolatedString(parts))
} }
fn scan_multiline_string(&mut self, _start: usize) -> Result<TokenKind, LexError> {
let mut parts: Vec<StringPart> = Vec::new();
let mut current_literal = String::new();
// Skip the first newline after opening """ if present
if self.peek() == Some('\n') {
self.advance();
} else if self.peek() == Some('\r') {
self.advance();
if self.peek() == Some('\n') {
self.advance();
}
}
loop {
match self.advance() {
Some('"') => {
// Check for closing """
if self.peek() == Some('"') {
let mut lookahead = self.chars.clone();
lookahead.next(); // consume first peeked "
if lookahead.peek() == Some(&'"') {
// Closing """ found
self.advance(); // second "
self.advance(); // third "
break;
}
}
// Not closing triple-quote, just a regular " in the string
current_literal.push('"');
}
Some('\\') => {
// Handle escape sequences (same as regular strings)
match self.peek() {
Some('{') => {
self.advance();
current_literal.push('{');
}
Some('}') => {
self.advance();
current_literal.push('}');
}
_ => {
let escape_start = self.pos;
let escaped = match self.advance() {
Some('n') => '\n',
Some('r') => '\r',
Some('t') => '\t',
Some('\\') => '\\',
Some('"') => '"',
Some('0') => '\0',
Some('\'') => '\'',
Some(c) => {
return Err(LexError {
message: format!("Invalid escape sequence: \\{}", c),
span: Span::new(escape_start - 1, self.pos),
});
}
None => {
return Err(LexError {
message: "Unterminated multiline string".into(),
span: Span::new(_start, self.pos),
});
}
};
current_literal.push(escaped);
}
}
}
Some('{') => {
// Interpolation (same as regular strings)
if !current_literal.is_empty() {
parts.push(StringPart::Literal(std::mem::take(&mut current_literal)));
}
let mut expr_text = String::new();
let mut brace_depth = 1;
loop {
match self.advance() {
Some('{') => {
brace_depth += 1;
expr_text.push('{');
}
Some('}') => {
brace_depth -= 1;
if brace_depth == 0 {
break;
}
expr_text.push('}');
}
Some(c) => expr_text.push(c),
None => {
return Err(LexError {
message: "Unterminated interpolation in multiline string"
.into(),
span: Span::new(_start, self.pos),
});
}
}
}
parts.push(StringPart::Expr(expr_text));
}
Some(c) => current_literal.push(c),
None => {
return Err(LexError {
message: "Unterminated multiline string".into(),
span: Span::new(_start, self.pos),
});
}
}
}
// Strip common leading whitespace from all lines
let strip_indent = |s: &str| -> String {
if s.is_empty() {
return String::new();
}
let lines: Vec<&str> = s.split('\n').collect();
// Find minimum indentation of non-empty lines
let min_indent = lines
.iter()
.filter(|line| !line.trim().is_empty())
.map(|line| line.len() - line.trim_start().len())
.min()
.unwrap_or(0);
// Strip that indentation from each line
lines
.iter()
.map(|line| {
if line.len() >= min_indent {
&line[min_indent..]
} else {
line.trim_start()
}
})
.collect::<Vec<_>>()
.join("\n")
};
// Strip trailing whitespace-only line before closing """
let trim_trailing = |s: &mut String| {
// Remove trailing spaces/tabs (indent before closing """)
while s.ends_with(' ') || s.ends_with('\t') {
s.pop();
}
// Remove the trailing newline
if s.ends_with('\n') {
s.pop();
if s.ends_with('\r') {
s.pop();
}
}
};
if parts.is_empty() {
trim_trailing(&mut current_literal);
let result = strip_indent(&current_literal);
return Ok(TokenKind::String(result));
}
// Add remaining literal
if !current_literal.is_empty() {
trim_trailing(&mut current_literal);
parts.push(StringPart::Literal(current_literal));
}
// For interpolated multiline strings, strip indent from literal parts
// First, collect all literal content to find min indent
let mut all_text = String::new();
for part in &parts {
if let StringPart::Literal(lit) = part {
all_text.push_str(lit);
}
}
let lines: Vec<&str> = all_text.split('\n').collect();
let min_indent = lines
.iter()
.filter(|line| !line.trim().is_empty())
.map(|line| line.len() - line.trim_start().len())
.min()
.unwrap_or(0);
if min_indent > 0 {
for part in &mut parts {
if let StringPart::Literal(lit) = part {
let stripped_lines: Vec<&str> = lit
.split('\n')
.map(|line| {
if line.len() >= min_indent {
&line[min_indent..]
} else {
line.trim_start()
}
})
.collect();
*lit = stripped_lines.join("\n");
}
}
}
Ok(TokenKind::InterpolatedString(parts))
}
fn scan_char(&mut self, start: usize) -> Result<TokenKind, LexError> { fn scan_char(&mut self, start: usize) -> Result<TokenKind, LexError> {
let c = match self.advance() { let c = match self.advance() {
Some('\\') => match self.advance() { Some('\\') => match self.advance() {
@@ -771,6 +999,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,
@@ -781,6 +1010,7 @@ impl<'a> Lexer<'a> {
"trait" => TokenKind::Trait, "trait" => TokenKind::Trait,
"impl" => TokenKind::Impl, "impl" => TokenKind::Impl,
"for" => TokenKind::For, "for" => TokenKind::For,
"extern" => TokenKind::Extern,
"is" => TokenKind::Is, "is" => TokenKind::Is,
"pure" => TokenKind::Pure, "pure" => TokenKind::Pure,
"total" => TokenKind::Total, "total" => TokenKind::Total,
@@ -789,6 +1019,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()),

View File

@@ -403,6 +403,12 @@ impl Linter {
Declaration::Function(f) => { Declaration::Function(f) => {
self.defined_functions.insert(f.name.name.clone()); self.defined_functions.insert(f.name.name.clone());
} }
Declaration::ExternFn(e) => {
self.defined_functions.insert(e.name.name.clone());
}
Declaration::ExternLet(e) => {
self.define_var(&e.name.name);
}
Declaration::Let(l) => { Declaration::Let(l) => {
self.define_var(&l.name.name); self.define_var(&l.name.name);
} }

View File

@@ -193,10 +193,12 @@ fn main() {
eprintln!(" lux compile <file.lux> --run"); eprintln!(" lux compile <file.lux> --run");
eprintln!(" lux compile <file.lux> --emit-c [-o file.c]"); eprintln!(" lux compile <file.lux> --emit-c [-o file.c]");
eprintln!(" lux compile <file.lux> --target js [-o file.js]"); eprintln!(" lux compile <file.lux> --target js [-o file.js]");
eprintln!(" lux compile <file.lux> --watch");
std::process::exit(1); std::process::exit(1);
} }
let run_after = args.iter().any(|a| a == "--run"); let run_after = args.iter().any(|a| a == "--run");
let emit_c = args.iter().any(|a| a == "--emit-c"); let emit_c = args.iter().any(|a| a == "--emit-c");
let watch = args.iter().any(|a| a == "--watch");
let target_js = args.iter() let target_js = args.iter()
.position(|a| a == "--target") .position(|a| a == "--target")
.and_then(|i| args.get(i + 1)) .and_then(|i| args.get(i + 1))
@@ -212,6 +214,16 @@ fn main() {
} else { } else {
compile_to_c(&args[2], output_path, run_after, emit_c); compile_to_c(&args[2], output_path, run_after, emit_c);
} }
if watch {
// Build the args to replay for each recompilation (without --watch)
let compile_args: Vec<String> = args.iter()
.skip(1)
.filter(|a| a.as_str() != "--watch")
.cloned()
.collect();
watch_and_rerun(&args[2], &compile_args);
}
} }
"repl" => { "repl" => {
// Start REPL // Start REPL
@@ -985,7 +997,7 @@ fn compile_to_js(path: &str, output_path: Option<&str>, run_after: bool) {
// Generate JavaScript code // Generate JavaScript code
let mut backend = JsBackend::new(); let mut backend = JsBackend::new();
let js_code = match backend.generate(&program) { let js_code = match backend.generate(&program, loader.module_cache()) {
Ok(code) => code, Ok(code) => code,
Err(e) => { Err(e) => {
eprintln!("{} JS codegen: {}", c(colors::RED, "error:"), e); eprintln!("{} JS codegen: {}", c(colors::RED, "error:"), e);
@@ -1351,6 +1363,64 @@ fn watch_file(path: &str) {
} }
} }
fn watch_and_rerun(path: &str, compile_args: &[String]) {
use std::time::{Duration, SystemTime};
use std::path::Path;
let file_path = Path::new(path);
if !file_path.exists() {
eprintln!("File not found: {}", path);
std::process::exit(1);
}
println!();
println!("Watching {} for changes (Ctrl+C to stop)...", path);
let mut last_modified = std::fs::metadata(file_path)
.and_then(|m| m.modified())
.unwrap_or(SystemTime::UNIX_EPOCH);
loop {
std::thread::sleep(Duration::from_millis(500));
let modified = match std::fs::metadata(file_path).and_then(|m| m.modified()) {
Ok(m) => m,
Err(_) => continue,
};
if modified > last_modified {
last_modified = modified;
// Clear screen
print!("\x1B[2J\x1B[H");
println!("=== Compiling {} ===", path);
println!();
let result = std::process::Command::new(std::env::current_exe().unwrap())
.args(compile_args)
.status();
match result {
Ok(status) if status.success() => {
println!();
println!("=== Success ===");
}
Ok(_) => {
println!();
println!("=== Failed ===");
}
Err(e) => {
eprintln!("Error running compiler: {}", e);
}
}
println!();
println!("Watching for changes...");
}
}
}
fn serve_static_files(dir: &str, port: u16) { fn serve_static_files(dir: &str, port: u16) {
use std::io::{Write, BufRead, BufReader}; use std::io::{Write, BufRead, BufReader};
use std::net::TcpListener; use std::net::TcpListener;
@@ -2218,6 +2288,48 @@ fn extract_module_doc(source: &str, path: &str) -> Result<ModuleDoc, String> {
is_public: matches!(t.visibility, ast::Visibility::Public), is_public: matches!(t.visibility, ast::Visibility::Public),
}); });
} }
ast::Declaration::ExternFn(ext) => {
let params: Vec<String> = ext.params.iter()
.map(|p| format!("{}: {}", p.name.name, format_type(&p.typ)))
.collect();
let js_note = ext.js_name.as_ref()
.map(|n| format!(" = \"{}\"", n))
.unwrap_or_default();
let signature = format!(
"extern fn {}({}): {}{}",
ext.name.name,
params.join(", "),
format_type(&ext.return_type),
js_note
);
let doc = extract_doc_comment(source, ext.span.start);
functions.push(FunctionDoc {
name: ext.name.name.clone(),
signature,
description: doc,
is_public: matches!(ext.visibility, ast::Visibility::Public),
properties: vec![],
});
}
ast::Declaration::ExternLet(ext) => {
let js_note = ext.js_name.as_ref()
.map(|n| format!(" = \"{}\"", n))
.unwrap_or_default();
let signature = format!(
"extern let {}: {}{}",
ext.name.name,
format_type(&ext.typ),
js_note
);
let doc = extract_doc_comment(source, ext.span.start);
functions.push(FunctionDoc {
name: ext.name.name.clone(),
signature,
description: doc,
is_public: matches!(ext.visibility, ast::Visibility::Public),
properties: vec![],
});
}
ast::Declaration::Effect(e) => { ast::Declaration::Effect(e) => {
let doc = extract_doc_comment(source, e.span.start); let doc = extract_doc_comment(source, e.span.start);
let ops: Vec<String> = e.operations.iter() let ops: Vec<String> = e.operations.iter()
@@ -3855,6 +3967,49 @@ c")"#;
assert_eq!(eval(source).unwrap(), r#""literal {braces}""#); assert_eq!(eval(source).unwrap(), r#""literal {braces}""#);
} }
#[test]
fn test_multiline_string() {
let source = r#"
let s = """
hello
world
"""
let result = String.length(s)
"#;
// "hello\nworld" = 11 chars
assert_eq!(eval(source).unwrap(), "11");
}
#[test]
fn test_multiline_string_with_quotes() {
// Quotes are fine in the middle of triple-quoted strings
let source = "let s = \"\"\"\n She said \"hello\" to him.\n\"\"\"";
assert_eq!(eval(source).unwrap(), r#""She said "hello" to him.""#);
}
#[test]
fn test_multiline_string_interpolation() {
let source = r#"
let name = "Lux"
let s = """
Hello, {name}!
"""
"#;
assert_eq!(eval(source).unwrap(), r#""Hello, Lux!""#);
}
#[test]
fn test_multiline_string_empty() {
let source = r#"let s = """""""#;
assert_eq!(eval(source).unwrap(), r#""""#);
}
#[test]
fn test_multiline_string_inline() {
let source = r#"let s = """hello world""""#;
assert_eq!(eval(source).unwrap(), r#""hello world""#);
}
// Option tests // Option tests
#[test] #[test]
fn test_option_constructors() { fn test_option_constructors() {
@@ -3968,6 +4123,232 @@ c")"#;
assert_eq!(eval("let x = { a: 1, b: 2 } == { a: 1, b: 3 }").unwrap(), "false"); assert_eq!(eval("let x = { a: 1, b: 2 } == { a: 1, b: 3 }").unwrap(), "false");
} }
#[test]
fn test_record_spread() {
let source = r#"
let base = { x: 1, y: 2, z: 3 }
let updated = { ...base, y: 20 }
let result = updated.y
"#;
assert_eq!(eval(source).unwrap(), "20");
}
#[test]
fn test_deep_path_record_update() {
// Basic deep path: { ...base, pos.x: val } desugars to { ...base, pos: { ...base.pos, x: val } }
let source = r#"
let npc = { name: "Goblin", pos: { x: 10, y: 20 } }
let moved = { ...npc, pos.x: 50, pos.y: 60 }
let result = moved.pos.x
"#;
assert_eq!(eval(source).unwrap(), "50");
// Verify other fields are preserved through spread
let source2 = r#"
let npc = { name: "Goblin", pos: { x: 10, y: 20 } }
let moved = { ...npc, pos.x: 50 }
let result = moved.pos.y
"#;
assert_eq!(eval(source2).unwrap(), "20");
// Verify top-level spread fields preserved
let source3 = r#"
let npc = { name: "Goblin", pos: { x: 10, y: 20 } }
let moved = { ...npc, pos.x: 50 }
let result = moved.name
"#;
assert_eq!(eval(source3).unwrap(), "\"Goblin\"");
// Mix of flat and deep path fields
let source4 = r#"
let npc = { name: "Goblin", pos: { x: 10, y: 20 }, hp: 100 }
let updated = { ...npc, pos.x: 50, hp: 80 }
let result = (updated.pos.x, updated.hp, updated.name)
"#;
assert_eq!(eval(source4).unwrap(), "(50, 80, \"Goblin\")");
}
#[test]
fn test_deep_path_record_multilevel() {
// Multi-level deep path: world.physics.gravity
let source = r#"
let world = { name: "Earth", physics: { gravity: { x: 0, y: -10 }, drag: 1 } }
let updated = { ...world, physics.gravity.y: -20 }
let result = (updated.physics.gravity.y, updated.physics.drag, updated.name)
"#;
assert_eq!(eval(source).unwrap(), "(-20, 1, \"Earth\")");
}
#[test]
fn test_deep_path_conflict_error() {
// Field appears as both flat and deep path — should error
let result = eval(r#"
let base = { pos: { x: 1, y: 2 } }
let bad = { ...base, pos: { x: 10, y: 20 }, pos.x: 30 }
"#);
assert!(result.is_err());
}
#[test]
fn test_extern_fn_parse() {
// Extern fn should parse successfully
let source = r#"
extern fn getElementById(id: String): String
let x = 42
"#;
assert_eq!(eval(source).unwrap(), "42");
}
#[test]
fn test_extern_fn_with_js_name() {
// Extern fn with JS name override
let source = r#"
extern fn getCtx(el: String, kind: String): String = "getContext"
let x = 42
"#;
assert_eq!(eval(source).unwrap(), "42");
}
#[test]
fn test_extern_fn_call_errors_in_interpreter() {
// Calling an extern fn in the interpreter should produce a clear error
let source = r#"
extern fn alert(msg: String): Unit
let x = alert("hello")
"#;
let result = eval(source);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("extern") || err.contains("Extern") || err.contains("JavaScript"),
"Error should mention extern/JavaScript: {}", err);
}
#[test]
fn test_pub_extern_fn() {
// pub extern fn should parse
let source = r#"
pub extern fn requestAnimationFrame(callback: fn(): Unit): Int
let x = 42
"#;
assert_eq!(eval(source).unwrap(), "42");
}
#[test]
fn test_extern_fn_js_codegen() {
// Verify JS backend emits extern fn calls without _lux suffix
use crate::codegen::js_backend::JsBackend;
use crate::parser::Parser;
use crate::lexer::Lexer;
let source = r#"
extern fn getElementById(id: String): String
extern fn getContext(el: String, kind: String): String = "getContext"
fn main(): Unit = {
let el = getElementById("canvas")
let ctx = getContext(el, "2d")
()
}
"#;
let tokens = Lexer::new(source).tokenize().unwrap();
let program = Parser::new(tokens).parse_program().unwrap();
let mut backend = JsBackend::new();
let js = backend.generate(&program, &std::collections::HashMap::new()).unwrap();
// getElementById should appear as-is (no _lux suffix)
assert!(js.contains("getElementById("), "JS should call getElementById directly: {}", js);
// getContext should use the JS name override
assert!(js.contains("getContext("), "JS should call getContext directly: {}", js);
// main should still be mangled
assert!(js.contains("main_lux"), "main should be mangled: {}", js);
}
#[test]
fn test_list_get_js_codegen() {
use crate::codegen::js_backend::JsBackend;
use crate::parser::Parser;
use crate::lexer::Lexer;
let source = r#"
fn main(): Unit = {
let xs = [10, 20, 30]
let result = List.get(xs, 1)
()
}
"#;
let tokens = Lexer::new(source).tokenize().unwrap();
let program = Parser::new(tokens).parse_program().unwrap();
let mut backend = JsBackend::new();
let js = backend.generate(&program, &std::collections::HashMap::new()).unwrap();
assert!(js.contains("Lux.Some"), "JS should contain Lux.Some for List.get: {}", js);
assert!(js.contains("Lux.None"), "JS should contain Lux.None for List.get: {}", js);
}
#[test]
fn test_let_main_js_codegen() {
use crate::codegen::js_backend::JsBackend;
use crate::parser::Parser;
use crate::lexer::Lexer;
let source = r#"
let main = fn() => {
print("hello from let main")
}
"#;
let tokens = Lexer::new(source).tokenize().unwrap();
let program = Parser::new(tokens).parse_program().unwrap();
let mut backend = JsBackend::new();
let js = backend.generate(&program, &std::collections::HashMap::new()).unwrap();
// Should contain the let binding
assert!(js.contains("const main"), "JS should contain 'const main': {}", js);
// Should auto-invoke main()
assert!(js.contains("main();"), "JS should auto-invoke main(): {}", js);
// Should NOT contain main_lux (let bindings aren't mangled)
assert!(!js.contains("main_lux"), "let main should not be mangled: {}", js);
}
#[test]
fn test_handler_js_codegen() {
use crate::codegen::js_backend::JsBackend;
use crate::parser::Parser;
use crate::lexer::Lexer;
let source = r#"
effect Log {
fn info(msg: String): Unit
fn debug(msg: String): Unit
}
handler consoleLogger: Log {
fn info(msg) = {
Console.print("[INFO] " + msg)
resume(())
}
fn debug(msg) = {
Console.print("[DEBUG] " + msg)
resume(())
}
}
"#;
let tokens = Lexer::new(source).tokenize().unwrap();
let program = Parser::new(tokens).parse_program().unwrap();
let mut backend = JsBackend::new();
let js = backend.generate(&program, &std::collections::HashMap::new()).unwrap();
// Handler should be emitted as a const object
assert!(js.contains("const consoleLogger_lux"), "JS should contain handler const: {}", js);
// Should have operation methods
assert!(js.contains("info: function(msg)"), "JS should contain info operation: {}", js);
assert!(js.contains("debug: function(msg)"), "JS should contain debug operation: {}", js);
// Should define resume locally
assert!(js.contains("const resume = (x) => x"), "JS should define resume: {}", js);
}
#[test] #[test]
fn test_invalid_escape_sequence() { fn test_invalid_escape_sequence() {
let result = eval(r#"let x = "\z""#); let result = eval(r#"let x = "\z""#);
@@ -5441,4 +5822,225 @@ c")"#;
check_file("projects/rest-api/main.lux").unwrap(); check_file("projects/rest-api/main.lux").unwrap();
} }
} }
// === Map type tests ===
#[test]
fn test_map_new_and_size() {
let source = r#"
let m = Map.new()
let result = Map.size(m)
"#;
assert_eq!(eval(source).unwrap(), "0");
}
#[test]
fn test_map_set_and_get() {
let source = r#"
let m = Map.new()
let m2 = Map.set(m, "name", "Alice")
let result = Map.get(m2, "name")
"#;
assert_eq!(eval(source).unwrap(), "Some(\"Alice\")");
}
#[test]
fn test_map_get_missing() {
let source = r#"
let m = Map.new()
let result = Map.get(m, "missing")
"#;
assert_eq!(eval(source).unwrap(), "None");
}
#[test]
fn test_map_contains() {
let source = r#"
let m = Map.set(Map.new(), "x", 1)
let result = (Map.contains(m, "x"), Map.contains(m, "y"))
"#;
assert_eq!(eval(source).unwrap(), "(true, false)");
}
#[test]
fn test_map_remove() {
let source = r#"
let m = Map.set(Map.set(Map.new(), "a", 1), "b", 2)
let m2 = Map.remove(m, "a")
let result = (Map.size(m2), Map.contains(m2, "a"), Map.contains(m2, "b"))
"#;
assert_eq!(eval(source).unwrap(), "(1, false, true)");
}
#[test]
fn test_map_keys_and_values() {
let source = r#"
let m = Map.set(Map.set(Map.new(), "b", 2), "a", 1)
let result = Map.keys(m)
"#;
assert_eq!(eval(source).unwrap(), "[\"a\", \"b\"]");
}
#[test]
fn test_map_from_list() {
let source = r#"
let m = Map.fromList([("x", 10), ("y", 20)])
let result = (Map.get(m, "x"), Map.size(m))
"#;
assert_eq!(eval(source).unwrap(), "(Some(10), 2)");
}
#[test]
fn test_map_to_list() {
let source = r#"
let m = Map.set(Map.set(Map.new(), "b", 2), "a", 1)
let result = Map.toList(m)
"#;
assert_eq!(eval(source).unwrap(), "[(\"a\", 1), (\"b\", 2)]");
}
#[test]
fn test_map_merge() {
let source = r#"
let m1 = Map.fromList([("a", 1), ("b", 2)])
let m2 = Map.fromList([("b", 3), ("c", 4)])
let merged = Map.merge(m1, m2)
let result = (Map.get(merged, "a"), Map.get(merged, "b"), Map.get(merged, "c"))
"#;
assert_eq!(eval(source).unwrap(), "(Some(1), Some(3), Some(4))");
}
#[test]
fn test_map_immutability() {
let source = r#"
let m1 = Map.fromList([("a", 1)])
let m2 = Map.set(m1, "b", 2)
let result = (Map.size(m1), Map.size(m2))
"#;
assert_eq!(eval(source).unwrap(), "(1, 2)");
}
#[test]
fn test_map_is_empty() {
let source = r#"
let m1 = Map.new()
let m2 = Map.set(m1, "x", 1)
let result = (Map.isEmpty(m1), Map.isEmpty(m2))
"#;
assert_eq!(eval(source).unwrap(), "(true, false)");
}
#[test]
fn test_map_type_annotation() {
let source = r#"
fn lookup(m: Map<String, Int>, key: String): Option<Int> =
Map.get(m, key)
let m = Map.fromList([("age", 30)])
let result = lookup(m, "age")
"#;
assert_eq!(eval(source).unwrap(), "Some(30)");
}
// Ref cell tests
#[test]
fn test_ref_new_and_get() {
let source = r#"
let r = Ref.new(42)
let result = Ref.get(r)
"#;
assert_eq!(eval(source).unwrap(), "42");
}
#[test]
fn test_ref_set() {
let source = r#"
let r = Ref.new(0)
let _ = Ref.set(r, 10)
let result = Ref.get(r)
"#;
assert_eq!(eval(source).unwrap(), "10");
}
#[test]
fn test_ref_update() {
let source = r#"
let r = Ref.new(5)
let _ = Ref.update(r, fn(n) => n + 1)
let result = Ref.get(r)
"#;
assert_eq!(eval(source).unwrap(), "6");
}
#[test]
fn test_ref_multiple_updates() {
let source = r#"
let counter = Ref.new(0)
let _ = Ref.set(counter, 1)
let _ = Ref.update(counter, fn(n) => n * 10)
let _ = Ref.set(counter, Ref.get(counter) + 5)
let result = Ref.get(counter)
"#;
assert_eq!(eval(source).unwrap(), "15");
}
#[test]
fn test_ref_with_string() {
let source = r#"
let r = Ref.new("hello")
let _ = Ref.set(r, "world")
let result = Ref.get(r)
"#;
assert_eq!(eval(source).unwrap(), "\"world\"");
}
#[test]
fn test_file_copy() {
use std::io::Write;
// Create a temp file, copy it, verify contents
let dir = std::env::temp_dir().join("lux_test_file_copy");
let _ = std::fs::create_dir_all(&dir);
let src = dir.join("src.txt");
let dst = dir.join("dst.txt");
std::fs::File::create(&src).unwrap().write_all(b"hello copy").unwrap();
let _ = std::fs::remove_file(&dst);
let source = format!(r#"
fn main(): Unit with {{File}} =
File.copy("{}", "{}")
let _ = run main() with {{}}
let result = "done"
"#, src.display(), dst.display());
let result = eval(&source);
assert!(result.is_ok(), "File.copy failed: {:?}", result);
let contents = std::fs::read_to_string(&dst).unwrap();
assert_eq!(contents, "hello copy");
// Cleanup
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_effectful_callback_propagation() {
// WISH-7: effectful callbacks in List.forEach should propagate effects
// This should type-check successfully because Console effect is inferred
let source = r#"
fn printAll(items: List<String>): Unit =
List.forEach(items, fn(x: String): Unit => Console.print(x))
let result = "ok"
"#;
let result = eval(source);
assert!(result.is_ok(), "Effectful callback should type-check: {:?}", result);
}
#[test]
fn test_effectful_callback_in_map() {
// Effectful callback in List.map should propagate effects
let source = r#"
fn readAll(paths: List<String>): List<String> =
List.map(paths, fn(p: String): String => File.read(p))
let result = "ok"
"#;
let result = eval(source);
assert!(result.is_ok(), "Effectful callback in map should type-check: {:?}", result);
}
} }

View File

@@ -52,6 +52,8 @@ impl Module {
Declaration::Let(l) => l.visibility == Visibility::Public, Declaration::Let(l) => l.visibility == Visibility::Public,
Declaration::Type(t) => t.visibility == Visibility::Public, Declaration::Type(t) => t.visibility == Visibility::Public,
Declaration::Trait(t) => t.visibility == Visibility::Public, Declaration::Trait(t) => t.visibility == Visibility::Public,
Declaration::ExternFn(e) => e.visibility == Visibility::Public,
Declaration::ExternLet(e) => e.visibility == Visibility::Public,
// Effects, handlers, and impls are always public for now // Effects, handlers, and impls are always public for now
Declaration::Effect(_) | Declaration::Handler(_) | Declaration::Impl(_) => true, Declaration::Effect(_) | Declaration::Handler(_) | Declaration::Impl(_) => true,
} }
@@ -279,6 +281,12 @@ impl ModuleLoader {
} }
Declaration::Type(t) if t.visibility == Visibility::Public => { Declaration::Type(t) if t.visibility == Visibility::Public => {
exports.insert(t.name.name.clone()); exports.insert(t.name.name.clone());
// Also export constructors for ADT types
if let crate::ast::TypeDef::Enum(variants) = &t.definition {
for variant in variants {
exports.insert(variant.name.name.clone());
}
}
} }
Declaration::Effect(e) => { Declaration::Effect(e) => {
// Effects are always exported // Effects are always exported
@@ -288,6 +296,12 @@ impl ModuleLoader {
// Handlers are always exported // Handlers are always exported
exports.insert(h.name.name.clone()); exports.insert(h.name.name.clone());
} }
Declaration::ExternFn(e) if e.visibility == Visibility::Public => {
exports.insert(e.name.name.clone());
}
Declaration::ExternLet(e) if e.visibility == Visibility::Public => {
exports.insert(e.name.name.clone());
}
_ => {} _ => {}
} }
} }

View File

@@ -238,6 +238,7 @@ impl Parser {
match self.peek_kind() { match self.peek_kind() {
TokenKind::Fn => Ok(Declaration::Function(self.parse_function_decl(visibility, doc)?)), TokenKind::Fn => Ok(Declaration::Function(self.parse_function_decl(visibility, doc)?)),
TokenKind::Extern => self.parse_extern_decl(visibility, doc),
TokenKind::Effect => Ok(Declaration::Effect(self.parse_effect_decl(doc)?)), TokenKind::Effect => Ok(Declaration::Effect(self.parse_effect_decl(doc)?)),
TokenKind::Handler => Ok(Declaration::Handler(self.parse_handler_decl()?)), TokenKind::Handler => Ok(Declaration::Handler(self.parse_handler_decl()?)),
TokenKind::Type => Ok(Declaration::Type(self.parse_type_decl(visibility, doc)?)), TokenKind::Type => Ok(Declaration::Type(self.parse_type_decl(visibility, doc)?)),
@@ -245,7 +246,8 @@ impl Parser {
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::Run => Err(self.error("Bare 'run' expressions are not allowed at top level. Use 'let _ = run ...' or 'let result = run ...'")),
_ => Err(self.error("Expected declaration (fn, effect, handler, type, trait, impl, or let)")), TokenKind::Handle => Err(self.error("Bare 'handle' expressions are not allowed at top level. Use 'let _ = handle ...' or 'let result = handle ...'")),
_ => Err(self.error("Expected declaration (fn, extern, effect, handler, type, trait, impl, or let)")),
} }
} }
@@ -322,6 +324,109 @@ impl Parser {
}) })
} }
/// Parse extern declaration: dispatch to extern fn or extern let
fn parse_extern_decl(&mut self, visibility: Visibility, doc: Option<String>) -> Result<Declaration, ParseError> {
// Peek past 'extern' to see if it's 'fn' or 'let'
if self.pos + 1 < self.tokens.len() {
match &self.tokens[self.pos + 1].kind {
TokenKind::Fn => Ok(Declaration::ExternFn(self.parse_extern_fn_decl(visibility, doc)?)),
TokenKind::Let => Ok(Declaration::ExternLet(self.parse_extern_let_decl(visibility, doc)?)),
_ => Err(self.error("Expected 'fn' or 'let' after 'extern'")),
}
} else {
Err(self.error("Expected 'fn' or 'let' after 'extern'"))
}
}
/// Parse extern let declaration: extern let name: Type = "jsName"
fn parse_extern_let_decl(&mut self, visibility: Visibility, doc: Option<String>) -> Result<ExternLetDecl, ParseError> {
let start = self.current_span();
self.expect(TokenKind::Extern)?;
self.expect(TokenKind::Let)?;
let name = self.parse_ident()?;
// Type annotation
self.expect(TokenKind::Colon)?;
let typ = self.parse_type()?;
// Optional JS name override: = "jsName"
let js_name = if self.check(TokenKind::Eq) {
self.advance();
match self.peek_kind() {
TokenKind::String(s) => {
let name = s.clone();
self.advance();
Some(name)
}
_ => return Err(self.error("Expected string literal for JS name in extern let")),
}
} else {
None
};
let span = start.merge(self.previous_span());
Ok(ExternLetDecl {
visibility,
doc,
name,
typ,
js_name,
span,
})
}
/// Parse extern function declaration: extern fn name<T>(params): ReturnType = "jsName"
fn parse_extern_fn_decl(&mut self, visibility: Visibility, doc: Option<String>) -> Result<ExternFnDecl, ParseError> {
let start = self.current_span();
self.expect(TokenKind::Extern)?;
self.expect(TokenKind::Fn)?;
let name = self.parse_ident()?;
// Optional type parameters
let type_params = if self.check(TokenKind::Lt) {
self.parse_type_params()?
} else {
Vec::new()
};
self.expect(TokenKind::LParen)?;
let params = self.parse_params()?;
self.expect(TokenKind::RParen)?;
// Return type
self.expect(TokenKind::Colon)?;
let return_type = self.parse_type()?;
// Optional JS name override: = "jsName"
let js_name = if self.check(TokenKind::Eq) {
self.advance();
match self.peek_kind() {
TokenKind::String(s) => {
let name = s.clone();
self.advance();
Some(name)
}
_ => return Err(self.error("Expected string literal for JS name in extern fn")),
}
} else {
None
};
let span = start.merge(self.previous_span());
Ok(ExternFnDecl {
visibility,
doc,
name,
type_params,
params,
return_type,
js_name,
span,
})
}
/// Parse effect declaration /// Parse effect declaration
fn parse_effect_decl(&mut self, doc: Option<String>) -> Result<EffectDecl, ParseError> { fn parse_effect_decl(&mut self, doc: Option<String>) -> Result<EffectDecl, ParseError> {
let start = self.current_span(); let start = self.current_span();
@@ -845,6 +950,7 @@ impl Parser {
/// Parse function parameters /// Parse function parameters
fn parse_params(&mut self) -> Result<Vec<Parameter>, ParseError> { fn parse_params(&mut self) -> Result<Vec<Parameter>, ParseError> {
let mut params = Vec::new(); let mut params = Vec::new();
self.skip_newlines();
while !self.check(TokenKind::RParen) { while !self.check(TokenKind::RParen) {
let start = self.current_span(); let start = self.current_span();
@@ -854,9 +960,11 @@ impl Parser {
let span = start.merge(self.previous_span()); let span = start.merge(self.previous_span());
params.push(Parameter { name, typ, span }); params.push(Parameter { name, typ, span });
self.skip_newlines();
if !self.check(TokenKind::RParen) { if !self.check(TokenKind::RParen) {
self.expect(TokenKind::Comma)?; self.expect(TokenKind::Comma)?;
self.skip_newlines();
} }
} }
@@ -1775,6 +1883,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
@@ -1792,6 +1901,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()?);
@@ -1916,9 +2026,27 @@ impl Parser {
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()) {
self.parse_constructor_pattern() self.parse_constructor_pattern_with_module(None)
} else { } else {
let ident = self.parse_ident()?; let ident = self.parse_ident()?;
// Check for module-qualified constructor: module.Constructor
if self.check(TokenKind::Dot) {
// Peek ahead to see if next is an uppercase identifier
let dot_pos = self.pos;
self.advance(); // skip dot
if let TokenKind::Ident(next_name) = self.peek_kind() {
if next_name
.chars()
.next()
.map_or(false, |c| c.is_uppercase())
{
return self
.parse_constructor_pattern_with_module(Some(ident));
}
}
// Not a module-qualified constructor, backtrack
self.pos = dot_pos;
}
Ok(Pattern::Var(ident)) Ok(Pattern::Var(ident))
} }
} }
@@ -1928,25 +2056,40 @@ impl Parser {
} }
} }
fn parse_constructor_pattern(&mut self) -> Result<Pattern, ParseError> { fn parse_constructor_pattern_with_module(
let start = self.current_span(); &mut self,
module: Option<Ident>,
) -> Result<Pattern, ParseError> {
let start = module
.as_ref()
.map(|m| m.span)
.unwrap_or_else(|| self.current_span());
let name = self.parse_ident()?; let name = self.parse_ident()?;
if self.check(TokenKind::LParen) { if self.check(TokenKind::LParen) {
self.advance(); self.advance();
self.skip_newlines();
let mut fields = Vec::new(); let mut fields = Vec::new();
while !self.check(TokenKind::RParen) { while !self.check(TokenKind::RParen) {
fields.push(self.parse_pattern()?); fields.push(self.parse_pattern()?);
self.skip_newlines();
if !self.check(TokenKind::RParen) { if !self.check(TokenKind::RParen) {
self.expect(TokenKind::Comma)?; self.expect(TokenKind::Comma)?;
self.skip_newlines();
} }
} }
self.expect(TokenKind::RParen)?; self.expect(TokenKind::RParen)?;
let span = start.merge(self.previous_span()); let span = start.merge(self.previous_span());
Ok(Pattern::Constructor { name, fields, span })
} else {
let span = name.span;
Ok(Pattern::Constructor { Ok(Pattern::Constructor {
module,
name,
fields,
span,
})
} else {
let span = start.merge(name.span);
Ok(Pattern::Constructor {
module,
name, name,
fields: Vec::new(), fields: Vec::new(),
span, span,
@@ -1957,12 +2100,15 @@ impl Parser {
fn parse_tuple_pattern(&mut self) -> Result<Pattern, ParseError> { fn parse_tuple_pattern(&mut self) -> Result<Pattern, ParseError> {
let start = self.current_span(); let start = self.current_span();
self.expect(TokenKind::LParen)?; self.expect(TokenKind::LParen)?;
self.skip_newlines();
let mut elements = Vec::new(); let mut elements = Vec::new();
while !self.check(TokenKind::RParen) { while !self.check(TokenKind::RParen) {
elements.push(self.parse_pattern()?); elements.push(self.parse_pattern()?);
self.skip_newlines();
if !self.check(TokenKind::RParen) { if !self.check(TokenKind::RParen) {
self.expect(TokenKind::Comma)?; self.expect(TokenKind::Comma)?;
self.skip_newlines();
} }
} }
@@ -2092,6 +2238,7 @@ impl Parser {
fn parse_lambda_params(&mut self) -> Result<Vec<Parameter>, ParseError> { fn parse_lambda_params(&mut self) -> Result<Vec<Parameter>, ParseError> {
let mut params = Vec::new(); let mut params = Vec::new();
self.skip_newlines();
while !self.check(TokenKind::RParen) { while !self.check(TokenKind::RParen) {
let start = self.current_span(); let start = self.current_span();
@@ -2107,9 +2254,11 @@ impl Parser {
let span = start.merge(self.previous_span()); let span = start.merge(self.previous_span());
params.push(Parameter { name, typ, span }); params.push(Parameter { name, typ, span });
self.skip_newlines();
if !self.check(TokenKind::RParen) { if !self.check(TokenKind::RParen) {
self.expect(TokenKind::Comma)?; self.expect(TokenKind::Comma)?;
self.skip_newlines();
} }
} }
@@ -2150,6 +2299,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)?;
@@ -2163,6 +2346,7 @@ impl Parser {
fn parse_tuple_or_paren_expr(&mut self) -> Result<Expr, ParseError> { fn parse_tuple_or_paren_expr(&mut self) -> Result<Expr, ParseError> {
let start = self.current_span(); let start = self.current_span();
self.expect(TokenKind::LParen)?; self.expect(TokenKind::LParen)?;
self.skip_newlines();
if self.check(TokenKind::RParen) { if self.check(TokenKind::RParen) {
self.advance(); self.advance();
@@ -2173,16 +2357,19 @@ impl Parser {
} }
let first = self.parse_expr()?; let first = self.parse_expr()?;
self.skip_newlines();
if self.check(TokenKind::Comma) { if self.check(TokenKind::Comma) {
// Tuple // Tuple
let mut elements = vec![first]; let mut elements = vec![first];
while self.check(TokenKind::Comma) { while self.check(TokenKind::Comma) {
self.advance(); self.advance();
self.skip_newlines();
if self.check(TokenKind::RParen) { if self.check(TokenKind::RParen) {
break; break;
} }
elements.push(self.parse_expr()?); elements.push(self.parse_expr()?);
self.skip_newlines();
} }
self.expect(TokenKind::RParen)?; self.expect(TokenKind::RParen)?;
let span = start.merge(self.previous_span()); let span = start.merge(self.previous_span());
@@ -2213,12 +2400,34 @@ impl Parser {
return self.parse_record_expr_rest(start); 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 ident.path: 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);
if matches!(lookahead, Some(TokenKind::Colon)) { if matches!(lookahead, Some(TokenKind::Colon)) {
return self.parse_record_expr_rest(start); return self.parse_record_expr_rest(start);
} }
// Check for deep path record: { ident.ident...: expr }
if matches!(lookahead, Some(TokenKind::Dot)) {
let mut look = self.pos + 2;
loop {
match self.tokens.get(look).map(|t| &t.kind) {
Some(TokenKind::Ident(_)) => {
look += 1;
match self.tokens.get(look).map(|t| &t.kind) {
Some(TokenKind::Colon) => {
return self.parse_record_expr_rest(start);
}
Some(TokenKind::Dot) => {
look += 1;
continue;
}
_ => break,
}
}
_ => break,
}
}
}
} }
// It's a block // It's a block
@@ -2226,8 +2435,9 @@ 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 raw_fields: Vec<(Vec<Ident>, Expr)> = Vec::new();
let mut spread = None; let mut spread = None;
let mut has_deep_paths = false;
// Check for spread: { ...expr, ... } // Check for spread: { ...expr, ... }
if self.check(TokenKind::DotDotDot) { if self.check(TokenKind::DotDotDot) {
@@ -2244,9 +2454,21 @@ impl Parser {
while !self.check(TokenKind::RBrace) { while !self.check(TokenKind::RBrace) {
let name = self.parse_ident()?; let name = self.parse_ident()?;
// Check for dotted path: pos.x, pos.x.y, etc.
let mut path = vec![name];
while self.check(TokenKind::Dot) {
self.advance(); // consume .
let segment = self.parse_ident()?;
path.push(segment);
}
if path.len() > 1 {
has_deep_paths = true;
}
self.expect(TokenKind::Colon)?; self.expect(TokenKind::Colon)?;
let value = self.parse_expr()?; let value = self.parse_expr()?;
fields.push((name, value)); raw_fields.push((path, value));
self.skip_newlines(); self.skip_newlines();
if self.check(TokenKind::Comma) { if self.check(TokenKind::Comma) {
@@ -2257,10 +2479,119 @@ impl Parser {
self.expect(TokenKind::RBrace)?; self.expect(TokenKind::RBrace)?;
let span = start.merge(self.previous_span()); let span = start.merge(self.previous_span());
if has_deep_paths {
Self::desugar_deep_fields(spread, raw_fields, span)
} else {
// No deep paths — use flat fields directly (common case, no allocation overhead)
let fields = raw_fields
.into_iter()
.map(|(mut path, value)| (path.remove(0), value))
.collect();
Ok(Expr::Record {
spread,
fields,
span,
})
}
}
/// Desugar deep path record fields into nested record spread expressions.
/// `{ ...base, pos.x: vx, pos.y: vy }` becomes `{ ...base, pos: { ...base.pos, x: vx, y: vy } }`
fn desugar_deep_fields(
spread: Option<Box<Expr>>,
raw_fields: Vec<(Vec<Ident>, Expr)>,
outer_span: Span,
) -> Result<Expr, ParseError> {
use std::collections::HashMap;
// Group fields by first path segment, preserving order
let mut groups: Vec<(String, Vec<(Vec<Ident>, Expr)>)> = Vec::new();
let mut group_map: HashMap<String, usize> = HashMap::new();
for (path, value) in raw_fields {
let key = path[0].name.clone();
if let Some(&idx) = group_map.get(&key) {
groups[idx].1.push((path, value));
} else {
group_map.insert(key.clone(), groups.len());
groups.push((key, vec![(path, value)]));
}
}
let mut fields = Vec::new();
for (_, group) in groups {
let first_ident = group[0].0[0].clone();
let has_flat = group.iter().any(|(p, _)| p.len() == 1);
let has_deep = group.iter().any(|(p, _)| p.len() > 1);
if has_flat && has_deep {
return Err(ParseError {
message: format!(
"Field '{}' appears as both a direct field and a deep path prefix",
first_ident.name
),
span: first_ident.span,
});
}
if has_flat {
if group.len() > 1 {
return Err(ParseError {
message: format!("Duplicate field '{}'", first_ident.name),
span: group[1].0[0].span,
});
}
let (_, value) = group.into_iter().next().unwrap();
fields.push((first_ident, value));
} else {
// Deep paths — create nested record with spread from parent
let sub_spread = spread.as_ref().map(|s| {
Box::new(Expr::Field {
object: s.clone(),
field: first_ident.clone(),
span: first_ident.span,
})
});
// Strip first segment from all paths
let sub_fields: Vec<(Vec<Ident>, Expr)> = group
.into_iter()
.map(|(mut path, value)| {
path.remove(0);
(path, value)
})
.collect();
let has_nested_deep = sub_fields.iter().any(|(p, _)| p.len() > 1);
if has_nested_deep {
// Recursively desugar deeper paths
let nested =
Self::desugar_deep_fields(sub_spread, sub_fields, first_ident.span)?;
fields.push((first_ident, nested));
} else {
// All sub-paths are single-segment — build Record directly
let flat_fields: Vec<(Ident, Expr)> = sub_fields
.into_iter()
.map(|(mut path, value)| (path.remove(0), value))
.collect();
fields.push((
first_ident.clone(),
Expr::Record {
spread: sub_spread,
fields: flat_fields,
span: first_ident.span,
},
));
}
}
}
Ok(Expr::Record { Ok(Expr::Record {
spread, spread,
fields, fields,
span, span: outer_span,
}) })
} }

View File

@@ -245,6 +245,48 @@ impl SymbolTable {
Declaration::Handler(h) => self.visit_handler(h, scope_idx), Declaration::Handler(h) => self.visit_handler(h, scope_idx),
Declaration::Trait(t) => self.visit_trait(t, scope_idx), Declaration::Trait(t) => self.visit_trait(t, scope_idx),
Declaration::Impl(i) => self.visit_impl(i, scope_idx), Declaration::Impl(i) => self.visit_impl(i, scope_idx),
Declaration::ExternFn(ext) => {
let is_public = matches!(ext.visibility, Visibility::Public);
let params: Vec<String> = ext
.params
.iter()
.map(|p| format!("{}: {}", p.name.name, self.type_expr_to_string(&p.typ)))
.collect();
let sig = format!(
"extern fn {}({}): {}",
ext.name.name,
params.join(", "),
self.type_expr_to_string(&ext.return_type)
);
let mut symbol = self.new_symbol(
ext.name.name.clone(),
SymbolKind::Function,
ext.span,
Some(sig),
is_public,
);
symbol.documentation = ext.doc.clone();
let id = self.add_symbol(scope_idx, symbol);
self.add_reference(id, ext.name.span, true, true);
}
Declaration::ExternLet(ext) => {
let is_public = matches!(ext.visibility, Visibility::Public);
let sig = format!(
"extern let {}: {}",
ext.name.name,
self.type_expr_to_string(&ext.typ)
);
let mut symbol = self.new_symbol(
ext.name.name.clone(),
SymbolKind::Variable,
ext.span,
Some(sig),
is_public,
);
symbol.documentation = ext.doc.clone();
let id = self.add_symbol(scope_idx, symbol);
self.add_reference(id, ext.name.span, true, true);
}
} }
} }

View File

@@ -5,9 +5,9 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::ast::{ use crate::ast::{
self, BinaryOp, Declaration, EffectDecl, Expr, FunctionDecl, HandlerDecl, Ident, ImplDecl, self, BinaryOp, Declaration, EffectDecl, ExternFnDecl, Expr, FunctionDecl, HandlerDecl, Ident,
ImportDecl, LetDecl, Literal, LiteralKind, MatchArm, Parameter, Pattern, Program, Span, ImplDecl, ImportDecl, LetDecl, Literal, LiteralKind, MatchArm, Parameter, Pattern, Program,
Statement, TraitDecl, TypeDecl, TypeExpr, UnaryOp, VariantFields, Span, Statement, TraitDecl, TypeDecl, TypeExpr, UnaryOp, VariantFields,
}; };
use crate::diagnostics::{find_similar_names, format_did_you_mean, Diagnostic, ErrorCode, Severity}; use crate::diagnostics::{find_similar_names, format_did_you_mean, Diagnostic, ErrorCode, Severity};
use crate::exhaustiveness::{check_exhaustiveness, missing_patterns_hint}; use crate::exhaustiveness::{check_exhaustiveness, missing_patterns_hint};
@@ -981,6 +981,13 @@ impl TypeChecker {
if !fields.is_empty() { if !fields.is_empty() {
self.env.bind(&name, TypeScheme::mono(Type::Record(fields))); self.env.bind(&name, TypeScheme::mono(Type::Record(fields)));
} }
// Also copy type definitions so imported types are usable
for (type_name, type_def) in &module_checker.env.types {
if !self.env.types.contains_key(type_name) {
self.env.types.insert(type_name.clone(), type_def.clone());
}
}
} }
ImportKind::Direct => { ImportKind::Direct => {
// Import a specific name directly // Import a specific name directly
@@ -1220,6 +1227,22 @@ impl TypeChecker {
let trait_impl = self.collect_impl(impl_decl); let trait_impl = self.collect_impl(impl_decl);
self.env.trait_impls.push(trait_impl); self.env.trait_impls.push(trait_impl);
} }
Declaration::ExternFn(ext) => {
// Register extern fn type signature (like a regular function but no body)
let param_types: Vec<Type> = ext
.params
.iter()
.map(|p| self.resolve_type(&p.typ))
.collect();
let return_type = self.resolve_type(&ext.return_type);
let fn_type = Type::function(param_types, return_type);
self.env.bind(&ext.name.name, TypeScheme::mono(fn_type));
}
Declaration::ExternLet(ext) => {
// Register extern let with its declared type
let typ = self.resolve_type(&ext.typ);
self.env.bind(&ext.name.name, TypeScheme::mono(typ));
}
} }
} }
@@ -1951,6 +1974,17 @@ impl TypeChecker {
let func_type = self.infer_expr(func); let func_type = self.infer_expr(func);
let arg_types: Vec<Type> = args.iter().map(|a| self.infer_expr(a)).collect(); let arg_types: Vec<Type> = args.iter().map(|a| self.infer_expr(a)).collect();
// Propagate effects from callback arguments to enclosing scope
for arg_type in &arg_types {
if let Type::Function { effects, .. } = arg_type {
for effect in &effects.effects {
if self.inferring_effects {
self.inferred_effects.insert(effect.clone());
}
}
}
}
// Check property constraints from where clauses // Check property constraints from where clauses
if let Expr::Var(func_id) = func { if let Expr::Var(func_id) = func {
if let Some(constraints) = self.property_constraints.get(&func_id.name).cloned() { if let Some(constraints) = self.property_constraints.get(&func_id.name).cloned() {
@@ -2061,6 +2095,18 @@ impl TypeChecker {
if let Some((_, field_type)) = fields.iter().find(|(n, _)| n == &operation.name) { if let Some((_, field_type)) = fields.iter().find(|(n, _)| n == &operation.name) {
// It's a function call on a module field // It's a function call on a module field
let arg_types: Vec<Type> = args.iter().map(|a| self.infer_expr(a)).collect(); let arg_types: Vec<Type> = args.iter().map(|a| self.infer_expr(a)).collect();
// Propagate effects from callback arguments to enclosing scope
for arg_type in &arg_types {
if let Type::Function { effects, .. } = arg_type {
for effect in &effects.effects {
if self.inferring_effects {
self.inferred_effects.insert(effect.clone());
}
}
}
}
let result_type = Type::var(); let result_type = Type::var();
let expected_fn = Type::function(arg_types, result_type.clone()); let expected_fn = Type::function(arg_types, result_type.clone());
@@ -2120,6 +2166,17 @@ impl TypeChecker {
// Check argument types // Check argument types
let arg_types: Vec<Type> = args.iter().map(|a| self.infer_expr(a)).collect(); let arg_types: Vec<Type> = args.iter().map(|a| self.infer_expr(a)).collect();
// Propagate effects from callback arguments to enclosing scope
for arg_type in &arg_types {
if let Type::Function { effects, .. } = arg_type {
for effect in &effects.effects {
if self.inferring_effects {
self.inferred_effects.insert(effect.clone());
}
}
}
}
if arg_types.len() != op.params.len() { if arg_types.len() != op.params.len() {
self.errors.push(TypeError { self.errors.push(TypeError {
message: format!( message: format!(
@@ -2442,7 +2499,7 @@ impl TypeChecker {
Vec::new() Vec::new()
} }
Pattern::Constructor { name, fields, span } => { Pattern::Constructor { name, fields, span, .. } => {
// Look up constructor // Look up constructor
// For now, handle Option specially // For now, handle Option specially
match name.name.as_str() { match name.name.as_str() {
@@ -2569,6 +2626,7 @@ impl TypeChecker {
// Start with spread fields if present // Start with spread fields if present
let mut field_types: Vec<(String, Type)> = if let Some(spread_expr) = spread { 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.infer_expr(spread_expr);
let spread_type = self.env.expand_type_alias(&spread_type);
match spread_type { match spread_type {
Type::Record(spread_fields) => spread_fields, Type::Record(spread_fields) => spread_fields,
_ => { _ => {
@@ -2965,6 +3023,12 @@ 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()));
}
"Ref" if resolved_args.len() == 1 => {
return Type::Ref(Box::new(resolved_args[0].clone()));
}
_ => {} _ => {}
} }
} }

View File

@@ -47,6 +47,10 @@ 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>),
/// Ref type — mutable reference cell holding a value of type T
Ref(Box<Type>),
/// Versioned type (e.g., User @v2) /// Versioned type (e.g., User @v2)
Versioned { Versioned {
base: Box<Type>, base: Box<Type>,
@@ -118,7 +122,8 @@ 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) | Type::Ref(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 +163,8 @@ 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::Ref(inner) => Type::Ref(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(),
@@ -207,7 +214,12 @@ impl Type {
} }
vars vars
} }
Type::List(inner) | Type::Option(inner) => inner.free_vars(), Type::List(inner) | Type::Option(inner) | Type::Ref(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 +291,8 @@ 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::Ref(inner) => write!(f, "Ref<{}>", 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)
} }
@@ -946,6 +960,46 @@ impl TypeEnv {
params: vec![("path".to_string(), Type::String)], params: vec![("path".to_string(), Type::String)],
return_type: Type::Unit, return_type: Type::Unit,
}, },
EffectOpDef {
name: "copy".to_string(),
params: vec![
("source".to_string(), Type::String),
("dest".to_string(), Type::String),
],
return_type: Type::Unit,
},
EffectOpDef {
name: "glob".to_string(),
params: vec![("pattern".to_string(), Type::String)],
return_type: Type::List(Box::new(Type::String)),
},
EffectOpDef {
name: "tryRead".to_string(),
params: vec![("path".to_string(), Type::String)],
return_type: Type::App {
constructor: Box::new(Type::Named("Result".to_string())),
args: vec![Type::String, Type::String],
},
},
EffectOpDef {
name: "tryWrite".to_string(),
params: vec![
("path".to_string(), Type::String),
("content".to_string(), Type::String),
],
return_type: Type::App {
constructor: Box::new(Type::Named("Result".to_string())),
args: vec![Type::Unit, Type::String],
},
},
EffectOpDef {
name: "tryDelete".to_string(),
params: vec![("path".to_string(), Type::String)],
return_type: Type::App {
constructor: Box::new(Type::Named("Result".to_string())),
args: vec![Type::Unit, Type::String],
},
},
], ],
}, },
); );
@@ -1489,6 +1543,16 @@ impl TypeEnv {
Type::Option(Box::new(Type::var())), Type::Option(Box::new(Type::var())),
), ),
), ),
(
"findIndex".to_string(),
Type::function(
vec![
Type::List(Box::new(Type::var())),
Type::function(vec![Type::var()], Type::Bool),
],
Type::Option(Box::new(Type::Int)),
),
),
( (
"any".to_string(), "any".to_string(),
Type::function( Type::function(
@@ -1533,6 +1597,50 @@ impl TypeEnv {
Type::Unit, Type::Unit,
), ),
), ),
(
"sort".to_string(),
Type::function(
vec![Type::List(Box::new(Type::var()))],
Type::List(Box::new(Type::var())),
),
),
(
"sortBy".to_string(),
{
let elem = Type::var();
Type::function(
vec![
Type::List(Box::new(elem.clone())),
Type::function(vec![elem.clone(), elem], Type::Int),
],
Type::List(Box::new(Type::var())),
)
},
),
(
"zip".to_string(),
Type::function(
vec![
Type::List(Box::new(Type::var())),
Type::List(Box::new(Type::var())),
],
Type::List(Box::new(Type::Tuple(vec![Type::var(), Type::var()]))),
),
),
(
"flatten".to_string(),
Type::function(
vec![Type::List(Box::new(Type::List(Box::new(Type::var()))))],
Type::List(Box::new(Type::var())),
),
),
(
"contains".to_string(),
Type::function(
vec![Type::List(Box::new(Type::var())), Type::var()],
Type::Bool,
),
),
]); ]);
env.bind("List", TypeScheme::mono(list_module_type)); env.bind("List", TypeScheme::mono(list_module_type));
@@ -1775,6 +1883,99 @@ 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));
// Ref module
let ref_inner = || Type::var();
let ref_type = || Type::Ref(Box::new(Type::var()));
let ref_module_type = Type::Record(vec![
(
"new".to_string(),
Type::function(vec![ref_inner()], ref_type()),
),
(
"get".to_string(),
Type::function(vec![ref_type()], ref_inner()),
),
(
"set".to_string(),
Type::function(vec![ref_type(), ref_inner()], Type::Unit),
),
(
"update".to_string(),
Type::function(
vec![ref_type(), Type::function(vec![ref_inner()], ref_inner())],
Type::Unit,
),
),
]);
env.bind("Ref", TypeScheme::mono(ref_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())),
@@ -1908,6 +2109,10 @@ impl TypeEnv {
"toString".to_string(), "toString".to_string(),
Type::function(vec![Type::Int], Type::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)); env.bind("Int", TypeScheme::mono(int_module_type));
@@ -1917,6 +2122,10 @@ impl TypeEnv {
"toString".to_string(), "toString".to_string(),
Type::function(vec![Type::Float], Type::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.bind("Float", TypeScheme::mono(float_module_type));
@@ -2003,6 +2212,12 @@ 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::Ref(inner) => {
Type::Ref(Box::new(self.expand_type_alias(inner)))
}
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)),
@@ -2163,6 +2378,16 @@ 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),
// Ref
(Type::Ref(a), Type::Ref(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

@@ -14,6 +14,7 @@
pub type Html<M> = pub type Html<M> =
| Element(String, List<Attr<M>>, List<Html<M>>) | Element(String, List<Attr<M>>, List<Html<M>>)
| Text(String) | Text(String)
| RawHtml(String)
| Empty | Empty
// Attributes that can be applied to elements // Attributes that can be applied to elements
@@ -41,6 +42,7 @@ pub type Attr<M> =
| OnKeyDown(fn(String): M) | OnKeyDown(fn(String): M)
| OnKeyUp(fn(String): M) | OnKeyUp(fn(String): M)
| DataAttr(String, String) | DataAttr(String, String)
| Attribute(String, String)
// ============================================================================ // ============================================================================
// Element builders - Container elements // Element builders - Container elements
@@ -180,6 +182,28 @@ pub fn video<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
pub fn audio<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = pub fn audio<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
Element("audio", attrs, children) Element("audio", attrs, children)
// ============================================================================
// Element builders - Document / Head elements
// ============================================================================
pub fn meta<M>(attrs: List<Attr<M>>): Html<M> =
Element("meta", attrs, [])
pub fn link<M>(attrs: List<Attr<M>>): Html<M> =
Element("link", attrs, [])
pub fn script<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
Element("script", attrs, children)
pub fn iframe<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
Element("iframe", attrs, children)
pub fn figure<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
Element("figure", attrs, children)
pub fn figcaption<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
Element("figcaption", attrs, children)
// ============================================================================ // ============================================================================
// Element builders - Tables // Element builders - Tables
// ============================================================================ // ============================================================================
@@ -285,6 +309,12 @@ pub fn onKeyUp<M>(h: fn(String): M): Attr<M> =
pub fn data<M>(name: String, value: String): Attr<M> = pub fn data<M>(name: String, value: String): Attr<M> =
DataAttr(name, value) DataAttr(name, value)
pub fn attr<M>(name: String, value: String): Attr<M> =
Attribute(name, value)
pub fn rawHtml<M>(content: String): Html<M> =
RawHtml(content)
// ============================================================================ // ============================================================================
// Utility functions // Utility functions
// ============================================================================ // ============================================================================
@@ -319,6 +349,7 @@ pub fn renderAttr<M>(attr: Attr<M>): String =
Checked(false) => "", Checked(false) => "",
Name(n) => " name=\"" + n + "\"", Name(n) => " name=\"" + n + "\"",
DataAttr(name, value) => " data-" + name + "=\"" + value + "\"", DataAttr(name, value) => " data-" + name + "=\"" + value + "\"",
Attribute(name, value) => " " + name + "=\"" + value + "\"",
// Event handlers are ignored in static rendering // Event handlers are ignored in static rendering
OnClick(_) => "", OnClick(_) => "",
OnInput(_) => "", OnInput(_) => "",
@@ -355,6 +386,7 @@ pub fn render<M>(html: Html<M>): String =
} }
}, },
Text(content) => escapeHtml(content), Text(content) => escapeHtml(content),
RawHtml(content) => content,
Empty => "" Empty => ""
} }
@@ -368,15 +400,47 @@ pub fn escapeHtml(s: String): String = {
s4 s4
} }
// Render a full HTML document // Render a full HTML document (basic)
pub fn document(title: String, headExtra: List<Html<M>>, bodyContent: List<Html<M>>): String = { pub fn document(title: String, headExtra: List<Html<M>>, bodyContent: List<Html<M>>): String = {
let headElements = List.concat([ let headElements = List.concat([
[Element("meta", [DataAttr("charset", "UTF-8")], [])], [Element("meta", [Attribute("charset", "UTF-8")], [])],
[Element("meta", [Name("viewport"), Value("width=device-width, initial-scale=1.0")], [])], [Element("meta", [Name("viewport"), Attribute("content", "width=device-width, initial-scale=1.0")], [])],
[Element("title", [], [Text(title)])], [Element("title", [], [Text(title)])],
headExtra headExtra
]) ])
let doc = Element("html", [DataAttr("lang", "en")], [ let doc = Element("html", [Attribute("lang", "en")], [
Element("head", [], headElements),
Element("body", [], bodyContent)
])
"<!DOCTYPE html>\n" + render(doc)
}
// Render a full HTML document with SEO meta tags
pub fn seoDocument(
title: String,
description: String,
url: String,
ogImage: String,
headExtra: List<Html<M>>,
bodyContent: List<Html<M>>
): String = {
let headElements = List.concat([
[Element("meta", [Attribute("charset", "UTF-8")], [])],
[Element("meta", [Name("viewport"), Attribute("content", "width=device-width, initial-scale=1.0")], [])],
[Element("title", [], [Text(title)])],
[Element("meta", [Name("description"), Attribute("content", description)], [])],
[Element("meta", [Attribute("property", "og:title"), Attribute("content", title)], [])],
[Element("meta", [Attribute("property", "og:description"), Attribute("content", description)], [])],
[Element("meta", [Attribute("property", "og:type"), Attribute("content", "website")], [])],
[Element("meta", [Attribute("property", "og:url"), Attribute("content", url)], [])],
[Element("meta", [Attribute("property", "og:image"), Attribute("content", ogImage)], [])],
[Element("meta", [Name("twitter:card"), Attribute("content", "summary_large_image")], [])],
[Element("meta", [Name("twitter:title"), Attribute("content", title)], [])],
[Element("meta", [Name("twitter:description"), Attribute("content", description)], [])],
[Element("link", [Attribute("rel", "canonical"), Href(url)], [])],
headExtra
])
let doc = Element("html", [Attribute("lang", "en")], [
Element("head", [], headElements), Element("head", [], headElements),
Element("body", [], bodyContent) Element("body", [], bodyContent)
]) ])

View File

@@ -625,6 +625,41 @@ pub fn router(routes: List<Route>, notFound: fn(Request): Response): Handler =
} }
} }
// ============================================================
// Static File Serving
// ============================================================
// Serve a static file from disk
pub fn serveStaticFile(basePath: String, requestPath: String): Response with {File} = {
let filePath = basePath + requestPath
if File.exists(filePath) then {
let content = File.read(filePath)
let mime = getMimeType(filePath)
{ status: 200, headers: [("Content-Type", mime)], body: content }
} else
{ status: 404, headers: textHeaders(), body: "Not Found" }
}
// ============================================================
// Form Body Parsing
// ============================================================
// Parse URL-encoded form body (same format as query strings)
pub fn parseFormBody(body: String): List<(String, String)> =
parseQueryParams(body)
// Get a form field value by name
pub fn getFormField(fields: List<(String, String)>, name: String): Option<String> =
getParam(fields, name)
// ============================================================
// Response Helpers
// ============================================================
// Send a Response using HttpServer effect (convenience wrapper)
pub fn sendResponse(resp: Response): Unit with {HttpServer} =
HttpServer.respondWithHeaders(resp.status, resp.body, resp.headers)
// ============================================================ // ============================================================
// Example Usage // Example Usage
// ============================================================ // ============================================================