Skip to content
Merged
74 changes: 74 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2133,6 +2133,79 @@ check-zero-range-negative-control: bin/schema test/zero_range_negative_main.cpp
test/zero_range_negative_main.cpp -o build/schema_test_zero_range_negative
./build/schema_test_zero_range_negative

# THE ENUM-BOUND GATE and its NEGATIVE CONTROL (docs/SPEC-TABLES.md §2.4,
# §11, schema#605). A POSITIONAL ARRAY WHOSE BOUND FOLDS FROM AN ENUM is
# refused in a table body and a union arm, on the bound's PROVENANCE rather
# than its spelling: `[E.Max]T`, `[E.Count]T` and `[N]T` under a `const N`
# that folds from either are one bound however it is spelled.
#
# The gate runs the compiler over test/tables/enumbound, one unit a shape, and
# reads three answers back. Every refused shape must be refused and its
# diagnostic must name the field or the arm, the enum, the constant where the
# bound reaches the enum through one, and `[E]T` as the fix. The THREE
# POSITIVE CONTROLS must compile: the packet wire, whose `[E.Max]T` is a plain
# array the connect gate covers; a bound that folds from no enum, which is
# refused for reaching an enum and never for being positional; and the
# `type`-held case, which is schema#606's ruling and not this refusal's.
#
# The NEGATIVE CONTROL is §2.4's own: it REMOVES THE CONSTANT FOLD FROM THE
# BOUND CHECK through `go build -overlay` (no tracked file is written), and
# every row whose bound reaches its enum through a constant must then compile
# clean. It is targeted rather than blanket, so the two DIRECT spellings must
# stay refused under the same sabotage: a control that turned the whole rule
# off would go red for a reason that says nothing about the fold.
.PHONY: check-enum-bound-negative-control
check-enum-bound-negative-control: bin/schema
@mkdir -p build/enum-bound
@set -e; for u in BodyMax:ShipType.Max BodyCount:ShipType.Count \
BodyConstMax:SlotCount BodyConstCount:SlotCount BodyFolded:SlotCount; do \
unit=$${u%%:*}; bound=$${u#*:}; \
if ./bin/schema check test/tables/enumbound/$$unit.schema > build/enum-bound/$$unit.log 2>&1; then \
echo "GATE FAILED: $$unit.schema compiled in a table body"; exit 1; \
fi; \
grep -q "\[$$bound\]int32 is refused in a table body" build/enum-bound/$$unit.log || \
{ echo "GATE FAILED: $$unit.schema was refused, but not as a table body's own bound"; cat build/enum-bound/$$unit.log; exit 1; }; \
grep -q "spell it \[ShipType\]int32" build/enum-bound/$$unit.log || \
{ echo "GATE FAILED: $$unit.schema names no fix"; cat build/enum-bound/$$unit.log; exit 1; }; \
done
@set -e; for u in ArmMax:ShipType.Max ArmCount:ShipType.Count \
ArmConstMax:SlotCount ArmConstCount:SlotCount ArmFolded:SlotCount; do \
unit=$${u%%:*}; bound=$${u#*:}; \
if ./bin/schema check test/tables/enumbound/$$unit.schema > build/enum-bound/$$unit.log 2>&1; then \
echo "GATE FAILED: $$unit.schema compiled in a union arm"; exit 1; \
fi; \
grep -q "union Payload: arm ships: \[$$bound\]int32 is refused in a union arm" build/enum-bound/$$unit.log || \
{ echo "GATE FAILED: $$unit.schema was refused, but not at the arm"; cat build/enum-bound/$$unit.log; exit 1; }; \
grep -q "table Fleet's field payload reaches Payload" build/enum-bound/$$unit.log || \
{ echo "GATE FAILED: $$unit.schema names no table reaching the union"; cat build/enum-bound/$$unit.log; exit 1; }; \
done
@set -e; for unit in BodyConstMax BodyConstCount BodyFolded ArmConstMax ArmConstCount ArmFolded; do \
grep -q "the bound SlotCount folds from ShipType\." build/enum-bound/$$unit.log || \
{ echo "GATE FAILED: $$unit.schema names no constant, and the constant is all a reader can see"; cat build/enum-bound/$$unit.log; exit 1; }; \
done
@set -e; for unit in ControlPacket ControlPlain ControlTypeHeld; do \
./bin/schema check test/tables/enumbound/$$unit.schema > build/enum-bound/$$unit.log 2>&1 || \
{ echo "POSITIVE CONTROL FAILED: $$unit.schema did not compile"; cat build/enum-bound/$$unit.log; exit 1; }; \
done
@echo "gate: ten shapes refused on the bound's provenance, three controls compile"
@sed 's|if entry == nil \|\| entry.decl == nil \|\| visiting\[e.Name\] {|if true { // SABOTAGED: the constant fold removed from the bound check|' \
internal/check/tablekeyed.go > build/tablekeyed-no-fold.gotext
@grep -q SABOTAGED build/tablekeyed-no-fold.gotext || \
{ echo "NEGATIVE CONTROL FAILED: the sabotage patched nothing"; exit 1; }
@printf '{"Replace":{"%s/internal/check/tablekeyed.go":"%s/build/tablekeyed-no-fold.gotext"}}\n' \
"$(CURDIR)" "$(CURDIR)" > build/enum-bound-no-fold-overlay.json
@go build -overlay=build/enum-bound-no-fold-overlay.json -o build/schema-no-enum-bound-fold ./cmd/schema
@set -e; for unit in BodyConstMax BodyConstCount BodyFolded ArmConstMax ArmConstCount ArmFolded; do \
./build/schema-no-enum-bound-fold check test/tables/enumbound/$$unit.schema > build/enum-bound/$$unit-nofold.log 2>&1 || \
{ echo "NEGATIVE CONTROL FAILED: $$unit.schema was still refused without the constant fold, so the fold is not what refuses it"; cat build/enum-bound/$$unit-nofold.log; exit 1; }; \
done
@set -e; for unit in BodyMax BodyCount ArmMax ArmCount; do \
if ./build/schema-no-enum-bound-fold check test/tables/enumbound/$$unit.schema > build/enum-bound/$$unit-nofold.log 2>&1; then \
echo "NEGATIVE CONTROL FAILED: $$unit.schema compiled too, so the sabotage removed the whole rule and not the fold"; exit 1; \
fi; \
done
@echo "negative control: without the constant fold, six folded rows compile clean and the four direct spellings stay refused"

# THE VARIANT-ORDER NEGATIVE CONTROL (SPEC §3.1, issue #462). An enum value
# rides as its declaration ordinal and a flags variant as its bit position, so
# the projection carries both declarations' variant names in declaration order:
Expand Down Expand Up @@ -2822,6 +2895,7 @@ test: build/schema_test build/schema_test_guard build/schema_test_tables build/s
$(MAKE) wide-table-odd-length-negative-control
$(MAKE) wide-table-byte-length-negative-control
$(MAKE) check-zero-range-negative-control
$(MAKE) check-enum-bound-negative-control
$(MAKE) projection-variant-order-negative-control
$(MAKE) projection-wire-law-negative-control
$(MAKE) projection-union-arm-order-negative-control
Expand Down
28 changes: 12 additions & 16 deletions docs/SPEC-TABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1182,17 +1182,15 @@ diagnostic does not name the field, the enum and the fix.
and every row whose bound reaches its enum through a constant compiles
clean without it.

**CHECKER STATUS: `[E.Max]T` IS REFUSED, THE OTHER SPELLINGS ARE NOT.**
`schema check` refuses `[E.Max]T` in a table body and in a union arm, naming
the field, the enum and `[E]T` as the fix. It accepts `[E.Count]T` and `[N]T`
under a `const N` that folds from either, with no diagnostic and exit 0, so a
unit that spells the bound either of those ways compiles and carries the
positional class this rule exists to close. The rule above follows the bound's
PROVENANCE and the checker still follows its spelling, and closing that gap is
owed as schema#540. Two sections rest on the refusal being made whole, §4.1's
**CHECKER STATUS: THE REFUSAL FOLLOWS THE PROVENANCE.** `schema check` refuses
every spelling above in a table body and in a union arm, reading the bound's
provenance and not its text: `[E.Max]T`, `[E.Count]T`, and `[N]T` under a
`const N` that folds from either at any depth of constant arithmetic. The
diagnostic names the field, the enum, the constant where the bound reaches the
enum through one, and `[E]T` as the fix; an arm's names the arm and the table
that reaches the union. Two sections rest on this refusal being whole, §4.1's
count of the silent class and SPEC.md §3.1's one exception to reachability, and
each is written from this rule rather than from the tree. This paragraph is
deleted by the implementation PR that closes the gap.
both stand on the tree as well as on the rule.

**RULING STATUS: the type-held case is ruled on schema#606.** Until then a
`type` no table reaches keeps the spelling and a `type` a table reaches is
Expand Down Expand Up @@ -9843,12 +9841,10 @@ in build version (§20.5).
table closure, `| max = K` headroom and variant id collisions, each
diagnostic naming the keying field that pulled the enum in. A slot value no variant names is a SAVE failure, not a silent `None`
(§3.2).
**CHECKER STATUS: `[E.Max]T` is refused in a table body and in a union arm.
`[E.Count]T` and `[N]T` under a `const N` that folds from either are
accepted there today with no diagnostic**, because the checker still
follows the spelling where the rule follows the provenance, owed as
schema#540 (§2.4), and this sentence is deleted by the implementation PR
that closes the gap.
**CHECKER STATUS: `[E.Max]T`, `[E.Count]T` and `[N]T` under a `const N` that
folds from either are all refused in a table body and in a union arm**, on
the bound's provenance, and an arm's diagnostic names the arm and the table
that reaches the union (§2.4).
**RULING STATUS: the type-held case is ruled on schema#606**, and until
then a `type` a table reaches is not refused (§2.4).
- **Maps** (§2.8): a map in a `type` body; a key that is an enum (the
Expand Down
8 changes: 4 additions & 4 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1938,10 +1938,10 @@ Only the table wire keys the slots.
**And a positional array whose bound comes from an enum is REFUSED in a table
body and a union arm, by name**, with `[E]T` named as the fix. The refusal
follows where the bound comes from and not how it is spelled, so `[E.Max]T`,
`[E.Count]T` and `[N]T` under a `const N = E.Max` all take it. *The compiler
refuses `[E.Max]T` today and still reads the other two as plain bounds, so
`[E.Count]T` and the constant fold still
compile ([#540](https://github.com/mas-bandwidth/schema/issues/540)).*
`[E.Count]T` and `[N]T` under a `const N = E.Max` all take it, at any depth of
constant arithmetic. The diagnostic names the constant where the bound reaches
the enum through one, and an arm's names the arm and the table that reaches
the union.
An ordinal-indexed array is a positional
vocabulary, and a table has exactly one of those — `flags` — so the refusal is
what keeps the closed class closed: you cannot reopen it by spelling the bound
Expand Down
7 changes: 1 addition & 6 deletions docs/VERSIONING.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ does today.
| a flags variant inserted or removed | silent | refuses | moves, and so does the protocol id |
| a flags variant reordered or renamed in place | **silent** | **refuses** | moves: the cook projection digests each variant's bit position, and the protocol id moves too |
| a union arm reordered or renamed | `unknown` for an arm this reader lacks; a reorder is silent and safe | warns on a vanished name | moves, and the protocol id with it where a `type` reaches the union: the arm names are what a same-typed reorder moves |
| a keyed array made positional | `kind_mismatch` | refuses; and in a TABLE body the positional spelling is refused by name (SPEC-TABLES.md §2.4, §11) | moves |
| a keyed array made positional | `kind_mismatch` | refuses; and in a TABLE body and in a UNION ARM the positional spelling is refused by name, on the bound's provenance, so `[E.Max]T`, `[E.Count]T` and `[N]T` under a `const N` that folds from either are all refused there (SPEC-TABLES.md §2.4, §11) | moves |
| a keyed array's key enum swapped for another | `unknown`, one per slot; the kind stays | refuses | moves |
| a map's KEY kind changed, or its KEY bound tightened (SPEC-TABLES.md §2.8) | a changed kind is one `kind_mismatch` for the map, which reads empty. A tightened bound drops the entries that no longer fit and counts `clamped`, one per entry | **refuses** a changed kind, warns on a tightened bound | moves |
| an array changed between `[]T` and `[..N]T` (SPEC-TABLES.md §2.9) | nothing where the count fits the new bound, `clamped` past it: the two are the same bytes | warns on the direction that ADDS a bound, as any capacity shrunk; passes on the one that removes it | moves: the storage is a reference and a count on one side and the maximum inline on the other |
Expand Down Expand Up @@ -1071,11 +1071,6 @@ repository not yet behind it. The 3.0.0 release holds the list at zero.
- #432: the cook triple, and the byte-order sentences in five places.
- #441: the retired-names ledger.
- #446: the evolution table's fixtures.
- #540: the refusal following the bound's PROVENANCE rather than its spelling.
`[E.Max]T` is refused in a table body and a union arm, but `[E.Count]T` and
`[N]T` under a `const N` that folds from either still compile there, which
SPEC-TABLES.md §2.4 and §11 state as refused, so those two spellings reopen
the class §4.1 closed. The `type`-held case is ruled on #606.
- #525: retain-unknown in the eight ports, `internal/tablewire`'s own
retention and the fuzzer leg that needs it, the MESSAGE form's `LoadRetain`,
and the conformance rows. The C++ reference and the two report counters are
Expand Down
12 changes: 9 additions & 3 deletions internal/check/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -1464,9 +1464,14 @@ func (c *checker) resolveField(owner string, f *ast.Field, inTable bool) *ir.Fie
out.ArrayBound = out.KeyEnumRef.Max
out.ArrayExpr = f.Array.Hi
default:
if !c.checkPositionalKeyedSpelling(f, inTable) {
return nil
}
// A POSITIONAL BOUND THAT FOLDS FROM AN ENUM is refused in a
// table body and a union arm on the bound's PROVENANCE
// (docs/SPEC-TABLES.md §2.4, §11), and that reads a closure
// rather than a field, so it runs in
// checkPositionalEnumBoundInClosure once the closure is known.
// The bound still evaluates here: the refusal reports on the
// resolved field, and a bound that cannot be evaluated is a
// different diagnostic that belongs at the field.
hi, ok := c.evalInt(f.Array.Hi)
if !ok {
return nil
Expand Down Expand Up @@ -2246,6 +2251,7 @@ func (c *checker) checkTables() {
}
c.tableClosure = closure
c.checkTableArmsReached(closure)
c.checkPositionalEnumBoundInClosure()

names := make([]string, 0, len(closure))
for name := range closure {
Expand Down
Loading
Loading