diff --git a/lang/el-compiler/src/codegen.el b/lang/el-compiler/src/codegen.el index 6a0aa3b..65ba9df 100644 --- a/lang/el-compiler/src/codegen.el +++ b/lang/el-compiler/src/codegen.el @@ -2010,40 +2010,65 @@ fn is_float_call(call_expr: Map) -> Bool { // Builtins that return an Instant. Used by is_instant_expr and the BinOp // dispatch - `now() + 5.seconds` types as Instant only because we can see // that now() is an Instant-returning Call. -fn is_instant_call(call_expr: Map) -> Bool { - let func = call_expr["func"] - let fk: String = func["expr"] - if !str_eq(fk, "Ident") { return false } - let name: String = func["name"] - if str_eq(name, "now") { return true } - if str_eq(name, "el_now_instant") { return true } - if str_eq(name, "unix_seconds") { return true } - if str_eq(name, "unix_millis") { return true } - if str_eq(name, "instant_from_iso8601") { return true } - if str_eq(name, "el_instant_add_dur") { return true } - if str_eq(name, "el_instant_sub_dur") { return true } - return false +// load_signatures — El-level return types, read once from +// tools/check/signatures.rel. Previously 19 hardcoded names across +// is_instant_call and is_duration_call. The header cannot carry these: it +// declares everything as el_val_t, because El has one type. +fn load_signatures() -> Void { + if !str_eq(state_get("__sig_loaded"), "") { return } + state_set("__sig_loaded", "1") + let path: String = env("EL_SIGNATURES") + if str_eq(path, "") { let path = "tools/check/signatures.rel" } + if !fs_exists(path) { return } + let lines = str_split_lines(fs_read(path)) + let n: Int = native_list_len(lines) + let i = 0 + while i < n { + let line: String = str_trim(native_list_get(lines, i)) + if !str_eq(line, "") { + if !str_starts_with(line, "#") { + let parts = str_split(line, " ") + let np: Int = native_list_len(parts) + let name: String = "" + let ty: String = "" + let seen = 0 + let j = 0 + while j < np { + let tok: String = str_trim(native_list_get(parts, j)) + if !str_eq(tok, "") { + if seen == 0 { let name = tok } + if seen == 2 { let ty = tok } + let seen = seen + 1 + } + let j = j + 1 + } + if seen >= 3 { state_set("__sig_" + name, ty) } + } + } + let i = i + 1 + } } -// Builtins that return a Duration. Same role as is_instant_call. -fn is_duration_call(call_expr: Map) -> Bool { - let func = call_expr["func"] +fn call_returns(expr: Map, want: String) -> Bool { + load_signatures() + let func = expr["func"] let fk: String = func["expr"] if !str_eq(fk, "Ident") { return false } let name: String = func["name"] - if str_eq(name, "el_duration_from_nanos") { return true } - if str_eq(name, "duration_seconds") { return true } - if str_eq(name, "duration_millis") { return true } - if str_eq(name, "duration_nanos") { return true } - if str_eq(name, "el_instant_diff") { return true } - if str_eq(name, "el_duration_add") { return true } - if str_eq(name, "el_duration_sub") { return true } - if str_eq(name, "el_duration_scale") { return true } - if str_eq(name, "el_duration_div") { return true } - if str_eq(name, "ttl_cache_age") { return true } - return false + return str_eq(state_get("__sig_" + name), want) } +fn is_instant_call(expr: Map) -> Bool { + call_returns(expr, "Instant") +} + + +// Builtins that return a Duration. Same role as is_instant_call. +fn is_duration_call(expr: Map) -> Bool { + call_returns(expr, "Duration") +} + + // Phase 1.5 - Calendar / CalendarTime / Rhythm / LocalDate / LocalTime / // LocalDateTime / Zone are first-class boxed types. Each has its own name // set in process state, populated from typed `let` bindings and parameter diff --git a/lang/tests/integration/temporal_signatures.sh b/lang/tests/integration/temporal_signatures.sh new file mode 100755 index 0000000..7ec9b79 --- /dev/null +++ b/lang/tests/integration/temporal_signatures.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Control for temporal signatures as data. +# +# Unlike the other checks this one is read BY the compiler, not after it: the +# El-level return type decides which runtime wrapper to emit, and that is +# dispatch, not adjudication. What moved out is the data. +set -uo pipefail +ELC="${1:?usage: temporal_signatures.sh }" +LANG_DIR="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; F=0 +chk(){ [ "$2" = "$3" ] && printf ' ok %s\n' "$1" || { printf ' FAIL %s\n expected %s got %s\n' "$1" "$2" "$3"; F=$((F+1)); }; } +cd "$LANG_DIR" + +printf 'fn main() {\n let a = now()\n let b = el_duration_from_nanos(5)\n let c = a + b\n println("ok")\n}\n' > "$W/i.el" +chk "an inferred Instant + Duration dispatches to the typed wrapper" \ + "1" "$("$ELC" "$W/i.el" 2>/dev/null | grep -c el_instant_add_dur)" +chk "with no signature file, the type is unknown and it does not" \ + "0" "$(EL_SIGNATURES=/nonexistent "$ELC" "$W/i.el" 2>/dev/null | grep -c el_instant_add_dur)" + +printf 'fn main() {\n let a: Instant = now()\n let b: Instant = now()\n let c: Instant = a + b\n println("x")\n}\n' > "$W/b.el" +chk "Instant + Instant is still refused" \ + "1" "$("$ELC" "$W/b.el" 2>/dev/null | grep -c 'TIME_TYPE_ERROR: Instant + Instant')" + +printf 'fn main() {\n let a: Instant = now()\n let d: Duration = el_duration_from_nanos(1)\n let c: Instant = a + d\n println("x")\n}\n' > "$W/g.el" +chk "Instant + Duration is allowed" \ + "0" "$("$ELC" "$W/g.el" 2>/dev/null | grep -c TIME_TYPE_ERROR)" + +echo; echo " 4 assertions, $((4-F)) passed, $F failed"; exit $F diff --git a/lang/tools/check/signatures.rel b/lang/tools/check/signatures.rel new file mode 100644 index 0000000..c0f502c --- /dev/null +++ b/lang/tools/check/signatures.rel @@ -0,0 +1,33 @@ +# signatures.rel — El-level return types for runtime builtins. +# +# el_runtime.h declares every builtin as returning el_val_t, because El has ONE +# type. That single type is why the whole seam is cheap, and it is also why the +# header cannot say that now() returns an Instant while unix_seconds() returns +# an Int. The El-level type is real and the C boundary erases it. +# +# So the compiler needs this, and unlike the other checks it needs it at +# EMISSION time: Instant + Duration must become el_instant_add_dur, and that is +# dispatch, not adjudication. What moved here is the DATA -- previously 19 +# hardcoded names across two functions in codegen.el. What stays in the emitter +# is choosing which call to emit, which is an emitter's actual job. +# +# returns + +now returns Instant +el_now_instant returns Instant +unix_seconds returns Instant +unix_millis returns Instant +instant_from_iso8601 returns Instant +el_instant_add_dur returns Instant +el_instant_sub_dur returns Instant + +el_duration_from_nanos returns Duration +duration_seconds returns Duration +duration_millis returns Duration +duration_nanos returns Duration +el_instant_diff returns Duration +el_duration_add returns Duration +el_duration_sub returns Duration +el_duration_scale returns Duration +el_duration_div returns Duration +ttl_cache_age returns Duration