From c48db6c2a8f3e2a6dbd2f2edea1c61c9a07ed406 Mon Sep 17 00:00:00 2001 From: bigmerge Date: Mon, 17 Aug 2026 09:49:50 -0500 Subject: [PATCH] ANSWER: is `3` a position, or a convention we agreed on? Both, at different layers, and the split is the same as everywhere else. The NUMERAL is convention -- int_to_str was already form 1, because no position determines that twelve is written 1 then 2 in base ten. The NUMBER is a position: three things are three things regardless of notation. But the sharper answer follows from `love = 0`. A bare `3` is a MAGNITUDE WITH NO AXIS. It is not a position until something gives it a direction, which is exactly why 3.days needs a calendar and why time_add(t, n, "min") had to carry its axis as a string. PREDICTIONS AND RESULTS P1 numeral = convention, number = position TRUE P2 a bare literal is dimensionless until context types it TRUE P3 there is a measurable place where El guesses TRUE P4 Instant + Int is not caught though Duration + Int is TRUE P5 the rule catches it TRUE P6 nothing legitimate in the tree relies on it TRUE P3/P4 IS THE DEFECT, and it was found by reasoning from the philosophy and then measured. Duration + Int was refused -- "an Int carries no unit" -- while let t: Instant = now() let u: Instant = t + 3 compiled to raw (t + 3) and reported CLEAN. Adding a dimensionless number to a point is worse than adding it to a displacement: it silently moves the instant by an unspecified amount. 3 of what? Whatever the representation happens to be, which is the leak itself. The asymmetry had no justification; the rule was simply never written. P6 MATTERED. Two calendar tests looked like Instant + Int: let later: Instant = i + 1.hour let later: Instant = base + 15.hours They are not. `1.hour` lexes to a Duration -- el_duration_from_nanos(1LL * 3600000000000LL) -- and both stay clean. That is the whole answer demonstrated in one line: t + 3 is refused because 3 has no axis; t + 1.hour is accepted because .hour supplies one. 104/104 native + 2 new, integration green, fixpoint ok. --- lang/el-compiler/src/codegen.el | 9 +++++++++ lang/tests/native/test_compiler.el | 20 ++++++++++++++++++++ lang/tools/check/temporal.rel | 1 + 3 files changed, 30 insertions(+) diff --git a/lang/el-compiler/src/codegen.el b/lang/el-compiler/src/codegen.el index ebd9829..380e32a 100644 --- a/lang/el-compiler/src/codegen.el +++ b/lang/el-compiler/src/codegen.el @@ -610,6 +610,15 @@ fn cg_expr(expr: Map) -> String { record_call(state_get("__cg_current_fn"), "temporal:instant_plus_instant") return "0 /* TIME_TYPE_ERROR: Instant + Instant */" } + // A bare literal is a MAGNITUDE WITH NO AXIS. Duration + Int + // was already refused because an Int carries no unit; adding + // one to a POINT is worse, because it silently moves the + // instant by an unspecified amount. The asymmetry had no + // justification -- it was simply never written. + if is_int_expr(right) { + record_call(state_get("__cg_current_fn"), "temporal:instant_plus_int") + return "0 /* TIME_TYPE_ERROR: Instant + Int */" + } } if left_is_dur { if right_is_inst { diff --git a/lang/tests/native/test_compiler.el b/lang/tests/native/test_compiler.el index 177fb15..cf95597 100644 --- a/lang/tests/native/test_compiler.el +++ b/lang/tests/native/test_compiler.el @@ -967,3 +967,23 @@ test "test-keyword-is-still-reserved" { let out: String = compile_capture(src) assert str_contains(out, "int main"), "the suite still compiles, which requires test to remain a keyword" } + +// ── A bare literal is a magnitude with no axis ─────────────────────────────── +// +// Duration + Int was already refused because an Int carries no unit. Adding one +// to a POINT is worse: it moves the instant by an unspecified amount. The +// asymmetry had no justification; it was simply never written. + +test "instant-plus-bare-int-is-refused" { + let src: String = "fn main() { let t: Instant = now() let u: Instant = t + 3 println(\"x\") }" + let out: String = compile_capture(src) + assert str_contains(out, "TIME_TYPE_ERROR: Instant + Int"), "3 of what?" +} + +test "instant-plus-unit-suffix-is-allowed" { + // .hour supplies the axis, so the magnitude becomes a displacement. + let src: String = "fn main() { let t: Instant = now() let u: Instant = t + 1.hour println(\"x\") }" + let out: String = compile_capture(src) + assert str_contains(out, "el_instant_add_dur"), "a unit suffix makes it a Duration" + assert !str_contains(out, "TIME_TYPE_ERROR"), "and the addition is legal" +} diff --git a/lang/tools/check/temporal.rel b/lang/tools/check/temporal.rel index ae8fb16..50d49c9 100644 --- a/lang/tools/check/temporal.rel +++ b/lang/tools/check/temporal.rel @@ -11,6 +11,7 @@ # means instant_plus_instant means Instant + Instant is not allowed — a point plus a point is not a point. Subtract them for a Duration, or add a Duration. +instant_plus_int means Instant + Int is not allowed — a bare literal is a magnitude with no axis. 3 of what? Adding it to a point moves the instant by an unspecified amount. Use a Duration. duration_plus_int means Duration + Int is not allowed — an Int carries no unit. Use duration_seconds(n) or N.seconds. duration_minus_int means Duration - Int is not allowed — an Int carries no unit. instant_cmp_duration means Instant < Duration is not allowed — a point and a displacement are not on the same scale.