GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||
GeezerVison is showing github.com — Write a state as a value of its set: phase(this) == open · Issue #502 · liquid-java/liquidjava Open Labels enhancementNew feature or requestNew feature or requesterror messagesRelated to error messagesRelated to error messages DescriptionBuilds on #498 (named state sets) and #435. MotivationA state is written as a predicate on the object, To see that This form also says which set a spec constrains: Design1. Syntax:
|
| Written | Means |
|---|---|
g(e) == s or s == g(e) |
s(e) |
g(e) != s or s != g(e) |
!s(e) |
e is any expression accepted as a state argument today (this, a parameter, _, old(this)). The grammar does not change, because s already parses as a variable. Both forms stay valid and can be mixed. The new form is preferred in docs and examples. Comparisons other than == and != (g(e) < s) are not given a meaning: s is an unknown variable there, as today.
Resolution. In g(e) == s, s is resolved as a state when all of these hold:
g is a state set function;s is the name of one of g's states;s is not a variable in the spec's scope: the parameters, the context's variables where the spec is read, and _, this and return. Those three are always variables, even if a state has that name.A variable keeps its meaning, so a spec that is valid today means the same: to = "phase(this) == closed" in void choose(int closed) still compares with the parameter. The state is resolved by the set it is compared with, so two sets, or two classes, may have states with the same name.
A state name is unique within its class. Two sets of one class may not share a state name. @StateSet(name = "phase", value = {"closed", "open"}) together with @StateSet(name = "latch", value = {"open", "locked"}) is an error: State 'open' is in two state sets of Door: phase and latch. Today that is accepted, but open(this) silently picks one of the two sets. The new form would say which set, but it is read as the shorthand, which cannot. Two classes may still share state names (ambiguous_state_names_correct). Likewise, a state may not have the name of a set of its class, generated names included: a state state1 next to the unnamed first set state1 is an error. Before, only named sets were checked.
Errors, all Error like the other state set errors:
g(e) == s where s is not a variable in scope but is a state of another set of the same class: State 'locked' is not a state of 'doorState' (its states are closed, opened).set(e) == state is rewritten to state(e), and != to !state(e), by one helper (StateValues in rj_language). The helper is given the states and a test for "is a variable".
All state sets are registered first. A new first pass of RefinementProcessor registers every @StateSet (classes, interfaces and @ExternalRefinementsFor specs) before any spec is read. Today MethodsFirstChecker and ExternalRefinementTypeChecker register a class's sets while they visit it. A spec that mentions a class visited later then sees no sets, which is #452 for external specs. That pass is its own PR (#508), and it fixes #452. It runs after FieldGhostsGeneration and also registers @RefinementPredicate ghosts, which are only allowed on types, so the name checks for named sets still see every ghost.
Translated once, where the spec is written, in the spec's own scope. This happens when the spec string is parsed into the stored predicate, so every syntactic reader sees the shorthand. The readers are the frame equalities, the state machine (isQuery on return refinements), the hints, DecidingArgument, StateErrorCategories and #495's relevance pass. The parse sites are:
TypeChecker.getRefinementFromAnnotation, for @Refinement on parameters, returns, fields and locals;AuxStateHandler.createStatePredicate, for method from/to;AuxStateHandler.setConstructorStates, for constructor to, which does not go through createStatePredicate;IsClosed(Door d) { phase(d) == closed } used in void test(int closed), closed is still the state, as in closed(d).There is no late resolution in the caller's context. A caller's variable can therefore never capture a state name from a spec written elsewhere. If a parse site were missed, the name would stay an unknown variable and give the existing "not found" error, never a different meaning.
Parity with the shorthand. Wherever the new form is translated, it behaves exactly as the shorthand it becomes, including the shorthand's existing limitations. Those are fixed separately for both forms:
No change to the Z3 encoding, the state machine or the error categories.
phase(this) == open becomes open(this) before the frame logic runs, so it counts as a mention of phase, exactly like open(this). A to that uses a set's function directly, as in phase(this) == phase(other), is a separate question from #498, now in #506.
In messages, hints and the derivation steps, a state of a named set is printed as set(x) == state, and !state(x) as set(x) != state. This holds whichever form the spec used. States of unnamed sets keep state(x), because state2(x) == closed reads worse than closed(x). Only the rendering changes: the predicates, simplification steps and #495's relevance pass work on the same expressions.
to, from/to, !=, old, a branch keeping a set, and a boolean query whose return refinement uses the new form (state machine parity);_, where phase(this) == _ on a parameter still compares with the parameter's value.ErrorStateHintMultipleSets message.Five findings, all real, and the issue body is amended:
| Finding | Amendment |
|---|---|
A parameter named like a state (to = "phase(this) == closed" in void choose(int closed)) would change meaning |
A variable in scope always keeps its meaning; a state is resolved only when no variable has that name. A test was added. |
Constructor to does not go through createStatePredicate |
setConstructorStates is a caller too. The correct fixture has a constructor. |
| Aliases are expanded after states are translated | An existing bug for the shorthand too (Closed(_) rejects a closed object). Filed as #503, fixed first as PR 0. |
| A state of another object counts as a mention for the frame | An existing soundness bug for the shorthand, reproduced (x.observe(y) forgets x's state). Filed as #504. Mentions by set function follow the same rule as states, so both forms stay identical. |
| Hierarchy scope covers only immediate supertypes and uses simple names | An existing false error, reproduced. Filed as #505. The late translation still gives the right meaning before Z3. |
Two findings, both real:
| Finding | Amendment |
|---|---|
Counting every set-function application as a mention (old section 3) would drop the frame of to = "phase(this) == phase(this)", which keeps the set today |
Section 3 is removed. The new form becomes open(this) before the frame logic runs, so it never needed it. phase(this) == phase(other) is an existing question from #498 and is now #506. |
StateMachineBuilder.isQuery reads stored return refinements syntactically, before the late translation, so _ == true --> phase(this) == open would not be recognised as a query |
The early translation now runs wherever a spec string is parsed: getRefinementFromAnnotation as well as from/to and constructors. The late one is the single toSolverPredicate step from #503. A query fixture was added to PR 1. |
One finding, real: a caller's variable could capture a state name in an alias body. With IsClosed(Door d) { phase(d) == closed } used in void test(int closed), expanding the alias before resolving would compare with the parameter closed.
Amended: in toSolverPredicate, each alias body is translated in the alias's own scope, where only its parameters are variables, before it is expanded. The variable-shadowing fixture now also covers this case.
Codex found that a state named _ could be taken for the annotated value: @Refinement("phase(this) == _") int value with @StateSet({"_", "open"}). Amended: _ is always a variable. Regression test added.
While amending, the same capture problem as round 3 also applied to the late resolution as a whole. A spec parsed before its class's sets were registered (a class visited later) was resolved only before Z3, in the caller's context, where a caller variable named like the state would capture it. Amended:
@StateSet before any spec is read. This is its own PR, and it fixes External specs: a @StateSet declared later in the file is not found by an interface earlier in it #452, which has the same cause.One finding, real: the first pass must keep the order the name checks rely on. A named set must still clash with an @RefinementPredicate ghost or a field ghost of the same class. Today those are registered before the sets are checked.
Amended, and implemented in #508:
FieldGhostsGeneration.@RefinementPredicate ghosts in the same loop as @Ghost and @StateSet, before the names are checked.ErrorStateSetNames gained the @RefinementPredicate case.One finding, real: two sets of one class can share a state name. With phase = {closed, open} and latch = {open, locked}, phase(this) == open names one set, but it is read as open(this), which changeStatesToRefinements resolves by name to either set. The shorthand already has this ambiguity today, and picks one silently.
Amended: a state name must be unique within its class; two sets sharing one is an error. Two classes may still share names. The error fixture covers it.
Two findings, both real, and both fixed in PR 1:
| Finding | Amendment |
|---|---|
A state named return would capture return in a non-void method's to |
_, this and return are always variables. Covered in CorrectStateValues. |
A state named like a generated set (@StateSet({"state1", "open"})) clashed with the set function state1 |
The name check from #498 now runs for generated names too. Covered in ErrorStateValues. |