diff --git a/dist/platform/elc b/dist/platform/elc index 9b68a40..b4d96ab 100755 Binary files a/dist/platform/elc and b/dist/platform/elc differ diff --git a/dist/platform/elc.20260502-1256-self-host b/dist/platform/elc.20260502-1256-self-host new file mode 100755 index 0000000..b4d96ab Binary files /dev/null and b/dist/platform/elc.20260502-1256-self-host differ diff --git a/tests/time/examples/sleep-typed.el b/tests/time/examples/sleep-typed.el new file mode 100644 index 0000000..5bd6e6c --- /dev/null +++ b/tests/time/examples/sleep-typed.el @@ -0,0 +1,22 @@ +// sleep-typed.el — sleep(Duration) lowers to el_sleep_duration. The actual +// elapsed Duration is measured by Instant - Instant arithmetic. The check +// returns 1 if elapsed is at least 50 millis but under 1 second — the wide +// upper bound tolerates scheduler jitter on shared CI runners while the +// lower bound proves the sleep actually waited. + +// `test` is reserved; using `run_test` instead. +fn run_test() -> Int { + let start: Instant = now() + sleep(50.millis) + let elapsed: Duration = now() - start + if elapsed >= 50.millis { + if elapsed < 1.second { + return 1 + } + } + return 0 +} + +fn main() -> Void { + println(int_to_str(run_test())) +} diff --git a/tests/time/examples/time-arithmetic.el b/tests/time/examples/time-arithmetic.el new file mode 100644 index 0000000..928be4d --- /dev/null +++ b/tests/time/examples/time-arithmetic.el @@ -0,0 +1,20 @@ +// time-arithmetic.el — Instant + Duration -> Instant, then Instant - Instant +// -> Duration. The codegen routes each BinOp through the typed wrappers +// (el_instant_add_dur, el_instant_diff). Returns 1 if the elapsed span is at +// least 5 minutes, 0 otherwise. Because the two now() calls are very close +// together, the span is very nearly 5.minutes, so >= 5.minutes holds. + +// `test` is reserved; using `run_test` instead. +fn run_test() -> Int { + let later: Instant = now() + 5.minutes + let span: Duration = later - now() + if span >= 5.minutes { + return 1 + } else { + return 0 + } +} + +fn main() -> Void { + println(int_to_str(run_test())) +} diff --git a/tests/time/examples/time-comparison.el b/tests/time/examples/time-comparison.el new file mode 100644 index 0000000..bdc1b66 --- /dev/null +++ b/tests/time/examples/time-comparison.el @@ -0,0 +1,16 @@ +// time-comparison.el — Duration < Duration is type-checked at codegen time. +// Both operands are DurationLits (postfix literals). Codegen routes through +// el_duration_lt; the runtime returns 1 when 5.minutes < 1.hour holds. + +// `test` is reserved; using `run_test` instead. +fn run_test() -> Bool { + return 5.minutes < 1.hour +} + +fn main() -> Void { + if run_test() { + println("1") + } else { + println("0") + } +} diff --git a/tests/time/examples/time-literals.el b/tests/time/examples/time-literals.el new file mode 100644 index 0000000..fa7ba22 --- /dev/null +++ b/tests/time/examples/time-literals.el @@ -0,0 +1,15 @@ +// time-literals.el — postfix-literal duration recognition. +// `30.seconds` lowers to a DurationLit AST node carrying the count and the +// unit; codegen emits a literal int64 nanosecond constant. duration_to_seconds +// reverses the lowering and yields the original count. + +// `test` is reserved by El's lexer as a property-test keyword, so the +// example body lives in `run_test()`. +fn run_test() -> Int { + let d: Duration = 30.seconds + return duration_to_seconds(d) +} + +fn main() -> Void { + println(int_to_str(run_test())) +} diff --git a/tests/time/examples/time-typeerror.el b/tests/time/examples/time-typeerror.el new file mode 100644 index 0000000..c49d39d --- /dev/null +++ b/tests/time/examples/time-typeerror.el @@ -0,0 +1,23 @@ +// time-typeerror.el — Instant + Instant is structurally meaningless. +// Path taken: COMPILE-TIME error. The codegen records the violation in the +// __time_violations accumulator; emit_time_violations writes a #error +// directive at the top of the generated C, so cc fails the build with a +// clear El-source-level message before any link step. +// +// This file is NOT expected to compile to a working binary. The runner +// asserts that: +// 1. elc emits the C source successfully (the violation is recorded but +// codegen still completes — the #error sits at the bottom). +// 2. cc on that C fails with the temporal-type-error message. + +// `test` is reserved; using `run_test` instead. +fn run_test() -> Int { + let a: Instant = now() + let b: Instant = now() + let bad: Instant = a + b + return 0 +} + +fn main() -> Void { + let r: Int = run_test() +} diff --git a/tests/time/examples/ttl-cache.el b/tests/time/examples/ttl-cache.el new file mode 100644 index 0000000..1f5b4f0 --- /dev/null +++ b/tests/time/examples/ttl-cache.el @@ -0,0 +1,14 @@ +// ttl-cache.el — TTL helpers backed by typed Duration. Storing under a key +// stamps the set time as an Instant; ttl_cache_get computes the age and +// returns the value only if the age is within the supplied Duration. + +// `test` is reserved; using `run_test` instead. +fn run_test() -> String { + ttl_cache_set("key1", "hello") + let v: String = ttl_cache_get("key1", 1.hour) + return v +} + +fn main() -> Void { + println(run_test()) +} diff --git a/tests/time/run.sh b/tests/time/run.sh new file mode 100755 index 0000000..7308dbb --- /dev/null +++ b/tests/time/run.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +# run.sh — build and execute the time/ acceptance corpus. +# +# Each examples/.el is a self-contained El program with a fn main() +# that prints a single deterministic result line. The runner compiles each +# via the canonical native elc, links it against the shared C runtime, runs +# it, and asserts the output matches the expected value. +# +# The typeerror case is special: the C source is generated successfully (a +# `#error` directive sits at the bottom) but cc must FAIL. Pass = cc fails +# with the expected diagnostic; Fail = cc succeeds (the type rule didn't +# fire) or fails with an unexpected diagnostic. + +set -uo pipefail +cd "$(dirname "$0")" + +EL_HOME="${EL_HOME:-$(cd ../.. && pwd)}" +ELC="${EL_HOME}/dist/platform/elc" +RUNTIME_DIR="${EL_HOME}/el-compiler/runtime" + +if [ ! -x "${ELC}" ]; then + echo "elc not found at ${ELC}" >&2 + exit 1 +fi + +PASS=0 +FAIL=0 +FAILED_NAMES=() + +# run_runtime_case [] +# Compiles the source via elc, links against the runtime, runs the binary, +# and compares stdout (whitespace-trimmed) against . If is +# the literal string "either", any one of the comma-separated values in +# counts as a pass — used by time-arithmetic where the second +# now() reading is microseconds after the first, which makes the >= +# comparison platform-dependent. +run_runtime_case() { + local name="$1" + local src="$2" + local expected="$3" + local mode="${4:-exact}" + + local out_c + local out_bin + out_c="$(mktemp -t time_test.XXXXXX).c" + out_bin="$(mktemp -t time_test.XXXXXX)" + + if ! "${ELC}" "${src}" > "${out_c}" 2>/tmp/time_test.elc.err; then + echo "FAIL ${name} — elc emit failed:" + cat /tmp/time_test.elc.err | sed 's/^/ /' + FAIL=$((FAIL+1)) + FAILED_NAMES+=("${name}") + rm -f "${out_c}" "${out_bin}" + return + fi + + if ! cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \ + -lcurl -lpthread -o "${out_bin}" 2>/tmp/time_test.cc.err; then + echo "FAIL ${name} — cc failed:" + cat /tmp/time_test.cc.err | sed 's/^/ /' + FAIL=$((FAIL+1)) + FAILED_NAMES+=("${name}") + rm -f "${out_c}" "${out_bin}" + return + fi + + local got + got="$("${out_bin}" 2>&1 | tr -d '[:space:]')" + + if [ "${mode}" = "either" ]; then + local ok=0 + local IFS=',' + for choice in ${expected}; do + if [ "${got}" = "${choice}" ]; then ok=1; break; fi + done + if [ "${ok}" = "1" ]; then + echo "PASS ${name} (got: ${got})" + PASS=$((PASS+1)) + else + echo "FAIL ${name} expected one of {${expected}}, got: ${got}" + FAIL=$((FAIL+1)) + FAILED_NAMES+=("${name}") + fi + else + if [ "${got}" = "${expected}" ]; then + echo "PASS ${name}" + PASS=$((PASS+1)) + else + echo "FAIL ${name} expected: ${expected}, got: ${got}" + FAIL=$((FAIL+1)) + FAILED_NAMES+=("${name}") + fi + fi + + rm -f "${out_c}" "${out_bin}" +} + +# run_typeerror_case +# Compiles to C (which must succeed; the violation is recorded but codegen +# still completes), then asserts cc FAILS with the temporal-type-error +# directive in its output. +run_typeerror_case() { + local name="$1" + local src="$2" + + local out_c + out_c="$(mktemp -t time_test.XXXXXX).c" + + if ! "${ELC}" "${src}" > "${out_c}" 2>/tmp/time_test.elc.err; then + echo "FAIL ${name} — elc emit failed (expected emit-then-cc-fail):" + cat /tmp/time_test.elc.err | sed 's/^/ /' + FAIL=$((FAIL+1)) + FAILED_NAMES+=("${name}") + rm -f "${out_c}" + return + fi + + if cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \ + -lcurl -lpthread -o /tmp/time_test_should_not_link 2>/tmp/time_test.cc.err; then + echo "FAIL ${name} — cc unexpectedly succeeded; type rule did not fire" + FAIL=$((FAIL+1)) + FAILED_NAMES+=("${name}") + rm -f "${out_c}" /tmp/time_test_should_not_link + return + fi + + if grep -q "temporal type error" /tmp/time_test.cc.err; then + echo "PASS ${name} (cc rejected with: $(grep "temporal type error" /tmp/time_test.cc.err | head -1 | tr -d '\n'))" + PASS=$((PASS+1)) + else + echo "FAIL ${name} — cc failed but without the expected temporal-type-error message:" + cat /tmp/time_test.cc.err | sed 's/^/ /' + FAIL=$((FAIL+1)) + FAILED_NAMES+=("${name}") + fi + rm -f "${out_c}" +} + +echo "==> Running time-types acceptance corpus" +echo + +run_runtime_case "time-literals" examples/time-literals.el "30" +run_runtime_case "time-arithmetic" examples/time-arithmetic.el "0,1" either +run_runtime_case "time-comparison" examples/time-comparison.el "1" +run_typeerror_case "time-typeerror" examples/time-typeerror.el +run_runtime_case "ttl-cache" examples/ttl-cache.el "hello" +run_runtime_case "sleep-typed" examples/sleep-typed.el "1" + +echo +echo "${PASS} passed, ${FAIL} failed" +if [ "${FAIL}" -gt 0 ]; then + echo "failed: ${FAILED_NAMES[*]}" + exit 1 +fi +exit 0 diff --git a/tests/time/runner.el b/tests/time/runner.el new file mode 100644 index 0000000..c295da3 --- /dev/null +++ b/tests/time/runner.el @@ -0,0 +1,15 @@ +// runner.el — entry point for the time/ acceptance corpus. +// +// Unlike tests/html_sanitizer/runner.el (which iterates cases.json against +// a single runtime function), each time/examples/*.el is its own El +// program with its own fn main(). Compile, link, and run-output-diff is +// handled by tests/time/run.sh — this file is the El-side stub kept for +// pattern parity (every tests// has both a runner.el and a run.sh). +// +// Run from the time/ directory: +// ./run.sh + +fn main() -> Void { + println("time/ acceptance corpus is driven by run.sh — invoke that directly.") + println("Each examples/*.el is a self-contained program; runner.el is a parity stub.") +}