Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f83adf458 | |||
| 9f734b037c | |||
| 049a7712f4 | |||
| c64cbd21e2 | |||
| 37488e9485 | |||
| 8641b4045e | |||
| 49d68fbb20 | |||
| 77a0658d56 | |||
| bff0ad4f22 | |||
| 49f96126b2 | |||
| c954142063 | |||
| 3fd5fec965 | |||
| 5476cbb2b1 | |||
| 65792f7e4c | |||
| c09023003d | |||
| 15b9ccd9e2 | |||
| 9163af81aa | |||
| 3dababa4ad | |||
| 5888258c9f | |||
| a9dc38ed82 | |||
| 4af2b687e1 |
@@ -1,4 +1,4 @@
|
||||
name: El CI — dev
|
||||
name: El CI -dev
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
@@ -22,20 +23,24 @@ jobs:
|
||||
apt-get install -y gcc libcurl4-openssl-dev
|
||||
|
||||
# Gen2: compile the bootstrap C source into a working elc binary
|
||||
# -Wl,--allow-multiple-definition: is_digit/is_whitespace exist in both
|
||||
# elc-bootstrap.c (pre-dates runtime text primitives) and el_runtime.c.
|
||||
# Both definitions are equivalent; allow the linker to pick one.
|
||||
- name: Build elc from bootstrap (gen2)
|
||||
run: |
|
||||
gcc -O2 \
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-bootstrap.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread \
|
||||
-lcurl -lpthread -lm \
|
||||
-Wl,--allow-multiple-definition \
|
||||
-o dist/elc-gen2
|
||||
chmod +x dist/elc-gen2
|
||||
echo "gen2 elc built"
|
||||
dist/elc-gen2 --version || true
|
||||
|
||||
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
|
||||
- name: Self-host: compile El compiler with gen2 (gen3)
|
||||
- name: Self-host compile El compiler with gen2 (gen3)
|
||||
run: |
|
||||
mkdir -p dist/platform
|
||||
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
|
||||
@@ -43,37 +48,48 @@ jobs:
|
||||
-I el-compiler/runtime \
|
||||
dist/elc-gen3.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
-lcurl -lpthread \
|
||||
-lcurl -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
echo "gen3 (self-hosted) elc built"
|
||||
dist/platform/elc --version || true
|
||||
|
||||
# Run all four test suites — all must pass
|
||||
- name: Run tests — text
|
||||
# Run all four test suites -all must pass
|
||||
- name: Run tests -text
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/text/run.sh
|
||||
|
||||
- name: Run tests — calendar
|
||||
- name: Run tests -calendar
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/calendar/run.sh
|
||||
|
||||
- name: Run tests — time
|
||||
- name: Run tests -time
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/time/run.sh
|
||||
|
||||
- name: Run tests — html_sanitizer
|
||||
- name: Run tests -html_sanitizer
|
||||
run: |
|
||||
ELC="$(pwd)/dist/platform/elc" \
|
||||
EL_HOME="$(pwd)" \
|
||||
bash tests/html_sanitizer/run.sh
|
||||
|
||||
# Native El test suites (elc --test, compile-link-run)
|
||||
- name: Run tests -native (text)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/el-compiler/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o /tmp/el_native_text
|
||||
/tmp/el_native_text
|
||||
|
||||
# Publish artifact to GCP Artifact Registry (dev)
|
||||
- name: Publish elc to Artifact Registry (dev)
|
||||
env:
|
||||
|
||||
Vendored
+2049
-45
File diff suppressed because it is too large
Load Diff
Vendored
BIN
Binary file not shown.
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`
|
||||
|
||||
+2489
-286
File diff suppressed because it is too large
Load Diff
@@ -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,82 @@
|
||||
// test_env.el - native test suite for runtime/env.el
|
||||
//
|
||||
// Covers: env() for reading environment variables, args() returning a list,
|
||||
// state_set/get/del/keys via the env module re-exports, uuid_new/uuid_v4
|
||||
// format validation.
|
||||
|
||||
test "env-missing-returns-empty" {
|
||||
let v: String = env("__EL_NO_SUCH_VAR_XYZ__")
|
||||
assert v == "", "missing env var returns empty string"
|
||||
}
|
||||
|
||||
test "env-path-is-set" {
|
||||
// PATH is expected to be set in virtually any UNIX environment.
|
||||
let v: String = env("PATH")
|
||||
assert str_len(v) > 0, "PATH env var is non-empty"
|
||||
}
|
||||
|
||||
test "env-home-is-set" {
|
||||
// HOME is present on macOS/Linux test environments.
|
||||
let v: String = env("HOME")
|
||||
assert str_len(v) > 0, "HOME env var is non-empty"
|
||||
}
|
||||
|
||||
test "args-returns-list" {
|
||||
let a: [String] = args()
|
||||
// Even with no arguments, the list should be non-null (at minimum the
|
||||
// program name is argv[0]).
|
||||
let n: Int = native_list_len(a)
|
||||
assert n >= 0, "args returns a list (may be empty if runtime strips argv)"
|
||||
}
|
||||
|
||||
test "uuid-new-format" {
|
||||
let id: String = uuid_new()
|
||||
// UUID v4: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx (36 chars)
|
||||
let n: Int = str_len(id)
|
||||
assert n == 36, "uuid_new returns 36-character string"
|
||||
// Check the dashes at correct positions
|
||||
let d1: String = str_char_at(id, 8)
|
||||
let d2: String = str_char_at(id, 13)
|
||||
let d3: String = str_char_at(id, 18)
|
||||
let d4: String = str_char_at(id, 23)
|
||||
assert d1 == "-", "dash at position 8"
|
||||
assert d2 == "-", "dash at position 13"
|
||||
assert d3 == "-", "dash at position 18"
|
||||
assert d4 == "-", "dash at position 23"
|
||||
}
|
||||
|
||||
test "uuid-v4-format" {
|
||||
let id: String = uuid_v4()
|
||||
let n: Int = str_len(id)
|
||||
assert n == 36, "uuid_v4 returns 36-character string"
|
||||
// The version nibble must be '4'
|
||||
let version_char: String = str_char_at(id, 14)
|
||||
assert version_char == "4", "uuid_v4 version nibble is '4'"
|
||||
}
|
||||
|
||||
test "uuid-uniqueness" {
|
||||
let id1: String = uuid_new()
|
||||
let id2: String = uuid_new()
|
||||
assert !str_eq(id1, id2), "two uuid_new calls produce different UUIDs"
|
||||
}
|
||||
|
||||
test "env-state-set-get-via-env-module" {
|
||||
// runtime/env.el re-exports state_set / state_get.
|
||||
state_set("env_test_key", "env_test_val")
|
||||
let v: String = state_get("env_test_key")
|
||||
assert v == "env_test_val", "state_set/get work via env module"
|
||||
state_del("env_test_key")
|
||||
let after: String = state_get("env_test_key")
|
||||
assert after == "", "state_del removes key"
|
||||
}
|
||||
|
||||
test "env-state-keys-json-via-env-module" {
|
||||
state_set("esk_a", "1")
|
||||
state_set("esk_b", "2")
|
||||
let ks: String = state_keys()
|
||||
assert str_starts_with(ks, "["), "state_keys returns JSON array"
|
||||
assert str_contains(ks, "esk_a"), "esk_a in keys"
|
||||
assert str_contains(ks, "esk_b"), "esk_b in keys"
|
||||
state_del("esk_a")
|
||||
state_del("esk_b")
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// test_fs.el - native test suite for runtime/fs.el
|
||||
//
|
||||
// Covers: fs_write/read round-trip, fs_exists, fs_mkdir, fs_list,
|
||||
// fs_list_json, and edge cases (empty file, overwrite, non-existent path).
|
||||
|
||||
test "fs-write-and-read" {
|
||||
let path: String = "/tmp/el_test_fs_basic.txt"
|
||||
let content: String = "hello from El"
|
||||
let ok: Bool = fs_write(path, content)
|
||||
assert ok, "fs_write returns true on success"
|
||||
let got: String = fs_read(path)
|
||||
assert got == content, "fs_read returns what was written"
|
||||
}
|
||||
|
||||
test "fs-exists" {
|
||||
let path: String = "/tmp/el_test_fs_exists.txt"
|
||||
fs_write(path, "exists check")
|
||||
let exists: Bool = fs_exists(path)
|
||||
assert exists, "fs_exists returns true for created file"
|
||||
let missing: Bool = fs_exists("/tmp/__el_no_such_file_xyz__.txt")
|
||||
assert !missing, "fs_exists returns false for non-existent file"
|
||||
}
|
||||
|
||||
test "fs-read-nonexistent" {
|
||||
let got: String = fs_read("/tmp/__el_no_such_file_abc__.txt")
|
||||
assert got == "", "reading nonexistent file returns empty string"
|
||||
}
|
||||
|
||||
test "fs-write-overwrite" {
|
||||
let path: String = "/tmp/el_test_fs_overwrite.txt"
|
||||
fs_write(path, "original content")
|
||||
fs_write(path, "new content")
|
||||
let got: String = fs_read(path)
|
||||
assert got == "new content", "second write overwrites first"
|
||||
}
|
||||
|
||||
test "fs-write-empty-file" {
|
||||
let path: String = "/tmp/el_test_fs_empty.txt"
|
||||
let ok: Bool = fs_write(path, "")
|
||||
assert ok, "writing empty file succeeds"
|
||||
let got: String = fs_read(path)
|
||||
assert got == "", "reading empty file returns empty string"
|
||||
let exists: Bool = fs_exists(path)
|
||||
assert exists, "empty file still exists"
|
||||
}
|
||||
|
||||
test "fs-write-multiline" {
|
||||
let path: String = "/tmp/el_test_fs_multiline.txt"
|
||||
let content: String = "line one\nline two\nline three"
|
||||
fs_write(path, content)
|
||||
let got: String = fs_read(path)
|
||||
assert got == content, "multiline content round-trips"
|
||||
let lines: [String] = str_split_lines(got)
|
||||
let n: Int = native_list_len(lines)
|
||||
assert n == 3, "three lines after read"
|
||||
}
|
||||
|
||||
test "fs-mkdir" {
|
||||
let dir: String = "/tmp/el_test_fs_mkdir_dir"
|
||||
let ok: Bool = fs_mkdir(dir)
|
||||
assert ok, "fs_mkdir returns true"
|
||||
let exists: Bool = fs_exists(dir)
|
||||
assert exists, "created directory exists"
|
||||
}
|
||||
|
||||
test "fs-list" {
|
||||
let dir: String = "/tmp/el_test_fs_list_dir"
|
||||
fs_mkdir(dir)
|
||||
fs_write(dir + "/a.txt", "a")
|
||||
fs_write(dir + "/b.txt", "b")
|
||||
let files: [String] = fs_list(dir)
|
||||
// Filter out empty strings from trailing newline
|
||||
let n: Int = native_list_len(files)
|
||||
// We expect at least 2 entries (a.txt and b.txt)
|
||||
assert n >= 2, "fs_list returns at least 2 entries"
|
||||
}
|
||||
|
||||
test "fs-list-json" {
|
||||
let dir: String = "/tmp/el_test_fs_listjson_dir"
|
||||
fs_mkdir(dir)
|
||||
fs_write(dir + "/x.txt", "x")
|
||||
fs_write(dir + "/y.txt", "y")
|
||||
let result: String = fs_list_json(dir)
|
||||
assert str_starts_with(result, "["), "fs_list_json returns JSON array"
|
||||
assert str_ends_with(result, "]"), "fs_list_json JSON array is closed"
|
||||
// Should contain at least one filename
|
||||
assert str_len(result) > 2, "fs_list_json result is non-empty array"
|
||||
}
|
||||
|
||||
test "fs-write-and-read-special-chars" {
|
||||
let path: String = "/tmp/el_test_fs_special.txt"
|
||||
let content: String = "tabs:\there\nnewlines: ok\n\"quoted\""
|
||||
fs_write(path, content)
|
||||
let got: String = fs_read(path)
|
||||
assert got == content, "special chars in content round-trip"
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// test_json.el - native test suite for runtime/json.el
|
||||
//
|
||||
// Covers: json_get (dot-path), typed extractors (int, bool, float),
|
||||
// json_array_len/get, json_set, json_build_object, json_build_array,
|
||||
// and json_escape_string.
|
||||
|
||||
test "json-get-string" {
|
||||
let obj: String = "{\"name\":\"alice\",\"role\":\"admin\"}"
|
||||
let name: String = json_get(obj, "name")
|
||||
assert name == "alice", "get string field name"
|
||||
let role: String = json_get(obj, "role")
|
||||
assert role == "admin", "get string field role"
|
||||
let missing: String = json_get(obj, "xyz")
|
||||
assert missing == "", "missing key returns empty string"
|
||||
}
|
||||
|
||||
test "json-get-int" {
|
||||
let obj: String = "{\"count\":42,\"offset\":0}"
|
||||
let count: Int = json_get_int(obj, "count")
|
||||
assert count == 42, "get int field count"
|
||||
let offset: Int = json_get_int(obj, "offset")
|
||||
assert offset == 0, "get int field offset = 0"
|
||||
}
|
||||
|
||||
test "json-get-bool" {
|
||||
let obj: String = "{\"active\":true,\"deleted\":false}"
|
||||
let active: Bool = json_get_bool(obj, "active")
|
||||
assert active, "get bool true"
|
||||
let deleted: Bool = json_get_bool(obj, "deleted")
|
||||
assert !deleted, "get bool false"
|
||||
}
|
||||
|
||||
test "json-dot-path" {
|
||||
let obj: String = "{\"user\":{\"name\":\"bob\",\"age\":30}}"
|
||||
let name: String = json_get(obj, "user.name")
|
||||
assert name == "bob", "dot-path traversal user.name"
|
||||
let age: String = json_get(obj, "user.age")
|
||||
assert age == "30", "dot-path traversal user.age"
|
||||
}
|
||||
|
||||
test "json-array-len" {
|
||||
let arr: String = "[\"a\",\"b\",\"c\"]"
|
||||
let n: Int = json_array_len(arr)
|
||||
assert n == 3, "array length 3"
|
||||
let empty_arr: String = "[]"
|
||||
let m: Int = json_array_len(empty_arr)
|
||||
assert m == 0, "empty array length 0"
|
||||
}
|
||||
|
||||
test "json-array-get" {
|
||||
let arr: String = "[\"foo\",\"bar\",\"baz\"]"
|
||||
let first: String = json_array_get_string(arr, 0)
|
||||
assert first == "foo", "first element"
|
||||
let second: String = json_array_get_string(arr, 1)
|
||||
assert second == "bar", "second element"
|
||||
let third: String = json_array_get_string(arr, 2)
|
||||
assert third == "baz", "third element"
|
||||
}
|
||||
|
||||
test "json-get-raw" {
|
||||
let obj: String = "{\"items\":[1,2,3],\"meta\":{\"page\":1}}"
|
||||
let raw_arr: String = json_get_raw(obj, "items")
|
||||
let arr_len: Int = json_array_len(raw_arr)
|
||||
assert arr_len == 3, "raw array has 3 elements"
|
||||
let raw_meta: String = json_get_raw(obj, "meta")
|
||||
let page: String = json_get(raw_meta, "page")
|
||||
assert page == "1", "page from nested raw object"
|
||||
}
|
||||
|
||||
test "json-set" {
|
||||
let obj: String = "{\"name\":\"alice\"}"
|
||||
let updated: String = json_set(obj, "name", "\"bob\"")
|
||||
let name: String = json_get(updated, "name")
|
||||
assert name == "bob", "set replaces existing key"
|
||||
}
|
||||
|
||||
test "json-build-object" {
|
||||
let kvs: [String] = native_list_empty()
|
||||
let kvs = native_list_append(kvs, "name")
|
||||
let kvs = native_list_append(kvs, "alice")
|
||||
let kvs = native_list_append(kvs, "role")
|
||||
let kvs = native_list_append(kvs, "admin")
|
||||
let obj: String = json_build_object(kvs)
|
||||
let name: String = json_get(obj, "name")
|
||||
let role: String = json_get(obj, "role")
|
||||
assert name == "alice", "built object name field"
|
||||
assert role == "admin", "built object role field"
|
||||
}
|
||||
|
||||
test "json-build-array" {
|
||||
let items: [String] = native_list_empty()
|
||||
let items = native_list_append(items, "\"alpha\"")
|
||||
let items = native_list_append(items, "\"beta\"")
|
||||
let items = native_list_append(items, "\"gamma\"")
|
||||
let arr: String = json_build_array(items)
|
||||
let n: Int = json_array_len(arr)
|
||||
assert n == 3, "built array has 3 elements"
|
||||
let first: String = json_array_get_string(arr, 0)
|
||||
assert first == "alpha", "first built element"
|
||||
let third: String = json_array_get_string(arr, 2)
|
||||
assert third == "gamma", "third built element"
|
||||
}
|
||||
|
||||
test "json-escape-string" {
|
||||
let escaped: String = json_escape_string("say \"hello\"")
|
||||
assert str_contains(escaped, "\\\""), "double quotes are escaped"
|
||||
let tab_esc: String = json_escape_string("a\tb")
|
||||
assert str_contains(tab_esc, "\\t"), "tabs are escaped"
|
||||
let plain: String = json_escape_string("no special chars")
|
||||
assert plain == "no special chars", "plain string unchanged"
|
||||
}
|
||||
|
||||
test "json-get-float" {
|
||||
let obj: String = "{\"price\":9.99,\"tax\":0.0}"
|
||||
let price: Float = json_get_float(obj, "price")
|
||||
let diff: Float = price - 9.99
|
||||
assert diff > -0.001, "price close to 9.99 low"
|
||||
assert diff < 0.001, "price close to 9.99 high"
|
||||
let tax: Float = json_get_float(obj, "tax")
|
||||
assert tax == 0.0, "tax is zero"
|
||||
}
|
||||
|
||||
test "json-nested-array-index" {
|
||||
let obj: String = "{\"tags\":[\"go\",\"el\",\"rust\"]}"
|
||||
let tags_raw: String = json_get_raw(obj, "tags")
|
||||
let count: Int = json_array_len(tags_raw)
|
||||
assert count == 3, "tags array has 3 elements"
|
||||
let first_tag: String = json_array_get_string(tags_raw, 0)
|
||||
assert first_tag == "go", "first tag is go"
|
||||
let last_tag: String = json_array_get_string(tags_raw, 2)
|
||||
assert last_tag == "rust", "last tag is rust"
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// test_math.el - native test suite for runtime/math.el
|
||||
//
|
||||
// Covers: integer math (abs, max, min), float math (sqrt, log, sin, cos, pi),
|
||||
// float conversions, format_float, and decimal_round.
|
||||
|
||||
test "el-abs" {
|
||||
let pos: Int = el_abs(5)
|
||||
assert pos == 5, "abs of positive"
|
||||
let neg: Int = el_abs(-5)
|
||||
assert neg == 5, "abs of negative"
|
||||
let zero: Int = el_abs(0)
|
||||
assert zero == 0, "abs of zero"
|
||||
let large: Int = el_abs(-1000000)
|
||||
assert large == 1000000, "abs of large negative"
|
||||
}
|
||||
|
||||
test "el-max" {
|
||||
let m: Int = el_max(3, 7)
|
||||
assert m == 7, "max of 3 and 7"
|
||||
let m2: Int = el_max(7, 3)
|
||||
assert m2 == 7, "max is commutative"
|
||||
let same: Int = el_max(5, 5)
|
||||
assert same == 5, "max of equal values"
|
||||
let neg: Int = el_max(-3, -7)
|
||||
assert neg == -3, "max of two negatives"
|
||||
}
|
||||
|
||||
test "el-min" {
|
||||
let m: Int = el_min(3, 7)
|
||||
assert m == 3, "min of 3 and 7"
|
||||
let m2: Int = el_min(7, 3)
|
||||
assert m2 == 3, "min is commutative"
|
||||
let same: Int = el_min(5, 5)
|
||||
assert same == 5, "min of equal values"
|
||||
let neg: Int = el_min(-3, -7)
|
||||
assert neg == -7, "min of two negatives"
|
||||
}
|
||||
|
||||
test "math-pi" {
|
||||
let pi: Float = math_pi()
|
||||
// pi ~ 3.14159265358979
|
||||
// Check it's between 3.141 and 3.142
|
||||
let too_low: Float = pi - 3.141
|
||||
let too_high: Float = 3.142 - pi
|
||||
assert too_low > 0.0, "pi > 3.141"
|
||||
assert too_high > 0.0, "pi < 3.142"
|
||||
}
|
||||
|
||||
test "math-sqrt" {
|
||||
let r4: Float = math_sqrt(4.0)
|
||||
let diff4: Float = r4 - 2.0
|
||||
assert diff4 == 0.0, "sqrt(4) == 2.0"
|
||||
let r9: Float = math_sqrt(9.0)
|
||||
let diff9: Float = r9 - 3.0
|
||||
assert diff9 == 0.0, "sqrt(9) == 3.0"
|
||||
let r1: Float = math_sqrt(1.0)
|
||||
let diff1: Float = r1 - 1.0
|
||||
assert diff1 == 0.0, "sqrt(1) == 1.0"
|
||||
let r0: Float = math_sqrt(0.0)
|
||||
assert r0 == 0.0, "sqrt(0) == 0.0"
|
||||
}
|
||||
|
||||
test "math-sin-cos" {
|
||||
// sin(0) == 0, cos(0) == 1
|
||||
let s0: Float = math_sin(0.0)
|
||||
assert s0 == 0.0, "sin(0) == 0.0"
|
||||
let c0: Float = math_cos(0.0)
|
||||
assert c0 == 1.0, "cos(0) == 1.0"
|
||||
// sin(pi/2) ~ 1.0, cos(pi/2) ~ 0.0
|
||||
let half_pi: Float = math_pi() / 2.0
|
||||
let s_half: Float = math_sin(half_pi)
|
||||
// Check within 0.000001 of 1.0
|
||||
let diff_s: Float = s_half - 1.0
|
||||
assert diff_s > -0.000001, "sin(pi/2) close to 1.0 low"
|
||||
assert diff_s < 0.000001, "sin(pi/2) close to 1.0 high"
|
||||
}
|
||||
|
||||
test "int-to-float-and-back" {
|
||||
let f: Float = int_to_float(42)
|
||||
let back: Int = float_to_int(f)
|
||||
assert back == 42, "42 round-trips int->float->int"
|
||||
let neg: Float = int_to_float(-7)
|
||||
let neg_back: Int = float_to_int(neg)
|
||||
assert neg_back == -7, "-7 round-trips"
|
||||
let zero: Float = int_to_float(0)
|
||||
let zero_back: Int = float_to_int(zero)
|
||||
assert zero_back == 0, "0 round-trips"
|
||||
}
|
||||
|
||||
test "float-to-int-truncates" {
|
||||
let t1: Int = float_to_int(3.9)
|
||||
assert t1 == 3, "3.9 truncates to 3"
|
||||
let t2: Int = float_to_int(3.1)
|
||||
assert t2 == 3, "3.1 truncates to 3"
|
||||
let t3: Int = float_to_int(-3.7)
|
||||
assert t3 == -3, "-3.7 truncates toward zero to -3"
|
||||
}
|
||||
|
||||
test "str-to-float-and-back" {
|
||||
let f: Float = str_to_float("3.14")
|
||||
// Check it's between 3.13 and 3.15
|
||||
let lo: Float = f - 3.13
|
||||
let hi: Float = 3.15 - f
|
||||
assert lo > 0.0, "3.14 parsed > 3.13"
|
||||
assert hi > 0.0, "3.14 parsed < 3.15"
|
||||
let zero: Float = str_to_float("0.0")
|
||||
assert zero == 0.0, "parse 0.0"
|
||||
}
|
||||
|
||||
test "format-float" {
|
||||
let s0: String = format_float(3.14159, 2)
|
||||
assert s0 == "3.14", "format to 2 decimals"
|
||||
let s1: String = format_float(1.0, 0)
|
||||
assert s1 == "1", "format to 0 decimals"
|
||||
let s2: String = format_float(0.0, 3)
|
||||
assert s2 == "0.000", "format zero to 3 decimals"
|
||||
let s3: String = format_float(-2.5, 1)
|
||||
assert s3 == "-2.5", "format negative to 1 decimal"
|
||||
}
|
||||
|
||||
test "decimal-round" {
|
||||
let r0: Float = decimal_round(2.5, 0)
|
||||
assert r0 == 3.0, "round 2.5 to 0 places"
|
||||
let r1: Float = decimal_round(2.45, 1)
|
||||
assert r1 == 2.5, "round 2.45 to 1 place"
|
||||
let neg: Float = decimal_round(-2.5, 0)
|
||||
assert neg == -3.0, "round -2.5 to 0 places (half-away-from-zero)"
|
||||
let exact: Float = decimal_round(1.0, 2)
|
||||
assert exact == 1.0, "rounding exact value unchanged"
|
||||
}
|
||||
|
||||
test "math-log" {
|
||||
// log10(100) == 2
|
||||
let l100: Float = math_log(100.0)
|
||||
let diff: Float = l100 - 2.0
|
||||
assert diff > -0.000001, "log10(100) close to 2 low"
|
||||
assert diff < 0.000001, "log10(100) close to 2 high"
|
||||
// log10(1) == 0
|
||||
let l1: Float = math_log(1.0)
|
||||
assert l1 == 0.0, "log10(1) == 0"
|
||||
}
|
||||
|
||||
test "math-ln" {
|
||||
// ln(1) == 0
|
||||
let l1: Float = math_ln(1.0)
|
||||
assert l1 == 0.0, "ln(1) == 0"
|
||||
// ln(e) ~ 1.0 — e ~ 2.71828
|
||||
let e: Float = 2.718281828
|
||||
let le: Float = math_ln(e)
|
||||
let diff: Float = le - 1.0
|
||||
assert diff > -0.000001, "ln(e) close to 1 low"
|
||||
assert diff < 0.000001, "ln(e) close to 1 high"
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// test_state.el - native test suite for runtime/state.el
|
||||
//
|
||||
// Covers: state_set/get/del, state_has, state_get_or, state_keys,
|
||||
// and edge cases such as empty values, overwrite, and multiple keys.
|
||||
|
||||
test "state-set-get-basic" {
|
||||
state_set("test_key", "hello")
|
||||
let v: String = state_get("test_key")
|
||||
assert v == "hello", "get returns set value"
|
||||
}
|
||||
|
||||
test "state-get-missing" {
|
||||
let v: String = state_get("__nonexistent_key_xyz__")
|
||||
assert v == "", "missing key returns empty string"
|
||||
}
|
||||
|
||||
test "state-overwrite" {
|
||||
state_set("ow_key", "first")
|
||||
state_set("ow_key", "second")
|
||||
let v: String = state_get("ow_key")
|
||||
assert v == "second", "second write overwrites first"
|
||||
}
|
||||
|
||||
test "state-del" {
|
||||
state_set("del_key", "to be deleted")
|
||||
state_del("del_key")
|
||||
let v: String = state_get("del_key")
|
||||
assert v == "", "deleted key returns empty string"
|
||||
}
|
||||
|
||||
test "state-del-nonexistent" {
|
||||
// Should not panic or error on deleting a non-existent key.
|
||||
state_del("__never_set_key__")
|
||||
let v: String = state_get("__never_set_key__")
|
||||
assert v == "", "del of nonexistent key is a no-op"
|
||||
}
|
||||
|
||||
test "state-has" {
|
||||
state_set("has_key", "value")
|
||||
assert state_has("has_key"), "has returns true for set key"
|
||||
assert !state_has("__no_has_key__"), "has returns false for absent key"
|
||||
state_del("has_key")
|
||||
assert !state_has("has_key"), "has returns false after del"
|
||||
}
|
||||
|
||||
test "state-get-or" {
|
||||
state_set("gor_key", "actual")
|
||||
let v1: String = state_get_or("gor_key", "default")
|
||||
assert v1 == "actual", "get_or returns value when key set"
|
||||
let v2: String = state_get_or("__absent_gor_key__", "fallback")
|
||||
assert v2 == "fallback", "get_or returns default when key absent"
|
||||
}
|
||||
|
||||
test "state-multiple-keys" {
|
||||
state_set("mk_a", "alpha")
|
||||
state_set("mk_b", "beta")
|
||||
state_set("mk_c", "gamma")
|
||||
let a: String = state_get("mk_a")
|
||||
let b: String = state_get("mk_b")
|
||||
let c: String = state_get("mk_c")
|
||||
assert a == "alpha", "key a correct"
|
||||
assert b == "beta", "key b correct"
|
||||
assert c == "gamma", "key c correct"
|
||||
state_del("mk_a")
|
||||
state_del("mk_b")
|
||||
state_del("mk_c")
|
||||
}
|
||||
|
||||
test "state-keys-returns-json-array" {
|
||||
state_set("keys_test_1", "v1")
|
||||
state_set("keys_test_2", "v2")
|
||||
let ks: String = state_keys()
|
||||
// The result is a JSON array string like ["keys_test_1","keys_test_2",...]
|
||||
assert str_starts_with(ks, "["), "state_keys returns JSON array"
|
||||
assert str_ends_with(ks, "]"), "state_keys JSON array is closed"
|
||||
assert str_contains(ks, "keys_test_1"), "keys array contains keys_test_1"
|
||||
assert str_contains(ks, "keys_test_2"), "keys array contains keys_test_2"
|
||||
state_del("keys_test_1")
|
||||
state_del("keys_test_2")
|
||||
}
|
||||
|
||||
test "state-numeric-value-as-string" {
|
||||
state_set("num_key", "42")
|
||||
let v: String = state_get("num_key")
|
||||
let n: Int = str_to_int(v)
|
||||
assert n == 42, "stored numeric string round-trips to int"
|
||||
state_del("num_key")
|
||||
}
|
||||
|
||||
test "state-long-value" {
|
||||
let long_val: String = str_repeat("abcdefghij", 100)
|
||||
state_set("long_val_key", long_val)
|
||||
let got: String = state_get("long_val_key")
|
||||
assert got == long_val, "long value round-trips correctly"
|
||||
let got_len: Int = str_len(got)
|
||||
assert got_len == 1000, "long value is 1000 bytes"
|
||||
state_del("long_val_key")
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
// test_string.el - native test suite for runtime/string.el
|
||||
//
|
||||
// Covers: type conversions, core primitives, comparison and search,
|
||||
// case conversion, whitespace trimming, replacement, repetition,
|
||||
// reversal, prefix/suffix stripping, padding, counting, character
|
||||
// classification, splitting, and joining.
|
||||
|
||||
test "int-to-str" {
|
||||
let s: String = int_to_str(42)
|
||||
assert s == "42", "int 42 to string"
|
||||
let neg: String = int_to_str(-7)
|
||||
assert neg == "-7", "negative int to string"
|
||||
let zero: String = int_to_str(0)
|
||||
assert zero == "0", "zero to string"
|
||||
}
|
||||
|
||||
test "str-to-int" {
|
||||
let n: Int = str_to_int("123")
|
||||
assert n == 123, "parse 123"
|
||||
let neg: Int = str_to_int("-5")
|
||||
assert neg == -5, "parse negative"
|
||||
let zero: Int = str_to_int("0")
|
||||
assert zero == 0, "parse zero"
|
||||
}
|
||||
|
||||
test "bool-to-str" {
|
||||
let t: String = bool_to_str(true)
|
||||
assert t == "true", "true to string"
|
||||
let f: String = bool_to_str(false)
|
||||
assert f == "false", "false to string"
|
||||
}
|
||||
|
||||
test "str-len" {
|
||||
let n: Int = str_len("hello")
|
||||
assert n == 5, "length of hello"
|
||||
let e: Int = str_len("")
|
||||
assert e == 0, "length of empty string"
|
||||
let space: Int = str_len("a b")
|
||||
assert space == 3, "length with spaces"
|
||||
}
|
||||
|
||||
test "str-eq" {
|
||||
assert str_eq("abc", "abc"), "identical strings are equal"
|
||||
assert !str_eq("abc", "ABC"), "case-sensitive comparison"
|
||||
assert str_eq("", ""), "empty strings are equal"
|
||||
assert !str_eq("a", "b"), "different single chars"
|
||||
}
|
||||
|
||||
test "str-slice" {
|
||||
let s: String = str_slice("hello world", 6, 11)
|
||||
assert s == "world", "slice end of string"
|
||||
let start: String = str_slice("hello world", 0, 5)
|
||||
assert start == "hello", "slice start of string"
|
||||
let empty: String = str_slice("hello", 2, 2)
|
||||
assert empty == "", "zero-length slice"
|
||||
}
|
||||
|
||||
test "str-starts-ends-with" {
|
||||
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"
|
||||
assert !str_ends_with("hello world", "hello"), "does not end with hello"
|
||||
assert str_starts_with("abc", ""), "empty prefix always matches"
|
||||
assert str_ends_with("abc", ""), "empty suffix always matches"
|
||||
}
|
||||
|
||||
test "str-contains" {
|
||||
assert str_contains("hello world", "world"), "contains world"
|
||||
assert str_contains("hello world", "lo wo"), "contains interior substring"
|
||||
assert !str_contains("hello world", "xyz"), "does not contain xyz"
|
||||
assert str_contains("abc", ""), "empty sub is always contained"
|
||||
assert !str_contains("", "x"), "empty string does not contain nonempty sub"
|
||||
}
|
||||
|
||||
test "str-index-of" {
|
||||
let i: Int = str_index_of("hello world", "world")
|
||||
assert i == 6, "index of world"
|
||||
let j: Int = str_index_of("hello world", "xyz")
|
||||
assert j == -1, "not found returns -1"
|
||||
let k: Int = str_index_of("aabbcc", "bb")
|
||||
assert k == 2, "index of bb in aabbcc"
|
||||
}
|
||||
|
||||
test "str-last-index-of" {
|
||||
let i: Int = str_last_index_of("abcabc", "bc")
|
||||
assert i == 4, "last occurrence of bc"
|
||||
let j: Int = str_last_index_of("hello", "xyz")
|
||||
assert j == -1, "not found returns -1"
|
||||
let k: Int = str_last_index_of("aaa", "a")
|
||||
assert k == 2, "last single-char match"
|
||||
}
|
||||
|
||||
test "str-to-upper-lower" {
|
||||
let up: String = str_to_upper("hello")
|
||||
assert up == "HELLO", "to upper"
|
||||
let lo: String = str_to_lower("WORLD")
|
||||
assert lo == "world", "to lower"
|
||||
let mixed: String = str_to_upper("Hello World")
|
||||
assert mixed == "HELLO WORLD", "mixed to upper"
|
||||
let empty: String = str_to_lower("")
|
||||
assert empty == "", "empty stays empty"
|
||||
}
|
||||
|
||||
test "str-trim" {
|
||||
let s: String = str_trim(" hello ")
|
||||
assert s == "hello", "trim both ends"
|
||||
let lonly: String = str_trim(" hello")
|
||||
assert lonly == "hello", "trim left only"
|
||||
let ronly: String = str_trim("hello ")
|
||||
assert ronly == "hello", "trim right only"
|
||||
let tabs: String = str_trim("\thello\n")
|
||||
assert tabs == "hello", "trim tabs and newlines"
|
||||
let empty: String = str_trim(" ")
|
||||
assert empty == "", "all whitespace trims to empty"
|
||||
}
|
||||
|
||||
test "str-replace" {
|
||||
let s: String = str_replace("hello world", "world", "there")
|
||||
assert s == "hello there", "replace word"
|
||||
let none: String = str_replace("hello", "xyz", "abc")
|
||||
assert none == "hello", "no match leaves string unchanged"
|
||||
let multi: String = str_replace("aaa", "a", "b")
|
||||
assert multi == "bbb", "replace all occurrences"
|
||||
let empty_from: String = str_replace("hello", "", "x")
|
||||
assert empty_from == "hello", "empty from returns original"
|
||||
}
|
||||
|
||||
test "str-repeat" {
|
||||
let s: String = str_repeat("ab", 3)
|
||||
assert s == "ababab", "repeat 3 times"
|
||||
let once: String = str_repeat("x", 1)
|
||||
assert once == "x", "repeat once"
|
||||
let zero: String = str_repeat("abc", 0)
|
||||
assert zero == "", "repeat zero times"
|
||||
let neg: String = str_repeat("abc", -1)
|
||||
assert neg == "", "negative repeat is empty"
|
||||
}
|
||||
|
||||
test "str-reverse" {
|
||||
let s: String = str_reverse("hello")
|
||||
assert s == "olleh", "reverse hello"
|
||||
let single: String = str_reverse("a")
|
||||
assert single == "a", "reverse single char"
|
||||
let empty: String = str_reverse("")
|
||||
assert empty == "", "reverse empty string"
|
||||
let palindrome: String = str_reverse("racecar")
|
||||
assert palindrome == "racecar", "reverse palindrome"
|
||||
}
|
||||
|
||||
test "str-strip-prefix-suffix" {
|
||||
let p: String = str_strip_prefix("foobar", "foo")
|
||||
assert p == "bar", "strip prefix foo"
|
||||
let no_prefix: String = str_strip_prefix("foobar", "baz")
|
||||
assert no_prefix == "foobar", "no match leaves string unchanged"
|
||||
let s: String = str_strip_suffix("hello.md", ".md")
|
||||
assert s == "hello", "strip suffix .md"
|
||||
let no_suffix: String = str_strip_suffix("hello.md", ".txt")
|
||||
assert no_suffix == "hello.md", "non-matching suffix unchanged"
|
||||
}
|
||||
|
||||
test "str-strip-chars" {
|
||||
let s: String = str_strip_chars(" \thello \n", " \t\n")
|
||||
assert s == "hello", "strip whitespace chars"
|
||||
let dots: String = str_strip_chars("...hello...", ".")
|
||||
assert dots == "hello", "strip dot chars"
|
||||
let all: String = str_strip_chars("aaa", "a")
|
||||
assert all == "", "strip all chars leaves empty"
|
||||
}
|
||||
|
||||
test "str-pad-left-right" {
|
||||
let l: String = str_pad_left("42", 5, "0")
|
||||
assert l == "00042", "zero-pad left to width 5"
|
||||
let r: String = str_pad_right("hi", 5, "-")
|
||||
assert r == "hi---", "dash-pad right to width 5"
|
||||
let no_pad: String = str_pad_left("hello", 3, "x")
|
||||
assert no_pad == "hello", "no pad when string already wide enough"
|
||||
}
|
||||
|
||||
test "str-count" {
|
||||
let n: Int = str_count("abc abc abc", "abc")
|
||||
assert n == 3, "count three occurrences"
|
||||
let overlap: Int = str_count("aaa", "aa")
|
||||
assert overlap == 1, "non-overlapping count"
|
||||
let zero: Int = str_count("hello", "xyz")
|
||||
assert zero == 0, "not found gives 0"
|
||||
let empty_sub: Int = str_count("hello", "")
|
||||
assert empty_sub == 0, "empty sub gives 0"
|
||||
}
|
||||
|
||||
test "str-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 "str-count-chars-and-digits" {
|
||||
let digits: Int = str_count_digits("abc123def456")
|
||||
assert digits == 6, "six digits"
|
||||
let none: Int = str_count_digits("hello")
|
||||
assert none == 0, "no digits"
|
||||
let chars: Int = str_count_chars("hello")
|
||||
assert chars == 5, "five ASCII chars"
|
||||
}
|
||||
|
||||
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"
|
||||
assert is_alphanumeric("abc123"), "abc123 is alphanumeric"
|
||||
assert !is_alphanumeric("abc!"), "abc! is not alphanumeric"
|
||||
assert is_uppercase("ABC"), "ABC is uppercase"
|
||||
assert is_lowercase("abc"), "abc is lowercase"
|
||||
assert !is_uppercase("Abc"), "mixed is not uppercase"
|
||||
}
|
||||
|
||||
test "str-split" {
|
||||
let parts: [String] = str_split("a,b,c", ",")
|
||||
let n: Int = native_list_len(parts)
|
||||
assert n == 3, "split into 3 parts"
|
||||
let p0: String = native_list_get(parts, 0)
|
||||
let p1: String = native_list_get(parts, 1)
|
||||
let p2: String = native_list_get(parts, 2)
|
||||
assert p0 == "a", "first part"
|
||||
assert p1 == "b", "second part"
|
||||
assert p2 == "c", "third part"
|
||||
}
|
||||
|
||||
test "str-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"
|
||||
let l0: String = native_list_get(lines, 0)
|
||||
let l1: String = native_list_get(lines, 1)
|
||||
let l2: String = native_list_get(lines, 2)
|
||||
assert l0 == "alpha", "first line"
|
||||
assert l1 == "beta", "second line strips CR"
|
||||
assert l2 == "gamma", "third line"
|
||||
}
|
||||
|
||||
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"
|
||||
let empty_parts: [String] = native_list_empty()
|
||||
let empty_result: String = str_join(empty_parts, ",")
|
||||
assert empty_result == "", "join empty list gives empty string"
|
||||
}
|
||||
|
||||
test "str-char-at" {
|
||||
let c: String = str_char_at("hello", 1)
|
||||
assert c == "e", "char at index 1"
|
||||
let first: String = str_char_at("abc", 0)
|
||||
assert first == "a", "first char"
|
||||
let oob: String = str_char_at("abc", 10)
|
||||
assert oob == "", "out of bounds returns empty"
|
||||
}
|
||||
|
||||
test "url-encode-decode" {
|
||||
let encoded: String = url_encode("hello world")
|
||||
assert !str_contains(encoded, " "), "space is encoded"
|
||||
let decoded: String = url_decode(encoded)
|
||||
assert decoded == "hello world", "round-trip decode"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// test_time.el - native test suite for runtime/time.el
|
||||
//
|
||||
// Covers: time_now (positive timestamp), time_to_parts (UTC decomposition),
|
||||
// time_format (ISO 8601 and strftime tokens), time_add/diff, unix_timestamp,
|
||||
// duration helpers, and instant conversions.
|
||||
|
||||
test "time-now-positive" {
|
||||
let ts: Int = time_now()
|
||||
// time_now() returns milliseconds since epoch.
|
||||
// Any value greater than 1_700_000_000_000 (Nov 2023) is valid in 2025+.
|
||||
assert ts > 1700000000000, "time_now returns a reasonable recent timestamp"
|
||||
}
|
||||
|
||||
test "unix-timestamp-positive" {
|
||||
let s: Int = unix_timestamp()
|
||||
// Should be greater than 1_700_000_000 (seconds, Nov 2023)
|
||||
assert s > 1700000000, "unix_timestamp returns seconds > 1700000000"
|
||||
}
|
||||
|
||||
test "now-ns-positive" {
|
||||
let ns: Int = now_ns()
|
||||
// Should be a large nanosecond value
|
||||
assert ns > 0, "now_ns returns positive value"
|
||||
}
|
||||
|
||||
test "time-to-parts-epoch" {
|
||||
// Unix epoch: 0 ms = 1970-01-01T00:00:00.000Z
|
||||
let parts: String = time_to_parts(0)
|
||||
let year: String = json_get(parts, "year")
|
||||
let month: String = json_get(parts, "month")
|
||||
let day: String = json_get(parts, "day")
|
||||
let hour: String = json_get(parts, "hour")
|
||||
assert year == "1970", "epoch year is 1970"
|
||||
assert month == "1", "epoch month is 1"
|
||||
assert day == "1", "epoch day is 1"
|
||||
assert hour == "0", "epoch hour is 0"
|
||||
}
|
||||
|
||||
test "time-to-parts-known-date" {
|
||||
// 2024-03-15T12:30:45.000Z
|
||||
// seconds = 2024-03-15 12:30:45 UTC
|
||||
// epoch ms: use a known value
|
||||
// 2024-03-15 00:00:00 UTC = 1710460800 seconds
|
||||
// + 12*3600 + 30*60 + 45 = 43200 + 1800 + 45 = 45045 seconds
|
||||
// total: 1710505845 seconds = 1710505845000 ms
|
||||
let ts: Int = 1710505845000
|
||||
let parts: String = time_to_parts(ts)
|
||||
let year: String = json_get(parts, "year")
|
||||
let month: String = json_get(parts, "month")
|
||||
let day: String = json_get(parts, "day")
|
||||
let hour: String = json_get(parts, "hour")
|
||||
let minute: String = json_get(parts, "minute")
|
||||
let second: String = json_get(parts, "second")
|
||||
assert year == "2024", "year is 2024"
|
||||
assert month == "3", "month is 3 (March)"
|
||||
assert day == "15", "day is 15"
|
||||
assert hour == "12", "hour is 12"
|
||||
assert minute == "30", "minute is 30"
|
||||
assert second == "45", "second is 45"
|
||||
}
|
||||
|
||||
test "time-format-iso" {
|
||||
// Epoch = 1970-01-01T00:00:00.000Z
|
||||
let formatted: String = time_format(0, "ISO")
|
||||
assert formatted == "1970-01-01T00:00:00.000Z", "epoch formats to ISO correctly"
|
||||
}
|
||||
|
||||
test "time-format-iso-empty-fmt" {
|
||||
// Empty format string should also produce ISO 8601
|
||||
let formatted: String = time_format(0, "")
|
||||
assert formatted == "1970-01-01T00:00:00.000Z", "empty fmt produces ISO"
|
||||
}
|
||||
|
||||
test "time-format-strftime" {
|
||||
// 2024-03-15T12:30:45.000Z
|
||||
let ts: Int = 1710505845000
|
||||
let fmt: String = time_format(ts, "%Y-%m-%d")
|
||||
assert fmt == "2024-03-15", "strftime %Y-%m-%d"
|
||||
let hms: String = time_format(ts, "%H:%M:%S")
|
||||
assert hms == "12:30:45", "strftime %H:%M:%S"
|
||||
}
|
||||
|
||||
test "time-add-milliseconds" {
|
||||
let base: Int = 1000000
|
||||
let result: Int = time_add(base, 500, "ms")
|
||||
assert result == 1000500, "add 500 ms"
|
||||
}
|
||||
|
||||
test "time-add-seconds" {
|
||||
let base: Int = 0
|
||||
let result: Int = time_add(base, 60, "sec")
|
||||
assert result == 60000, "add 60 seconds = 60000 ms"
|
||||
}
|
||||
|
||||
test "time-add-minutes" {
|
||||
let base: Int = 0
|
||||
let result: Int = time_add(base, 2, "min")
|
||||
assert result == 120000, "add 2 minutes = 120000 ms"
|
||||
}
|
||||
|
||||
test "time-add-hours" {
|
||||
let base: Int = 0
|
||||
let result: Int = time_add(base, 1, "hour")
|
||||
assert result == 3600000, "add 1 hour = 3600000 ms"
|
||||
}
|
||||
|
||||
test "time-add-days" {
|
||||
let base: Int = 0
|
||||
let result: Int = time_add(base, 1, "day")
|
||||
assert result == 86400000, "add 1 day = 86400000 ms"
|
||||
}
|
||||
|
||||
test "time-diff-seconds" {
|
||||
let t1: Int = 0
|
||||
let t2: Int = 90000
|
||||
let d: Int = time_diff(t1, t2, "sec")
|
||||
assert d == 90, "diff 90000 ms = 90 seconds"
|
||||
}
|
||||
|
||||
test "time-diff-negative" {
|
||||
let t1: Int = 5000
|
||||
let t2: Int = 2000
|
||||
let d: Int = time_diff(t1, t2, "sec")
|
||||
assert d == -3, "negative diff when t2 < t1"
|
||||
}
|
||||
|
||||
test "duration-helpers" {
|
||||
let d_secs: Int = duration_seconds(5)
|
||||
assert d_secs == 5000000000, "5 seconds in nanoseconds"
|
||||
let d_ms: Int = duration_millis(100)
|
||||
assert d_ms == 100000000, "100 ms in nanoseconds"
|
||||
let d_ns: Int = duration_nanos(42)
|
||||
assert d_ns == 42, "42 nanos is identity"
|
||||
let back_secs: Int = duration_to_seconds(d_secs)
|
||||
assert back_secs == 5, "convert back to seconds"
|
||||
let back_ms: Int = duration_to_millis(d_ms)
|
||||
assert back_ms == 100, "convert back to milliseconds"
|
||||
}
|
||||
|
||||
test "instant-conversions" {
|
||||
let inst: Int = unix_millis(1000)
|
||||
assert inst == 1000000000, "unix_millis(1000) in nanoseconds"
|
||||
let inst_secs: Int = unix_seconds(2)
|
||||
assert inst_secs == 2000000000, "unix_seconds(2) in nanoseconds"
|
||||
let back_ms: Int = instant_to_unix_millis(inst)
|
||||
assert back_ms == 1000, "instant to unix millis"
|
||||
let back_secs: Int = instant_to_unix_seconds(inst_secs)
|
||||
assert back_secs == 2, "instant to unix seconds"
|
||||
}
|
||||
|
||||
test "time-from-parts" {
|
||||
// time_from_parts(secs, ns, tz) -> secs*1000 + ns/1_000_000
|
||||
let ts: Int = time_from_parts(1000, 500000000, "UTC")
|
||||
assert ts == 1000500, "time_from_parts: 1000 secs + 500ms"
|
||||
}
|
||||
Reference in New Issue
Block a user