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. 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 and if we can call it pinned down, that solves one of the largest outstanding questions.