@@ -282,10 +282,11 @@ proposition whose CEL evaluation would have errored:
282282 under ` Cel.mulOk x.toInt64 100 ` — so the guard is exactly CEL's condition
283283 and an intermediate outside the 32-bit range is not an overflow, matching
284284 CEL. Division and modulo widen too (fixing ` int32Min / -1 ` , which wraps in
285- ` Int32 ` but not in CEL). Without that typing (values bound by a
286- comprehension variable, elements reached through indexing) the guard falls
287- back to the operand's own Lean type, so ` Int32 ` /` UInt32 ` operands demand the
288- 32-bit range — conservatively stricter than CEL, sound but not exact.
285+ ` Int32 ` but not in CEL). Comprehension binders and indexed elements are typed
286+ too (see below), so this reaches inside ` all ` /` exists ` bodies; only values
287+ with no proto type at all (free identifiers) fall back to the operand's own
288+ Lean type, where ` Int32 ` /` UInt32 ` operands demand the 32-bit range —
289+ conservatively stricter than CEL, sound but not exact.
289290 ` Nat ` size arithmetic guards truncating subtraction. Constant arithmetic
290291 folds and range-checks at generation time (an always-erroring rule is
291292 rejected); concatenation and timestamp/duration arithmetic are total and
@@ -298,15 +299,18 @@ proposition whose CEL evaluation would have errored:
298299 scalar field rule, ` this ` -rooted select paths in message rules (including
299300 map-selection leaves and enum ints, which CEL compares as ` int32 ` ), and
300301 custom CEL on ` repeated.items ` /` map.keys ` /` map.values ` elements; it also
301- rejects negative literals against unsigned fields and ` double ` literals
302- outside ` Float32 ` 's finite range on a ` float ` field. Literals meeting a
302+ rejects negative literals against unsigned fields. Literals meeting a
303303 * widened* arithmetic result are checked at 64-bit width instead, so
304304 ` this * 10000 <= 5000000000 ` on an ` int32 ` field is accepted — the product
305305 is ` Int64 ` . The same typing makes mixed-width comparisons elaborate
306306 (` this.small < this.big ` across ` int32 ` /` int64 ` widens the narrower side).
307- Values with no resolvable proto type — comprehension binders
308- (` this.all(v, v > 3000000000) ` ), indexed elements, free identifiers — are
309- * not* checked and can still wrap.
307+ - ** Container elements are typed** : a path may descend into a container, so
308+ the value a comprehension binds (` this.all(v, …) ` — a repeated field's
309+ elements, a map's keys) and the value indexing yields (` this.items[0] ` — an
310+ element, a map's value) carry the same descriptor typing a named field does.
311+ Paths compose through binders, so ` this.all(o, o.lots.all(n, n > 3e9)) ` is
312+ range-checked at the innermost element's type. This closes the last
313+ silent-wrap corner: only free identifiers now go untyped.
310314- ** Indexing** : CEL errors on an out-of-range index or missing map key, so
311315 ` l[i] ` / ` m['k'] ` translate to ` (l[i]?).get h ` under a pending
312316 ` (l[i]?).isSome ` guard rather than a panicking ` l[i]! ` . Guards discharge at
@@ -320,20 +324,30 @@ proposition whose CEL evaluation would have errored:
320324- ** Enums work as ints** : the plugin resolves every ` this ` path against the
321325 descriptors, so enum-typed values (field rules, message-rule leaves,
322326 repeated/map element rules) render through the generated enum's ` .toInt32 `
323- view, matching CEL's enum-as-int semantics. Standard enum rules keep their
324- constructor-test form. (Enum values reached through * indexing * — an
325- enum-valued ` m['k'] ` compared to an int — still fail at Lean elaboration.)
327+ view, matching CEL's enum-as-int semantics — including enum values a
328+ comprehension binds or indexing reaches ( ` this.tier_history[0] >= 1 ` ), under
329+ the index guard. Standard enum rules keep their constructor-test form.
326330- ** Map selection sugar works** : ` this.labels.priority ` becomes guarded key
327331 indexing (missing key ⇒ false, CEL's error), and ` has(this.labels.priority) `
328332 becomes key presence — at any path depth.
329333- Warnings (stderr / JSON) flag translations with caveats: ` has() ` presence
330334 semantics beyond ` Option ` fields, evaluation-time-dependent ` now ` ,
331335 placeholder timestamp/duration types, free identifiers kept verbatim,
332336 non-literal regex patterns.
333- - ` string(e) ` uses ` toString ` , whose formatting may diverge from CEL's for
334- floats. ` float ` fields compare at ` Float32 ` precision where CEL compares at
335- double precision, so a ` double ` literal that is not exactly representable in
336- ` Float32 ` rounds (its * magnitude* is range-checked, its precision is not).
337+ - ** ` float ` fields are compared at double precision** , as CEL does. A proto
338+ ` float ` is Lean ` Float32 ` , but CEL converts it to a double before any numeric
339+ operation, so a typed ` float ` value is emitted as ` x.toFloat ` wherever it
340+ meets a literal, another numeric field, or arithmetic (` this <= 1.1 ` on a
341+ ` float ` field becomes ` x.toFloat ≤ 1.1 ` ). Widening Float32 → Float is exact,
342+ so this only removes a rounding: without it the literal would elaborate at
343+ ` Float32 ` and ` this == 1.1 ` would be * satisfiable* in Lean (by the ` Float32 `
344+ nearest 1.1) while being unsatisfiable in CEL — the kernel proving a
345+ proposition whose CEL evaluation is false. Standard ` float.* ` rules are left
346+ narrow deliberately: their bounds come from the descriptor and are already
347+ exactly ` Float32 ` values, so the comparison cannot round either way. A
348+ ` float ` /` double ` field comparison now elaborates too (the ` float ` side
349+ widens). What remains: ` string(e) ` uses ` toString ` , whose formatting may
350+ diverge from CEL's for floats.
337351
338352## Codegen semantics and limitations
339353
@@ -542,17 +556,15 @@ non-generated, option-only proto dependencies such as
542556 now-dependent rules (` timestamp.lt_now ` /` within ` ), which need a
543557 validation-time story for ` Cel.now ` .
5445583 . Residual conservative spots (sound, but stricter than CEL or late-failing).
545- Descriptor-aware numerics closed the literal-wrapping hole and made
546- arithmetic guards CEL-exact wherever a rule's ` this ` -rooted path resolves
547- to a proto field; what is left is where no such path exists:
548- - values bound by a comprehension variable (` this.all(v, v * 2 > 0) ` ) or
549- reached through indexing (` this.items[0].qty ` ) carry no descriptor type,
550- so their literals are unchecked and their 32-bit guards stay
551- conservative — and enum values reached through indexing still lack the
552- ` .toInt32 ` view;
559+ Descriptor-aware numerics — now reaching comprehension binders and indexed
560+ elements — closed the literal-wrapping hole and made arithmetic guards
561+ CEL-exact wherever a rule's value resolves to a proto type. What is left:
562+ - free identifiers carry no descriptor type, so their literals are
563+ unchecked and their 32-bit guards stay conservative;
553564 - error absorption under negation (` !(err && false) ` ) translates to
554565 failure;
555- - ` float ` fields compare at ` Float32 ` precision, CEL at double precision.
566+ - index proofs inside ` exists_one ` /` map ` /` filter ` bodies and negated
567+ quantifier bodies have no sound discharge point and are rejected.
5565684 . No formal relation between ` Protovalidate.Cel.Regex ` and RE2 beyond the
557569 per-pattern differential batteries (see
558570 [ regex assurance] ( #regex-assurance-what-the-guards-do-and-do-not-say ) ), nor
0 commit comments