Compare commits

...

25 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
Will Anderson 32f0cf7b5d Add html-page.el example and rebuild elc binary
examples/html-page.el demonstrates HTML template syntax:
- <!doctype html> prefix handling
- Attribute values (static and interpolated)
- {#each list as item} iteration
- Auto-escaped interpolation via {expr}
- Self-closing void elements (meta, br, etc.)

Rebuilt dist/platform/elc from modified compiler source. The new
binary is self-hosted from the HTML-capable compiler source and
passes the standard identity check.
2026-05-04 13:02:54 -05:00
Will Anderson 65e26cd7a5 Add HTML template codegen and runtime for JS backend
JS codegen (codegen-js.el):
- js_cg_html_template: emits an IIFE that builds HTML via += concat
- js_cg_html_element_str / js_cg_html_parts / js_cg_html_attrs_str:
  mirror the C codegen structure using JS string accumulator
- js_cg_html_each: {#each} compiles to a JS for-loop
- Reuses existing js_str_lit / js_escape from the file header

Runtime (el_runtime.js):
- html_escape(s): replaces & < > " ' using regex chains
- html_raw(s): identity function
- Both exported from the runtime module exports object
2026-05-04 13:02:50 -05:00
Will Anderson 1fd7cd5545 Add HTML template codegen and runtime for C backend
C codegen (codegen.el):
- cg_html_template: emits a GCC/Clang statement-expression that
  builds the HTML string via el_str_concat chains
- cg_html_element_str / cg_html_parts / cg_html_attrs_str: recursive
  element and attribute emitters
- cg_html_each: {#each} compiles to a C for-loop with el_list_get
- __html_counter state tracks unique accumulator variable names
- Handles both 'static' (raw string) and 'dynamic' (expr node) attrs
  matching the parser's attribute kind convention

Runtime (el_runtime.c / el_runtime.h):
- html_escape(s): escapes & < > " ' for safe interpolation
- html_raw(s): identity function for raw() bypass
- Both use the existing html_buf_t infrastructure from el_html_sanitize
2026-05-04 13:02:44 -05:00
Will Anderson 71689520b6 Add HTML template syntax to El parser
Adds native HTML template literals to the El parser. Templates are
detected in value position when Lt is followed by a known HTML tag
name (is_html_tag_name) or by '!' for <!doctype html>.

New parser helpers:
- is_html_tag_name / is_void_element: classify tag names
- parse_html_text_tokens: collect intertoken text content
- parse_html_attrs: parse name, name="val", name={expr} attributes
- parse_html_children: recursive children with {expr}, {#each} support
- parse_html_element / parse_html_template: entry points

Adds Hash token kind ('#') to lexer for {#each} block syntax.

AST nodes: HtmlTemplate, html:Element, html:Text, html:Interp,
html:Raw, html:Each, html:Doctype. Doctype flag is stored on the
root element node rather than as a separate AST layer.

HTML templates parse correctly after 'return' and as the sole
expression in a function body. The 'return' keyword is required when
other let bindings precede the template, as El has no newline-as-
statement-terminator and '<' would otherwise be parsed as comparison.
2026-05-04 13:02:36 -05:00
23 changed files with 7112 additions and 348 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.
+39
View File
@@ -2602,6 +2602,45 @@ el_val_t el_html_sanitize(el_val_t input_v, el_val_t allowlist_v) {
return el_wrap_str(result);
}
/* ── html_escape / html_raw ──────────────────────────────────────────────── */
/*
* html_escape(s) escape a user-supplied string for safe inline interpolation
* in HTML text content or attribute values. Escapes: & < > " '
*
* html_raw(s) identity function; used by the `raw()` escape hatch in El HTML
* templates to explicitly opt out of escaping.
*/
el_val_t html_escape(el_val_t sv) {
const char* s = EL_CSTR(sv);
if (!s) return EL_STR("");
html_buf_t out;
html_buf_init(&out);
for (const char* p = s; *p; p++) {
unsigned char c = (unsigned char)*p;
switch (c) {
case '&': html_buf_puts(&out, "&amp;"); break;
case '<': html_buf_puts(&out, "&lt;"); break;
case '>': html_buf_puts(&out, "&gt;"); break;
case '"': html_buf_puts(&out, "&quot;"); break;
case '\'': html_buf_puts(&out, "&#39;"); break;
default: html_buf_putc(&out, (char)c); break;
}
}
char* result = el_strbuf(out.len);
memcpy(result, out.data, out.len);
result[out.len] = '\0';
html_buf_free(&out);
return el_wrap_str(result);
}
el_val_t html_raw(el_val_t sv) {
/* Identity — returns the value unchanged. The name exists so generated
* code can call html_raw(expr) instead of expr directly, making it clear
* at the call site that escaping is intentionally bypassed. */
return sv;
}
/* ── JSON ────────────────────────────────────────────────────────────────── */
/* True iff the segment is non-empty and every byte is an ASCII digit. We treat
+7
View File
@@ -214,6 +214,13 @@ el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
* where each value is the array of attribute names allowed for that tag. */
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
/* ── HTML template helpers ───────────────────────────────────────────────────
* Used by compiled El HTML template expressions.
* html_escape(s) — escape & < > " ' for safe inline interpolation.
* html_raw(s) — identity; explicit opt-out from escaping (`raw()` form). */
el_val_t html_escape(el_val_t s);
el_val_t html_raw(el_val_t s);
/* ── Filesystem ──────────────────────────────────────────────────────────── */
el_val_t fs_read(el_val_t path);
+18
View File
@@ -128,6 +128,22 @@ function str_pad_right(s, width, pad) {
return String(s).padEnd(width, String(pad));
}
// ── HTML template helpers ────────────────────────────────────────────────────
// Used by compiled El HTML template expressions.
// html_escape(s) — escape & < > " ' for safe inline interpolation.
// html_raw(s) — identity; explicit opt-out from escaping (raw() form).
function html_escape(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function html_raw(s) { return s; }
// ── Math ────────────────────────────────────────────────────────────────────
function el_abs(n) { return Math.abs(n); }
@@ -1017,6 +1033,8 @@ export {
fs_read, fs_write, fs_list,
json_parse, json_stringify, json_get, json_get_string, json_get_int,
time_now, time_now_utc, sleep_ms,
// HTML template helpers
html_escape, html_raw,
bool_to_str, exit_program, args, env,
state_set, state_get, state_del, state_keys,
el_cgi_init,
+307 -3
View File
@@ -191,6 +191,138 @@ fn js_is_int_call(call_expr: Map<String, Any>) -> Bool {
return false
}
// HTML template codegen (JS)
//
// HTML template expressions compile to a JS IIFE that builds the HTML string
// using string concatenation. Interpolated values go through html_escape();
// raw() bypasses escaping. {#each} blocks compile to Array.forEach or a
// for-loop that pushes fragments into a parts array.
//
// Entry point: js_cg_html_template(expr) JS expression string.
fn js_next_html_id() -> String {
let csv: String = state_get("__js_html_counter")
let n = 0
if !str_eq(csv, "") {
let n = str_to_int(csv)
}
let n = n + 1
state_set("__js_html_counter", native_int_to_str(n))
native_int_to_str(n)
}
fn js_cg_html_parts(children: [Map<String, Any>], acc_var: String) -> String {
let n: Int = native_list_len(children)
let i = 0
let out = ""
while i < n {
let child: Map<String, Any> = native_list_get(children, i)
let html_kind: String = child["html"]
if str_eq(html_kind, "Text") {
let text: String = child["text"]
let out = out + acc_var + " += " + js_str_lit(text) + "; "
}
if str_eq(html_kind, "Doctype") {
let out = out + acc_var + " += \"<!doctype html>\"; "
}
if str_eq(html_kind, "Interp") {
let val_node = child["value"]
let val_c: String = js_cg_expr(val_node)
let out = out + acc_var + " += html_escape(" + val_c + "); "
}
if str_eq(html_kind, "Raw") {
let val_node = child["value"]
let val_c: String = js_cg_expr(val_node)
let out = out + acc_var + " += html_raw(" + val_c + "); "
}
if str_eq(html_kind, "Element") {
let elem_c: String = js_cg_html_element_str(child, acc_var)
let out = out + elem_c
}
if str_eq(html_kind, "Each") {
let each_c: String = js_cg_html_each(child, acc_var)
let out = out + each_c
}
let i = i + 1
}
out
}
fn js_cg_html_attrs_str(attrs: [Map<String, Any>], acc_var: String) -> String {
let n: Int = native_list_len(attrs)
let i = 0
let out = ""
while i < n {
let attr: Map<String, Any> = native_list_get(attrs, i)
let attr_name: String = attr["name"]
let kind: String = attr["kind"]
// open-attr snippet: " name=\""
let open_val: String = " " + attr_name + "=\""
if str_eq(kind, "static") {
let sv: String = attr["value"]
let out = out + acc_var + " += " + js_str_lit(open_val) + "; "
let out = out + acc_var + " += " + js_str_lit(sv) + "; "
let out = out + acc_var + " += " + js_str_lit("\"") + "; "
} else {
if str_eq(kind, "dynamic") {
let val_node = attr["value"]
let val_c: String = js_cg_expr(val_node)
let out = out + acc_var + " += " + js_str_lit(open_val) + "; "
let out = out + acc_var + " += html_escape(" + val_c + "); "
let out = out + acc_var + " += " + js_str_lit("\"") + "; "
} else {
// Boolean attribute
let out = out + acc_var + " += " + js_str_lit(" " + attr_name) + "; "
}
}
let i = i + 1
}
out
}
fn js_cg_html_element_str(elem: Map<String, Any>, acc_var: String) -> String {
let tag: String = elem["tag"]
let attrs: [Map<String, Any>] = elem["attrs"]
let children: [Map<String, Any>] = elem["children"]
let self_closing: Bool = elem["self_closing"]
let out = acc_var + " += " + js_str_lit("<" + tag) + "; "
let out = out + js_cg_html_attrs_str(attrs, acc_var)
if self_closing {
let out = out + acc_var + " += \"/>\"" + "; "
} else {
let out = out + acc_var + " += \">\"; "
let out = out + js_cg_html_parts(children, acc_var)
let out = out + acc_var + " += " + js_str_lit("</" + tag + ">") + "; "
}
out
}
fn js_cg_html_each(node: Map<String, Any>, acc_var: String) -> String {
let list_expr = node["list"]
let item_name: String = node["item"]
let body_children: [Map<String, Any>] = node["body"]
let id: String = js_next_html_id()
let list_var: String = "_html_list_" + id
let len_var: String = "_html_len_" + id
let idx_var: String = "_html_i_" + id
let list_c: String = js_cg_expr(list_expr)
let inner_c: String = js_cg_html_parts(body_children, acc_var)
"{ const " + list_var + " = " + list_c + "; const " + len_var + " = el_list_len(" + list_var + "); for (let " + idx_var + " = 0; " + idx_var + " < " + len_var + "; " + idx_var + "++) { const " + item_name + " = el_list_get(" + list_var + ", " + idx_var + "); " + inner_c + "} } "
}
fn js_cg_html_template(expr: Map<String, Any>) -> String {
let root = expr["root"]
let id: String = js_next_html_id()
let acc: String = "_html_" + id
let doctype_flag: Bool = root["doctype"]
let doctype_prefix: String = ""
if doctype_flag {
let doctype_prefix = acc + " += \"<!doctype html>\"; "
}
let body: String = js_cg_html_element_str(root, acc)
"(() => { let " + acc + " = \"\"; " + doctype_prefix + body + "return " + acc + "; })()"
}
// Expression codegen
//
// js_cg_expr returns a JS expression string (not a statement).
@@ -569,6 +701,10 @@ fn js_cg_expr(expr: Map<String, Any>) -> String {
return js_cg_lambda(expr)
}
if kind == "HtmlTemplate" {
return js_cg_html_template(expr)
}
"null"
}
@@ -812,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"]
@@ -1032,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", "")
@@ -1156,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 {
+374
View File
@@ -175,6 +175,167 @@ fn duration_unit_nanos(unit: String) -> String {
"1LL"
}
// HTML template codegen
//
// cg_html_template(expr) emits a C statement-expression `({ ... })` that
// builds the HTML string by chaining el_str_concat calls.
//
// Interpolated values are passed through html_escape(); the raw() form
// bypasses escaping. {#each} blocks compile to C for-loops that index into
// the list with el_list_get / el_list_len.
//
// A per-template accumulator variable `_html_N` holds the growing string.
// A global counter stored in state keeps names unique.
fn next_html_id() -> String {
let csv: String = state_get("__html_counter")
let n = 0
if !str_eq(csv, "") {
let n = str_to_int(csv)
}
let n = n + 1
state_set("__html_counter", native_int_to_str(n))
native_int_to_str(n)
}
// Emit children nodes into a flat list of C fragment strings (parts).
// Each part is either a static string fragment (already C-literal form) or
// a dynamic expression that produces an el_val_t string.
// We build them all into parts, then the caller wraps with concat chain.
fn cg_html_parts(children: [Map<String, Any>], acc_var: String) -> String {
let n: Int = native_list_len(children)
let i = 0
let out = ""
while i < n {
let child: Map<String, Any> = native_list_get(children, i)
let html_kind: String = child["html"]
if str_eq(html_kind, "Text") {
let text: String = child["text"]
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(text) + ")); "
}
if str_eq(html_kind, "Doctype") {
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"<!doctype html>\")); "
}
if str_eq(html_kind, "Interp") {
let val_node = child["value"]
let val_c: String = cg_expr(val_node)
let out = out + acc_var + " = el_str_concat(" + acc_var + ", html_escape(" + val_c + ")); "
}
if str_eq(html_kind, "Raw") {
let val_node = child["value"]
let val_c: String = cg_expr(val_node)
let out = out + acc_var + " = el_str_concat(" + acc_var + ", html_raw(" + val_c + ")); "
}
if str_eq(html_kind, "Element") {
let elem_c: String = cg_html_element_str(child, acc_var)
let out = out + elem_c
}
if str_eq(html_kind, "Each") {
let each_c: String = cg_html_each(child, acc_var)
let out = out + each_c
}
let i = i + 1
}
out
}
// Generate open-tag attribute fragments inline.
// Parser stores attrs with "kind": "static" | "dynamic" | "bool".
// Static: "value" is the raw string value (not an expr node).
// Dynamic: "value" is an expr node.
// Bool: no "value" field.
fn cg_html_attrs_str(attrs: [Map<String, Any>], acc_var: String) -> String {
let n: Int = native_list_len(attrs)
let i = 0
let out = ""
// Closing-quote snippet: EL_STR("\"") in C text.
let close_q: String = "EL_STR(" + c_str_lit("\"") + ")"
while i < n {
let attr: Map<String, Any> = native_list_get(attrs, i)
let attr_name: String = attr["name"]
let kind: String = attr["kind"]
// Build: EL_STR(" name=\"")
let open_val: String = " " + attr_name + "=\""
let open_attr: String = "EL_STR(" + c_str_lit(open_val) + ")"
if str_eq(kind, "static") {
// Static attribute: value is a raw string.
let sv: String = attr["value"]
let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); "
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(sv) + ")); "
let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + close_q + "); "
} else {
if str_eq(kind, "dynamic") {
// Dynamic attribute: value is an expr node html_escape it.
let val_node = attr["value"]
let val_c: String = cg_expr(val_node)
let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); "
let out = out + acc_var + " = el_str_concat(" + acc_var + ", html_escape(" + val_c + ")); "
let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + close_q + "); "
} else {
// Boolean attribute (no value): emit " name"
let bool_attr: String = "EL_STR(" + c_str_lit(" " + attr_name) + ")"
let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + bool_attr + "); "
}
}
let i = i + 1
}
out
}
// Generate code for a single element, appending into acc_var.
fn cg_html_element_str(elem: Map<String, Any>, acc_var: String) -> String {
let tag: String = elem["tag"]
let attrs: [Map<String, Any>] = elem["attrs"]
let children: [Map<String, Any>] = elem["children"]
let self_closing: Bool = elem["self_closing"]
// Open tag: <tagname
let out = acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"<" + tag + "\")); "
let out = out + cg_html_attrs_str(attrs, acc_var)
if self_closing {
// Self-closing void element: />
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"/>\")); "
} else {
// Close open tag: >
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\">\")); "
let out = out + cg_html_parts(children, acc_var)
let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"</" + tag + ">\")); "
}
out
}
// Generate code for {#each list as item} ... {/each}.
fn cg_html_each(node: Map<String, Any>, acc_var: String) -> String {
let list_expr = node["list"]
let item_name: String = node["item"]
let body_children: [Map<String, Any>] = node["body"]
let id: String = next_html_id()
let list_var: String = "_html_list_" + id
let len_var: String = "_html_len_" + id
let idx_var: String = "_html_i_" + id
let list_c: String = cg_expr(list_expr)
let inner_c: String = cg_html_parts(body_children, acc_var)
// Emit: { el_val_t _list = expr; int _len = el_list_len(_list);
// for (int _i = 0; _i < _len; _i++) {
// el_val_t item = el_list_get(_list, _i); inner_c } }
"{ el_val_t " + list_var + " = (" + list_c + "); el_val_t " + len_var + " = el_list_len(" + list_var + "); for (el_val_t " + idx_var + " = 0; " + idx_var + " < " + len_var + "; " + idx_var + "++) { el_val_t " + item_name + " = el_list_get(" + list_var + ", " + idx_var + "); " + inner_c + "} } "
}
// Top-level HTML template codegen returns a C statement-expression string.
fn cg_html_template(expr: Map<String, Any>) -> String {
let root = expr["root"]
let id: String = next_html_id()
let acc: String = "_html_" + id
// If the root element has doctype:true the parser tagged it from <!doctype html>
let doctype_flag: Bool = root["doctype"]
let doctype_prefix: String = ""
if doctype_flag {
let doctype_prefix = acc + " = el_str_concat(" + acc + ", EL_STR(\"<!doctype html>\")); "
}
let body: String = cg_html_element_str(root, acc)
"({ el_val_t " + acc + " = EL_STR(\"\"); " + doctype_prefix + body + acc + "; })"
}
fn cg_expr(expr: Map<String, Any>) -> String {
let kind: String = expr["expr"]
@@ -787,6 +948,10 @@ fn cg_expr(expr: Map<String, Any>) -> String {
return cg_match(expr)
}
if kind == "HtmlTemplate" {
return cg_html_template(expr)
}
"EL_NULL"
}
@@ -1143,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.
@@ -2394,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
}
@@ -2538,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.
@@ -2598,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
@@ -2697,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
+18 -4
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: //
@@ -713,8 +722,13 @@ fn lex(source: String) -> [Map<String, Any>] {
let tokens = native_list_append(tokens, make_tok("QuestionMark", "?"))
let i = i + 1
} else {
// unknown char skip
let i = i + 1
if ch == "#" {
let tokens = native_list_append(tokens, make_tok("Hash", "#"))
let i = i + 1
} else {
// unknown char skip
let i = i + 1
}
}
}
}
+508
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 {
@@ -142,6 +151,460 @@ fn parse_params(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
// Expression parsing
// HTML template parser
//
// HTML templates are written as unquoted HTML in expression position:
// return <div class="x"><h1>{title}</h1></div>
//
// The parser detects an HTML template when parse_primary sees Lt followed
// by a lowercase ident (a known or assumed HTML element name) or `!` (for
// <!doctype html>). It then recursively parses the HTML into an AST.
//
// AST nodes produced:
// { "expr": "HtmlTemplate", "root": child_node }
// { "html": "Element", "tag": "div", "attrs": [...], "children": [...], "self_closing": bool }
// { "html": "Text", "text": "..." }
// { "html": "Interp", "value": expr_node }
// { "html": "Each", "list": expr_node, "item": "name", "body": [...] }
// { "html": "Doctype" }
// { "html": "Raw", "value": expr_node }
fn is_html_tag_name(name: String) -> Bool {
if str_eq(name, "a") { return true }
if str_eq(name, "abbr") { return true }
if str_eq(name, "address") { return true }
if str_eq(name, "area") { return true }
if str_eq(name, "article") { return true }
if str_eq(name, "aside") { return true }
if str_eq(name, "audio") { return true }
if str_eq(name, "b") { return true }
if str_eq(name, "base") { return true }
if str_eq(name, "blockquote") { return true }
if str_eq(name, "body") { return true }
if str_eq(name, "br") { return true }
if str_eq(name, "button") { return true }
if str_eq(name, "canvas") { return true }
if str_eq(name, "caption") { return true }
if str_eq(name, "cite") { return true }
if str_eq(name, "code") { return true }
if str_eq(name, "col") { return true }
if str_eq(name, "colgroup") { return true }
if str_eq(name, "data") { return true }
if str_eq(name, "datalist") { return true }
if str_eq(name, "dd") { return true }
if str_eq(name, "del") { return true }
if str_eq(name, "details") { return true }
if str_eq(name, "dfn") { return true }
if str_eq(name, "dialog") { return true }
if str_eq(name, "div") { return true }
if str_eq(name, "dl") { return true }
if str_eq(name, "dt") { return true }
if str_eq(name, "em") { return true }
if str_eq(name, "embed") { return true }
if str_eq(name, "fieldset") { return true }
if str_eq(name, "figcaption") { return true }
if str_eq(name, "figure") { return true }
if str_eq(name, "footer") { return true }
if str_eq(name, "form") { return true }
if str_eq(name, "h1") { return true }
if str_eq(name, "h2") { return true }
if str_eq(name, "h3") { return true }
if str_eq(name, "h4") { return true }
if str_eq(name, "h5") { return true }
if str_eq(name, "h6") { return true }
if str_eq(name, "head") { return true }
if str_eq(name, "header") { return true }
if str_eq(name, "hr") { return true }
if str_eq(name, "html") { return true }
if str_eq(name, "i") { return true }
if str_eq(name, "iframe") { return true }
if str_eq(name, "img") { return true }
if str_eq(name, "input") { return true }
if str_eq(name, "ins") { return true }
if str_eq(name, "kbd") { return true }
if str_eq(name, "label") { return true }
if str_eq(name, "legend") { return true }
if str_eq(name, "li") { return true }
if str_eq(name, "link") { return true }
if str_eq(name, "main") { return true }
if str_eq(name, "map") { return true }
if str_eq(name, "mark") { return true }
if str_eq(name, "menu") { return true }
if str_eq(name, "meta") { return true }
if str_eq(name, "meter") { return true }
if str_eq(name, "nav") { return true }
if str_eq(name, "noscript") { return true }
if str_eq(name, "object") { return true }
if str_eq(name, "ol") { return true }
if str_eq(name, "optgroup") { return true }
if str_eq(name, "option") { return true }
if str_eq(name, "output") { return true }
if str_eq(name, "p") { return true }
if str_eq(name, "param") { return true }
if str_eq(name, "picture") { return true }
if str_eq(name, "pre") { return true }
if str_eq(name, "progress") { return true }
if str_eq(name, "q") { return true }
if str_eq(name, "rp") { return true }
if str_eq(name, "rt") { return true }
if str_eq(name, "ruby") { return true }
if str_eq(name, "s") { return true }
if str_eq(name, "samp") { return true }
if str_eq(name, "script") { return true }
if str_eq(name, "section") { return true }
if str_eq(name, "select") { return true }
if str_eq(name, "small") { return true }
if str_eq(name, "source") { return true }
if str_eq(name, "span") { return true }
if str_eq(name, "strong") { return true }
if str_eq(name, "style") { return true }
if str_eq(name, "sub") { return true }
if str_eq(name, "summary") { return true }
if str_eq(name, "sup") { return true }
if str_eq(name, "table") { return true }
if str_eq(name, "tbody") { return true }
if str_eq(name, "td") { return true }
if str_eq(name, "template") { return true }
if str_eq(name, "textarea") { return true }
if str_eq(name, "tfoot") { return true }
if str_eq(name, "th") { return true }
if str_eq(name, "thead") { return true }
if str_eq(name, "time") { return true }
if str_eq(name, "title") { return true }
if str_eq(name, "tr") { return true }
if str_eq(name, "track") { return true }
if str_eq(name, "u") { return true }
if str_eq(name, "ul") { return true }
if str_eq(name, "var") { return true }
if str_eq(name, "video") { return true }
if str_eq(name, "wbr") { return true }
false
}
fn is_void_element(name: String) -> Bool {
if str_eq(name, "area") { return true }
if str_eq(name, "base") { return true }
if str_eq(name, "br") { return true }
if str_eq(name, "col") { return true }
if str_eq(name, "embed") { return true }
if str_eq(name, "hr") { return true }
if str_eq(name, "img") { return true }
if str_eq(name, "input") { return true }
if str_eq(name, "link") { return true }
if str_eq(name, "meta") { return true }
if str_eq(name, "param") { return true }
if str_eq(name, "source") { return true }
if str_eq(name, "track") { return true }
if str_eq(name, "wbr") { return true }
false
}
// Collect tokens as text content until we hit Lt, LBrace, Eof, or a
// closing-tag marker (Lt Slash). Returns { "text": "...", "pos": p }
fn parse_html_text_tokens(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let parts: [String] = native_list_empty()
let p = pos
let running = true
while running {
let k = tok_kind(tokens, p)
if str_eq(k, "Eof") {
let running = false
} else {
if str_eq(k, "Lt") {
let running = false
} else {
if str_eq(k, "LBrace") {
let running = false
} else {
// Check for </: Lt already stops us, but Slash alone
// (after consuming whitespace) also stops text.
// Anything else is text content.
let v = tok_value(tokens, p)
let parts = native_list_append(parts, v)
let p = p + 1
}
}
}
}
{ "text": str_join(parts, " "), "pos": p }
}
// Parse an attribute list: (attrname | attrname="val" | attrname={expr})*
// Stops at Gt or Slash (for self-closing />).
fn parse_html_attrs(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let attrs: [Map<String, Any>] = native_list_empty()
let p = pos
let running = true
while running {
let k = tok_kind(tokens, p)
if str_eq(k, "Gt") {
let running = false
} else {
if str_eq(k, "Slash") {
let running = false
} else {
if str_eq(k, "Eof") {
let running = false
} else {
// Attribute name: could be Ident or keyword used as attr name
let attr_name = tok_value(tokens, p)
let p = p + 1
let k2 = tok_kind(tokens, p)
if str_eq(k2, "Eq") {
let p = p + 1
let k3 = tok_kind(tokens, p)
if str_eq(k3, "Str") {
// static: attr="value"
let attr_val = tok_value(tokens, p)
let p = p + 1
let attrs = native_list_append(attrs, { "name": attr_name, "kind": "static", "value": attr_val })
} else {
if str_eq(k3, "LBrace") {
// dynamic: attr={expr}
let r = parse_expr(tokens, p + 1)
let val_node = r["node"]
let p = r["pos"]
let p = expect(tokens, p, "RBrace")
let attrs = native_list_append(attrs, { "name": attr_name, "kind": "dynamic", "value": val_node })
} else {
// malformed, skip
}
}
} else {
// boolean attribute
let attrs = native_list_append(attrs, { "name": attr_name, "kind": "bool" })
}
}
}
}
}
{ "attrs": attrs, "pos": p }
}
// Parse the children of an HTML element until we see the closing tag </tag>
// or EOF. Returns { "children": [...], "pos": p_after_closing_tag }
fn parse_html_children(tokens: [Map<String, Any>], pos: Int, parent_tag: String) -> Map<String, Any> {
let children: [Map<String, Any>] = native_list_empty()
let p = pos
let running = true
while running {
let k = tok_kind(tokens, p)
if str_eq(k, "Eof") {
let running = false
} else {
if str_eq(k, "Lt") {
// Check for closing tag: </
let k2 = tok_kind(tokens, p + 1)
if str_eq(k2, "Slash") {
// </tagname> consume and stop
let p = p + 2
// skip tag name
let close_name = tok_value(tokens, p)
let p = p + 1
// consume >
let p = expect(tokens, p, "Gt")
let running = false
} else {
if str_eq(k2, "Not") {
// Possible <!doctype html>
let k3_v = tok_value(tokens, p + 2)
if str_eq(k3_v, "doctype") {
// consume <!doctype html>
let p = p + 2
// skip until >
let scanning = true
while scanning {
let ck = tok_kind(tokens, p)
if str_eq(ck, "Eof") { let scanning = false }
if str_eq(ck, "Gt") {
let p = p + 1
let scanning = false
} else {
let p = p + 1
}
}
let children = native_list_append(children, { "html": "Doctype" })
} else {
let p = p + 1
}
} else {
// nested element
let r = parse_html_element(tokens, p)
let child = r["node"]
let p = r["pos"]
let children = native_list_append(children, child)
}
}
} else {
if str_eq(k, "LBrace") {
// Interpolation: {expr} or {#each ...} or {/each}
let k2 = tok_kind(tokens, p + 1)
if str_eq(k2, "Hash") {
// {#each list as item}
let k3_v = tok_value(tokens, p + 2)
if str_eq(k3_v, "each") {
let p = p + 3
// parse list expr up to "as" keyword
let prev_no_block: String = state_get("__no_block_expr")
state_set("__no_block_expr", "1")
let r_list = parse_expr(tokens, p)
state_set("__no_block_expr", prev_no_block)
let list_expr = r_list["node"]
let p = r_list["pos"]
// expect "as"
let p = expect(tokens, p, "As")
// item variable name
let item_name = tok_value(tokens, p)
let p = p + 1
// consume closing }
let p = expect(tokens, p, "RBrace")
// parse body until {/each}
let r_body = parse_html_each_body(tokens, p)
let body_children = r_body["children"]
let p = r_body["pos"]
let each_node: Map<String, Any> = { "html": "Each", "list": list_expr, "item": item_name, "body": body_children }
let children = native_list_append(children, each_node)
} else {
let p = p + 1
}
} else {
if str_eq(k2, "Slash") {
// {/each} end of each block, stop
// skip {/each}
let p = p + 2
// skip "each"
let p = p + 1
// skip }
let p = expect(tokens, p, "RBrace")
let running = false
} else {
// regular {expr}
let r = parse_expr(tokens, p + 1)
let interp_val = r["node"]
let p = r["pos"]
let p = expect(tokens, p, "RBrace")
// Check if the expr is a call to raw()
let is_raw_call = false
let interp_kind: String = interp_val["expr"]
if str_eq(interp_kind, "Call") {
let fn_node = interp_val["func"]
let fn_kind: String = fn_node["expr"]
if str_eq(fn_kind, "Ident") {
let fn_name_v: String = fn_node["name"]
if str_eq(fn_name_v, "raw") {
let is_raw_call = true
}
}
}
if is_raw_call {
let raw_args = interp_val["args"]
let raw_inner = native_list_get(raw_args, 0)
let children = native_list_append(children, { "html": "Raw", "value": raw_inner })
} else {
let children = native_list_append(children, { "html": "Interp", "value": interp_val })
}
}
}
} else {
// Text tokens collect run of non-special tokens
let r_text = parse_html_text_tokens(tokens, p)
let text_str: String = r_text["text"]
let p = r_text["pos"]
let text_trimmed: String = str_trim(text_str)
if !str_eq(text_trimmed, "") {
let children = native_list_append(children, { "html": "Text", "text": text_trimmed })
}
}
}
}
}
{ "children": children, "pos": p }
}
// Parse body of {#each} until {/each}. Mirrors parse_html_children but
// stops at the {/each} sentinel rather than a closing element tag.
fn parse_html_each_body(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
parse_html_children(tokens, pos, "__each__")
}
// Parse a single HTML element: <tag attrs> children </tag>
// or self-closing: <tag attrs/>
// Pos points to the Lt token.
fn parse_html_element(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let p = pos
// consume <
let p = expect(tokens, p, "Lt")
// tag name
let tag_name = tok_value(tokens, p)
let p = p + 1
// parse attributes
let r_attrs = parse_html_attrs(tokens, p)
let attrs = r_attrs["attrs"]
let p = r_attrs["pos"]
// check for self-closing /> or void element
let k = tok_kind(tokens, p)
let self_closing = false
if str_eq(k, "Slash") {
// />
let p = p + 1
let p = expect(tokens, p, "Gt")
let self_closing = true
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p)
}
// consume >
let p = expect(tokens, p, "Gt")
// void elements have no children, no closing tag
if is_void_element(tag_name) {
return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p)
}
// parse children
let r_children = parse_html_children(tokens, p, tag_name)
let children = r_children["children"]
let p = r_children["pos"]
make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": children, "self_closing": false }, p)
}
// Entry point for HTML template parsing.
// Pos points to Lt (or Lt Not for <!doctype>).
// May parse an optional <!doctype html> prefix followed by the root element.
fn parse_html_template(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let p = pos
// Check for <!doctype html>
let doctype = false
let k = tok_kind(tokens, p)
let k2 = tok_kind(tokens, p + 1)
if str_eq(k, "Lt") {
if str_eq(k2, "Not") {
let k3_v = tok_value(tokens, p + 2)
if str_eq(k3_v, "doctype") {
let doctype = true
// consume <!doctype html>
let p = p + 2
let scanning = true
while scanning {
let ck = tok_kind(tokens, p)
if str_eq(ck, "Eof") { let scanning = false }
if str_eq(ck, "Gt") {
let p = p + 1
let scanning = false
} else {
let p = p + 1
}
}
}
}
}
// Parse root element
let r = parse_html_element(tokens, p)
let root = r["node"]
let p = r["pos"]
let root_with_doctype = root
if doctype {
let root_with_doctype = { "html": root["html"], "tag": root["tag"], "attrs": root["attrs"], "children": root["children"], "self_closing": root["self_closing"], "doctype": true }
}
make_result({ "expr": "HtmlTemplate", "root": root_with_doctype }, p)
}
fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
let k = tok_kind(tokens, pos)
let v = tok_value(tokens, pos)
@@ -166,6 +629,22 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
return make_result({ "expr": "Bool", "value": v }, pos + 1)
}
// HTML template literal: <tagname ...>...</tagname> or <!doctype html>...
// Detected in value position only; `<` in comparison position is already
// consumed by parse_binop before parse_primary is reached.
if k == "Lt" {
let k2 = tok_kind(tokens, pos + 1)
if str_eq(k2, "Not") {
return parse_html_template(tokens, pos)
}
if str_eq(k2, "Ident") {
let tag_candidate = tok_value(tokens, pos + 1)
if is_html_tag_name(tag_candidate) {
return parse_html_template(tokens, pos)
}
}
}
// Identifier
if k == "Ident" {
return make_result({ "expr": "Ident", "name": v }, pos + 1)
@@ -1084,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
+40
View File
@@ -0,0 +1,40 @@
// html-page.el Example of native HTML template syntax in El.
//
// El HTML templates let you write HTML directly in expression position.
// Interpolated values are automatically HTML-escaped.
// Use raw(expr) to bypass escaping when you know the content is safe.
//
// Compile and run:
// ./dist/platform/elc examples/html-page.el > /tmp/html-page.c
// cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
// -o /tmp/html-page /tmp/html-page.c el-compiler/runtime/el_runtime.c
// /tmp/html-page
fn render_item(item: String) -> String {
return <li class="item">{item}</li>
}
fn render_page(title: String, items: [String]) -> String {
return <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{title}</title>
</head>
<body>
<h1>{title}</h1>
<ul>
{#each items as item}
<li class="item">{item}</li>
{/each}
</ul>
<p>Built with El HTML templates</p>
</body>
</html>
}
fn main() -> Void {
let items: [String] = ["Lexer", "Parser", "Codegen", "Runtime"]
let page: String = render_page("El Compiler Stages", items)
println(page)
}
+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"
}