novika-lang / novika

🪱 Novika is a free-form, moldable, interpreted programming language
https://novika-lang.github.io/
MIT License
15 stars 0 forks source link

Multiple cursors (cursor block)? #105

Open homonoidian opened 1 year ago

homonoidian commented 1 year ago

Simply allowing to insert the same thing in different places (as in multi-cursor text editors such as VS Code or Kakoune) won't be fun. However, adding the support for a cursor block seems like an... interesting idea, even though it'd make the language even more obscure (how far can we go though, huh?)

Making |to "collapse" the cursors will keep most of existing code compatible with this new feature; at least compatible with the engine which would otherwise be bamboozled by multiple cursors. Some other cursor words such as |swap, |+, <| can support multiple cursors out-of-the-box.

We can then add |to* which can be used like so: [ 1 2 3 4 ] [ 1 4 ] |to* leaves: [ [ 1 | 2 3 4 | ] ] (note multiple cursor literals => multiple cursors added). Then we can have for instance [ 1 | 2 3 | 4 ] swap which will result in [ 3 | 2 1 | 4 ] (i.e. 1 3 swap => 3 1, then "plug" both back into the block). If there aren't enough arguments, they're taken before the leftmost cursor, as in: [ 1 2 | 3 4 | 5 ] rot => 1 2 "..." 4 rot => 2 4 "..." 1 => "plug" back in [ 2 4 | 3 1 | 5 ].

Uhm... This does create a lot of problems, for instance what shove-order should the thing have, what cherry-order should the thing have? This issue doesn't answer these questions; instead it swaps answers to them here and there, which doesn't help, but who cares?

Nevertheless, since I don't like making "ultimate" decisions, I think we should rather expose the cursor block to the runtime (what did ya think I'd do huh :smiling_imp:). The latter will make the language obscure², but again, who cares, really? So something like |to* can be implemented in Novika itself, assuming |block leaves the cursor block:

[ dup |block resub ] @: |to*

For instance, in the following block [ 2 3 | 4 5 | 6 7 | 8 9 ] the cursor block will be [ 2 4 6 | ]. If we then do |: drop (drop everything before cursorS), we'll get [ 2 | 4 | 6 | 8 9 ], the cursor block [ 1 3 5 | ] as expected.

Of course to not make things slow we should use integer-cursor or a thin, protocol-conforming integer wrapper instead of spinning up a whole new block for every cursor. Only when multiple cursors are needed should we "actually do the thing". Some object ballet, why not!

And remember blocks are stacks, so you can operate on a stack with multiple cursors! Wouldn't that be fun?

So, the biggest problem now is the following. How do we manage the cursor block? I mean, object-manage, memory-manage, reference-manage, whatever. What if someone holds on to it and then we empty it by e.g. collapsing with |to? On this we can say |block is as dynamic as stack or conts, for instance, so why would you not expect it to disappear right from underneath you? This is less of an issue for me.

As for the management part, I think each block may (optionally!) have its own, personal cursor block. It's only ever created once (when multiple cursors are requested) and then used appropriately, emptied and whatnot, until the block that is its owner is GCd, or until whoever keeps a reference to the cursor block is GCd.

Now to address the elephant in the room, so to speak: WHAT THE HELL SHOULD HAPPEN WHEN WE SHOVE?, given multiple cursors?

Well, first, we must understand that the cursor in the cursor block is in some sense second-order. The interpreter could use it (and move it!) to find the current insertion point — to keep state. Then there is always an active cursor to tell us where we should do an insertion. And after insertion this second-order cursor will move forward, somewhat reminiscent of the stuff that is happening in stack & code blocks: in stack block the cursor is moved forward after insertion, and in code it is moved forward by magic (by the engine, actually). So with the second-order cursor block cursor, it's moved after insertion but by magic, because no actual insertion is happening but rather there is "remote" insertion, to the block that is the cursor block's owner.

[ 100 | 200 | 300 | ] $: withCursorsAfterEachItem

withCursorsAfterEachItem |block leaves:  [[ 1 2 3 | ]]
withCursorsAfterEachItem 'Didn\'t expect this did you?' shove

withCursorsAfterEachItem leaves: [[ 100 | 200 | 300 'Didn\'t expect this did you?' | ]]
withCursorsAfterEachItem |block leaves:  [[ 1 2 4 | ]]

"swap [ 1 2 4 | ] => swapping(4) [ 1 2 | ] => swapping(4) swapping(2) [ 1 | ]"
withCursorsAfterEachItem swap leaves: [[ 100 | 'Didn\'t expect this did you?' 300 200 ]]

With such solution though, it's hard to keep the cursors intact as I've shown in the examples above, we have to pop them or else consistency will go nuts (as if it already didn't). So this definitely something to think about.

Also, we can go third-order and have multiple cursors in the cursor block. The fact that this should be possible hints at an implementation that recurses somewhere, but it'll recurse somewhere anyway — it's Novika we're talking about. This gives us an opportunity to implement multiple cursors as discussed in the very beginning:

[ 100 200 300 ] $: items

items [ 1 2 3 ] |to*                "Cursor block of items is:  [ 1 2 3 | ]"

items |block [ 1 2 3 ] |to*   "Cursor block of items is:  [ 1 | 2 | 3 | ]"

items 'Heya!' shove               "Cursor block of items is: [ 2 | 3 | 4 | ]"

items leaves: [[ 100 'Heya!' | 200 'Heya!' | 300 'Heya!' | ]]

Note that this example is incompatible with the example above. Again, what'll happen in the cursor block during insertion (and deletion too, frankly) is unclear. Whatever the implementation will be, it should allow terse usage, most of these examples to work, and also multiple orders as I've demonstrated.

Implementing all of this somehow, will make it possible to add something to the "is a block" list: not only are stack, code, array, object, dictionary, continuation, "call stack" aka continuation stack, block friend list — blocks, but block's cursor is a block, too! That can get anyone's head to spin, and I like that.

homonoidian commented 1 year ago

And for the dessert you can think about a block that is its own cursor block :)