Documentation
¶
Overview ¶
Package typeimmutable is a nogo analyzer that enforces the immutability RFC-234 relies on: a `values.Type` graph may be written only by the function that built it.
Why this has to be a build-time gate rather than a test ¶
`ExactTypeHandle.Type`, `QuantifiedObjectValue.FlowedType` and `fieldValue.Type` return the thawed graph CACHED ON AN INTERNED HANDLE. They used to return a fresh copy per call, and that copy was the largest single object allocation in the planner — 175.7M objects per sweep, of which those three accessors were 73%. Sharing removed 128.9M of them.
What the copy bought was a permission nothing in production used: a census over 103 packages found 21 writes into a Type field and every one is into a graph its own function had just constructed. But "nothing does this today" is not a property, it is an observation, and the first write to a shared graph does not corrupt one caller's copy — it corrupts every value flowing that shape, process-wide, because the graph hangs off an INTERNED node. That failure is silent, order-dependent, and crosses parallel tests.
Why it covers tests ¶
Because that is where the mutation actually lives. Deduped by position, 60 of the 81 mutation sites in this repo are in `_test.go`, and the test side writes seven fields production never touches at all (`RecordName`, `Fields`, `Nullable`, `TypeCode`, `ElementType`, `InnerType`, `PrimitiveType.Nullable`). A gate scoped to production sources would have watched the half that was already clean. It is not hypothetical either: flipping the accessors with the old tests in place made ONE test's mutation reach TWO unrelated tests through the intern table.
The rule, and what it does NOT catch ¶
The not-covered list first, because a gate's scope sentence written from the code it just matched is how these over-claim.
- A write reached through a function CALL is invisible: `mutate(rt)` where `mutate` writes `rt.Nullable` is diagnosed at `mutate`'s own body if that body is in an analyzed package, and not at the call site. Interprocedural provenance is not attempted.
- A write through a value stored in a container (`m[k].Fields[i].Name = …` via a map of pointers, a slice of `*RecordType`) resolves its root to the container, not to the graph, and is refused only if the container itself is not local — which is stricter than the truth, not looser.
- Reflection, `unsafe`, and `encoding/gob`-style rehydration are not modelled at all.
- Generated code excluded via nogo_config, as for every analyzer here.
What it DOES catch, by shape:
- a write to a field of a Type reached from a PARAMETER, a call result, a receiver's field, or a package-level var — anything the function did not build;
- a write THROUGH a reference (a slice index or a pointer deref) where the reference was not itself locally constructed — which is the shallow copy-on-write hazard: `withLegs := *record` copies the struct header and SHARES `Fields`, so `withLegs.Fields[i].Name = …` writes the interned graph while looking local;
- `++`/`--`, which is `*ast.IncDecStmt` and not an assignment;
- `&t.Field`, which hands a writable pointer out of the function.
A write to a direct field of a locally-copied struct (`withLegs.Legs = legs`) is ALLOWED: that writes the copy's own header and is the sanctioned copy-on-write form.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var Allowlist = map[string]map[string]string{
"pkg/recordlayer/query/plan/cascades/values/qov_source_layout.go": {
"restoreQOVRecordLayout": "writes a parameter; sole external caller passes a private thaw()",
},
"pkg/recordlayer/query/plan/cascades/values/ordinal_layout_test.go": {
"TestOrdinalLayoutSnapshotsEveryMutableInputAndGetter": "mutates its own fixture inputs to prove the snapshot is isolated",
},
"pkg/recordlayer/query/plan/cascades/plan_leg_concat_layout_test.go": {
"TestPlanLegConcatLayout_ReturnsADefensiveCopy": "writes the returned layout to prove it is a copy; the write is the assertion",
},
"pkg/relational/core/query/inline_values_translation_test.go": {
"TestInlineValuesTranslationRejectsCollectionTypeDrift": "mutates a collection graph to simulate drift; declining is the assertion",
},
"pkg/recordlayer/query/plan/cascades/values/proto_field_exact_type_test.go": {
"TestFieldTypeForProtoFieldReturnsIndependentTypeGraphs": "asserts the callee's freshness contract; the mutation IS the test",
},
}
Allowlist maps a repo-relative file path (matched as a path suffix) to the function names within it that are permitted to write a Type they did not build. Every entry is a deliberate contract that must be stated at the site.
var Analyzer = newAnalyzer(Allowlist)
Analyzer is the nogo entry point.
Functions ¶
This section is empty.
Types ¶
This section is empty.