chasm
chasm copied to clipboard
Empty slices in maps
So far we have two types of targets that a transformer can replace: node targets and list slice targets. I propose adding a third type of target, a map empty slice target.
Motivation
You can insert into a list by replacing an empty slice in the list. However, there is currently no way to insert into a map without replacing the whole map. Therefore I am proposing this to be able to insert into a map.
This would solve the issue raised by @CheaterCodes here, and make it easier to improve the chasm tree further before release if necessary.
Implementation
An empty map slice in chasm will look something like the following, with a boolean specifying whether we're targetting the entire map or an empty slice within the map.
{
node: foo.some_map,
inside: true, // true if this is an empty map slice ("inside" the map), false if it's the whole map (the default).
}
The locking behaviour of an empty map slice would be similar to that of an empty list slice; that is, never conflicting with another empty map slice, and only ever conflicting with locks that target the map itself or any of its parents.
The Chasm implementation will have to be updated to support a third type of slice.
I think this is a more fundamental issue. Chasm map nodes are not designed to be mutated. Besides locals, another example is the root classes
node.
Initially, this was designed as a map, but was later switched for exactly this reason.
More specifically, maps in the class tree are generally treated as objects rather than maps. An "object" here means that there is only a fixed set of members that may exist, you cannot add arbitrary members.
If we want to change this, I'm in principle fine with that. However, I would prefer a more general solution that can also be applied to the root classes node, and maybe others.
For reference, the root map solves this problem by just being a list that you index with a filter. We could apply the same to locals:
locals[l -> l.name = "P1"][0].descriptor
The nice thing about this is that we can just accept duplicates and deal with them later. The disadvantage is that it's not really a map and we're not ensuring uniqueness. It also places some additional burden on a potential optimizer to make this fast (the class tree root has more than 6000 entries).
Another option would be to make maps just syntax sugar for the above. I.e. the following two lines would behave identically (null checking omitted for simplicity):
classes["com/example/ExampleClass"]
classes[entry -> entry.key = "com/example/ExampleClass"][0].value
This has basically the same effects as above, except with syntax sugar and maybe simplifies the optimizer which could rely on a consistent pattern.
Now for probably my favorite: Allow node targets to target non-existent nodes. So if you want to add a class, you simply target the map entry that you want to add. If it doesn't exist, you'll get a null passed in your sources. Otherwise transformers will notice that they are trying to add a class that already exists (and can decide whether to overwrite it, modify it, or fail.
If this still isn't enough, we can combine the last proposal with a multi-node target, allowing you to modify multiple nodes at once. However this can probably be achieved easily by making multiple transformations instead.
After some internal debate, the last option seems to be the most popular, so let me elaborate on this.
Let's say we have a map like this:
locals: {
P1: { ... }
}
A transformation that specifies its target as P2 will then be invoked with transformation(null)
, since P2 doesn't exist.
If there is another transformation that targets the same node, it will be invoked as transformation2(transformation1(null))
.
The transformation is then responsible for deciding whether to replace, modify or crash.
One problem we have encountered with this strategy is that the target in the example P2 is null. This means that we don't have a path available for later reference. This will likely require a significant restructure of how node paths are handled.
I just remembered why this wasn't considered when deciding to structure classes
as a list: If it was a map, renaming classes would be a bit difficult. While you could still achieve a similar outcome by moving the class node from one key to another, this results in a strong ordering between a class rename and all transformations within said class.
This might be ok, but it's certainly something to consider.
This is currently pending some investigation into #105 . I think that we can use a list for locals instead, but this needs verification.