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:
Vendored
BIN
Binary file not shown.
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: //
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user