Repository navigation
fix(check): require ?. for fields only one side of a join has - #30
Merged
Merged
Conversation
Join turned a field only one side listed into `T?` on a closed object, so Check accepted `.x` on it. At run time the key is missing, not nil, and Run failed with `key "x" not found`. This reached any join of object types: list literals, if(...), ||, and host functions typed with Join. Type gains Absent, a set of fields whose key may be missing. Join marks one-sided fields absent instead of nil-able, and Check asks for ?. or ?[ to read them, as it already does for an open object's typed unlisted fields. A field both sides list stays present; Optional keeps meaning "present, may be nil". Against an open side, a one-sided field also joins that side's Elem, since the key may hold it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guide, spec, env-design guide and llms.txt now say that a join of objects marks a field only one side has as absent (`name?: type`), that reading it needs ?. or ?[, and how that differs from a present field whose value may be nil (`name: type?`). The JSON Schema advice now says when to use .OrNil() and when to use Absent. The guide test covers the new examples. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Approve: The join fix holds up: Absent is set on a fresh object in joinFields, copies made by OrNil/NonNil keep it, and every field read in check.go goes through mayBeAbsent.
This closes the A2.1 gap in the workflow library's soundness review. Optional keeps its meaning of "may be nil", so the workflow library's nil-filled schema properties still type the same way.
Questions
- Q1. This ships as a patch (v1.3.1), but
Checknow rejects some expressions v1.3.0 accepted, and those can run fine on the data they were written for. Is a patch the version you want for that, or should it be a minor bump? (types.go:297)
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.
What changes for the user
Checknow reports.xon a joined object when only one side of the join hasx, and asks for?.or?[. The joined type prints that field asx?: T(may be absent), distinct fromx: T?(present, may be nil).See it
Env:
bisObjectOf({y: string}), value{"y": "s"}.Before (v1.3.0):
After:
Why
Checkmust agree withRun: an expression that passesCheckmust not fail with a missing field.Jointurned a field that only one side had intoT?on a closed object.Checkthen allowed.x, but at run time the key is missing, not nil, andRunfails. Every join of object types had this problem: list literals,if(...),||, and host functions typed withJoin(such ascoalesce). The workflow library's soundness review found it (A2.1).Decisions to check
Type.Absent map[string]bool, the fields whose key may be missing. Absence belongs to the object, not to the field's value type, and a set on the object keeps field types unchanged when they are read. Rejected: anAbsentflag on the field's ownType. Every place that reads a field type would have to clear the flag so that it does not leak into results.Joinkeeps a one-sided field's type and marks it absent. It does not make the typeT?.Optionalkeeps one meaning: present, may be nil. The workflow library depends on this, because it sets missing optional JSON Schema properties to nil. Rejected: making the join an open object. That would drop the field's own type, and misspelled field names would no longer be reported.Absentdirectly. A host that leaves keys out of objects, instead of setting them to nil, can now describe that. The guide's JSON Schema advice says when to use.OrNil()and when to useAbsent.Elem. The open side may hold that key with anElemvalue. v1.3.0 typed the fieldT?, which was also unsound.Anystill needs?.. We know the key may be missing even when its value type is unknown. Rejected: skipping the check forAnyfields, asCheckdoes for unlisted fields of an open object whoseElemis untyped.obj["x"]gets "use ?[", andobj.xgets "use ?.". This also applies to the existing message for an open object's typedElem.has()does not prove that a field is present.?.,?[andtryare the ways to read it.b.x != nilitself fails at run time when the key is missing, so it could not be the guard.How to verify
go test ./... go vet ./... go run ./examples/type_checkingThe regression cases are in
TestCheckMatchesRun(marked A2.1). Each rejected case is also run, to confirm thatRunfails. Each accepted case is run, to confirm thatRunsucceeds.TestJoincovers the new join types.Risk and rollout
Checknow rejects some expressions that v1.3.0 accepted. These are plain.xor["x"]reads of a field that only some of the joined objects have. Joins come from list literals,if(...),||,any/filter/mapover such lists, and host functions typed withJoin. Each newly rejected expression can fail inRun, so the fix is to change.to?.. Types from joins against an open object can also change, fromT?to a union with that object'sElem(orany). A joined type'sString()output changes fromx: T?tox?: Tfor one-sided fields. Fields that every side has are unchanged.After merge, release patch v1.3.1. Then workflow#42 adopts it.
🤖 Generated with Claude Code