EXPERIMENT: temporal adjudication moves out; the placeholder stays

The previous pass moved the type DATA and left the judgment inline, which I
stated rather than hid. This finishes it.

PREDICTIONS AND RESULTS
  P1 codegen can emit operand-type relations               TRUE
                                                           "main calls temporal:instant_plus_instant"
  P2 the affine rules are a small closed set as data       TRUE  6 rules
  P3 violations still caught at build time                 TRUE  exit=1
  P4 the reporter leaves codegen                           TRUE  4538 -> 4507
  P5 the TIME_TYPE_ERROR placeholder must STAY             TRUE

P5 is the boundary of this whole approach. The emitter has to emit SOMETHING
for an illegal expression -- it cannot emit nothing and it cannot decide what
the program meant. So the placeholder is irreducible in the same way the AST
traversal was: what moved is the judgment and the wording, not the fact that
something must be written.

The rules are affine algebra and the set is closed because there are only two
kinds of thing. An Instant is a POINT, a Duration is a DISPLACEMENT: add a
displacement to a point, subtract two points for a displacement, combine
displacements. Nothing else is meaningful, which is why the enumeration in
temporal.rel cannot grow the way an allowlist does.

A defect in my own checker, found by running it: the .rel file uses aligned
columns and my awk assumed a single space, so the message came out with the
rule key still prefixed. Same class as the multi-line header parse in the arity
pass -- formatting assumptions that only fail when you look at the output.

98/98 native, 6/6 temporal_query.sh, fixpoint ok.
This commit is contained in:
bigmerge
2026-08-17 09:27:42 -05:00
parent e01e079bda
commit e8e25a07b4
4 changed files with 77 additions and 38 deletions
+7 -38
View File
@@ -589,7 +589,7 @@ fn cg_expr(expr: Map<String, Any>) -> String {
if left_is_ct {
if op == "Plus" {
if right_is_ct {
time_record_violation("caltime_plus_caltime", "CalendarTime + CalendarTime is not allowed (use cal_to_instant + Duration)")
record_call(state_get("__cg_current_fn"), "temporal:caltime_plus_caltime")
return "0 /* TIME_TYPE_ERROR: CalendarTime + CalendarTime */"
}
}
@@ -607,7 +607,7 @@ fn cg_expr(expr: Map<String, Any>) -> String {
return "el_instant_add_dur(" + left_c + ", " + right_c + ")"
}
if right_is_inst {
time_record_violation("instant_plus_instant", "Instant + Instant is not allowed")
record_call(state_get("__cg_current_fn"), "temporal:instant_plus_instant")
return "0 /* TIME_TYPE_ERROR: Instant + Instant */"
}
}
@@ -619,13 +619,13 @@ fn cg_expr(expr: Map<String, Any>) -> String {
return "el_duration_add(" + left_c + ", " + right_c + ")"
}
if is_int_expr(right) {
time_record_violation("duration_plus_int", "Duration + Int is not allowed (use duration_seconds(n) or N.seconds)")
record_call(state_get("__cg_current_fn"), "temporal:duration_plus_int")
return "0 /* TIME_TYPE_ERROR: Duration + Int */"
}
}
if right_is_dur {
if is_int_expr(left) {
time_record_violation("duration_plus_int", "Int + Duration is not allowed")
record_call(state_get("__cg_current_fn"), "temporal:duration_plus_int")
return "0 /* TIME_TYPE_ERROR: Int + Duration */"
}
}
@@ -644,7 +644,7 @@ fn cg_expr(expr: Map<String, Any>) -> String {
return "el_duration_sub(" + left_c + ", " + right_c + ")"
}
if is_int_expr(right) {
time_record_violation("duration_minus_int", "Duration - Int is not allowed")
record_call(state_get("__cg_current_fn"), "temporal:duration_minus_int")
return "0 /* TIME_TYPE_ERROR: Duration - Int */"
}
}
@@ -673,14 +673,14 @@ fn cg_expr(expr: Map<String, Any>) -> String {
if left_is_inst {
if right_is_inst { return "el_instant_lt(" + left_c + ", " + right_c + ")" }
if right_is_dur {
time_record_violation("instant_cmp_duration", "Instant < Duration is not allowed")
record_call(state_get("__cg_current_fn"), "temporal:instant_cmp_duration")
return "0 /* TIME_TYPE_ERROR: Instant < Duration */"
}
}
if left_is_dur {
if right_is_dur { return "el_duration_lt(" + left_c + ", " + right_c + ")" }
if right_is_inst {
time_record_violation("duration_cmp_instant", "Duration < Instant is not allowed")
record_call(state_get("__cg_current_fn"), "temporal:duration_cmp_instant")
return "0 /* TIME_TYPE_ERROR: Duration < Instant */"
}
}
@@ -2353,15 +2353,6 @@ fn is_duration_expr(expr: Map<String, Any>) -> Bool {
// Record a temporal-type violation. Surfaced as `#error` directives at the
// top of the generated C, identical machinery to cap_record_violation.
// kinds: "instant_plus_instant", "duration_plus_int", etc.
fn time_record_violation(kind: String, detail: String) -> Bool {
let csv: String = state_get("__time_violations")
if str_eq(csv, "") { let csv = "," }
let entry: String = kind + ":" + detail
let key: String = "," + entry + ","
if str_contains(csv, key) { return true }
state_set("__time_violations", csv + entry + ",")
return true
}
// Recursive type-propagation: is `expr` known-Int at codegen time?
// This unifies the BinOp(+) dispatch so chained arithmetic over Int
@@ -2542,24 +2533,6 @@ fn float_operand_c(expr: Map<String, Any>, expr_c: String) -> String {
// as a CSV entry "kind:detail" via time_record_violation. Each entry maps
// to a single #error so downstream cc fails the build with a clear El-
// source-level message before the bogus C even links.
fn emit_time_violations() -> Void {
let csv: String = state_get("__time_violations")
if str_eq(csv, "") { return }
if str_eq(csv, ",") { return }
let n: Int = str_len(csv)
let i: Int = 1
while i < n {
let next_comma: Int = str_index_of(str_slice(csv, i, n), ",")
if next_comma < 0 { return }
let entry: String = str_slice(csv, i, i + next_comma)
let colon: Int = str_index_of(entry, ":")
if colon > 0 {
let detail: String = str_slice(entry, colon + 1, str_len(entry))
emit_line("#error \"temporal type error: " + detail + "\"")
}
let i = i + next_comma + 1
}
}
// -- Builtin arity table -------------------------------------------------------
//
@@ -3232,7 +3205,6 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
// Clear capability-violation accumulator from any prior compile.
// Clear arity-violation accumulator from any prior compile.
// Clear temporal-type-violation accumulator from any prior compile.
state_set("__time_violations", "")
// Preamble
emit_line("#include <stdint.h>")
@@ -3498,7 +3470,6 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
// so a misuse of a known builtin (wrong arg count) fails the build
// with a clear message naming the builtin and its expected arity.
// Temporal-type violations (Instant + Instant, Duration + Int, -).
emit_time_violations()
// Return empty string - output was streamed via println
""
@@ -4007,7 +3978,6 @@ fn emit_streaming_preamble(sigs: [Map<String, Any>], source: String) -> Void {
if cgi_count >= 1 { let kind = "cgi" }
if svc_count >= 1 { let kind = "service" }
state_set("__program_kind", kind)
state_set("__time_violations", "")
emit_line("#include <stdint.h>")
emit_line("#include <stdlib.h>")
@@ -4530,7 +4500,6 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
emit_line(" return 0;")
emit_line("}")
emit_blank()
emit_time_violations()
el_arena_pop(main_arena_mark)
""