33 Commits

Author SHA1 Message Date
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
19 changed files with 3210 additions and 127 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.3" 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.4" version = "0.1.12"
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.4\n" printf " \033[1;35m \033[0m v0.1.12\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.4"; version = "0.1.12";
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.4"; version = "0.1.12";
src = ./.; src = ./.;
cargoLock.lockFile = ./Cargo.lock; cargoLock.lockFile = ./Cargo.lock;

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,

View File

@@ -146,6 +146,8 @@ pub struct CBackend {
imported_modules: HashSet<String>, imported_modules: HashSet<String>,
/// Variable name renames: Lux name → C variable name (for let binding name mangling) /// Variable name renames: Lux name → C variable name (for let binding name mangling)
var_renames: HashMap<String, String>, var_renames: HashMap<String, String>,
/// Inner type for Option-typed variables (variable name -> inner C type, e.g. "LuxInt")
var_option_inner_types: HashMap<String, String>,
} }
impl CBackend { impl CBackend {
@@ -177,6 +179,7 @@ impl CBackend {
module_functions: HashMap::new(), module_functions: HashMap::new(),
imported_modules: HashSet::new(), imported_modules: HashSet::new(),
var_renames: HashMap::new(), var_renames: HashMap::new(),
var_option_inner_types: HashMap::new(),
} }
} }
@@ -279,7 +282,7 @@ impl CBackend {
Declaration::Let(let_decl) => { Declaration::Let(let_decl) => {
// Skip run expressions - they're handled in the main wrapper // Skip run expressions - they're handled in the main wrapper
if !matches!(&let_decl.value, Expr::Run { .. }) { if !matches!(&let_decl.value, Expr::Run { .. }) {
self.emit_global_let(&let_decl.name)?; self.emit_global_let(let_decl)?;
} }
} }
_ => {} _ => {}
@@ -599,6 +602,18 @@ impl CBackend {
self.writeln(&format!("{} {}({}) {{", ret_type, mangled_name, full_params)); self.writeln(&format!("{} {}({}) {{", ret_type, mangled_name, full_params));
self.indent += 1; self.indent += 1;
// Register parameter types so match/option inference can use them
for p in &func.params {
if let Ok(c_type) = self.type_expr_to_c(&p.typ) {
let escaped = self.escape_c_keyword(&p.name.name);
self.var_types.insert(escaped.clone(), c_type);
// Track Option inner types for match pattern extraction
if let Some(inner) = self.option_inner_type_from_type_expr(&p.typ) {
self.var_option_inner_types.insert(escaped, inner);
}
}
}
let old_has_evidence = self.has_evidence; let old_has_evidence = self.has_evidence;
if is_effectful { if is_effectful {
self.has_evidence = true; self.has_evidence = true;
@@ -1149,7 +1164,12 @@ impl CBackend {
self.writeln(" void (*delete_file)(void* env, LuxString path);"); self.writeln(" void (*delete_file)(void* env, LuxString path);");
self.writeln(" LuxBool (*isDir)(void* env, LuxString path);"); self.writeln(" LuxBool (*isDir)(void* env, LuxString path);");
self.writeln(" void (*mkdir)(void* env, LuxString path);"); self.writeln(" void (*mkdir)(void* env, LuxString path);");
self.writeln(" void (*copy)(void* env, LuxString src, LuxString dst);");
self.writeln(" LuxList* (*readDir)(void* env, LuxString path);"); self.writeln(" LuxList* (*readDir)(void* env, LuxString path);");
self.writeln(" LuxList* (*glob)(void* env, LuxString pattern);");
self.writeln(" Result (*tryRead)(void* env, LuxString path);");
self.writeln(" Result (*tryWrite)(void* env, LuxString path, LuxString content);");
self.writeln(" Result (*tryDelete)(void* env, LuxString path);");
self.writeln(" void* env;"); self.writeln(" void* env;");
self.writeln("} LuxFileHandler;"); self.writeln("} LuxFileHandler;");
self.writeln(""); self.writeln("");
@@ -1336,6 +1356,20 @@ impl CBackend {
self.writeln(" mkdir(path, 0755);"); self.writeln(" mkdir(path, 0755);");
self.writeln("}"); self.writeln("}");
self.writeln(""); self.writeln("");
self.writeln("static void lux_file_copy(LuxString src, LuxString dst) {");
self.writeln(" FILE* fin = fopen(src, \"rb\");");
self.writeln(" if (!fin) return;");
self.writeln(" FILE* fout = fopen(dst, \"wb\");");
self.writeln(" if (!fout) { fclose(fin); return; }");
self.writeln(" char buf[4096];");
self.writeln(" size_t n;");
self.writeln(" while ((n = fread(buf, 1, sizeof(buf), fin)) > 0) {");
self.writeln(" fwrite(buf, 1, n, fout);");
self.writeln(" }");
self.writeln(" fclose(fin);");
self.writeln(" fclose(fout);");
self.writeln("}");
self.writeln("");
self.writeln("#include <dirent.h>"); self.writeln("#include <dirent.h>");
self.writeln("// Forward declarations needed by lux_file_readDir"); self.writeln("// Forward declarations needed by lux_file_readDir");
self.writeln("static LuxList* lux_list_new(int64_t capacity);"); self.writeln("static LuxList* lux_list_new(int64_t capacity);");
@@ -1356,6 +1390,85 @@ impl CBackend {
self.writeln(" return result;"); self.writeln(" return result;");
self.writeln("}"); self.writeln("}");
self.writeln(""); self.writeln("");
self.writeln("#include <glob.h>");
self.writeln("static LuxList* lux_file_glob(LuxString pattern) {");
self.writeln(" LuxList* result = lux_list_new(16);");
self.writeln(" glob_t globbuf;");
self.writeln(" int ret = glob(pattern, 0, NULL, &globbuf);");
self.writeln(" if (ret == 0) {");
self.writeln(" for (size_t i = 0; i < globbuf.gl_pathc; i++) {");
self.writeln(" size_t len = strlen(globbuf.gl_pathv[i]);");
self.writeln(" LuxString name = (LuxString)lux_rc_alloc(len + 1, LUX_TAG_STRING);");
self.writeln(" memcpy(name, globbuf.gl_pathv[i], len + 1);");
self.writeln(" lux_list_push(result, (void*)name);");
self.writeln(" }");
self.writeln(" globfree(&globbuf);");
self.writeln(" }");
self.writeln(" return result;");
self.writeln("}");
self.writeln("");
self.writeln("static Result lux_file_tryRead(LuxString path) {");
self.writeln(" Result r;");
self.writeln(" FILE* f = fopen(path, \"r\");");
self.writeln(" if (!f) {");
self.writeln(" char buf[512];");
self.writeln(" snprintf(buf, sizeof(buf), \"Failed to read file '%s': %s\", path, strerror(errno));");
self.writeln(" size_t len = strlen(buf);");
self.writeln(" LuxString msg = (LuxString)lux_rc_alloc(len + 1, LUX_TAG_STRING);");
self.writeln(" memcpy(msg, buf, len + 1);");
self.writeln(" r.tag = Result_TAG_ERR;");
self.writeln(" r.data.err.field0 = (void*)msg;");
self.writeln(" return r;");
self.writeln(" }");
self.writeln(" fseek(f, 0, SEEK_END);");
self.writeln(" long size = ftell(f);");
self.writeln(" fseek(f, 0, SEEK_SET);");
self.writeln(" LuxString content = (LuxString)lux_rc_alloc(size + 1, LUX_TAG_STRING);");
self.writeln(" size_t read_size = fread(content, 1, size, f);");
self.writeln(" content[read_size] = '\\0';");
self.writeln(" fclose(f);");
self.writeln(" r.tag = Result_TAG_OK;");
self.writeln(" r.data.ok.field0 = (void*)content;");
self.writeln(" return r;");
self.writeln("}");
self.writeln("");
self.writeln("static Result lux_file_tryWrite(LuxString path, LuxString content) {");
self.writeln(" Result r;");
self.writeln(" FILE* f = fopen(path, \"w\");");
self.writeln(" if (!f) {");
self.writeln(" char buf[512];");
self.writeln(" snprintf(buf, sizeof(buf), \"Failed to write file '%s': %s\", path, strerror(errno));");
self.writeln(" size_t len = strlen(buf);");
self.writeln(" LuxString msg = (LuxString)lux_rc_alloc(len + 1, LUX_TAG_STRING);");
self.writeln(" memcpy(msg, buf, len + 1);");
self.writeln(" r.tag = Result_TAG_ERR;");
self.writeln(" r.data.err.field0 = (void*)msg;");
self.writeln(" return r;");
self.writeln(" }");
self.writeln(" fputs(content, f);");
self.writeln(" fclose(f);");
self.writeln(" r.tag = Result_TAG_OK;");
self.writeln(" r.data.ok.field0 = NULL;");
self.writeln(" return r;");
self.writeln("}");
self.writeln("");
self.writeln("static Result lux_file_tryDelete(LuxString path) {");
self.writeln(" Result r;");
self.writeln(" if (remove(path) != 0) {");
self.writeln(" char buf[512];");
self.writeln(" snprintf(buf, sizeof(buf), \"Failed to delete file '%s': %s\", path, strerror(errno));");
self.writeln(" size_t len = strlen(buf);");
self.writeln(" LuxString msg = (LuxString)lux_rc_alloc(len + 1, LUX_TAG_STRING);");
self.writeln(" memcpy(msg, buf, len + 1);");
self.writeln(" r.tag = Result_TAG_ERR;");
self.writeln(" r.data.err.field0 = (void*)msg;");
self.writeln(" return r;");
self.writeln(" }");
self.writeln(" r.tag = Result_TAG_OK;");
self.writeln(" r.data.ok.field0 = NULL;");
self.writeln(" return r;");
self.writeln("}");
self.writeln("");
self.writeln("static LuxString default_file_read(void* env, LuxString path) {"); self.writeln("static LuxString default_file_read(void* env, LuxString path) {");
self.writeln(" (void)env;"); self.writeln(" (void)env;");
self.writeln(" return lux_file_read(path);"); self.writeln(" return lux_file_read(path);");
@@ -1391,11 +1504,36 @@ impl CBackend {
self.writeln(" lux_file_mkdir(path);"); self.writeln(" lux_file_mkdir(path);");
self.writeln("}"); self.writeln("}");
self.writeln(""); self.writeln("");
self.writeln("static void default_file_copy(void* env, LuxString src, LuxString dst) {");
self.writeln(" (void)env;");
self.writeln(" lux_file_copy(src, dst);");
self.writeln("}");
self.writeln("");
self.writeln("static LuxList* default_file_readDir(void* env, LuxString path) {"); self.writeln("static LuxList* default_file_readDir(void* env, LuxString path) {");
self.writeln(" (void)env;"); self.writeln(" (void)env;");
self.writeln(" return lux_file_readDir(path);"); self.writeln(" return lux_file_readDir(path);");
self.writeln("}"); self.writeln("}");
self.writeln(""); self.writeln("");
self.writeln("static LuxList* default_file_glob(void* env, LuxString pattern) {");
self.writeln(" (void)env;");
self.writeln(" return lux_file_glob(pattern);");
self.writeln("}");
self.writeln("");
self.writeln("static Result default_file_tryRead(void* env, LuxString path) {");
self.writeln(" (void)env;");
self.writeln(" return lux_file_tryRead(path);");
self.writeln("}");
self.writeln("");
self.writeln("static Result default_file_tryWrite(void* env, LuxString path, LuxString content) {");
self.writeln(" (void)env;");
self.writeln(" return lux_file_tryWrite(path, content);");
self.writeln("}");
self.writeln("");
self.writeln("static Result default_file_tryDelete(void* env, LuxString path) {");
self.writeln(" (void)env;");
self.writeln(" return lux_file_tryDelete(path);");
self.writeln("}");
self.writeln("");
self.writeln("static LuxFileHandler default_file_handler = {"); self.writeln("static LuxFileHandler default_file_handler = {");
self.writeln(" .read = default_file_read,"); self.writeln(" .read = default_file_read,");
self.writeln(" .write = default_file_write,"); self.writeln(" .write = default_file_write,");
@@ -1404,7 +1542,12 @@ impl CBackend {
self.writeln(" .delete_file = default_file_delete,"); self.writeln(" .delete_file = default_file_delete,");
self.writeln(" .isDir = default_file_isDir,"); self.writeln(" .isDir = default_file_isDir,");
self.writeln(" .mkdir = default_file_mkdir,"); self.writeln(" .mkdir = default_file_mkdir,");
self.writeln(" .copy = default_file_copy,");
self.writeln(" .readDir = default_file_readDir,"); self.writeln(" .readDir = default_file_readDir,");
self.writeln(" .glob = default_file_glob,");
self.writeln(" .tryRead = default_file_tryRead,");
self.writeln(" .tryWrite = default_file_tryWrite,");
self.writeln(" .tryDelete = default_file_tryDelete,");
self.writeln(" .env = NULL"); self.writeln(" .env = NULL");
self.writeln("};"); self.writeln("};");
self.writeln(""); self.writeln("");
@@ -2051,6 +2194,42 @@ impl CBackend {
self.writeln(" return result;"); self.writeln(" return result;");
self.writeln("}"); self.writeln("}");
self.writeln(""); self.writeln("");
// Sort helper: compare two void* as boxed ints
self.writeln("static int lux_compare_int(const void* a, const void* b) {");
self.writeln(" LuxInt va = *(LuxInt*)(*(void**)a);");
self.writeln(" LuxInt vb = *(LuxInt*)(*(void**)b);");
self.writeln(" return (va > vb) - (va < vb);");
self.writeln("}");
self.writeln("");
self.writeln("static int lux_compare_string(const void* a, const void* b) {");
self.writeln(" LuxString sa = (LuxString)(*(void**)a);");
self.writeln(" LuxString sb = (LuxString)(*(void**)b);");
self.writeln(" return strcmp(sa, sb);");
self.writeln("}");
self.writeln("");
self.writeln("static LuxList* lux_list_sort(LuxList* list) {");
self.writeln(" if (list->length <= 1) {");
self.writeln(" lux_incref(list);");
self.writeln(" return list;");
self.writeln(" }");
self.writeln(" LuxList* result = lux_list_new(list->length);");
self.writeln(" for (int64_t i = 0; i < list->length; i++) {");
self.writeln(" lux_incref(list->elements[i]);");
self.writeln(" result->elements[i] = list->elements[i];");
self.writeln(" }");
self.writeln(" result->length = list->length;");
self.writeln(" // Determine element type from first element and sort");
self.writeln(" if (result->length > 0 && result->elements[0] != NULL) {");
self.writeln(" uint32_t tag = LUX_RC_HEADER(result->elements[0])->tag;");
self.writeln(" if (tag == LUX_TAG_BOXED_INT) {");
self.writeln(" qsort(result->elements, result->length, sizeof(void*), lux_compare_int);");
self.writeln(" } else if (tag == LUX_TAG_STRING) {");
self.writeln(" qsort(result->elements, result->length, sizeof(void*), lux_compare_string);");
self.writeln(" }");
self.writeln(" }");
self.writeln(" return result;");
self.writeln("}");
self.writeln("");
// === Map Runtime Functions === // === Map Runtime Functions ===
self.writeln("static LuxMap* lux_map_new(int64_t capacity) {"); self.writeln("static LuxMap* lux_map_new(int64_t capacity) {");
self.writeln(" LuxMap* map = (LuxMap*)malloc(sizeof(LuxMap));"); self.writeln(" LuxMap* map = (LuxMap*)malloc(sizeof(LuxMap));");
@@ -2124,20 +2303,6 @@ impl CBackend {
self.writeln("static Option lux_option_none(void) { return (Option){Option_TAG_NONE}; }"); self.writeln("static Option lux_option_none(void) { return (Option){Option_TAG_NONE}; }");
self.writeln("static Option lux_option_some(void* value) { return (Option){Option_TAG_SOME, .data.some = {value}}; }"); self.writeln("static Option lux_option_some(void* value) { return (Option){Option_TAG_SOME, .data.some = {value}}; }");
self.writeln(""); self.writeln("");
self.writeln("// String indexOf / lastIndexOf — return Option<Int>");
self.writeln("static Option lux_string_indexOf(LuxString s, LuxString needle) {");
self.writeln(" char* pos = strstr(s, needle);");
self.writeln(" if (pos) return lux_option_some((void*)(intptr_t)(pos - s));");
self.writeln(" return lux_option_none();");
self.writeln("}");
self.writeln("static Option lux_string_lastIndexOf(LuxString s, LuxString needle) {");
self.writeln(" char* last = NULL;");
self.writeln(" char* pos = strstr(s, needle);");
self.writeln(" while (pos) { last = pos; pos = strstr(pos + 1, needle); }");
self.writeln(" if (last) return lux_option_some((void*)(intptr_t)(last - s));");
self.writeln(" return lux_option_none();");
self.writeln("}");
self.writeln("");
self.writeln("// === Boxing/Unboxing ==="); self.writeln("// === Boxing/Unboxing ===");
self.writeln("// All boxed values are RC-managed."); self.writeln("// All boxed values are RC-managed.");
self.writeln(""); self.writeln("");
@@ -2171,6 +2336,20 @@ impl CBackend {
self.writeln("}"); self.writeln("}");
self.writeln("static LuxString lux_unbox_string(void* p) { return (LuxString)p; }"); self.writeln("static LuxString lux_unbox_string(void* p) { return (LuxString)p; }");
self.writeln(""); self.writeln("");
self.writeln("// String indexOf / lastIndexOf — return Option<Int>");
self.writeln("static Option lux_string_indexOf(LuxString s, LuxString needle) {");
self.writeln(" char* pos = strstr(s, needle);");
self.writeln(" if (pos) return lux_option_some(lux_box_int((LuxInt)(pos - s)));");
self.writeln(" return lux_option_none();");
self.writeln("}");
self.writeln("static Option lux_string_lastIndexOf(LuxString s, LuxString needle) {");
self.writeln(" char* last = NULL;");
self.writeln(" char* pos = strstr(s, needle);");
self.writeln(" while (pos) { last = pos; pos = strstr(pos + 1, needle); }");
self.writeln(" if (last) return lux_option_some(lux_box_int((LuxInt)(last - s)));");
self.writeln(" return lux_option_none();");
self.writeln("}");
self.writeln("");
self.writeln("// === Polymorphic Drop Function ==="); self.writeln("// === Polymorphic Drop Function ===");
self.writeln("// Called when an object's refcount reaches zero."); self.writeln("// Called when an object's refcount reaches zero.");
self.writeln("// Recursively decrefs any owned references before freeing."); self.writeln("// Recursively decrefs any owned references before freeing.");
@@ -2706,7 +2885,11 @@ impl CBackend {
for param in &func.params { for param in &func.params {
let escaped = self.escape_c_keyword(&param.name.name); let escaped = self.escape_c_keyword(&param.name.name);
if let Ok(c_type) = self.type_expr_to_c(&param.typ) { if let Ok(c_type) = self.type_expr_to_c(&param.typ) {
self.var_types.insert(escaped, c_type); self.var_types.insert(escaped.clone(), c_type);
// Track Option inner types for match pattern extraction
if let Some(inner) = self.option_inner_type_from_type_expr(&param.typ) {
self.var_option_inner_types.insert(escaped, inner);
}
} }
} }
@@ -3067,6 +3250,13 @@ impl CBackend {
// Infer the type from the value expression // Infer the type from the value expression
let var_type = self.infer_expr_type(value).unwrap_or_else(|| "LuxInt".to_string()); let var_type = self.infer_expr_type(value).unwrap_or_else(|| "LuxInt".to_string());
// Track Option inner type for match pattern extraction
if var_type == "Option" {
if let Some(inner) = self.infer_option_inner_type(value) {
self.var_option_inner_types.insert(var_name.clone(), inner);
}
}
self.writeln(&format!("{} {} = {};", var_type, var_name, val)); self.writeln(&format!("{} {} = {};", var_type, var_name, val));
// Register the variable rename so nested expressions can find it // Register the variable rename so nested expressions can find it
@@ -3095,6 +3285,9 @@ impl CBackend {
if module_name.name == "Map" { if module_name.name == "Map" {
return self.emit_map_operation(&field.name, args); return self.emit_map_operation(&field.name, args);
} }
if module_name.name == "Ref" {
return self.emit_ref_operation(&field.name, args);
}
// Int module // Int module
if module_name.name == "Int" && field.name == "toString" { if module_name.name == "Int" && field.name == "toString" {
let arg = self.emit_expr(&args[0])?; let arg = self.emit_expr(&args[0])?;
@@ -3375,6 +3568,13 @@ impl CBackend {
// Record variable type for future inference // Record variable type for future inference
self.var_types.insert(escaped_name.clone(), typ.clone()); self.var_types.insert(escaped_name.clone(), typ.clone());
// Track Option inner type for match pattern extraction
if typ == "Option" {
if let Some(inner) = self.infer_option_inner_type(value) {
self.var_option_inner_types.insert(escaped_name.clone(), inner);
}
}
// Handle ownership transfer or RC registration // Handle ownership transfer or RC registration
if let Some(source_name) = source_var { if let Some(source_name) = source_var {
// Ownership transfer: unregister source, register dest // Ownership transfer: unregister source, register dest
@@ -3504,6 +3704,11 @@ impl CBackend {
return self.emit_map_operation(&operation.name, args); return self.emit_map_operation(&operation.name, args);
} }
// Ref module
if effect.name == "Ref" {
return self.emit_ref_operation(&operation.name, args);
}
// Built-in Console effect // Built-in Console effect
if effect.name == "Console" { if effect.name == "Console" {
if operation.name == "print" { if operation.name == "print" {
@@ -3679,6 +3884,16 @@ impl CBackend {
} }
return Ok("NULL".to_string()); return Ok("NULL".to_string());
} }
"copy" => {
let src = self.emit_expr(&args[0])?;
let dst = self.emit_expr(&args[1])?;
if self.has_evidence {
self.writeln(&format!("ev->file->copy(ev->file->env, {}, {});", src, dst));
} else {
self.writeln(&format!("lux_file_copy({}, {});", src, dst));
}
return Ok("NULL".to_string());
}
"readDir" | "listDir" => { "readDir" | "listDir" => {
let path = self.emit_expr(&args[0])?; let path = self.emit_expr(&args[0])?;
let temp = format!("_readdir_{}", self.fresh_name()); let temp = format!("_readdir_{}", self.fresh_name());
@@ -3690,6 +3905,50 @@ impl CBackend {
self.register_rc_var(&temp, "LuxList*"); self.register_rc_var(&temp, "LuxList*");
return Ok(temp); return Ok(temp);
} }
"glob" => {
let pattern = self.emit_expr(&args[0])?;
let temp = format!("_glob_{}", self.fresh_name());
if self.has_evidence {
self.writeln(&format!("LuxList* {} = ev->file->glob(ev->file->env, {});", temp, pattern));
} else {
self.writeln(&format!("LuxList* {} = lux_file_glob({});", temp, pattern));
}
self.register_rc_var(&temp, "LuxList*");
return Ok(temp);
}
"tryRead" => {
let path = self.emit_expr(&args[0])?;
let temp = format!("_file_tryread_{}", self.fresh_name());
if self.has_evidence {
self.writeln(&format!("Result {} = ev->file->tryRead(ev->file->env, {});", temp, path));
} else {
self.writeln(&format!("Result {} = lux_file_tryRead({});", temp, path));
}
return Ok(temp);
}
"tryWrite" => {
let path = self.emit_expr(&args[0])?;
let content = self.emit_expr(&args[1])?;
if self.has_evidence {
let temp = format!("_file_trywrite_{}", self.fresh_name());
self.writeln(&format!("Result {} = ev->file->tryWrite(ev->file->env, {}, {});", temp, path, content));
return Ok(temp);
} else {
let temp = format!("_file_trywrite_{}", self.fresh_name());
self.writeln(&format!("Result {} = lux_file_tryWrite({}, {});", temp, path, content));
return Ok(temp);
}
}
"tryDelete" => {
let path = self.emit_expr(&args[0])?;
let temp = format!("_file_trydelete_{}", self.fresh_name());
if self.has_evidence {
self.writeln(&format!("Result {} = ev->file->tryDelete(ev->file->env, {});", temp, path));
} else {
self.writeln(&format!("Result {} = lux_file_tryDelete({});", temp, path));
}
return Ok(temp);
}
_ => {} _ => {}
} }
} }
@@ -4615,6 +4874,184 @@ impl CBackend {
Ok(result_var) Ok(result_var)
} }
"sort" => {
if args.len() != 1 {
return Err(CGenError {
message: "List.sort takes 1 argument".to_string(),
span: None,
});
}
let list = self.emit_expr(&args[0])?;
let result_var = format!("_sorted_{}", self.fresh_name());
self.writeln(&format!(
"LuxList* {} = lux_list_sort({});",
result_var, list
));
self.register_rc_var(&result_var, "LuxList*");
Ok(result_var)
}
"sortBy" => {
if args.len() != 2 {
return Err(CGenError {
message: "List.sortBy takes 2 arguments".to_string(),
span: None,
});
}
let list = self.emit_expr(&args[0])?;
let closure = self.emit_expr(&args[1])?;
let result_var = format!("_sorted_{}", self.fresh_name());
// Copy the list then do insertion sort with custom comparator
self.writeln(&format!(
"LuxList* {} = lux_list_new({}->length);",
result_var, list
));
self.writeln(&format!(
"for (int64_t _i = 0; _i < {}->length; _i++) {{",
list
));
self.writeln(&format!(
" lux_incref({}->elements[_i]);",
list
));
self.writeln(&format!(
" {}->elements[_i] = {}->elements[_i];",
result_var, list
));
self.writeln("}");
self.writeln(&format!(
"{}->length = {}->length;",
result_var, list
));
// Insertion sort using the comparator closure
self.writeln(&format!(
"for (int64_t _i = 1; _i < {}->length; _i++) {{",
result_var
));
self.writeln(&format!(
" void* _key = {}->elements[_i];",
result_var
));
self.writeln(" int64_t _j = _i - 1;");
self.writeln(&format!(
" while (_j >= 0 && *(LuxInt*){}.fn({}.env, {}->elements[_j], _key) > 0) {{",
closure, closure, result_var
));
self.writeln(&format!(
" {}->elements[_j + 1] = {}->elements[_j];",
result_var, result_var
));
self.writeln(" _j--;");
self.writeln(" }");
self.writeln(&format!(
" {}->elements[_j + 1] = _key;",
result_var
));
self.writeln("}");
self.register_rc_var(&result_var, "LuxList*");
Ok(result_var)
}
"findIndex" => {
if args.len() != 2 {
return Err(CGenError { message: "List.findIndex takes 2 arguments".to_string(), span: None });
}
let elem_type = self.infer_callback_param_type(&args[1], 0);
let elem_is_prim = Self::is_primitive_c_type(&elem_type);
let list = self.emit_expr(&args[0])?;
let closure = self.emit_expr(&args[1])?;
let id = self.fresh_name();
let result_var = format!("_findidx_{}", id);
let i_var = format!("_i_{}", id);
let elem_var = format!("_elem_{}", id);
let fn_var = format!("_fn_{}", id);
let matches_var = format!("_matches_{}", id);
let arg_pass = if elem_is_prim {
format!("{}({})", Self::unbox_fn_for_type(&elem_type), elem_var)
} else {
elem_var.clone()
};
let fn_cast = if elem_is_prim {
format!("(LuxBool(*)(void*, {}))", elem_type)
} else {
"(LuxBool(*)(void*, void*))".to_string()
};
self.writeln(&format!("Option {} = lux_option_none();", result_var));
self.writeln(&format!("for (int64_t {} = 0; {} < {}->length; {}++) {{", i_var, i_var, list, i_var));
self.indent += 1;
self.writeln(&format!("void* {} = {}->elements[{}];", elem_var, list, i_var));
self.writeln(&format!("LuxClosure* {} = (LuxClosure*){};", fn_var, closure));
self.writeln(&format!("LuxBool {} = ({}{}->fn_ptr)({}->env, {});", matches_var, fn_cast, fn_var, fn_var, arg_pass));
self.writeln(&format!("if ({}) {{", matches_var));
self.indent += 1;
self.writeln(&format!("{} = lux_option_some((void*)(intptr_t){});", result_var, i_var));
self.writeln("break;");
self.indent -= 1;
self.writeln("}");
self.indent -= 1;
self.writeln("}");
if closure.starts_with("_closure_") || closure.starts_with("_fn_ref_") {
self.writeln(&format!("lux_decref_closure({});", closure));
}
Ok(result_var)
}
"zip" => {
if args.len() != 2 {
return Err(CGenError { message: "List.zip takes 2 arguments".to_string(), span: None });
}
let list1 = self.emit_expr(&args[0])?;
let list2 = self.emit_expr(&args[1])?;
let id = self.fresh_name();
let result_var = format!("_zip_{}", id);
let len_var = format!("_ziplen_{}", id);
let i_var = format!("_i_{}", id);
self.writeln(&format!("int64_t {} = {}->length < {}->length ? {}->length : {}->length;", len_var, list1, list2, list1, list2));
self.writeln(&format!("LuxList* {} = lux_list_new({});", result_var, len_var));
self.writeln(&format!("for (int64_t {} = 0; {} < {}; {}++) {{", i_var, i_var, len_var, i_var));
self.indent += 1;
self.writeln(&format!("LuxTuple* _tup = (LuxTuple*)lux_rc_alloc(sizeof(LuxTuple) + 2 * sizeof(void*), LUX_TAG_TUPLE);"));
self.writeln("_tup->length = 2;");
self.writeln(&format!("_tup->elements[0] = {}->elements[{}];", list1, i_var));
self.writeln(&format!("_tup->elements[1] = {}->elements[{}];", list2, i_var));
self.writeln(&format!("lux_incref({}->elements[{}]);", list1, i_var));
self.writeln(&format!("lux_incref({}->elements[{}]);", list2, i_var));
self.writeln(&format!("lux_list_push({}, (void*)_tup);", result_var));
self.indent -= 1;
self.writeln("}");
self.register_rc_var(&result_var, "LuxList*");
Ok(result_var)
}
"flatten" => {
if args.len() != 1 {
return Err(CGenError { message: "List.flatten takes 1 argument".to_string(), span: None });
}
let list = self.emit_expr(&args[0])?;
let id = self.fresh_name();
let result_var = format!("_flat_{}", id);
let i_var = format!("_i_{}", id);
let j_var = format!("_j_{}", id);
let inner_var = format!("_inner_{}", id);
self.writeln(&format!("LuxList* {} = lux_list_new(16);", result_var));
self.writeln(&format!("for (int64_t {} = 0; {} < {}->length; {}++) {{", i_var, i_var, list, i_var));
self.indent += 1;
self.writeln(&format!("LuxList* {} = (LuxList*){}->elements[{}];", inner_var, list, i_var));
self.writeln(&format!("for (int64_t {} = 0; {} < {}->length; {}++) {{", j_var, j_var, inner_var, j_var));
self.indent += 1;
self.writeln(&format!("lux_incref({}->elements[{}]);", inner_var, j_var));
self.writeln(&format!("lux_list_push({}, {}->elements[{}]);", result_var, inner_var, j_var));
self.indent -= 1;
self.writeln("}");
self.indent -= 1;
self.writeln("}");
self.register_rc_var(&result_var, "LuxList*");
Ok(result_var)
}
_ => Err(CGenError { _ => Err(CGenError {
message: format!("Unsupported List operation: {}", op), message: format!("Unsupported List operation: {}", op),
span: None, span: None,
@@ -4756,6 +5193,42 @@ impl CBackend {
} }
} }
fn emit_ref_operation(&mut self, op: &str, args: &[Expr]) -> Result<String, CGenError> {
match op {
"new" => {
let val = self.emit_expr(&args[0])?;
let boxed = self.box_value(&val, None);
let temp = format!("_ref_new_{}", self.fresh_name());
self.writeln(&format!("void** {} = (void**)malloc(sizeof(void*));", temp));
self.writeln(&format!("*{} = {};", temp, boxed));
Ok(temp)
}
"get" => {
let r = self.emit_expr(&args[0])?;
Ok(format!("(*({})) /* Ref.get */", r))
}
"set" => {
let r = self.emit_expr(&args[0])?;
let val = self.emit_expr(&args[1])?;
let boxed = self.box_value(&val, None);
self.writeln(&format!("*{} = {};", r, boxed));
Ok("0 /* Unit */".to_string())
}
"update" => {
let r = self.emit_expr(&args[0])?;
let f = self.emit_expr(&args[1])?;
let temp = format!("_ref_upd_{}", self.fresh_name());
self.writeln(&format!("void* {} = ((void*(*)(void*)){})(*({}));", temp, f, r));
self.writeln(&format!("*{} = {};", r, temp));
Ok("0 /* Unit */".to_string())
}
_ => Err(CGenError {
message: format!("Unsupported Ref operation: {}", op),
span: None,
}),
}
}
fn emit_expr_with_substitution(&mut self, expr: &Expr, from: &str, to: &str) -> Result<String, CGenError> { fn emit_expr_with_substitution(&mut self, expr: &Expr, from: &str, to: &str) -> Result<String, CGenError> {
// Simple substitution - in a real implementation, this would be more sophisticated // Simple substitution - in a real implementation, this would be more sophisticated
match expr { match expr {
@@ -4841,8 +5314,8 @@ impl CBackend {
if c_type == "void*" { if c_type == "void*" {
// Cast from void* to actual type // Cast from void* to actual type
if Self::is_primitive_c_type(&actual_type) { if Self::is_primitive_c_type(&actual_type) {
// For primitive types stored as void*, cast via intptr_t // For primitive types stored as boxed void*, dereference
self.writeln(&format!("{} {} = ({})(intptr_t)({});", actual_type, var_name, actual_type, c_expr)); self.writeln(&format!("{} {} = *({}*)({});", actual_type, var_name, actual_type, c_expr));
} else if !actual_type.ends_with('*') && actual_type != "void" { } else if !actual_type.ends_with('*') && actual_type != "void" {
// Struct types: cast to pointer and dereference // Struct types: cast to pointer and dereference
self.writeln(&format!("{} {} = *({}*)({});", actual_type, var_name, actual_type, c_expr)); self.writeln(&format!("{} {} = *({}*)({});", actual_type, var_name, actual_type, c_expr));
@@ -5047,8 +5520,8 @@ impl CBackend {
}, },
"List" => match field.name.as_str() { "List" => match field.name.as_str() {
"map" | "filter" | "concat" | "reverse" | "take" | "drop" "map" | "filter" | "concat" | "reverse" | "take" | "drop"
| "range" | "sort" | "sortBy" | "flatten" | "intersperse" => return Some("LuxList*".to_string()), | "range" | "sort" | "sortBy" | "flatten" | "intersperse" | "zip" => return Some("LuxList*".to_string()),
"head" | "tail" | "get" | "find" | "last" => return Some("Option".to_string()), "head" | "tail" | "get" | "find" | "findIndex" | "last" => return Some("Option".to_string()),
"length" => return Some("LuxInt".to_string()), "length" => return Some("LuxInt".to_string()),
"fold" | "foldLeft" => { "fold" | "foldLeft" => {
// Fold return type is the type of the init value (2nd arg) // Fold return type is the type of the init value (2nd arg)
@@ -5142,9 +5615,9 @@ impl CBackend {
if effect.name == "List" { if effect.name == "List" {
match operation.name.as_str() { match operation.name.as_str() {
// Operations returning lists // Operations returning lists
"map" | "filter" | "concat" | "reverse" | "take" | "drop" | "range" => Some("LuxList*".to_string()), "map" | "filter" | "concat" | "reverse" | "take" | "drop" | "range" | "sort" | "sortBy" | "zip" | "flatten" => Some("LuxList*".to_string()),
// Operations returning Option // Operations returning Option
"head" | "tail" | "get" | "find" => Some("Option".to_string()), "head" | "tail" | "get" | "find" | "findIndex" => Some("Option".to_string()),
// Operations returning Int // Operations returning Int
"length" => Some("LuxInt".to_string()), "length" => Some("LuxInt".to_string()),
// Fold returns the type of the init value // Fold returns the type of the init value
@@ -5156,7 +5629,7 @@ impl CBackend {
} }
} }
// Operations returning Bool // Operations returning Bool
"isEmpty" | "any" | "all" => Some("LuxBool".to_string()), "isEmpty" | "any" | "all" | "contains" => Some("LuxBool".to_string()),
_ => None, _ => None,
} }
} else if effect.name == "Random" { } else if effect.name == "Random" {
@@ -5175,9 +5648,10 @@ impl CBackend {
} else if effect.name == "File" { } else if effect.name == "File" {
match operation.name.as_str() { match operation.name.as_str() {
"read" => Some("LuxString".to_string()), "read" => Some("LuxString".to_string()),
"write" | "append" | "delete" | "mkdir" => Some("void".to_string()), "write" | "append" | "delete" | "mkdir" | "copy" => Some("void".to_string()),
"exists" | "isDir" => Some("LuxBool".to_string()), "exists" | "isDir" => Some("LuxBool".to_string()),
"readDir" | "listDir" => Some("LuxList*".to_string()), "readDir" | "listDir" | "glob" => Some("LuxList*".to_string()),
"tryRead" | "tryWrite" | "tryDelete" => Some("Result".to_string()),
_ => None, _ => None,
} }
} else if effect.name == "Http" { } else if effect.name == "Http" {
@@ -5383,7 +5857,13 @@ impl CBackend {
None None
} }
} }
Expr::Var(_) => None, Expr::Var(ident) => {
let escaped = self.escape_c_keyword(&ident.name);
// Check renamed variables first, then original name
let lookup_name = self.var_renames.get(&ident.name).unwrap_or(&escaped);
self.var_option_inner_types.get(lookup_name).cloned()
.or_else(|| self.var_option_inner_types.get(&escaped).cloned())
}
_ => None, _ => None,
} }
} }
@@ -5561,9 +6041,25 @@ impl CBackend {
} }
} }
fn emit_global_let(&mut self, name: &Ident) -> Result<(), CGenError> { fn emit_global_let(&mut self, let_decl: &crate::ast::LetDecl) -> Result<(), CGenError> {
// Declare global variable without initializer (initialized in main) // Infer type from the value expression (or type annotation)
self.writeln(&format!("static LuxInt {} = 0;", name.name)); let typ = if let Some(ref type_expr) = let_decl.typ {
self.type_expr_to_c(type_expr).unwrap_or_else(|_| "LuxInt".to_string())
} else {
self.infer_expr_type(&let_decl.value).unwrap_or_else(|| "LuxInt".to_string())
};
let default = match typ.as_str() {
"LuxString" => "NULL",
"LuxBool" => "false",
"LuxFloat" => "0.0",
"LuxList*" | "LuxClosure*" => "NULL",
"Option" => "{0}",
"Result" => "{0}",
_ if typ.ends_with('*') => "NULL",
_ => "0",
};
self.writeln(&format!("static {} {} = {};", typ, let_decl.name.name, default));
self.var_types.insert(let_decl.name.name.clone(), typ);
self.writeln(""); self.writeln("");
Ok(()) Ok(())
} }
@@ -5666,6 +6162,20 @@ impl CBackend {
self.type_expr_to_c(type_expr) self.type_expr_to_c(type_expr)
} }
/// Extract the inner C type from an Option<T> type expression
fn option_inner_type_from_type_expr(&self, type_expr: &TypeExpr) -> Option<String> {
if let TypeExpr::App(base, args) = type_expr {
if let TypeExpr::Named(name) = base.as_ref() {
if name.name == "Option" {
if let Some(inner) = args.first() {
return self.type_expr_to_c(inner).ok();
}
}
}
}
None
}
fn type_expr_to_c(&self, type_expr: &TypeExpr) -> Result<String, CGenError> { fn type_expr_to_c(&self, type_expr: &TypeExpr) -> Result<String, CGenError> {
match type_expr { match type_expr {
TypeExpr::Named(ident) => { TypeExpr::Named(ident) => {
@@ -6018,7 +6528,8 @@ impl CBackend {
// These List operations return new lists // These List operations return new lists
return matches!(field.name.as_str(), return matches!(field.name.as_str(),
"map" | "filter" | "concat" | "reverse" | "map" | "filter" | "concat" | "reverse" |
"take" | "drop" | "range" "take" | "drop" | "range" | "zip" | "flatten" |
"sort" | "sortBy"
); );
} }
if module.name == "String" { if module.name == "String" {
@@ -6044,7 +6555,8 @@ impl CBackend {
if effect.name == "List" { if effect.name == "List" {
matches!(operation.name.as_str(), matches!(operation.name.as_str(),
"map" | "filter" | "concat" | "reverse" | "map" | "filter" | "concat" | "reverse" |
"take" | "drop" | "range" "take" | "drop" | "range" | "zip" | "flatten" |
"sort" | "sortBy"
) )
} else if effect.name == "Process" { } else if effect.name == "Process" {
// Process.exec returns RC-managed string // Process.exec returns RC-managed string

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,
@@ -138,6 +144,12 @@ pub enum BuiltinFn {
MapFromList, MapFromList,
MapToList, MapToList,
MapMerge, MapMerge,
// Ref operations
RefNew,
RefGet,
RefSet,
RefUpdate,
} }
/// Runtime value /// Runtime value
@@ -170,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 {
@@ -191,6 +210,8 @@ impl Value {
Value::Constructor { .. } => "Constructor", Value::Constructor { .. } => "Constructor",
Value::Versioned { .. } => "Versioned", Value::Versioned { .. } => "Versioned",
Value::Json(_) => "Json", Value::Json(_) => "Json",
Value::ExternFn { .. } => "ExternFn",
Value::Ref(_) => "Ref",
} }
} }
@@ -246,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,
} }
@@ -401,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()),
} }
} }
} }
@@ -972,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);
@@ -1180,6 +1213,15 @@ impl Interpreter {
("merge".to_string(), Value::Builtin(BuiltinFn::MapMerge)), ("merge".to_string(), Value::Builtin(BuiltinFn::MapMerge)),
])); ]));
env.define("Map", map_module); 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
@@ -1390,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)
@@ -1909,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),
@@ -2716,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;
@@ -2742,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)?;
@@ -3274,6 +3460,56 @@ impl Interpreter {
} }
Ok(EvalResult::Value(Value::Map(map1))) 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()))),
}
}
} }
} }
@@ -3357,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()),
@@ -3864,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""#);
@@ -5559,4 +5940,107 @@ c")"#;
"#; "#;
assert_eq!(eval(source).unwrap(), "Some(30)"); 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
@@ -1917,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))
} }
} }
@@ -1929,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,
@@ -1958,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();
} }
} }
@@ -2093,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();
@@ -2108,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();
} }
} }
@@ -2151,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)?;
@@ -2164,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();
@@ -2174,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());
@@ -2214,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
@@ -2227,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) {
@@ -2245,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) {
@@ -2258,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() {
@@ -2969,6 +3026,9 @@ impl TypeChecker {
"Map" if resolved_args.len() == 2 => { "Map" if resolved_args.len() == 2 => {
return Type::Map(Box::new(resolved_args[0].clone()), Box::new(resolved_args[1].clone())); 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

@@ -49,6 +49,8 @@ pub enum Type {
Option(Box<Type>), Option(Box<Type>),
/// Map type (sugar for App(Map, [K, V])) /// Map type (sugar for App(Map, [K, V]))
Map(Box<Type>, Box<Type>), 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>,
@@ -120,7 +122,7 @@ impl Type {
} }
Type::Tuple(elements) => elements.iter().any(|e| e.contains_var(var)), Type::Tuple(elements) => elements.iter().any(|e| e.contains_var(var)),
Type::Record(fields) => fields.iter().any(|(_, t)| t.contains_var(var)), Type::Record(fields) => fields.iter().any(|(_, t)| t.contains_var(var)),
Type::List(inner) | Type::Option(inner) => inner.contains_var(var), Type::List(inner) | Type::Option(inner) | Type::Ref(inner) => inner.contains_var(var),
Type::Map(k, v) => k.contains_var(var) || v.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,
@@ -161,6 +163,7 @@ impl Type {
), ),
Type::List(inner) => Type::List(Box::new(inner.apply(subst))), Type::List(inner) => Type::List(Box::new(inner.apply(subst))),
Type::Option(inner) => Type::Option(Box::new(inner.apply(subst))), Type::Option(inner) => Type::Option(Box::new(inner.apply(subst))),
Type::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::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)),
@@ -211,7 +214,7 @@ 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) => { Type::Map(k, v) => {
let mut vars = k.free_vars(); let mut vars = k.free_vars();
vars.extend(v.free_vars()); vars.extend(v.free_vars());
@@ -288,6 +291,7 @@ impl fmt::Display for Type {
} }
Type::List(inner) => write!(f, "List<{}>", inner), Type::List(inner) => write!(f, "List<{}>", inner),
Type::Option(inner) => write!(f, "Option<{}>", inner), Type::Option(inner) => write!(f, "Option<{}>", inner),
Type::Ref(inner) => write!(f, "Ref<{}>", inner),
Type::Map(k, v) => write!(f, "Map<{}, {}>", k, v), 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)
@@ -956,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],
},
},
], ],
}, },
); );
@@ -1499,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(
@@ -1543,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));
@@ -1852,6 +1950,32 @@ impl TypeEnv {
]); ]);
env.bind("Map", TypeScheme::mono(map_module_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())),
@@ -2091,6 +2215,9 @@ impl TypeEnv {
Type::Map(k, v) => { Type::Map(k, v) => {
Type::Map(Box::new(self.expand_type_alias(k)), Box::new(self.expand_type_alias(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)),
@@ -2251,6 +2378,9 @@ 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 // Map
(Type::Map(k1, v1), Type::Map(k2, v2)) => { (Type::Map(k1, v1), Type::Map(k2, v2)) => {
let s1 = unify(k1, k2)?; let s1 = unify(k1, k2)?;

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
// ============================================================ // ============================================================