Compare commits
27 Commits
v0.1.6
...
fe30206cd0
| Author | SHA1 | Date | |
|---|---|---|---|
| fe30206cd0 | |||
| 563d62f526 | |||
| e9ec1bb84d | |||
| e46afd98eb | |||
| 64f33e4e4b | |||
| 293635f415 | |||
| 694e4ec999 | |||
| 78879ca94e | |||
| 01474b401f | |||
| 169de0b3c8 | |||
| 667a94b4dc | |||
| 1b629aaae4 | |||
| 0f8babfd8b | |||
| 582d603513 | |||
| fbb7ddb6c3 | |||
| 400acc3f35 | |||
| ea3a7ca2dd | |||
| 7b40421a6a | |||
| 26b94935e9 | |||
| 018a799c05 | |||
| ec78286165 | |||
| f2688072ac | |||
| 746643527d | |||
| 091ff1e422 | |||
| 1fc472a54c | |||
| caabaeeb9c | |||
| 4e43d3d50d |
15
Cargo.lock
generated
15
Cargo.lock
generated
@@ -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.5"
|
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]]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lux"
|
name = "lux"
|
||||||
version = "0.1.6"
|
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]
|
||||||
|
|||||||
@@ -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.6\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.6";
|
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.6";
|
version = "0.1.12";
|
||||||
src = ./.;
|
src = ./.;
|
||||||
cargoLock.lockFile = ./Cargo.lock;
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
|
|
||||||
|
|||||||
35
src/ast.rs
35
src/ast.rs
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1151,6 +1166,10 @@ impl CBackend {
|
|||||||
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(" 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("");
|
||||||
@@ -1371,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);");
|
||||||
@@ -1416,6 +1514,26 @@ impl CBackend {
|
|||||||
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,");
|
||||||
@@ -1426,6 +1544,10 @@ impl CBackend {
|
|||||||
self.writeln(" .mkdir = default_file_mkdir,");
|
self.writeln(" .mkdir = default_file_mkdir,");
|
||||||
self.writeln(" .copy = default_file_copy,");
|
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("");
|
||||||
@@ -2072,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));");
|
||||||
@@ -2145,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("");
|
||||||
@@ -2192,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.");
|
||||||
@@ -2727,7 +2885,11 @@ impl CBackend {
|
|||||||
for param in &func.params {
|
for param in &func.params {
|
||||||
let escaped = self.escape_c_keyword(¶m.name.name);
|
let escaped = self.escape_c_keyword(¶m.name.name);
|
||||||
if let Ok(c_type) = self.type_expr_to_c(¶m.typ) {
|
if let Ok(c_type) = self.type_expr_to_c(¶m.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(¶m.typ) {
|
||||||
|
self.var_option_inner_types.insert(escaped, inner);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3088,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
|
||||||
@@ -3116,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])?;
|
||||||
@@ -3396,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
|
||||||
@@ -3525,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" {
|
||||||
@@ -3721,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);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4646,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,
|
||||||
@@ -4787,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 {
|
||||||
@@ -4872,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));
|
||||||
@@ -5078,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)
|
||||||
@@ -5173,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
|
||||||
@@ -5187,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" {
|
||||||
@@ -5208,7 +5650,8 @@ impl CBackend {
|
|||||||
"read" => Some("LuxString".to_string()),
|
"read" => Some("LuxString".to_string()),
|
||||||
"write" | "append" | "delete" | "mkdir" | "copy" => 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" {
|
||||||
@@ -5414,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5592,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(())
|
||||||
}
|
}
|
||||||
@@ -5697,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) => {
|
||||||
@@ -6049,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" {
|
||||||
@@ -6075,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
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use crate::ast::*;
|
use crate::ast::*;
|
||||||
|
use crate::modules::Module;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
/// JavaScript code generation errors
|
/// JavaScript code generation errors
|
||||||
@@ -71,6 +72,14 @@ pub struct JsBackend {
|
|||||||
var_substitutions: HashMap<String, String>,
|
var_substitutions: HashMap<String, String>,
|
||||||
/// Effects actually used in the program (for tree-shaking runtime)
|
/// Effects actually used in the program (for tree-shaking runtime)
|
||||||
used_effects: HashSet<String>,
|
used_effects: HashSet<String>,
|
||||||
|
/// Extern function names mapped to their JS names
|
||||||
|
extern_fns: HashMap<String, String>,
|
||||||
|
/// Extern let names mapped to their JS names
|
||||||
|
extern_lets: HashMap<String, String>,
|
||||||
|
/// Module functions: (alias, fn_name) → mangled JS name
|
||||||
|
module_functions: HashMap<(String, String), String>,
|
||||||
|
/// Known module aliases
|
||||||
|
imported_modules: HashSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JsBackend {
|
impl JsBackend {
|
||||||
@@ -93,13 +102,20 @@ impl JsBackend {
|
|||||||
has_handlers: false,
|
has_handlers: false,
|
||||||
var_substitutions: HashMap::new(),
|
var_substitutions: HashMap::new(),
|
||||||
used_effects: HashSet::new(),
|
used_effects: HashSet::new(),
|
||||||
|
extern_fns: HashMap::new(),
|
||||||
|
extern_lets: HashMap::new(),
|
||||||
|
module_functions: HashMap::new(),
|
||||||
|
imported_modules: HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate JavaScript code from a Lux program
|
/// Generate JavaScript code from a Lux program
|
||||||
pub fn generate(&mut self, program: &Program) -> Result<String, JsGenError> {
|
pub fn generate(&mut self, program: &Program, modules: &HashMap<String, Module>) -> Result<String, JsGenError> {
|
||||||
self.output.clear();
|
self.output.clear();
|
||||||
|
|
||||||
|
// Process imported modules before the main program
|
||||||
|
self.process_imported_modules(program, modules)?;
|
||||||
|
|
||||||
// First pass: collect all function names, types, and effects
|
// First pass: collect all function names, types, and effects
|
||||||
for decl in &program.declarations {
|
for decl in &program.declarations {
|
||||||
match decl {
|
match decl {
|
||||||
@@ -112,6 +128,21 @@ impl JsBackend {
|
|||||||
Declaration::Type(t) => {
|
Declaration::Type(t) => {
|
||||||
self.collect_type(t)?;
|
self.collect_type(t)?;
|
||||||
}
|
}
|
||||||
|
Declaration::ExternFn(ext) => {
|
||||||
|
let js_name = ext
|
||||||
|
.js_name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| ext.name.name.clone());
|
||||||
|
self.extern_fns.insert(ext.name.name.clone(), js_name);
|
||||||
|
self.functions.insert(ext.name.name.clone());
|
||||||
|
}
|
||||||
|
Declaration::ExternLet(ext) => {
|
||||||
|
let js_name = ext
|
||||||
|
.js_name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| ext.name.name.clone());
|
||||||
|
self.extern_lets.insert(ext.name.name.clone(), js_name);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,6 +153,9 @@ impl JsBackend {
|
|||||||
// Emit runtime helpers (tree-shaken based on used effects)
|
// Emit runtime helpers (tree-shaken based on used effects)
|
||||||
self.emit_runtime();
|
self.emit_runtime();
|
||||||
|
|
||||||
|
// Emit imported module code (type constructors and functions)
|
||||||
|
self.emit_module_code(modules, program)?;
|
||||||
|
|
||||||
// Emit type constructors
|
// Emit type constructors
|
||||||
for decl in &program.declarations {
|
for decl in &program.declarations {
|
||||||
if let Declaration::Type(t) = decl {
|
if let Declaration::Type(t) = decl {
|
||||||
@@ -136,6 +170,13 @@ impl JsBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emit handlers
|
||||||
|
for decl in &program.declarations {
|
||||||
|
if let Declaration::Handler(h) = decl {
|
||||||
|
self.emit_handler(h)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any top-level let calls main (to avoid double invocation)
|
// Check if any top-level let calls main (to avoid double invocation)
|
||||||
let has_main_call = program.declarations.iter().any(|decl| {
|
let has_main_call = program.declarations.iter().any(|decl| {
|
||||||
if let Declaration::Let(l) = decl {
|
if let Declaration::Let(l) = decl {
|
||||||
@@ -166,6 +207,20 @@ impl JsBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for `let main = fn() => ...` pattern (not tracked in self.functions)
|
||||||
|
let has_let_main = program.declarations.iter().any(|d| {
|
||||||
|
if let Declaration::Let(l) = d {
|
||||||
|
l.name.name == "main" && matches!(&l.value, Expr::Lambda { .. })
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if has_let_main && !self.functions.contains("main") && !has_main_call {
|
||||||
|
self.writeln("");
|
||||||
|
self.writeln("// Entry point (let main)");
|
||||||
|
self.writeln("main();");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(self.output.clone())
|
Ok(self.output.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,6 +336,216 @@ impl JsBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Process imported modules: collect function names, types, and mappings
|
||||||
|
fn process_imported_modules(
|
||||||
|
&mut self,
|
||||||
|
program: &Program,
|
||||||
|
modules: &HashMap<String, Module>,
|
||||||
|
) -> Result<(), JsGenError> {
|
||||||
|
let mut processed = HashSet::new();
|
||||||
|
for import in &program.imports {
|
||||||
|
let module_path = import.path.segments.iter()
|
||||||
|
.map(|s| s.name.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("/");
|
||||||
|
let alias = if let Some(ref a) = import.alias {
|
||||||
|
a.name.clone()
|
||||||
|
} else {
|
||||||
|
import.path.segments.last()
|
||||||
|
.map(|s| s.name.clone())
|
||||||
|
.unwrap_or_else(|| module_path.clone())
|
||||||
|
};
|
||||||
|
self.imported_modules.insert(alias.clone());
|
||||||
|
self.process_single_module(&alias, &module_path, modules, &mut processed)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process a single module and its transitive imports
|
||||||
|
fn process_single_module(
|
||||||
|
&mut self,
|
||||||
|
alias: &str,
|
||||||
|
module_path: &str,
|
||||||
|
modules: &HashMap<String, Module>,
|
||||||
|
processed: &mut HashSet<String>,
|
||||||
|
) -> Result<(), JsGenError> {
|
||||||
|
if processed.contains(module_path) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
processed.insert(module_path.to_string());
|
||||||
|
|
||||||
|
let module = match modules.get(module_path) {
|
||||||
|
Some(m) => m,
|
||||||
|
None => return Ok(()), // Module not found — might be a built-in
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process transitive imports first
|
||||||
|
for sub_import in &module.program.imports {
|
||||||
|
let sub_path = sub_import.path.segments.iter()
|
||||||
|
.map(|s| s.name.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("/");
|
||||||
|
let sub_alias = sub_import.path.segments.last()
|
||||||
|
.map(|s| s.name.clone())
|
||||||
|
.unwrap_or_else(|| sub_path.clone());
|
||||||
|
self.process_single_module(&sub_alias, &sub_path, modules, processed)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect types from the module
|
||||||
|
for decl in &module.program.declarations {
|
||||||
|
if let Declaration::Type(t) = decl {
|
||||||
|
self.collect_type(t)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect functions from the module
|
||||||
|
for decl in &module.program.declarations {
|
||||||
|
if let Declaration::Function(f) = decl {
|
||||||
|
if f.visibility == Visibility::Public || module.exports.contains(&f.name.name) {
|
||||||
|
let mangled = format!("{}_{}_lux", alias, f.name.name);
|
||||||
|
self.functions.insert(mangled.clone());
|
||||||
|
self.module_functions.insert(
|
||||||
|
(alias.to_string(), f.name.name.clone()),
|
||||||
|
mangled,
|
||||||
|
);
|
||||||
|
|
||||||
|
if !f.effects.is_empty() {
|
||||||
|
self.effectful_functions.insert(format!("{}_{}_lux", alias, f.name.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit code for imported modules (type constructors, functions, handlers, let bindings)
|
||||||
|
fn emit_module_code(
|
||||||
|
&mut self,
|
||||||
|
modules: &HashMap<String, Module>,
|
||||||
|
program: &Program,
|
||||||
|
) -> Result<(), JsGenError> {
|
||||||
|
if self.module_functions.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.writeln("// === Imported Modules ===");
|
||||||
|
self.writeln("");
|
||||||
|
|
||||||
|
let mut processed = HashSet::new();
|
||||||
|
for import in &program.imports {
|
||||||
|
let module_path = import.path.segments.iter()
|
||||||
|
.map(|s| s.name.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("/");
|
||||||
|
let alias = if let Some(ref a) = import.alias {
|
||||||
|
a.name.clone()
|
||||||
|
} else {
|
||||||
|
import.path.segments.last()
|
||||||
|
.map(|s| s.name.clone())
|
||||||
|
.unwrap_or_else(|| module_path.clone())
|
||||||
|
};
|
||||||
|
self.emit_module_code_recursive(&alias, &module_path, modules, &mut processed)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_module_code_recursive(
|
||||||
|
&mut self,
|
||||||
|
alias: &str,
|
||||||
|
module_path: &str,
|
||||||
|
modules: &HashMap<String, Module>,
|
||||||
|
processed: &mut HashSet<String>,
|
||||||
|
) -> Result<(), JsGenError> {
|
||||||
|
if processed.contains(module_path) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
processed.insert(module_path.to_string());
|
||||||
|
|
||||||
|
let module = match modules.get(module_path) {
|
||||||
|
Some(m) => m.clone(),
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process transitive imports first
|
||||||
|
for sub_import in &module.program.imports {
|
||||||
|
let sub_path = sub_import.path.segments.iter()
|
||||||
|
.map(|s| s.name.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("/");
|
||||||
|
let sub_alias = sub_import.path.segments.last()
|
||||||
|
.map(|s| s.name.clone())
|
||||||
|
.unwrap_or_else(|| sub_path.clone());
|
||||||
|
self.emit_module_code_recursive(&sub_alias, &sub_path, modules, processed)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit type constructors
|
||||||
|
for decl in &module.program.declarations {
|
||||||
|
if let Declaration::Type(t) = decl {
|
||||||
|
self.emit_type_constructors(t)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit functions with module-prefixed names
|
||||||
|
for decl in &module.program.declarations {
|
||||||
|
if let Declaration::Function(f) = decl {
|
||||||
|
if f.visibility == Visibility::Public || module.exports.contains(&f.name.name) {
|
||||||
|
let mangled = format!("{}_{}_lux", alias, f.name.name);
|
||||||
|
self.emit_function_with_name(f, &mangled)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit a function with a custom mangled name (for module functions)
|
||||||
|
fn emit_function_with_name(
|
||||||
|
&mut self,
|
||||||
|
func: &FunctionDecl,
|
||||||
|
mangled_name: &str,
|
||||||
|
) -> Result<(), JsGenError> {
|
||||||
|
let is_effectful = !func.effects.is_empty();
|
||||||
|
|
||||||
|
// Build parameter list
|
||||||
|
let mut params: Vec<String> = func.params.iter().map(|p| p.name.name.clone()).collect();
|
||||||
|
|
||||||
|
// Effectful functions get handlers as first parameter
|
||||||
|
if is_effectful {
|
||||||
|
params.insert(0, "handlers".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function declaration
|
||||||
|
self.writeln(&format!(
|
||||||
|
"function {}({}) {{",
|
||||||
|
mangled_name,
|
||||||
|
params.join(", ")
|
||||||
|
));
|
||||||
|
self.indent += 1;
|
||||||
|
|
||||||
|
// Set context for effect handling
|
||||||
|
let prev_has_handlers = self.has_handlers;
|
||||||
|
self.has_handlers = is_effectful;
|
||||||
|
|
||||||
|
// Save and clear var substitutions for this function scope
|
||||||
|
let saved_substitutions = self.var_substitutions.clone();
|
||||||
|
self.var_substitutions.clear();
|
||||||
|
|
||||||
|
// Emit function body
|
||||||
|
let body_code = self.emit_expr(&func.body)?;
|
||||||
|
self.writeln(&format!("return {};", body_code));
|
||||||
|
|
||||||
|
self.has_handlers = prev_has_handlers;
|
||||||
|
self.var_substitutions = saved_substitutions;
|
||||||
|
|
||||||
|
self.indent -= 1;
|
||||||
|
self.writeln("}");
|
||||||
|
self.writeln("");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Emit the Lux runtime, tree-shaken based on used effects
|
/// Emit the Lux runtime, tree-shaken based on used effects
|
||||||
fn emit_runtime(&mut self) {
|
fn emit_runtime(&mut self) {
|
||||||
let uses_console = self.used_effects.contains("Console");
|
let uses_console = self.used_effects.contains("Console");
|
||||||
@@ -1045,6 +1310,55 @@ impl JsBackend {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Emit a handler declaration as a JS object
|
||||||
|
fn emit_handler(&mut self, handler: &HandlerDecl) -> Result<(), JsGenError> {
|
||||||
|
let handler_name = self.mangle_name(&handler.name.name);
|
||||||
|
|
||||||
|
self.writeln(&format!("const {} = {{", handler_name));
|
||||||
|
self.indent += 1;
|
||||||
|
|
||||||
|
for (i, imp) in handler.implementations.iter().enumerate() {
|
||||||
|
// Build parameter list for this operation (just the effect op params, not resume)
|
||||||
|
let params: Vec<String> = imp.params.iter().map(|p| p.name.clone()).collect();
|
||||||
|
|
||||||
|
self.writeln(&format!(
|
||||||
|
"{}: function({}) {{",
|
||||||
|
imp.op_name.name,
|
||||||
|
params.join(", ")
|
||||||
|
));
|
||||||
|
self.indent += 1;
|
||||||
|
|
||||||
|
// Set up handler context — handlers can use effects
|
||||||
|
let prev_has_handlers = self.has_handlers;
|
||||||
|
self.has_handlers = true;
|
||||||
|
|
||||||
|
let saved_substitutions = self.var_substitutions.clone();
|
||||||
|
|
||||||
|
// In the simple handler model, resume is the identity function.
|
||||||
|
// Expr::Resume nodes emit `resume(val)`, so define it in every operation.
|
||||||
|
self.writeln("const resume = (x) => x;");
|
||||||
|
|
||||||
|
let body_code = self.emit_expr(&imp.body)?;
|
||||||
|
self.writeln(&format!("return {};", body_code));
|
||||||
|
|
||||||
|
self.var_substitutions = saved_substitutions;
|
||||||
|
self.has_handlers = prev_has_handlers;
|
||||||
|
|
||||||
|
self.indent -= 1;
|
||||||
|
if i < handler.implementations.len() - 1 {
|
||||||
|
self.writeln("},");
|
||||||
|
} else {
|
||||||
|
self.writeln("}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.indent -= 1;
|
||||||
|
self.writeln("};");
|
||||||
|
self.writeln("");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Emit a top-level let binding
|
/// Emit a top-level let binding
|
||||||
fn emit_top_level_let(&mut self, let_decl: &LetDecl) -> Result<(), JsGenError> {
|
fn emit_top_level_let(&mut self, let_decl: &LetDecl) -> Result<(), JsGenError> {
|
||||||
let val = self.emit_expr(&let_decl.value)?;
|
let val = self.emit_expr(&let_decl.value)?;
|
||||||
@@ -1086,6 +1400,9 @@ impl JsBackend {
|
|||||||
} else if self.functions.contains(&ident.name) {
|
} else if self.functions.contains(&ident.name) {
|
||||||
// Function reference (used as value)
|
// Function reference (used as value)
|
||||||
Ok(self.mangle_name(&ident.name))
|
Ok(self.mangle_name(&ident.name))
|
||||||
|
} else if let Some(js_name) = self.extern_lets.get(&ident.name) {
|
||||||
|
// Extern let: use JS name directly (no mangling)
|
||||||
|
Ok(js_name.clone())
|
||||||
} else {
|
} else {
|
||||||
Ok(self.escape_js_keyword(&ident.name))
|
Ok(self.escape_js_keyword(&ident.name))
|
||||||
}
|
}
|
||||||
@@ -1218,6 +1535,37 @@ impl JsBackend {
|
|||||||
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for user-defined module function
|
||||||
|
let key = (module_name.name.clone(), field.name.clone());
|
||||||
|
if let Some(js_name) = self.module_functions.get(&key).cloned() {
|
||||||
|
let arg_strs: Result<Vec<_>, _> = args.iter().map(|a| self.emit_expr(a)).collect();
|
||||||
|
let args_str = arg_strs?.join(", ");
|
||||||
|
let is_effectful = self.effectful_functions.contains(&js_name);
|
||||||
|
|
||||||
|
if is_effectful && self.has_handlers {
|
||||||
|
let handlers_name = self.var_substitutions
|
||||||
|
.get("handlers")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "handlers".to_string());
|
||||||
|
return if args_str.is_empty() {
|
||||||
|
Ok(format!("{}({})", js_name, handlers_name))
|
||||||
|
} else {
|
||||||
|
Ok(format!("{}({}, {})", js_name, handlers_name, args_str))
|
||||||
|
};
|
||||||
|
} else if is_effectful {
|
||||||
|
return if args_str.is_empty() {
|
||||||
|
Ok(format!("{}(Lux.defaultHandlers)", js_name))
|
||||||
|
} else {
|
||||||
|
Ok(format!("{}(Lux.defaultHandlers, {})", js_name, args_str))
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return Ok(format!("{}({})", js_name, args_str));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1364,11 +1712,44 @@ impl JsBackend {
|
|||||||
return self.emit_map_operation(&operation.name, args);
|
return self.emit_map_operation(&operation.name, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special case: Ref module operations (not an effect)
|
||||||
|
if effect.name == "Ref" {
|
||||||
|
return self.emit_ref_operation(&operation.name, args);
|
||||||
|
}
|
||||||
|
|
||||||
// Special case: Html module operations (not an effect)
|
// Special case: Html module operations (not an effect)
|
||||||
if effect.name == "Html" {
|
if effect.name == "Html" {
|
||||||
return self.emit_html_operation(&operation.name, args);
|
return self.emit_html_operation(&operation.name, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for user-defined module function
|
||||||
|
let key = (effect.name.clone(), operation.name.clone());
|
||||||
|
if let Some(js_name) = self.module_functions.get(&key).cloned() {
|
||||||
|
let arg_strs: Result<Vec<_>, _> = args.iter().map(|a| self.emit_expr(a)).collect();
|
||||||
|
let args_str = arg_strs?.join(", ");
|
||||||
|
let is_effectful = self.effectful_functions.contains(&js_name);
|
||||||
|
|
||||||
|
if is_effectful && self.has_handlers {
|
||||||
|
let handlers_name = self.var_substitutions
|
||||||
|
.get("handlers")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "handlers".to_string());
|
||||||
|
return if args_str.is_empty() {
|
||||||
|
Ok(format!("{}({})", js_name, handlers_name))
|
||||||
|
} else {
|
||||||
|
Ok(format!("{}({}, {})", js_name, handlers_name, args_str))
|
||||||
|
};
|
||||||
|
} else if is_effectful {
|
||||||
|
return if args_str.is_empty() {
|
||||||
|
Ok(format!("{}(Lux.defaultHandlers)", js_name))
|
||||||
|
} else {
|
||||||
|
Ok(format!("{}(Lux.defaultHandlers, {})", js_name, args_str))
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return Ok(format!("{}({})", js_name, args_str));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let arg_strs: Result<Vec<_>, _> = args.iter().map(|a| self.emit_expr(a)).collect();
|
let arg_strs: Result<Vec<_>, _> = args.iter().map(|a| self.emit_expr(a)).collect();
|
||||||
let args_str = arg_strs?.join(", ");
|
let args_str = arg_strs?.join(", ");
|
||||||
|
|
||||||
@@ -1812,6 +2193,87 @@ impl JsBackend {
|
|||||||
end, start, start
|
end, start, start
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
"sort" => {
|
||||||
|
let list = self.emit_expr(&args[0])?;
|
||||||
|
Ok(format!(
|
||||||
|
"[...{}].sort((a, b) => a < b ? -1 : a > b ? 1 : 0)",
|
||||||
|
list
|
||||||
|
))
|
||||||
|
}
|
||||||
|
"sortBy" => {
|
||||||
|
let list = self.emit_expr(&args[0])?;
|
||||||
|
let func = self.emit_expr(&args[1])?;
|
||||||
|
Ok(format!("[...{}].sort({})", list, func))
|
||||||
|
}
|
||||||
|
"find" => {
|
||||||
|
let list = self.emit_expr(&args[0])?;
|
||||||
|
let pred = self.emit_expr(&args[1])?;
|
||||||
|
Ok(format!(
|
||||||
|
"((__l) => {{ const __r = __l.find({}); return __r !== undefined ? Lux.Some(__r) : Lux.None(); }})({})",
|
||||||
|
pred, list
|
||||||
|
))
|
||||||
|
}
|
||||||
|
"findIndex" => {
|
||||||
|
let list = self.emit_expr(&args[0])?;
|
||||||
|
let pred = self.emit_expr(&args[1])?;
|
||||||
|
Ok(format!(
|
||||||
|
"((__l) => {{ const __i = __l.findIndex({}); return __i !== -1 ? Lux.Some(__i) : Lux.None(); }})({})",
|
||||||
|
pred, list
|
||||||
|
))
|
||||||
|
}
|
||||||
|
"any" => {
|
||||||
|
let list = self.emit_expr(&args[0])?;
|
||||||
|
let pred = self.emit_expr(&args[1])?;
|
||||||
|
Ok(format!("{}.some({})", list, pred))
|
||||||
|
}
|
||||||
|
"all" => {
|
||||||
|
let list = self.emit_expr(&args[0])?;
|
||||||
|
let pred = self.emit_expr(&args[1])?;
|
||||||
|
Ok(format!("{}.every({})", list, pred))
|
||||||
|
}
|
||||||
|
"zip" => {
|
||||||
|
let list1 = self.emit_expr(&args[0])?;
|
||||||
|
let list2 = self.emit_expr(&args[1])?;
|
||||||
|
Ok(format!(
|
||||||
|
"((__a, __b) => __a.slice(0, Math.min(__a.length, __b.length)).map((__x, __i) => [__x, __b[__i]]))({})",
|
||||||
|
format!("{}, {}", list1, list2)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
"flatten" => {
|
||||||
|
let list = self.emit_expr(&args[0])?;
|
||||||
|
Ok(format!("{}.flat()", list))
|
||||||
|
}
|
||||||
|
"contains" => {
|
||||||
|
let list = self.emit_expr(&args[0])?;
|
||||||
|
let elem = self.emit_expr(&args[1])?;
|
||||||
|
Ok(format!(
|
||||||
|
"{}.some(__x => JSON.stringify(__x) === JSON.stringify({}))",
|
||||||
|
list, elem
|
||||||
|
))
|
||||||
|
}
|
||||||
|
"take" => {
|
||||||
|
let list = self.emit_expr(&args[0])?;
|
||||||
|
let n = self.emit_expr(&args[1])?;
|
||||||
|
Ok(format!("{}.slice(0, Math.max(0, {}))", list, n))
|
||||||
|
}
|
||||||
|
"drop" => {
|
||||||
|
let list = self.emit_expr(&args[0])?;
|
||||||
|
let n = self.emit_expr(&args[1])?;
|
||||||
|
Ok(format!("{}.slice(Math.max(0, {}))", list, n))
|
||||||
|
}
|
||||||
|
"forEach" => {
|
||||||
|
let list = self.emit_expr(&args[0])?;
|
||||||
|
let func = self.emit_expr(&args[1])?;
|
||||||
|
Ok(format!("({}.forEach({}), undefined)", list, func))
|
||||||
|
}
|
||||||
|
"get" => {
|
||||||
|
let list = self.emit_expr(&args[0])?;
|
||||||
|
let idx = self.emit_expr(&args[1])?;
|
||||||
|
Ok(format!(
|
||||||
|
"((__i, __l) => (__i >= 0 && __i < __l.length) ? Lux.Some(__l[__i]) : Lux.None())({}, {})",
|
||||||
|
idx, list
|
||||||
|
))
|
||||||
|
}
|
||||||
_ => Err(JsGenError {
|
_ => Err(JsGenError {
|
||||||
message: format!("Unknown List operation: {}", operation),
|
message: format!("Unknown List operation: {}", operation),
|
||||||
span: None,
|
span: None,
|
||||||
@@ -2389,6 +2851,37 @@ impl JsBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn emit_ref_operation(
|
||||||
|
&mut self,
|
||||||
|
operation: &str,
|
||||||
|
args: &[Expr],
|
||||||
|
) -> Result<String, JsGenError> {
|
||||||
|
match operation {
|
||||||
|
"new" => {
|
||||||
|
let val = self.emit_expr(&args[0])?;
|
||||||
|
Ok(format!("({{value: {}}})", val))
|
||||||
|
}
|
||||||
|
"get" => {
|
||||||
|
let r = self.emit_expr(&args[0])?;
|
||||||
|
Ok(format!("({}.value)", r))
|
||||||
|
}
|
||||||
|
"set" => {
|
||||||
|
let r = self.emit_expr(&args[0])?;
|
||||||
|
let val = self.emit_expr(&args[1])?;
|
||||||
|
Ok(format!("({}.value = {}, undefined)", r, val))
|
||||||
|
}
|
||||||
|
"update" => {
|
||||||
|
let r = self.emit_expr(&args[0])?;
|
||||||
|
let f = self.emit_expr(&args[1])?;
|
||||||
|
Ok(format!("({0}.value = {1}({0}.value), undefined)", r, f))
|
||||||
|
}
|
||||||
|
_ => Err(JsGenError {
|
||||||
|
message: format!("Unknown Ref operation: {}", operation),
|
||||||
|
span: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Emit Html module operations for type-safe HTML construction
|
/// Emit Html module operations for type-safe HTML construction
|
||||||
fn emit_html_operation(
|
fn emit_html_operation(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -2711,6 +3204,10 @@ impl JsBackend {
|
|||||||
|
|
||||||
/// Mangle a Lux name to a valid JavaScript name
|
/// Mangle a Lux name to a valid JavaScript name
|
||||||
fn mangle_name(&self, name: &str) -> String {
|
fn mangle_name(&self, name: &str) -> String {
|
||||||
|
// Extern functions use their JS name directly (no mangling)
|
||||||
|
if let Some(js_name) = self.extern_fns.get(name) {
|
||||||
|
return js_name.clone();
|
||||||
|
}
|
||||||
format!("{}_lux", name)
|
format!("{}_lux", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2808,7 +3305,7 @@ mod tests {
|
|||||||
|
|
||||||
let mut backend = JsBackend::new();
|
let mut backend = JsBackend::new();
|
||||||
let js_code = backend
|
let js_code = backend
|
||||||
.generate(&program)
|
.generate(&program, &std::collections::HashMap::new())
|
||||||
.map_err(|e| format!("Codegen error: {}", e))?;
|
.map_err(|e| format!("Codegen error: {}", e))?;
|
||||||
|
|
||||||
let output = Command::new("node")
|
let output = Command::new("node")
|
||||||
@@ -4068,7 +4565,7 @@ line3"
|
|||||||
|
|
||||||
let program = Parser::parse_source(source).expect("Should parse");
|
let program = Parser::parse_source(source).expect("Should parse");
|
||||||
let mut backend = JsBackend::new();
|
let mut backend = JsBackend::new();
|
||||||
let js_code = backend.generate(&program).expect("Should generate");
|
let js_code = backend.generate(&program, &std::collections::HashMap::new()).expect("Should generate");
|
||||||
|
|
||||||
// Core runtime is always present
|
// Core runtime is always present
|
||||||
assert!(js_code.contains("const Lux = {"), "Lux object should be defined");
|
assert!(js_code.contains("const Lux = {"), "Lux object should be defined");
|
||||||
@@ -4100,7 +4597,7 @@ line3"
|
|||||||
|
|
||||||
let program = Parser::parse_source(source).expect("Should parse");
|
let program = Parser::parse_source(source).expect("Should parse");
|
||||||
let mut backend = JsBackend::new();
|
let mut backend = JsBackend::new();
|
||||||
let js_code = backend.generate(&program).expect("Should generate");
|
let js_code = backend.generate(&program, &std::collections::HashMap::new()).expect("Should generate");
|
||||||
|
|
||||||
assert!(js_code.contains("Console:"), "Console handler should exist");
|
assert!(js_code.contains("Console:"), "Console handler should exist");
|
||||||
assert!(js_code.contains("Dom:"), "Dom handler should exist");
|
assert!(js_code.contains("Dom:"), "Dom handler should exist");
|
||||||
@@ -4123,7 +4620,7 @@ line3"
|
|||||||
|
|
||||||
let program = Parser::parse_source(source).expect("Should parse");
|
let program = Parser::parse_source(source).expect("Should parse");
|
||||||
let mut backend = JsBackend::new();
|
let mut backend = JsBackend::new();
|
||||||
let js_code = backend.generate(&program).expect("Should generate");
|
let js_code = backend.generate(&program, &std::collections::HashMap::new()).expect("Should generate");
|
||||||
|
|
||||||
// Only Console should be present
|
// Only Console should be present
|
||||||
assert!(js_code.contains("Console:"), "Console handler should exist");
|
assert!(js_code.contains("Console:"), "Console handler should exist");
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
115
src/formatter.rs
115
src/formatter.rs
@@ -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(¶ms.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()
|
||||||
|
|||||||
@@ -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()),
|
||||||
@@ -3888,6 +4136,95 @@ impl Interpreter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
("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;
|
||||||
|
|||||||
229
src/lexer.rs
229
src/lexer.rs
@@ -55,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
|
||||||
@@ -152,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"),
|
||||||
@@ -411,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)?,
|
||||||
@@ -669,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(¤t_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() {
|
||||||
@@ -784,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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
365
src/main.rs
365
src/main.rs
@@ -997,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);
|
||||||
@@ -2288,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()
|
||||||
@@ -3925,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() {
|
||||||
@@ -4038,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""#);
|
||||||
@@ -5630,6 +5941,58 @@ 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]
|
#[test]
|
||||||
fn test_file_copy() {
|
fn test_file_copy() {
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
314
src/parser.rs
314
src/parser.rs
@@ -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)?)),
|
||||||
@@ -246,7 +247,7 @@ impl Parser {
|
|||||||
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 ...'")),
|
||||||
TokenKind::Handle => Err(self.error("Bare 'handle' expressions are not allowed at top level. Use 'let _ = handle ...' or 'let result = handle ...'")),
|
TokenKind::Handle => Err(self.error("Bare 'handle' expressions are not allowed at top level. Use 'let _ = handle ...' or 'let result = handle ...'")),
|
||||||
_ => Err(self.error("Expected declaration (fn, effect, handler, type, trait, impl, or let)")),
|
_ => Err(self.error("Expected declaration (fn, extern, effect, handler, type, trait, impl, or let)")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,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();
|
||||||
@@ -846,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();
|
||||||
@@ -855,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1919,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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1931,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,
|
||||||
@@ -1960,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2095,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();
|
||||||
@@ -2110,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2200,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();
|
||||||
@@ -2210,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());
|
||||||
@@ -2250,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
|
||||||
@@ -2263,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) {
|
||||||
@@ -2281,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) {
|
||||||
@@ -2294,12 +2479,121 @@ 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 {
|
Ok(Expr::Record {
|
||||||
spread,
|
spread,
|
||||||
fields,
|
fields,
|
||||||
span,
|
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 {
|
||||||
|
spread,
|
||||||
|
fields,
|
||||||
|
span: outer_span,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_block_rest(&mut self, start: Span) -> Result<Expr, ParseError> {
|
fn parse_block_rest(&mut self, start: Span) -> Result<Expr, ParseError> {
|
||||||
let mut statements = Vec::new();
|
let mut statements = Vec::new();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2476,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() {
|
||||||
@@ -3003,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()));
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
126
src/types.rs
126
src/types.rs
@@ -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)
|
||||||
@@ -964,6 +968,38 @@ impl TypeEnv {
|
|||||||
],
|
],
|
||||||
return_type: Type::Unit,
|
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],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1507,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(
|
||||||
@@ -1551,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));
|
||||||
|
|
||||||
@@ -1860,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())),
|
||||||
@@ -2099,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)),
|
||||||
@@ -2259,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)?;
|
||||||
|
|||||||
@@ -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)
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -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
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user