EXPERIMENT: temporal types as data — and the pass that GREW the compiler
This block is structurally unlike the previous four. It does not only
adjudicate, it DISPATCHES: Instant + Duration must become el_instant_add_dur,
LocalDate + Duration must become el_local_date_add_dur. The emitted C depends on
the type answer, so it cannot move to a post-hoc query. Selecting which call to
emit is an emitter's actual job.
PREDICTIONS AND RESULTS
P1 the block conflates dispatch with adjudication TRUE
P2 adjudication can move, dispatch cannot TRUE
P3 this pass shrinks codegen far less than the last TRUE, and worse:
4513 -> 4537, it GREW
by 24 lines
P4 the rules are affine algebra, closed by construction TRUE
P5 no type propagation -- name tracking plus a
hardcoded list of which builtins return which type TRUE, 19 names
P3 is the honest result and it is not spun: moving 19 names into a data file
cost more lines than it saved, because a generic loader is larger than the
enumeration it replaces. The win is not line count. It is that adding a 20th
temporal builtin is now a one-line edit to signatures.rel instead of a compiler
change, and that the data is inspectable.
WHY THE HEADER CANNOT SUPPLY THIS, unlike arity: 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 exactly why the C boundary cannot say that
now() returns an Instant while unix_seconds() returns an Int. The El-level type
is real and the boundary erases it.
INCOMPLETE, and stated rather than hidden: P2 said adjudication could move to a
query. It has NOT. Violations still emit TIME_TYPE_ERROR inline from the
emitter. Only the type DATA moved. Moving the adjudication needs the operand
types recorded as relations, which is a further pass.
98/98 native, 4/4 temporal_signatures.sh, fixpoint ok.
This commit is contained in:
@@ -2010,40 +2010,65 @@ fn is_float_call(call_expr: Map<String, Any>) -> Bool {
|
|||||||
// Builtins that return an Instant. Used by is_instant_expr and the BinOp
|
// 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
|
// dispatch - `now() + 5.seconds` types as Instant only because we can see
|
||||||
// that now() is an Instant-returning Call.
|
// that now() is an Instant-returning Call.
|
||||||
fn is_instant_call(call_expr: Map<String, Any>) -> Bool {
|
// load_signatures — El-level return types, read once from
|
||||||
let func = call_expr["func"]
|
// tools/check/signatures.rel. Previously 19 hardcoded names across
|
||||||
let fk: String = func["expr"]
|
// is_instant_call and is_duration_call. The header cannot carry these: it
|
||||||
if !str_eq(fk, "Ident") { return false }
|
// declares everything as el_val_t, because El has one type.
|
||||||
let name: String = func["name"]
|
fn load_signatures() -> Void {
|
||||||
if str_eq(name, "now") { return true }
|
if !str_eq(state_get("__sig_loaded"), "") { return }
|
||||||
if str_eq(name, "el_now_instant") { return true }
|
state_set("__sig_loaded", "1")
|
||||||
if str_eq(name, "unix_seconds") { return true }
|
let path: String = env("EL_SIGNATURES")
|
||||||
if str_eq(name, "unix_millis") { return true }
|
if str_eq(path, "") { let path = "tools/check/signatures.rel" }
|
||||||
if str_eq(name, "instant_from_iso8601") { return true }
|
if !fs_exists(path) { return }
|
||||||
if str_eq(name, "el_instant_add_dur") { return true }
|
let lines = str_split_lines(fs_read(path))
|
||||||
if str_eq(name, "el_instant_sub_dur") { return true }
|
let n: Int = native_list_len(lines)
|
||||||
return false
|
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 call_returns(expr: Map<String, Any>, want: String) -> Bool {
|
||||||
fn is_duration_call(call_expr: Map<String, Any>) -> Bool {
|
load_signatures()
|
||||||
let func = call_expr["func"]
|
let func = expr["func"]
|
||||||
let fk: String = func["expr"]
|
let fk: String = func["expr"]
|
||||||
if !str_eq(fk, "Ident") { return false }
|
if !str_eq(fk, "Ident") { return false }
|
||||||
let name: String = func["name"]
|
let name: String = func["name"]
|
||||||
if str_eq(name, "el_duration_from_nanos") { return true }
|
return str_eq(state_get("__sig_" + name), want)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_instant_call(expr: Map<String, Any>) -> Bool {
|
||||||
|
call_returns(expr, "Instant")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Builtins that return a Duration. Same role as is_instant_call.
|
||||||
|
fn is_duration_call(expr: Map<String, Any>) -> Bool {
|
||||||
|
call_returns(expr, "Duration")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Phase 1.5 - Calendar / CalendarTime / Rhythm / LocalDate / LocalTime /
|
// Phase 1.5 - Calendar / CalendarTime / Rhythm / LocalDate / LocalTime /
|
||||||
// LocalDateTime / Zone are first-class boxed types. Each has its own name
|
// LocalDateTime / Zone are first-class boxed types. Each has its own name
|
||||||
// set in process state, populated from typed `let` bindings and parameter
|
// set in process state, populated from typed `let` bindings and parameter
|
||||||
|
|||||||
+28
@@ -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 <elc>}"
|
||||||
|
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
|
||||||
@@ -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.
|
||||||
|
#
|
||||||
|
# <builtin> returns <El type>
|
||||||
|
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user