Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Several repeat-related fixes #135

Merged
merged 11 commits into from
Jun 27, 2024
Merged

Several repeat-related fixes #135

merged 11 commits into from
Jun 27, 2024

Conversation

eyelidlessness
Copy link
Member

@eyelidlessness eyelidlessness commented Jun 19, 2024

This PR includes a handful of fixes and improvements addressing issues found in #110, generally with a common repeat-related theme.

The commit notes go into some detail of each fix. I'll highlight some high level details here as well.

  1. ad607dc Fix InconsistentChildrenStateError cases that currently affect scenario. This fix shouldn't affect the web-forms Vue UI client (as it only affects clients using Solid), but it was important to fix first so that the other fixes won't be obscured by introducing this same error.

  2. 0212b9f Fix a variety of issues where behavior is correct for the first repeat instance in a range (or within that repeat instance, or referencing into it) but incorrect for subsequent repeat instances. The change doesn't address all such cases, but it is probably further reaching in real usage than the set of newly passing tests suggests. I expect that it will address quite a few cases where repeat functionality feels broken/incomplete.

    Note that this is followed by another commit (57883df) marking several form init smoke tests failing that did not previously. This may seem like a regression, but I don't think it really is. Those tests had previously satisfied a broad check for producing an error, and their previous passing status was essentially a coincidence. We'll likely address these more meaningfully in some of the work related to Handle errors and warnings in a consistent way #80.

  3. 19016e6 (and eea137d) Fix a few issues related to the order of reactive state initialization for each form node. The mechanism of the fix involves moving the initialization of reference (i.e. the effective XPath reference to the node, which may change for repeat instances and their descendants) as well as shared bind computations—relevant, readonly, required—earlier in node initialization... effectively before any other reactivity for all non-root nodes.

    The immediate benefit, as visible in the main commit for the fix, is that we now correctly clear default values of non-relevant nodes on form load. While that isn't specifically repeat-related, the other benefit of this change is that it eliminates the aspect of form/repeat instance init where certain aspects of reactivity are not initially "ready" when they need to be initially referenced. As such, and reflected in followup commits, this fix also resolves any remaining asynchrony1 during form init (8642ed7). For repeats, this also fixes edge cases where certain aspects of reactive state lag after creating now repeat instances (90e1ad6).

    This broadly reaffirms our current intent that form state is reconciled fully, synchronously, for every form action (until a need to deviate arises).

  4. b44beec Fix some cases where relevant state was incorrectly inherited when applied to repeat instances themselves. The nature of this issue was that RepeatRange nodes (which are a fiction for our runtime data model, not real nodes in the primary instance) were incorrectly evaluating relevant expressions intended for their RepeatInstance children.

    The first (correctness) problem with this was that the expression would be evaluated against the wrong context, as RepeatRange.contextNode references the repeat range's parent element in the primary instance tree. The second problem: if this expression evaluated to false for a RepeatRange, that would be inherited from all of its RepeatInstance children even if the expression properly evaluates true for the instance itself. These issues are resolved.

    There are also scenario tests which consider the relevance of the repeat range itself... or at least, they check the presence of a prompt to create new instances of the repeat range, and we've ported those tests to check for that range's relevance. We now determine this RepeatRange relevance state by a heuristic, where the range itself is relevant if either of these cases are true:

    • Does the RepeatRange have >=1 relevant RepeatInstance children?
    • Does the RepeatRange have 0 RepeatInstance children at all?

    It's entirely possible this is the wrong heuristic. It's also entirely possible that there is a more appropriate way to represent the presence or absence of a repeat "prompt" besides relevance state. I am certain that scenario will need different logic when we introduce support for jr:count (Implement jr:count #87).

Footnotes

  1. Besides resource fetching, which is inherently async and which is at least presently handled before the now-synchronous Root initialization.

Copy link

changeset-bot bot commented Jun 19, 2024

🦋 Changeset detected

Latest commit: 46bf4ec

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@getodk/scenario Patch
@getodk/xforms-engine Patch
@getodk/ui-solid Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Contributor

@sadiqkhoja sadiqkhoja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me 👍.

Really liked the simplification around Root construction 💜.

@@ -272,7 +272,7 @@ describe('Tests ported from JavaRosa - repeats', () => {
* expandReference call in Triggerable.apply which ensures the result is
* updated for every repeat instance.
*/
it.fails('updates repeat count, inside and outside repeat', async () => {
it('updates repeat count, inside and outside repeat', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Porting notes need to be updated since the test is no longer failing.

eyelidlessness and others added 11 commits June 27, 2024 13:40
… when combined with `createMemo` and children state access.
… by extending the lookup of subscribable nodes to produce all nodes for a dependency’s node-set.

Note that this is a correctness improvement, but the solution is naive and likely comes with performance costs:

- Some expressions do not need to subscribe to all of the matching nodes, and may execute more often than needed as a consequence

- Lookup now always begins at the instance root, where it previously walked relative to the context node

- While this doesn’t do full tree traversal, it does traverse more of a given match’s siblings, and may traverse parallel subtrees where it hadn’t previously

We should consider measuring the performance impact of this change on real forms so we can determine whether it’s a meaningful optimization target. If it is, we’ll likely be looking at either:

- static analysis of XPath expressions and producing different dependency resolution strategies depending on the expression

- the old favorite, decoupling XPath evaluation from the browser/XML DOM
There is no logic which would have intentionally made these cases pass, and there are others like them failing already. This does not represent a meaningful regression; if anything it makes the same class of tests easier to address together.

This also changes those error-checking form-init-error smoke tests to not produce a `Scenario` instance in the init error assertions. This avoids an issue where assertion failure would produce enormous shell/console output where the `Scenario` instance clearly is not an error/Promise rejection.
This doesn’t fix a bug in and of itself, but it warrants a separate commit because it touches a fair bit of code. It eliminates some redundancy, introduces a bit of complexity to do that, but mostly it enables downstream work on bugs with these overlapping concerns:

- Earlier computation of `relevant`, allowing us to correctly write blank values where a node has a default value but is initially non-relevant. It’s expected that will **also support** later feature work, including:

  - `setvalue`/`odk-instance-first-load`

  - editing submissions (hunch; TBD pending clarification of requirements)

- Earlier computation of `readonly`, mainly for consistency with `relevant` as both have inheritance logic

- Correcting inheritance logic around both `relevant` and `readonly` as applied to repeats.
… extensive clarification in PORTING NOTES comments.

This is additional set up to demonstrate two incoming bug fixes:

- Clearing default values of nodes which are non-relevant on form load/init

- Correctly inheriting non-relevance of/within repeat ranges
The nature of this fix is that `relevant` computation is initialized and performed **before** `value` state is initialized.

This is necessary because:

- in the original initialization order, the `isRelevant` check references different reactive dependencies before and after initialization of `value`

- before `value` state init: the dependency would return a default value as the real `relevant` computation was not yet set up

- after `value` state init: the dependency would not trigger a `value` update clearing the non-relevant value, because `value` had not yet subscribed to the newly initialized `isRelevant` state dependency

This **also** moves all other `DescendantNode` shared state initialization to the construction phase. In turn, it also allows all `DescendantNode` subclasses to reference those initialized states directly in creation of their shared node state/client-facing `currentState`.

In the course of this, it also felt easier to reason about the various internal-facing reactive accessors **as accessors**, i.e. as zero-arity function calls rather than as getters.

- - -

Also notable: this eliminates the awkward `getInitializedState` workaround for `relevant` and other aspects of `DescendantNode` state init. Pure speculation as I write this, but it seems possible that I’ll be able to eliminate further init awkwardness like this (including possibly restoring synchronous `Root` construction).
As suspected in previous commit, none of this async init awkwardness is necessary now.

The `initializeForm` interface still has to be async because it handles resource fetching. We can consider breaking up those aspects of init in the future if that seems like it would be beneficial from a client perspective.
Moving `relevant` (etc) state init earlier in node construction also fixed this timing bug!
Extensive commentary in JSDoc, some of which is probably worth discussing in review.
@eyelidlessness eyelidlessness merged commit d121679 into main Jun 27, 2024
86 checks passed
eyelidlessness added a commit that referenced this pull request Jul 19, 2024
As discussed in review, it’s highly unlikely this is actually needed anymore!

Brief backstory context in case we ever have reason to doubt this:

0. The WHO VA form, which I’ve used as a frame of reference for many things, uses two references to `null` that don’t resolve to anything. The inferred intent is to treat `null` as a keyword, just as it would be in languages with the Billion Dollar Mistake.
1. In early prototyping (pre Web Forms project), the WHO VA form’s references to `null` produced errors where there was assertion logic checking that expected dependencies were found.
2. Early Web Forms work produced the same, for a time at least producing errors in the console. This produced a lot of distracting noise that made it harder to identify actual implementation errors in dependency resolution.
3. Subsequent improvements to dependency resolution have been successful enough that we’ve even eschewed logging when a dependency lookup doesn’t resolve (though I sometimes add it in local dev for testing/validation), at least as of #67.
4. As of #135, dependency resolution was expanded to be form-wide and match all possible nodes; there’s no case where a `null` reference **should** match a node and won’t.

I can imagine these potentially useful followup changes:

1. On parse, identify any path sub-expressions which **will never** resolve to any node in the form.
2. Short circuit lookup of such never-resolvable paths. There’s no sense walking the full form tree for something we know we won’t find!
3. Potentially warn when such never-resolvable paths are found. This wouldn’t be particularly useful for the `null` NameTest specifically (where the intent at least in WHO VA is clearly a null reference), but it could be useful for catching typos and other mistaken references like hierarchy confusion.
eyelidlessness added a commit that referenced this pull request Jul 22, 2024
As discussed in review, it’s highly unlikely this is actually needed anymore!

Brief backstory context in case we ever have reason to doubt this:

0. The WHO VA form, which I’ve used as a frame of reference for many things, uses two references to `null` that don’t resolve to anything. The inferred intent is to treat `null` as a keyword, just as it would be in languages with the Billion Dollar Mistake.
1. In early prototyping (pre Web Forms project), the WHO VA form’s references to `null` produced errors where there was assertion logic checking that expected dependencies were found.
2. Early Web Forms work produced the same, for a time at least producing errors in the console. This produced a lot of distracting noise that made it harder to identify actual implementation errors in dependency resolution.
3. Subsequent improvements to dependency resolution have been successful enough that we’ve even eschewed logging when a dependency lookup doesn’t resolve (though I sometimes add it in local dev for testing/validation), at least as of #67.
4. As of #135, dependency resolution was expanded to be form-wide and match all possible nodes; there’s no case where a `null` reference **should** match a node and won’t.

I can imagine these potentially useful followup changes:

1. On parse, identify any path sub-expressions which **will never** resolve to any node in the form.
2. Short circuit lookup of such never-resolvable paths. There’s no sense walking the full form tree for something we know we won’t find!
3. Potentially warn when such never-resolvable paths are found. This wouldn’t be particularly useful for the `null` NameTest specifically (where the intent at least in WHO VA is clearly a null reference), but it could be useful for catching typos and other mistaken references like hierarchy confusion.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants