Support mutation through non-terminal comma-separated-list outputs #45

Closed
opened 2026-07-26 14:51:06 +00:00 by UserCurt · 1 comment
Owner

set and remove currently do not allow paths that continue through multi-output comma-separated list (CSL).

For example, :

data = [ ...
    struct(A=struct(B=1)), ...
    struct(A=struct(B=2))];

% MPath approach
result = mpath.set(data, "/A<1>/B", 10);

% Equivalent MATLAB operations
child = data(1).A;
child.B = 10;
data(1).A = child;

To support non-terminal CSL selector, MPath must retrieve the selected output, modify its descendant, and write the changed output back. remove requires the same process when removing a descendant.

Terminal comma-separated-list assignment is a separate problem tracked by #42. This issue covers CSL selection that occurs before the final mutation target.

There are three considered planning approaches.

1. Reuse the forward chunks

The simplest approach is to reuse the chunks selected during forward traversal when writing changes values. The chunk planner itself would not change.

This approach relies on two assumptions:

  • listLength returns the same result whether matlab.indexing.IndexingContext is Expression or Assignment
  • If parent.(indexOps) successfully retrieves a child, then parent.(indexOps) = child supports the corresponding write-back operation

This is the proposed approach for initial support in the short term to enable non-terminal CSL mutation.

2. Plan write-back separately

In this approach, the chunk planner would run a second time during reverse write-back. The forward plan would determine how to retrieve each child, and the reverse plan would determine how to assign each changed child back into its parent. When a forward chunk cannot be used for assignment, the reverse planner could divide it into smaller chunks until it finds a valid write-back sequence.

This approach would mean retrieval and assignment could have different chunk boundaries. This can be confusing users because the write-back boundaries would remain internal, unless resolve is modified such that it conditionally applies this two-phase chunk planning based on an optional named argument.

The separate plans must also preserve resolved-input equivalence:

direct = mpath.set(data, path, value);
resolved = mpath.resolve(data, path);
replayed = mpath.set(data, resolved, value);

isequaln(direct, replayed)

3. Plan both directions together

In this approach, there would only be one chunk planner step but it would choose he boundaries that would work for both the forward traversal and reverse write-back.

Query operation would continue planning only for Expression. Mutation operations would require each non-terminal chunk to work under both Expression and Assignment, and the terminal chunk to work under Assignment only.

This removes the assumption that listLength behaves identically across indexing contexts because the planner would check both contexts. Though, it still has one assumption:

  • If assignment-aware planning accepts a chunk, then parent.(indexOps) = child supports the corresponding write-back operation.

MPath cannot verify this completely without performing the assignment.

With this approach, resolve should be modified such that it is aware whether it is a query or mutation operation as the chunk planner could result in different chunks depending on which is the case.

4. Plan both directions together with independent boundaries

The planner could build the forward traversal plan and reverse write-back plan together without requiring their chunk boundaries to align. For example:

  • /A/B<1>/C for forward traversal
  • /A<1>/B/C for reverse write-back

This is the most flexible approach but is the most complex as there are two plans being created. Option 2 cannot produce this result because its reverse planner starts with the forward chunks and can only divide them into smaller chunks.

This removes the assumption that listLength behaves identically across indexing contexts because the planner checks each plan using it applicable context. Though, it still has one assumption:

  • If assignment-aware planning accepts a reverse chunk, then executing parent.(indexOps) = child supports the corresponding write-back operation.

As with separate reverse planning, the implementation must preserve resolved-input equivalence.

With this approach, resolve should be modified such that it is aware whether it is a query or mutation operation and also, if it is a mutation operation, which of the two plans to return.

`set` and `remove` currently do not allow paths that continue through multi-output comma-separated list (CSL). For example, : ```matlab data = [ ... struct(A=struct(B=1)), ... struct(A=struct(B=2))]; % MPath approach result = mpath.set(data, "/A<1>/B", 10); % Equivalent MATLAB operations child = data(1).A; child.B = 10; data(1).A = child; ``` To support non-terminal CSL selector, MPath must retrieve the selected output, modify its descendant, and write the changed output back. `remove` requires the same process when removing a descendant. Terminal comma-separated-list assignment is a separate problem tracked by #42. This issue covers CSL selection that occurs before the final mutation target. There are three considered planning approaches. ## 1. Reuse the forward chunks The simplest approach is to reuse the chunks selected during forward traversal when writing changes values. The chunk planner itself would not change. This approach relies on two assumptions: - `listLength` returns the same result whether `matlab.indexing.IndexingContext` is `Expression` or `Assignment` - If `parent.(indexOps)` successfully retrieves a child, then `parent.(indexOps) = child` supports the corresponding write-back operation This is the proposed approach for initial support in the short term to enable non-terminal CSL mutation. ## 2. Plan write-back separately In this approach, the chunk planner would run a second time during reverse write-back. The forward plan would determine how to retrieve each child, and the reverse plan would determine how to assign each changed child back into its parent. When a forward chunk cannot be used for assignment, the reverse planner could divide it into smaller chunks until it finds a valid write-back sequence. This approach would mean retrieval and assignment could have different chunk boundaries. This can be confusing users because the write-back boundaries would remain internal, unless `resolve` is modified such that it conditionally applies this two-phase chunk planning based on an optional named argument. The separate plans must also preserve resolved-input equivalence: ```matlab direct = mpath.set(data, path, value); resolved = mpath.resolve(data, path); replayed = mpath.set(data, resolved, value); isequaln(direct, replayed) ``` ## 3. Plan both directions together In this approach, there would only be one chunk planner step but it would choose he boundaries that would work for both the forward traversal and reverse write-back. Query operation would continue planning only for `Expression`. Mutation operations would require each non-terminal chunk to work under both `Expression` and `Assignment`, and the terminal chunk to work under `Assignment` only. This removes the assumption that `listLength` behaves identically across indexing contexts because the planner would check both contexts. Though, it still has one assumption: - If assignment-aware planning accepts a chunk, then `parent.(indexOps) = child` supports the corresponding write-back operation. MPath cannot verify this completely without performing the assignment. With this approach, `resolve` should be modified such that it is aware whether it is a query or mutation operation as the chunk planner could result in different chunks depending on which is the case. ## 4. Plan both directions together with independent boundaries The planner could build the forward traversal plan and reverse write-back plan together without requiring their chunk boundaries to align. For example: - `/A/B<1>/C` for forward traversal - `/A<1>/B/C` for reverse write-back This is the most flexible approach but is the most complex as there are two plans being created. Option 2 cannot produce this result because its reverse planner starts with the forward chunks and can only divide them into smaller chunks. This removes the assumption that `listLength` behaves identically across indexing contexts because the planner checks each plan using it applicable context. Though, it still has one assumption: - If assignment-aware planning accepts a reverse chunk, then executing `parent.(indexOps) = child` supports the corresponding write-back operation. As with separate reverse planning, the implementation must preserve resolved-input equivalence. With this approach, `resolve` should be modified such that it is aware whether it is a query or mutation operation and also, if it is a mutation operation, which of the two plans to return.
UserCurt added this to the Release v1 milestone 2026-07-26 14:51:06 +00:00
Author
Owner

Completed by commit 23ff124. set and remove now support mutation through non-terminal CSL selectors. Option 1 has been chosen as the easiest short-term solution. Contemplation of other options is now separately tracked in #48, while sharing materialized route values is tracked by #47.

Completed by commit `23ff124`. `set` and `remove` now support mutation through non-terminal CSL selectors. Option 1 has been chosen as the easiest short-term solution. Contemplation of other options is now separately tracked in #48, while sharing materialized route values is tracked by #47.
Sign in to join this conversation.
No description provided.