The Beta/One Mutability Manifesto

We know that having it be too easy to create self-modifying code in Rebol is a bug-riddled trap. Locking source has shown a hint at what the system can do to help.

Yet we also know it's very inconvenient to not be able to modify series in the console. Beyond convenience, certainly a lot of historical code hinges on it.

Without further ado, the solution...

Best of Both Worlds: CELL-based constness (by reference)

Past approaches to immutability have tried to lock underlying series data, so that all references would be forced to interact with that read-only. The new idea is to sacrifice a bit in the ANY-SERIES! references to that data--what we call "cells" or "values"--to mark particular references to the series as being read-only.

(Due to the spiritual similarity to the C/C++ feature of "const", I've called this the "const bit")

So we start things out as being non-const...a plain LOAD results in something that you can do structural manipulation on just fine. But once you start evaluating that structure, operations can twist the evaluative view into being read-only where it has a good chance of stopping misunderstandings or bugs.

You can mark the arguments to functions as <const> (as well as apply it manually using CONST). Then if you DO something that's const, it will propagate that wave of constness down through any literal values it sees when it executes them.

>> do [b: [] append b 10]  ; DO doesn't have its parameter marked <const>
>> b
== [10]

>> do const [b: [] append b 20]  ; but you can add CONST explicitly
** Access Error: CONST or iterative value (see MUTABLE): []

>> loop 2 [b: [] append b 30]  ; LOOP has its `body` parameter marked <const>
** Access Error: CONST or iterative value (see MUTABLE): []

>> loop 2 [do [b: [] append b 40]]  ; LOOP's const view propagates into the DO
** Access Error: CONST or iterative value (see MUTABLE): []

>> block: [b: [] append b 50]
>> loop 2 [do block]  ; LOOP's "wave" of constness only affects inline literals
== [50 50]

>> loop 2 [b: mutable [] append b 60]
>> b
== [60 60]  ; MUTABLE can take in const "literals" in and un-consts them

What are the implications for the console? Well, the console just runs the code you give it with DO, so...

>> b: [] 
>> append b 70
>> b
== [70]

In addition to looping constructs, FUNC and FUNCTION have their body argument marked <const>.

It cures what ails ya

Remember that super-nasty problem I talked about, where you could accidentally return literals from a function body and callers could mutate them?

rebol2>> symbol-to-string: function [s] [
       switch s [
           + ["plus"]
           - ["minus"]
       ]
   ]

rebol2>> p: symbol-to-string '+
== "plus"

rebol2>> insert p "double-" append p "-ungood"
== "double-plus-ungood"

rebol2>> symbol-to-string '+
== "double-plus-ungood"  ; Oh noes

That's not a theoretical thing. A situation of that nature bit me once and took hours to find. I'm not the only one--and of course newbies (and experts alike) are bit by simpler variations of this all the time.

But now...

>> symbol-to-string: function [s] [
       switch s [
           '+ ["plus"]
           '- ["minus"]
       ]
   ]

>> p: symbol-to-string '+
== "plus"

>> insert p "double-" append p "-good"
** Access Error: CONST or iterative value (see MUTABLE): "plus"

>> p: symbol-to-string '+
== "plus"

>> p: mutable p

>> insert p "you-" append p "-asked-for-it"
== "you-plus-asked-for-it"

>> symbol-to-string '+
== "you-plus-asked-for-it"

That is COOL. Don't let anyone tell you it isn't. :slight_smile: There may be hope for mechanisms to implicate where const was added on things--or at least a debug mode where it favors storing that information over remembering the file and line. It's a balance of cost for such things, though.

Easy to Make Rebol2-Compatible FUNC/WHILE/etc. Variations

We haven't pinned down the dialect exactly, but it's possible to make tweaked versions of functions that have differing parameter conventions. Ignore the gnarly syntax for this (it will improve), but:

>> func-r2: reskinned [body [block!]] adapt :func []  ; no <const> on skinned body

>> aggregator: func-r2 [x] [data: [] append data x]

>> aggregator 10
== [10]

>> aggregator 20
== [10 20]

Really, when you consider the possibility for error (again think of the "plus" example above), it seems a lot more prudent to have to say data: mutable [] to get this behavior.

It's Good...but It's Still New...so TEST IT!

As I keep saying about these things, I'd love to sit around writing test cases and reasoning about them. But I sort of have to pick at a few tricky cases where I think there may be a fundamental flaw...and if I feel like it's covered, I have to go on to the next big thing.

There's little things to wonder about. For instance, it only applies this const rule to inert values. This means you can subvert it with quoting, because the quote causes an evaluation...like how getting a block out of a WORD!-evaluated variable works:

>> loop 2 [append '[] 10]
== [10 10]

Bug, or feature? From the perspective of implementing the API, this is a feature--it means that when you say:

REBVAL *block = rebValue("[1 2 3]");
rebElide("loop 2 [append", rebQ(block), "10]");

The quote is enough to make appending to it legal. It didn't go through a WORD!-fetch...it went through a C-variable-fetch, so it would look like an inert without that quote.

Anyway... PLEASE revisit your source and any calls to MUTABLE you may have...or superfluous COPYs, and let me know of any problems with this model as soon as possible. Because this is looking good for Beta/One and if we can call it pinned down, that solves one of the largest outstanding questions.

4 Likes

Cool, but it seems to me not so immediate to "see" if something is const vs mutable.

Well, very little in Rebol is obvious to see if you don't make an effort to make the code clear. You don't know where function calls begin or end, you don't know if something is being quoted at the callsite by the function you are calling or not... you don't know that a SET-WORD! is assigning or just a dialect part (...etc. etc. etc...)

If you want to be explicit you can put CONST or MUTABLE in as an annotation where you think it adds clarity, even if it's not strictly needed. For instance, when you have:

>> block: [append d: [] <item>]
>> loop 2 [do block]
== [<item> <item>]

>> loop 2 [do [append d: [] <item>]]
** Access Error: CONST or iterative value (see MUTABLE): []

One usage of CONST and MUTABLE would be to change the policy:

>> block: [append d: [] <item>]
>> loop 2 [do const block]
** Access Error: CONST or iterative value (see MUTABLE): []

>> loop 2 [do [append d: mutable [] <item>]]
== [<item> <item>]

Or you can use them to reinforce the policy:

>> block: [append d: [] <item>]
>> loop 2 [do mutable block]
== [<item> <item>]

>> loop 2 [do const [append d: [] <item>]]
** Access Error: CONST or iterative value (see MUTABLE): []

But an underlying great thing about this solution is that it's not about putting locking into LOAD or somehow building it into the evaluator in a way you can't turn off. I've shown the Rebol2 compatibility, and it can be pushed around as people like. Truly flexible, and the plus example shows how this is desperately needed.

1 Like

Overall I think CONST and MUTABLE have been a huge success...catching potential problems, while still letting those who really want to force mutability get their wish.

But just now in writing tests for what I suggested about Virtual Binds being CONST by default, I was confronted again by a behavior I call out above. When you quote things, the quoting makes them subvert the const wave...it's true for loops, and it's true for virtual binds too:

>> loop 2 [data: [], append data 10, append data 20, print mold data]
** Error: ...data is `const` because it is in the "wave" of the loop body

>> loop 2 [data: '(), append data 10, append data 20, print mold data]
(10 20)
(10 20 10 20)

On the other hand, you would be protected if you'd not used quoting, which looks more palatable these days using THE:

>> loop 2 [data: the (), append data 10, append data 20, print mold data]
** Error: ...data is `const` because it is in the "wave" of the loop body

That may seem unfortunate. But if quoting couldn't accomplish this, then every API that wanted to splice mutable values would have to use MUTABLE.

REBVAL *block = rebValue("[1 2 3]");
rebElide("loop 2 [append mutable", block, "10]");

...and if you had to do that for every block, it would undermine any cases where the block itself was actually const.

I think it's now inevitable that an evaluated QUOTED! will have the same semantics as if you had fetched the value out of a word, not the same semantics as if you'd gotten it as a parameter to a function like THE. So a good guideline is that you should either use var: copy '(...) or var: the (...) when declaring GROUP!s as variables. Because var: '(...) won't offer protections for constness to inform you about whether you likely intended a COPY.

This is a fuzzy medium, and so the checks aren't going to be foolproof. :man_shrugging:

2 Likes