Compare commits

..

21 Commits

Author SHA1 Message Date
Will Anderson 3f83adf458 ci: retrigger after ci-base image rebuild
El CI -dev / build-and-test (pull_request) Failing after 23s
2026-05-04 20:15:52 -05:00
Will Anderson 9f734b037c add comprehensive native test suite
El CI -dev / build-and-test (pull_request) Failing after 21s
2026-05-04 19:58:23 -05:00
Will Anderson 049a7712f4 ci: add -lm and allow-multiple-definition for bootstrap build
El CI -dev / build-and-test (pull_request) Failing after 54s
el_runtime.c uses pow/sqrt/log/sin/cos/exp - needs -lm.
elc-bootstrap.c predates the text-processing primitives commit so it
has its own C definitions of is_digit/is_whitespace; -Wl,--allow-multiple-definition
lets the linker accept both (equivalent implementations).
2026-05-04 16:08:47 -05:00
Will Anderson c64cbd21e2 retrigger - capture build error
El CI -dev / build-and-test (pull_request) Failing after 2m4s
2026-05-04 16:00:49 -05:00
Will Anderson 37488e9485 retrigger CI debug
El CI -dev / build-and-test (pull_request) Failing after 35s
2026-05-04 15:56:17 -05:00
Will Anderson 8641b4045e retrigger CI - free capacity
El CI -dev / build-and-test (pull_request) Failing after 1m1s
2026-05-04 15:53:20 -05:00
Will Anderson 49d68fbb20 retrigger CI - fix gitea DNS on host
El CI -dev / build-and-test (pull_request) Failing after 59s
2026-05-04 15:50:28 -05:00
Will Anderson 77a0658d56 retrigger CI attempt 5
El CI -dev / build-and-test (pull_request) Failing after 13m51s
2026-05-04 15:32:11 -05:00
Will Anderson bff0ad4f22 retrigger CI attempt 4
El CI -dev / build-and-test (pull_request) Failing after 32s
2026-05-04 15:30:08 -05:00
Will Anderson 49f96126b2 retrigger CI attempt 3
El CI -dev / build-and-test (pull_request) Failing after 39s
2026-05-04 15:27:52 -05:00
Will Anderson c954142063 retrigger CI after runner restart
El CI -dev / build-and-test (pull_request) Failing after 36s
2026-05-04 15:24:58 -05:00
Will Anderson 3fd5fec965 retrigger CI
El CI -dev / build-and-test (pull_request) Failing after 7s
2026-05-04 15:23:36 -05:00
Will Anderson 5476cbb2b1 ci: fix YAML - remove colon in step name, replace em dashes
El CI -dev / build-and-test (pull_request) Failing after 1s
2026-05-04 14:33:30 -05:00
Will Anderson 65792f7e4c ci: add workflow_dispatch trigger 2026-05-04 14:32:37 -05:00
Will Anderson c09023003d ci: retrigger after workflow bootstrap 2026-05-04 14:23:34 -05:00
Will Anderson 15b9ccd9e2 ci: retrigger 2026-05-04 14:13:56 -05:00
Will Anderson 9163af81aa ci: trigger CI run 2026-05-04 14:11:00 -05:00
Will Anderson 3dababa4ad dist: update elc-new binary to match elc 2026-05-04 13:27:57 -05:00
Will Anderson 5888258c9f rebuild elc: reporter=json, line numbers, em-dash FAIL format
Rebuild elc binary from the feat/native-testing source to match the
full reporter implementation:
- Lexer tracks line numbers in every token via state (__lex_line)
- Parser propagates line numbers into TestDef and Assert AST nodes
- Text reporter: "  FAIL  <name> — <msg>" (em dash, stderr)
- JSON reporter (--reporter=json): newline-delimited JSON to stdout
  with suite_start, test_start, test_pass, test_fail, suite_end events
- All fields: file (basename), line (test block), assert_line, message
- Fixed point verified: gen2 == gen3

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