feat!: name-based linkage, field-side declarations, and anchored timers (v4.0.0) - #54
Merged
Conversation
…rs (v4) Order-based linkage between serializable fields and their companion methods is removed. Method-side attributes take nameof(_field); every linkage can alternatively be declared on the field itself with the new [SaveFlag], [FieldChanged], and [DeserializeTimer] attributes, and both spellings generate identical code. Timers consolidate into [DeserializeTimer(nameof(Method))] on the timer field, replacing [TimerDrift] and [DeserializeTimerField]. Drift is the default and now stores the timer's next tick as anchored time, so downtime does not consume the remaining delay; wallClock: true keeps absolute deadlines with the unchanged wire format. The restart method is invoked only when a timer was running at save - no more TimeSpan.MinValue sentinel. The legacy "@TimerDrift" delta-time read survives permanently for old-version migration schemas, proven by a fixture whose v0 schema replays ReadDeltaTime while the current version reads anchored time. Removed attributes remain as error-[Obsolete] shells whose messages are the migration instructions. New diagnostics: SG3014 (unknown field reference), SG3015 (linked method missing or wrong signature - save flag, default, and changed shapes were previously never validated and produced broken output on mistakes), SG3016 (both linkage styles on one field), SG3017 (default value without a save flag, previously silently ignored). SG3008 now points at the field-side declaration. Also fixes the README install snippet, which named a nonexistent package id at a stale version, and documents the v4 patterns and migration. Back-to-back benchmarks against main on identical machine state show no measurable impact (warm single-edit 18.7 -> 19.8ms, cold ~equal, overlapping error bars). Packages bump to 4.0.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SerializableFieldSaveFlag, SerializableFieldDefault, and SerializableFieldChanged become error-[Obsolete] shells. With linkage declared only on the field, the mistakes those diagnostics guarded become unrepresentable: a default cannot exist without a save flag, a linkage cannot reference a missing field, duplicates are compiler-enforced, and styles cannot conflict. SG3010/SG3014/SG3016/SG3017 are retired; SG3015 remains the single method-resolution error. Snapshot Expected files are unchanged, proving the conversion is emission-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The callback is invoked by the generated setter, making it a generated-property option like the getter/setter/isVirtual arguments - not a serialization concern. As an argument of [SerializableField] it is unrepresentable on a [SerializableProperty] (whose user-written setter it could never hook), closing the silent-ignore case. The standalone [FieldChanged] attribute, introduced earlier in this unreleased major, is deleted outright. New SG3018 rejects a fieldChanged callback on a field with no generated setter (readonly, or omitted setter), where it could never fire. Snapshot Expected files remain unchanged: emission-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bool Method(ref T value), invoked by the generated setter after the equality check and before assignment: it may coerce the incoming value through the ref parameter, and returning false rejects the change. The field still holds the old value while the gate runs, so no oldValue parameter is needed. A survey of all 204 [SerializableProperty] sites in the engine found value coercion (clamps, normalization) to be the single largest reason for hand-written setters; with this gate plus fieldChanged, roughly three quarters of them are expressible as plain [SerializableField] declarations. Hand-written properties remain for custom getters and exotic setter semantics. SG3018 is generalized to cover any setter hook declared without a generated setter. The gate emission is pinned in the FieldSideLinkage snapshot (compile- gated) and unit tests assert the gate -> assign -> fieldChanged pipeline order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The v4 redesign of field↔method linkage and timer serialization, per the agreed design: order-based linkage is removed outright (v3.0.0 is the last release supporting it), linkage is declared on the field only, and timers become intuitive, anchored, and gated.
New surface
One linkage style — everything is declared on the serializable field, naming its companion methods:
The generated setter pipeline is equality check → allowFieldChange (coerce/veto) → assign → dirty tracking → fieldChanged. The gate needs no oldValue parameter: it runs before assignment, so the field itself still holds the old value.
The method-side attributes (
[SerializableFieldSaveFlag],[SerializableFieldDefault],[SerializableFieldChanged]) are removed entirely. Field-side declaration makes the old failure modes unrepresentable by construction: a default cannot exist without a save flag, a linkage cannot reference a missing field, duplicates are compiler-enforced (AllowMultiple = false), styles cannot conflict, and setter hooks cannot be declared on a[SerializableProperty]— whose user-written setter they could never hook.[SaveFlag]and[DeserializeTimer]remain standalone attributes because they are serialization concerns that legitimately apply to both fields and properties (property support unit-pinned).Why the gate earns its place: a survey of all 204
[SerializableProperty]sites in the engine found value coercion (clamps,value ?? \"\"normalization) to be the single largest reason for hand-written setters (~50 sites), ahead of post-change side effects (~55, covered by fieldChanged) and vetoes (~5). With both hooks, roughly three quarters of hand-written properties become expressible as plain[SerializableField]declarations; hand-written properties remain for custom getters (~45 sites) and exotic setter semantics.Timers — one attribute on the field, replacing
[TimerDrift]+[DeserializeTimerField]:Drift stores
Nextas anchored time (downtime no longer consumes the remaining delay, bytes are save-stable);wallClock: truekeeps absolute deadlines with the unchanged format. The restart method is invoked only when a timer was running — theTimeSpan.MinValuesentinel is gone.Compatibility
[Obsolete]shells whose messages are the migration instructions; the generator carries zero support code for them."@TimerDrift"delta-time read fossil survives permanently for old-version migration schemas — pinned by a fixture whose v0 schema replaysReadDeltaTimewhile the current version reads anchored time, in the same generated file.MigrateFrom. The setter hooks never touch the wire.Diagnostics
With the mistakes unrepresentable, most of the planned diagnostics deleted themselves: SG3010/SG3014/SG3016/SG3017 are retired (numbers reserved, never reused). What remains is SG3015 — the named method is missing or has the wrong signature — covering save-flag, default, changed, gate, and timer shapes, none of which were validated before v4 (mistakes silently generated broken code) — plus new SG3018: a setter hook (
fieldChangedorallowFieldChange) on a field with no generated setter (readonly, or omitted setter), where it could never fire. SG3008 is reworded for[DeserializeTimer].Also
The engine-side migration (~240 linkage sites; 8 drift-timer classes with version bumps) follows as a separate PR after publish, scripted — and can now also fold eligible
[SerializableProperty]sites down to[SerializableField]with hooks.