feat: native test/assert system -- elc --test runs test blocks in El

Add test { } and assert to the El language: the parser recognises TestDef
and Assert nodes; the C and JS codegens emit inert no-ops in normal mode
and a full test runner (with RUN/PASS/FAIL output and non-zero exit on
failure) when invoked with --test. compiler.el wires up compile_test /
compile_js_test and exposes --test / --reporter flags in the CLI.

Add two native test suites under tests/native/ (test_text.el,
test_codegen_js.el) covering string primitives, arithmetic, and list
operations. All 22 new native tests pass; the four existing run.sh
acceptance corpora are unaffected.
This commit is contained in:
Will Anderson
2026-05-04 13:24:55 -05:00
parent 32f0cf7b5d
commit 4af2b687e1
8 changed files with 679 additions and 6 deletions
BIN
View File
Binary file not shown.
+171 -3
View File
@@ -948,6 +948,10 @@ fn js_cg_stmt(stmt: Map<String, Any>, 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<String, Any>) -> 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<String, Any>, 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<String, Any>, 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<String, Any>], 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<String, Any>, 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<String, Any>], reporter: String, file_name: String) -> Void {
// Collect TestDef nodes in order.
let n: Int = native_list_len(stmts)
let test_defs: [Map<String, Any>] = 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<String, Any>], 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<String, Any>], source: String, reporter: String, file_name: String) -> String {
codegen_js_inner(stmts, source, false, "", true, reporter, file_name)
}
fn codegen_js_bundle(stmts: [Map<String, Any>], 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<String, Any>], source: String, bundle_mode: Bool, runtime_content: String) -> String {
fn codegen_js_inner(stmts: [Map<String, Any>], 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<String, Any>], 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 {
+209
View File
@@ -1308,6 +1308,13 @@ fn cg_stmt(stmt: Map<String, Any>, 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<String, Any>) -> 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<String, Any>]) -> 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 <name> — <msg>\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<String, Any>, 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<String, Any>, 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<String, Any>], 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<String, Any>, 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<String, Any>], reporter: String, file_name: String) -> Void {
// Collect all TestDef nodes in order.
let n: Int = native_list_len(stmts)
let test_defs: [Map<String, Any>] = 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<String, Any>], 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<String, Any>], source: String, reporter: String, file_name: String) -> String {
codegen_inner(stmts, source, true, reporter, file_name)
}
fn codegen_inner(stmts: [Map<String, Any>], 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<String, Any>], 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 <stdint.h>")
emit_line("#include <stdlib.h>")
emit_line("#include <stdio.h>")
emit_line("#include \"el_runtime.h\"")
// Cross-module forward declarations: for each imported module, emit
@@ -2862,6 +3062,15 @@ fn codegen(stmts: [Map<String, Any>], 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
+86 -1
View File
@@ -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<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = 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<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = 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=<value> 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-<pid>-<timestamp>.<suffix>
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 <source.el> # emit C to stdout
// elc --test <source.el> # emit C test runner to stdout
// elc --target=js <source.el> # emit JS (module) to stdout
// elc --target=js --test <source.el> # emit JS test runner to stdout
// elc --target=js --bundle <source.el> # emit self-contained JS (IIFE) to stdout
// elc --target=js --bundle --minify <source.el> # emit minified IIFE to stdout
// elc --target=js --bundle --obfuscate <source.el> # 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] <source.el> [<output>]")
println("el-compiler: usage: elc [--target=c|js] [--test] [--reporter=text|json] [--bundle] [--minify] [--obfuscate] [--emit-header] <source.el> [<output>]")
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
+11 -2
View File
@@ -98,7 +98,10 @@ fn lex_is_whitespace(ch: String) -> Bool {
}
fn make_tok(kind: String, value: String) -> Map<String, Any> {
{ "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<String, Any>] {
let total: Int = native_list_len(chars)
let tokens: [Map<String, Any>] = 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: //
+38
View File
@@ -27,6 +27,15 @@ fn tok_value(tokens: [Map<String, Any>], 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<String, Any>], 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<String, Any>], pos: Int, kind: String) -> Int {
let k = tok_kind(tokens, pos)
if k == kind {
@@ -1554,6 +1563,35 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
}, p)
}
// assert statement: assert <expr> or assert <expr>, "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`
+80
View File
@@ -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"
}
+84
View File
@@ -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"
}