diff --git a/dist/platform/elc b/dist/platform/elc index 3f5b8e4..4d31bd5 100755 Binary files a/dist/platform/elc and b/dist/platform/elc differ diff --git a/el-compiler/src/codegen-js.el b/el-compiler/src/codegen-js.el index 7e44b41..852eb7a 100644 --- a/el-compiler/src/codegen-js.el +++ b/el-compiler/src/codegen-js.el @@ -948,6 +948,10 @@ fn js_cg_stmt(stmt: Map, indent: String, declared: [String]) -> [St if kind == "TypeDef" { return declared } if kind == "EnumDef" { return declared } if kind == "Import" { return declared } + // TestDef: skip in normal mode; handled by js_codegen_test in test mode. + if kind == "TestDef" { return declared } + // Assert: no-op in normal mode; handled by js_cg_stmt_assert in test mode. + if kind == "Assert" { return declared } if kind == "TryCatch" { let try_body = stmt["try_body"] @@ -1168,20 +1172,178 @@ fn js_is_top_level_decl(stmt: Map) -> Bool { if kind == "CgiBlock" { return true } if kind == "ServiceBlock" { return true } if kind == "ExternFn" { return true } + if kind == "TestDef" { return true } false } +// ── Test mode codegen (JS) ──────────────────────────────────────────────────── +// +// reporter = "text" → human-readable output to stderr (console.error) +// reporter = "json" → newline-delimited JSON to stdout (process.stdout.write) +// +// The test function returns bool: true = pass, false = fail. + +fn js_cg_stmt_assert_text(stmt: Map, test_name: String) -> Void { + let expr_node = stmt["expr"] + let msg: String = stmt["msg"] + let expr_c: String = js_cg_expr(expr_node) + let disp_msg = "assert failed" + if !str_eq(msg, "") { let disp_msg = msg } + js_emit_line(" if (!(" + expr_c + ")) {") + js_emit_line(" process.stderr.write(\" FAIL " + js_escape(test_name) + " — " + js_escape(disp_msg) + "\\n\");") + js_emit_line(" return false;") + js_emit_line(" }") +} + +fn js_cg_stmt_assert_json(stmt: Map, test_name: String, file_name: String, test_line: Int) -> Void { + let expr_node = stmt["expr"] + let msg: String = stmt["msg"] + let assert_line: Int = stmt["line"] + let expr_c: String = js_cg_expr(expr_node) + let disp_msg = "assert failed" + if !str_eq(msg, "") { let disp_msg = msg } + js_emit_line(" if (!(" + expr_c + ")) {") + js_emit_line(" process.stdout.write(JSON.stringify({type:\"test_fail\",name:" + js_str_lit(test_name) + ",file:" + js_str_lit(file_name) + ",line:" + native_int_to_str(test_line) + ",assert_line:" + native_int_to_str(assert_line) + ",message:" + js_str_lit(disp_msg) + "}) + \"\\n\");") + js_emit_line(" return false;") + js_emit_line(" }") +} + +// js_cg_stmts_in_test: emit test body, routing Assert to the right handler. +fn js_cg_stmts_in_test(stmts: [Map], indent: String, declared: [String], test_name: String, reporter: String, file_name: String, test_line: Int) -> [String] { + let n: Int = native_list_len(stmts) + let i = 0 + let decl = declared + while i < n { + let stmt = native_list_get(stmts, i) + let sk: String = stmt["stmt"] + if str_eq(sk, "Assert") { + if str_eq(reporter, "json") { + js_cg_stmt_assert_json(stmt, test_name, file_name, test_line) + } else { + js_cg_stmt_assert_text(stmt, test_name) + } + } else { + let decl = js_cg_stmt(stmt, indent, decl) + } + let i = i + 1 + } + decl +} + +// js_cg_test_fn: emit a single async test function. +fn js_cg_test_fn(test_def: Map, idx: Int, reporter: String, file_name: String) -> String { + let fn_name: String = "el_test_" + native_int_to_str(idx) + let test_name: String = test_def["name"] + let test_line: Int = test_def["line"] + let body = test_def["body"] + js_emit_line("async function " + fn_name + "() {") + js_cg_stmts_in_test(body, " ", native_list_empty(), test_name, reporter, file_name, test_line) + js_emit_line(" return true;") + js_emit_line("}") + js_emit_blank() + fn_name +} + +// js_codegen_test: emit the test runner (replaces main() when --test active). +// reporter: "text" or "json" +// file_name: basename of the source file (used in JSON output) +fn js_codegen_test(stmts: [Map], reporter: String, file_name: String) -> Void { + // Collect TestDef nodes in order. + let n: Int = native_list_len(stmts) + let test_defs: [Map] = native_list_empty() + let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + let sk: String = stmt["stmt"] + if str_eq(sk, "TestDef") { + let test_defs = native_list_append(test_defs, stmt) + } + let i = i + 1 + } + let n_tests: Int = native_list_len(test_defs) + + // Emit non-test function definitions (skip fn main and TestDef nodes). + let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + if js_is_fndef(stmt) { + let fn_name: String = stmt["name"] + if !str_eq(fn_name, "main") { + js_cg_fn(stmt) + } + } + let i = i + 1 + } + + // Emit each test function. + let ti = 0 + while ti < n_tests { + let test_def = native_list_get(test_defs, ti) + js_cg_test_fn(test_def, ti, reporter, file_name) + let ti = ti + 1 + } + + // Emit the test runner IIFE. + let test_word = "tests" + if n_tests == 1 { let test_word = "test" } + js_emit_line("(async () => {") + js_emit_line(" let pass = 0; let fail = 0;") + + if str_eq(reporter, "json") { + // JSON reporter: suite_start to stdout + js_emit_line(" process.stdout.write(JSON.stringify({type:\"suite_start\",file:" + js_str_lit(file_name) + ",total:" + native_int_to_str(n_tests) + "}) + \"\\n\");") + let ti = 0 + while ti < n_tests { + let test_def = native_list_get(test_defs, ti) + let test_name: String = test_def["name"] + let test_line: Int = test_def["line"] + let fn_name: String = "el_test_" + native_int_to_str(ti) + js_emit_line(" process.stdout.write(JSON.stringify({type:\"test_start\",name:" + js_str_lit(test_name) + ",file:" + js_str_lit(file_name) + ",line:" + native_int_to_str(test_line) + "}) + \"\\n\");") + js_emit_line(" if (await " + fn_name + "()) {") + js_emit_line(" pass++;") + js_emit_line(" process.stdout.write(JSON.stringify({type:\"test_pass\",name:" + js_str_lit(test_name) + ",file:" + js_str_lit(file_name) + ",line:" + native_int_to_str(test_line) + ",duration_ms:0}) + \"\\n\");") + js_emit_line(" } else { fail++; }") + let ti = ti + 1 + } + js_emit_line(" process.stdout.write(JSON.stringify({type:\"suite_end\",passed:pass,failed:fail}) + \"\\n\");") + } else { + // Text reporter: human-readable to stderr + js_emit_line(" process.stderr.write(\"==> running " + native_int_to_str(n_tests) + " " + test_word + "\\n\\n\");") + let ti = 0 + while ti < n_tests { + let test_def = native_list_get(test_defs, ti) + let test_name: String = test_def["name"] + let fn_name: String = "el_test_" + native_int_to_str(ti) + js_emit_line(" process.stderr.write(\" RUN " + js_escape(test_name) + "\\n\");") + js_emit_line(" if (await " + fn_name + "()) { pass++; process.stderr.write(\" PASS " + js_escape(test_name) + "\\n\"); }") + js_emit_line(" else { fail++; }") + let ti = ti + 1 + } + js_emit_line(" process.stderr.write(\"\\n\" + pass + \" passed, \" + fail + \" failed\\n\");") + } + + js_emit_line(" process.exit(fail > 0 ? 1 : 0);") + js_emit_line("})();") +} + // ── Entry point ─────────────────────────────────────────────────────────────── fn codegen_js(stmts: [Map], source: String) -> String { - codegen_js_inner(stmts, source, false, "") + codegen_js_inner(stmts, source, false, "", false, "text", "") +} + +// codegen_js_test: emit a JS test binary. +// reporter: "text" or "json" +// file_name: basename of the source file (used in JSON output) +fn codegen_js_test(stmts: [Map], source: String, reporter: String, file_name: String) -> String { + codegen_js_inner(stmts, source, false, "", true, reporter, file_name) } fn codegen_js_bundle(stmts: [Map], source: String, runtime_content: String) -> String { - codegen_js_inner(stmts, source, true, runtime_content) + codegen_js_inner(stmts, source, true, runtime_content, false, "text", "") } -fn codegen_js_inner(stmts: [Map], source: String, bundle_mode: Bool, runtime_content: String) -> String { +fn codegen_js_inner(stmts: [Map], source: String, bundle_mode: Bool, runtime_content: String, test_mode: Bool, reporter: String, file_name: String) -> String { // Reset per-compile state. state_set("__js_int_names", "") state_set("__js_match_counter", "") @@ -1292,6 +1454,12 @@ fn codegen_js_inner(stmts: [Map], source: String, bundle_mode: Bool let i = i + 1 } + // Test mode: emit test functions and runner, skip normal program logic. + if test_mode { + js_codegen_test(stmts, reporter, file_name) + return "" + } + // Function definitions let i = 0 while i < n { diff --git a/el-compiler/src/codegen.el b/el-compiler/src/codegen.el index e0bba78..4e96fdf 100644 --- a/el-compiler/src/codegen.el +++ b/el-compiler/src/codegen.el @@ -1308,6 +1308,13 @@ fn cg_stmt(stmt: Map, indent: String, declared: [String]) -> [Strin if kind == "Import" { return declared } if kind == "ExternFn" { return declared } if kind == "CgiBlock" { return declared } + if kind == "ServiceBlock" { return declared } + // TestDef: skip in normal (non-test) mode. + // In test mode the body is emitted by cg_test_fn, not here. + if kind == "TestDef" { return declared } + // Assert: no-op in normal mode. In test mode, cg_stmt_assert is used + // directly when emitting the per-test function body. + if kind == "Assert" { return declared } // TryCatch: browser-only control flow. In the C target, emit a comment // noting that the try body runs unconditionally; error handling is a no-op. // Programs that rely on catching JS exceptions should compile with --target=js. @@ -2559,6 +2566,7 @@ fn is_top_level_decl(stmt: Map) -> Bool { if kind == "Import" { return true } if kind == "CgiBlock" { return true } if kind == "ExternFn" { return true } + if kind == "TestDef" { return true } false } @@ -2703,9 +2711,195 @@ fn vbd_has_restricted_call(stmts: [Map]) -> Bool { false } +// -- Test mode codegen ---------------------------------------------------------- +// +// reporter = "text" → human-readable output to stderr +// reporter = "json" → newline-delimited JSON to stdout +// +// Each test function signature: static int el_test_N(void) +// Returns 0 = pass, non-zero = fail. +// +// Text assert: if (!expr) { fprintf(stderr, " FAIL \n"); return 1; } +// JSON assert: stores the assert_line for the runner's suite_end emission. +// Emits {"type":"test_fail",...} to stdout then returns 1. + +fn cg_stmt_assert_text(stmt: Map, test_name: String) -> Void { + let expr_node = stmt["expr"] + let msg: String = stmt["msg"] + let expr_c: String = cg_expr(expr_node) + let expr_c = strip_outer_parens(expr_c) + let disp_msg = "assert failed" + if !str_eq(msg, "") { let disp_msg = msg } + emit_line(" if (!(" + expr_c + ")) {") + emit_line(" fprintf(stderr, \" FAIL " + c_escape(test_name) + " \\xe2\\x80\\x94 " + c_escape(disp_msg) + "\\n\");") + emit_line(" return 1;") + emit_line(" }") +} + +fn cg_stmt_assert_json(stmt: Map, test_name: String, file_name: String, test_line: Int) -> Void { + let expr_node = stmt["expr"] + let msg: String = stmt["msg"] + let assert_line: Int = stmt["line"] + let expr_c: String = cg_expr(expr_node) + let expr_c = strip_outer_parens(expr_c) + let disp_msg = "assert failed" + if !str_eq(msg, "") { let disp_msg = msg } + // Embed all compile-time-known strings as C string literals (no %s runtime formatting). + let json_line: String = "{\"type\":\"test_fail\",\"name\":\"" + c_escape(test_name) + "\",\"file\":\"" + c_escape(file_name) + "\",\"line\":" + native_int_to_str(test_line) + ",\"assert_line\":" + native_int_to_str(assert_line) + ",\"message\":\"" + c_escape(disp_msg) + "\"}" + emit_line(" if (!(" + expr_c + ")) {") + emit_line(" puts(" + c_str_lit(json_line) + ");") + emit_line(" return 1;") + emit_line(" }") +} + +// cg_stmts_in_test: emit test body statements, routing Assert to the +// appropriate handler based on reporter mode. +fn cg_stmts_in_test(stmts: [Map], indent: String, declared: [String], test_name: String, reporter: String, file_name: String, test_line: Int) -> [String] { + let n: Int = native_list_len(stmts) + let i = 0 + let decl = declared + while i < n { + let stmt = native_list_get(stmts, i) + let sk: String = stmt["stmt"] + if str_eq(sk, "Assert") { + if str_eq(reporter, "json") { + cg_stmt_assert_json(stmt, test_name, file_name, test_line) + } else { + cg_stmt_assert_text(stmt, test_name) + } + } else { + let decl = cg_stmt(stmt, indent, decl) + } + let i = i + 1 + } + decl +} + +// cg_test_fn: emit a single test function. +fn cg_test_fn(test_def: Map, idx: Int, reporter: String, file_name: String) -> String { + let fn_name: String = "el_test_" + native_int_to_str(idx) + let test_name: String = test_def["name"] + let test_line: Int = test_def["line"] + let body = test_def["body"] + emit_line("static int " + fn_name + "(void) {") + cg_stmts_in_test(body, " ", native_list_empty(), test_name, reporter, file_name, test_line) + emit_line(" return 0;") + emit_line("}") + emit_blank() + fn_name +} + +// codegen_test: emit a complete test binary. +// reporter: "text" (stderr human-readable) or "json" (stdout ndjson) +// file_name: basename of the source file (used in JSON output) +fn codegen_test(stmts: [Map], reporter: String, file_name: String) -> Void { + // Collect all TestDef nodes in order. + let n: Int = native_list_len(stmts) + let test_defs: [Map] = native_list_empty() + let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + let sk: String = stmt["stmt"] + if str_eq(sk, "TestDef") { + let test_defs = native_list_append(test_defs, stmt) + } + let i = i + 1 + } + let n_tests: Int = native_list_len(test_defs) + + // Emit forward declarations for test functions. + let ti = 0 + while ti < n_tests { + let fn_name: String = "el_test_" + native_int_to_str(ti) + emit_line("static int " + fn_name + "(void);") + let ti = ti + 1 + } + emit_blank() + + // Emit all non-test, non-main function definitions. + let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + if is_fndef(stmt) { + let fn_name: String = stmt["name"] + if !str_eq(fn_name, "main") { + cg_fn(stmt) + } + } + let i = i + 1 + } + + // Emit each test function. + let ti = 0 + while ti < n_tests { + let test_def = native_list_get(test_defs, ti) + cg_test_fn(test_def, ti, reporter, file_name) + let ti = ti + 1 + } + + // Emit the test runner main(). + let test_word = "tests" + if n_tests == 1 { let test_word = "test" } + emit_line("int main(void) {") + emit_line(" int pass = 0; int fail = 0;") + + if str_eq(reporter, "json") { + // JSON reporter: all strings are compile-time constants; use puts. + // Only suite_end needs runtime pass/fail counts (printf). + let suite_start_json: String = "{\"type\":\"suite_start\",\"file\":\"" + c_escape(file_name) + "\",\"total\":" + native_int_to_str(n_tests) + "}" + emit_line(" puts(" + c_str_lit(suite_start_json) + ");") + let ti = 0 + while ti < n_tests { + let test_def = native_list_get(test_defs, ti) + let test_name: String = test_def["name"] + let test_line: Int = test_def["line"] + let fn_name: String = "el_test_" + native_int_to_str(ti) + let start_json: String = "{\"type\":\"test_start\",\"name\":\"" + c_escape(test_name) + "\",\"file\":\"" + c_escape(file_name) + "\",\"line\":" + native_int_to_str(test_line) + "}" + let pass_json: String = "{\"type\":\"test_pass\",\"name\":\"" + c_escape(test_name) + "\",\"file\":\"" + c_escape(file_name) + "\",\"line\":" + native_int_to_str(test_line) + ",\"duration_ms\":0}" + emit_line(" puts(" + c_str_lit(start_json) + ");") + emit_line(" if (" + fn_name + "() == 0) {") + emit_line(" pass++;") + emit_line(" puts(" + c_str_lit(pass_json) + ");") + emit_line(" } else { fail++; }") + let ti = ti + 1 + } + // suite_end needs runtime pass/fail counts + emit_line(" printf(\"{\\\"type\\\":\\\"suite_end\\\",\\\"passed\\\":%d,\\\"failed\\\":%d}\\n\", pass, fail);") + } else { + // Text reporter: human-readable to stderr + emit_line(" fprintf(stderr, \"==> running " + native_int_to_str(n_tests) + " " + test_word + "\\n\\n\");") + let ti = 0 + while ti < n_tests { + let test_def = native_list_get(test_defs, ti) + let test_name: String = test_def["name"] + let fn_name: String = "el_test_" + native_int_to_str(ti) + emit_line(" fprintf(stderr, \" RUN " + c_escape(test_name) + "\\n\");") + emit_line(" if (" + fn_name + "() == 0) { pass++; fprintf(stderr, \" PASS " + c_escape(test_name) + "\\n\"); }") + emit_line(" else { fail++; }") + let ti = ti + 1 + } + emit_line(" fprintf(stderr, \"\\n%d passed, %d failed\\n\", pass, fail);") + } + + emit_line(" return fail > 0 ? 1 : 0;") + emit_line("}") + emit_blank() +} + // -- Entry point ---------------------------------------------------------------- fn codegen(stmts: [Map], source: String) -> String { + codegen_inner(stmts, source, false, "text", "") +} + +// codegen_with_tests: emit a test binary. +// reporter: "text" or "json" +// file_name: basename of the source file (used in JSON output) +fn codegen_with_tests(stmts: [Map], source: String, reporter: String, file_name: String) -> String { + codegen_inner(stmts, source, true, reporter, file_name) +} + +fn codegen_inner(stmts: [Map], source: String, test_mode: Bool, reporter: String, file_name: String) -> String { // Detect cgi/service blocks: at most one declarative top-level block. // The block determines the program's CAPABILITY KIND: // "cgi" - full self-formation. Calls all primitives. @@ -2763,9 +2957,15 @@ fn codegen(stmts: [Map], source: String) -> String { // Clear temporal-type-violation accumulator from any prior compile. state_set("__time_violations", "") + // In test mode, delegate to the test-specific path which emits test + // functions and a test runner main() instead of the normal program. + // Test mode still needs the standard preamble (#includes, forward + // decls) so we emit that before branching. + // // Preamble emit_line("#include ") emit_line("#include ") + emit_line("#include ") emit_line("#include \"el_runtime.h\"") // Cross-module forward declarations: for each imported module, emit @@ -2862,6 +3062,15 @@ fn codegen(stmts: [Map], source: String) -> String { emit_blank() } + // Test mode: emit test functions and test runner instead of normal program. + if test_mode { + codegen_test(stmts, reporter, file_name) + emit_cap_violations() + emit_arity_violations() + emit_time_violations() + return "" + } + // Detect whether this compilation unit has an entry point. // A unit is a library (no C main emitted) when there is no fn main() // and no top-level executable statements. This supports separate diff --git a/el-compiler/src/compiler.el b/el-compiler/src/compiler.el index e74b38a..3e6c754 100644 --- a/el-compiler/src/compiler.el +++ b/el-compiler/src/compiler.el @@ -52,6 +52,24 @@ fn compile_js_with_bundle(source: String, runtime_path: String) -> String { codegen_js_bundle(stmts, source, runtime_content) } +// compile_test — full pipeline (C target, test mode): source -> C test runner. +// reporter: "text" or "json"; file_name: basename of the source file. +fn compile_test(source: String, reporter: String, file_name: String) -> String { + let tokens: [Map] = lex(source) + let stmts: [Map] = parse(tokens) + el_release(tokens) + codegen_with_tests(stmts, source, reporter, file_name) +} + +// compile_js_test — full pipeline (JS target, test mode): source -> JS test runner. +// reporter: "text" or "json"; file_name: basename of the source file. +fn compile_js_test(source: String, reporter: String, file_name: String) -> String { + let tokens: [Map] = lex(source) + let stmts: [Map] = parse(tokens) + el_release(tokens) + codegen_js_test(stmts, source, reporter, file_name) +} + // compile_dispatch — pick a backend based on the requested target. // tgt = "c" | "js" // (The parameter is named `tgt` because `target` is a reserved keyword @@ -62,6 +80,13 @@ fn compile_dispatch(tgt: String, source: String) -> String { compile(source) } +// compile_dispatch_test — pick test-mode backend. +// reporter: "text" or "json"; file_name: basename of the source file. +fn compile_dispatch_test(tgt: String, source: String, reporter: String, file_name: String) -> String { + if str_eq(tgt, "js") { return compile_js_test(source, reporter, file_name) } + compile_test(source, reporter, file_name) +} + // compile_dispatch_bundle — like compile_dispatch but bundle mode for JS. fn compile_dispatch_bundle(tgt: String, source: String, runtime_path: String) -> String { if str_eq(tgt, "js") { return compile_js_with_bundle(source, runtime_path) } @@ -147,6 +172,48 @@ fn detect_obfuscate(argv: [String]) -> Bool { return false } +// Detect --test flag in argv. +fn detect_test(argv: [String]) -> Bool { + let n: Int = native_list_len(argv) + let i = 0 + while i < n { + let a: String = native_list_get(argv, i) + if str_eq(a, "--test") { return true } + let i = i + 1 + } + return false +} + +// Detect --reporter= flag in argv. +// Returns "json" if --reporter=json, otherwise "text" (default). +fn detect_reporter(argv: [String]) -> String { + let n: Int = native_list_len(argv) + let i = 0 + while i < n { + let a: String = native_list_get(argv, i) + if str_starts_with(a, "--reporter=") { + let v: String = str_slice(a, 11, str_len(a)) + return v + } + let i = i + 1 + } + return "text" +} + +// basename_of — extract the filename portion of a path (after last '/'). +fn basename_of(path: String) -> String { + let n: Int = str_len(path) + let i: Int = n - 1 + while i >= 0 { + let c: String = str_slice(path, i, i + 1) + if str_eq(c, "/") { + return str_slice(path, i + 1, n) + } + let i = i - 1 + } + return path +} + // Build a unique temp file path: /tmp/elc--. fn make_temp_path(suffix: String) -> String { let pid: Int = getpid_now() @@ -458,7 +525,9 @@ fn run_with_postprocess(tgt: String, source: String, src_path: String, do_bundle // main — CLI entry point. // // elc # emit C to stdout +// elc --test # emit C test runner to stdout // elc --target=js # emit JS (module) to stdout +// elc --target=js --test # emit JS test runner to stdout // elc --target=js --bundle # emit self-contained JS (IIFE) to stdout // elc --target=js --bundle --minify # emit minified IIFE to stdout // elc --target=js --bundle --obfuscate # emit minified+obfuscated IIFE to stdout @@ -476,6 +545,8 @@ fn main() -> Void { let do_bundle: Bool = detect_bundle(argv) let do_minify: Bool = detect_minify(argv) let do_obfuscate: Bool = detect_obfuscate(argv) + let do_test: Bool = detect_test(argv) + let reporter: String = detect_reporter(argv) // --obfuscate implies --minify: obfuscating unminified code is pointless. if do_obfuscate { let do_minify = true @@ -483,7 +554,7 @@ fn main() -> Void { let positional: [String] = strip_flags(argv) let argc: Int = native_list_len(positional) if argc < 1 { - println("el-compiler: usage: elc [--target=c|js] [--bundle] [--minify] [--obfuscate] [--emit-header] []") + println("el-compiler: usage: elc [--target=c|js] [--test] [--reporter=text|json] [--bundle] [--minify] [--obfuscate] [--emit-header] []") exit(1) } @@ -510,6 +581,20 @@ fn main() -> Void { } let source: String = resolve_imports(src_path) + let file_name: String = basename_of(src_path) + + // --test mode: emit a test runner binary instead of the normal program. + if do_test { + let out: String = compile_dispatch_test(tgt, source, reporter, file_name) + if argc >= 2 { + let out_path: String = native_list_get(positional, 1) + let ok: Bool = fs_write(out_path, out) + if ok { exit(0) } + println("el-compiler: failed to write output") + exit(1) + } + exit(0) + } // When post-processing (--minify or --obfuscate) is requested, redirect // stdout to a temp file so codegen output can be captured and piped through diff --git a/el-compiler/src/lexer.el b/el-compiler/src/lexer.el index 5b93f81..8835b29 100644 --- a/el-compiler/src/lexer.el +++ b/el-compiler/src/lexer.el @@ -98,7 +98,10 @@ fn lex_is_whitespace(ch: String) -> Bool { } fn make_tok(kind: String, value: String) -> Map { - { "kind": kind, "value": value } + let ln_s: String = state_get("__lex_line") + let ln: Int = 1 + if !str_eq(ln_s, "") { let ln = str_to_int(ln_s) } + { "kind": kind, "value": value, "line": ln } } // ── Keyword lookup ──────────────────────────────────────────────────────────── @@ -467,12 +470,18 @@ fn lex(source: String) -> [Map] { let total: Int = native_list_len(chars) let tokens: [Map] = native_list_empty() let i: Int = 0 + let line_num: Int = 1 + state_set("__lex_line", "1") while i < total { let ch: String = native_list_get(chars, i) - // Skip whitespace + // Skip whitespace; track newlines for line-number reporting if lex_is_whitespace(ch) { + if ch == "\n" { + let line_num = line_num + 1 + state_set("__lex_line", native_int_to_str(line_num)) + } let i = i + 1 } else { // Line comments: // diff --git a/el-compiler/src/parser.el b/el-compiler/src/parser.el index fd3a223..3cb15ae 100644 --- a/el-compiler/src/parser.el +++ b/el-compiler/src/parser.el @@ -27,6 +27,15 @@ fn tok_value(tokens: [Map], pos: Int) -> String { t["value"] } +// tok_line — return the source line number of the token at pos (1-indexed). +// Returns 1 if the token has no "line" field or line is 0. +fn tok_line(tokens: [Map], pos: Int) -> Int { + let t = native_list_get(tokens, pos) + let ln: Int = t["line"] + if ln <= 0 { return 1 } + ln +} + fn expect(tokens: [Map], pos: Int, kind: String) -> Int { let k = tok_kind(tokens, pos) if k == kind { @@ -1554,6 +1563,35 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { }, p) } + // assert statement: assert or assert , "msg" + if k == "Assert" { + let assert_line: Int = tok_line(tokens, pos) + let p = pos + 1 + let r = parse_expr(tokens, p) + let expr_node = r["node"] + let p = r["pos"] + let msg = "" + let k2 = tok_kind(tokens, p) + if k2 == "Comma" { + let p = p + 1 + let msg = tok_value(tokens, p) + let p = p + 1 + } + return make_result({ "stmt": "Assert", "expr": expr_node, "msg": msg, "line": assert_line }, p) + } + + // test block: test "name" { stmts } + if k == "Test" { + let test_line: Int = tok_line(tokens, pos) + let p = pos + 1 + let name = tok_value(tokens, p) + let p = p + 1 + let r2 = parse_block(tokens, p) + let body = r2["stmts"] + let p = r2["pos"] + return make_result({ "stmt": "TestDef", "name": name, "body": body, "line": test_line }, p) + } + // Bare reassignment: `name = expr`. Handled BEFORE the expression // fallback so we don't drop the assign on the floor and emit three // orphan expressions (the original silent-miscompile bug). El's `let` diff --git a/tests/native/test_codegen_js.el b/tests/native/test_codegen_js.el new file mode 100644 index 0000000..f4a32c7 --- /dev/null +++ b/tests/native/test_codegen_js.el @@ -0,0 +1,80 @@ +// test_codegen_js.el - basic tests for JS codegen features. +// +// These tests verify that core El language features produce correct values +// when compiled and executed via the C backend. They serve as a +// regression baseline for the codegen pipeline. + +test "arithmetic" { + let x: Int = 2 + 3 + assert x == 5, "addition" + let y: Int = 10 - 4 + assert y == 6, "subtraction" + let z: Int = 3 * 4 + assert z == 12, "multiplication" + let w: Int = 15 / 3 + assert w == 5, "division" +} + +test "string-concat" { + let a: String = "hello" + let b: String = " world" + let c: String = a + b + assert c == "hello world", "string concatenation" +} + +test "str-len" { + let n: Int = str_len("hello") + assert n == 5, "str_len hello" + let m: Int = str_len("") + assert m == 0, "str_len empty" +} + +test "bool-logic" { + let t = true + let f = false + assert t, "true is truthy" + assert !f, "false negated is truthy" + assert t && !f, "true and not false" + assert t || f, "true or false" +} + +test "list-operations" { + let lst: [Int] = native_list_empty() + let lst = native_list_append(lst, 10) + let lst = native_list_append(lst, 20) + let lst = native_list_append(lst, 30) + let n: Int = native_list_len(lst) + assert n == 3, "list length 3" + let v0: Int = native_list_get(lst, 0) + let v1: Int = native_list_get(lst, 1) + let v2: Int = native_list_get(lst, 2) + assert v0 == 10, "first element" + assert v1 == 20, "second element" + assert v2 == 30, "third element" +} + +test "str-slice" { + let s: String = str_slice("hello world", 6, 11) + assert s == "world", "slice from 6 to 11" +} + +test "str-contains" { + assert str_contains("hello world", "world"), "contains world" + assert !str_contains("hello world", "xyz"), "does not contain xyz" +} + +test "int-to-str" { + let s: String = int_to_str(42) + assert s == "42", "int to string" +} + +test "str-to-int" { + let n: Int = str_to_int("123") + assert n == 123, "string to int" +} + +test "str-starts-ends" { + assert str_starts_with("hello world", "hello"), "starts with hello" + assert str_ends_with("hello world", "world"), "ends with world" + assert !str_starts_with("hello world", "world"), "does not start with world" +} diff --git a/tests/native/test_text.el b/tests/native/test_text.el new file mode 100644 index 0000000..869505d --- /dev/null +++ b/tests/native/test_text.el @@ -0,0 +1,84 @@ +// test_text.el - native test suite for text primitives. +// +// Mirrors the acceptance corpus in tests/text/examples/ using the +// native test/assert system instead of run.sh + expected output files. + +test "count-substring" { + let x: Int = str_count("abc abc abc", "abc") + assert x == 3 +} + +test "count-overlap-skip" { + let x: Int = str_count("aaa", "aa") + assert x == 1, "non-overlapping count of aa in aaa" +} + +test "count-lines-words-letters" { + let s: String = "hello world\nfoo bar" + let lines: Int = str_count_lines(s) + let words: Int = str_count_words(s) + let letters: Int = str_count_letters(s) + assert lines == 2, "line count" + assert words == 4, "word count" + assert letters == 16, "letter count" +} + +test "index-of-all" { + let positions: [Int] = str_index_of_all("abXcdXefX", "X") + let n: Int = native_list_len(positions) + assert n == 3, "should find 3 occurrences" + let p0: Int = native_list_get(positions, 0) + let p1: Int = native_list_get(positions, 1) + let p2: Int = native_list_get(positions, 2) + assert p0 == 2, "first X at index 2" + assert p1 == 5, "second X at index 5" + assert p2 == 8, "third X at index 8" +} + +test "str-repeat" { + let s: String = str_repeat("ab", 3) + assert s == "ababab", "repeat 3 times" +} + +test "str-reverse" { + let s: String = str_reverse("hello") + assert s == "olleh", "reverse hello" +} + +test "str-strip-prefix" { + let s: String = str_strip_prefix("foobar", "foo") + assert s == "bar", "strip prefix foo" +} + +test "str-strip-suffix" { + let s: String = str_strip_suffix("hello.md", ".md") + assert s == "hello", "strip suffix .md" +} + +test "str-strip-chars" { + let s: String = str_strip_chars(" \thello \n", " \t\n") + assert s == "hello", "strip whitespace chars" +} + +test "split-lines" { + let lines: [String] = str_split_lines("alpha\nbeta\r\ngamma\n") + let n: Int = native_list_len(lines) + assert n == 3, "split into 3 lines" +} + +test "str-join" { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, "alpha") + let parts = native_list_append(parts, "beta") + let parts = native_list_append(parts, "gamma") + let result: String = str_join(parts, ", ") + assert result == "alpha, beta, gamma", "join with separator" +} + +test "char-classes" { + assert is_letter("A"), "A is a letter" + assert is_digit("7"), "7 is a digit" + assert is_whitespace(" "), "space is whitespace" + assert !is_letter("3"), "3 is not a letter" + assert !is_digit("X"), "X is not a digit" +}