add Instant + Duration as first-class types in El
Postfix literal syntax (30.seconds, 1.hour). Type-checked arithmetic (Instant + Duration -> Instant; Instant + Instant fails). TTL helpers backed by typed Duration. sleep(Duration) replaces ambiguous sleep(Int). Self-host fixed point holds at 5797 lines. Snapshot tagged at dist/platform/elc.20260502-1256-self-host. Phase 2 (every/after/at scheduling primitives) lands separately. Backlog: bl-937e9c30
This commit is contained in:
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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()))
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
Executable
+155
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env bash
|
||||
# run.sh — build and execute the time/ acceptance corpus.
|
||||
#
|
||||
# Each examples/<case>.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 <name> <source> <expected> [<extra>]
|
||||
# Compiles the source via elc, links against the runtime, runs the binary,
|
||||
# and compares stdout (whitespace-trimmed) against <expected>. If <extra> is
|
||||
# the literal string "either", any one of the comma-separated values in
|
||||
# <expected> 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 <name> <source>
|
||||
# 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
|
||||
@@ -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/<name>/ 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.")
|
||||
}
|
||||
Reference in New Issue
Block a user