2025 Retrospective: The Pieces Fall Into Place

(Not a perfect image, but I got tired of asking Sora to do better. This time next year it will be smarter, I'll update the image.)


Many Big Things Happened in 2025

I spent a little less than half the year traveling... drinking, karaoking, and doing improv... in Asheville, Charlotte, and Charleston. But the other time I spent in 100% design/coding mode... pretty much all day and night. So more was accomplished than one would think.

Some of that was implementing things already taken credit for in the 2024 retrospective. I had to move them from experimentation to practice, and bootstrap the system on it. I may make it look easy (?) but there's a lot of details to getting anything like this to work.

  • FENCE!
  • CHAIN!
  • --[dashed strings]--
  • Flexible Logic (~okay~ + ~null~)

The innovation of the "mostly unbound world" was also already laid out in the previous year. But I've finally articulated the principles behind it... in a way that any Reboler should be able to grasp (if they're serious enough about the language to actually read it):

https://rebol.metaeducation.com/t/ren-c-binding-in-a-nutshell/2614

And in 2025 we didn't just get FENCE! as a new list type, but is true purpose was discovered: to evaluate to its block version, but with top-level declarations getting new bindings:

>> {x: 20, print ["X is" x]}
== [x: 20, print ["X is" x]]

So FENCE! makes BLOCK!s, but the code has been effectively "fenced":

>> x: 10

>> eval {x: 20, print ["X is" x]}
X is 20

>> x
== 10

It was the missing tool for granular scoping in a world where binding is careful and correct (instead of careless and broken).

Note that passing an evaluative FENCE! as a function body would make those top-level declarations only once, because it's bound to a single object... making them effectively static:

>> foo: func [x] ({
       y: default [print "Y is a static here" x + 20]
       return y
   })

>> foo 1000
Y is a static here
== 1020

>> foo 304  ; Y won't be assigned again
== 1020

But we address that by having the functions take their body literally. So if you pass a FENCE! in that body spot it will do locals-gathering, making those top-level declarations come into existence per-instantiation as part of the frame:

>> foo: func [x] {
       y: default [print "Y is a local here" x + 20]
       return y
   }

>> foo 1000
Y is a local here
== 1020

>> foo 304
Y is a local here
== 324

That alone would make it a pretty notable year.

We could call it "Year of the FENCE!" and celebrate the resolution of decades of Rebol pain.

But that just happened 7 days ago! :open_mouth:


"Lift The Universe" Sent Shockwaves Through The System

Perhaps also in the running for "most significant breakthrough" is what has been going under the name "Lift The Universe".

https://rebol.metaeducation.com/t/solving-the-pox-of-the-lift-the-universe/2477

The name isn't very fitting for what it became, which is more like "Don't Decay The Metaverse".

It's the idea that an assignment like (^foo: ...) does not decay unstable antiforms, but stores those unstable states directly in the variable. Then (^foo) reads that unstable state back out.

This meant function frames stayed consistent in how they communicate values. It was a critical development, that even allowed unstable values to be stored in API handles.


Naming Advancements

It was a good year for names!

Critically, what I had been calling META and UNMETA became LIFT and UNLIFT.

>> null
== \~null~\  ; antiform

>> lift null
== ~null~

>> lift lift null
== '~null~

This freed up META to be part of the 3 "Sigils": META, PIN, and TIE:

>> meta [x]
== ^[x]

>> pin [x]
== @x

>> tie [x]
== $x

NONE became the name for empty SPLICE! (NULL's truthy cousin), bringing needed nuance to "opting-in with nothing".

Underscore _ became the character literal for space, replacing #" " This generalized to where you could make "runes" generically of any number of spaces (e.g. tab: ____).

FAILURE! straightened out as the name for the error antiform (vs. "raised!"), and FAIL creates these definitional errors, vs. abruptly stopping the system (which is done with PANIC). If you want to crash the system out entirely (previous meaning of "panic"), use CRASH.

UNTIL was retaken for WHILE in the negated sense, while the old meaning of arity-1 UNTIL was reassigned to "INSIST".

ATTEMPT became a LOOP that runs exactly once, which is much more useful than it sounds!

VOID! locked down being the name for the "comma antiform" (which later turned out to only be tangentially related to commas but actually be BLANK!) Then empty PACK! are known as "heavy void".. It goes beyond naming...setting up that isotopic relationship was important, because...


We Learned How To Coexist Safely With "ghosts"

This was probably the third most important development...

When VOID! is produced in multi-step evaluations, the evaluator will conservatively turn it into an empty PACK! to avoid accidental vanishing. Only functions that are marked explicilty as being "vanishable" override this safety feature (or you can use the ^ operator):

https://rebol.metaeducation.com/t/why-comment-vanishes-but-not-eval-of-comment/2563

Once this safety was in place, there was nothing stopping a non-branching IF from returning VOID!. Meaning an IF that took its branch could return an empty PACK!. ELSE and THEN could be sensitive to both "light void" and "light null".

If you have questions about this, read the post. It has some strange-seeming details like why EVAL of a GROUP! throws in the safety feature less than EVAL of a BLOCK!...and why you are thus not allowed to call EVAL:STEP on a GROUP!. It's actually fairly profound, but one great thing is it brought back what I truly wanted:

>> compose [a (if 1 = 2 ['b]) c]
== [a c]

Put simply: We couldn't do that when IF was afraid too afraid of vanishing to return VOID! and returned empty pack as its "didn't take branch" signal instead. Because a successful branch that produced an empty pack couldn't put that pack inside a pack to indicate the branch ran (to THEN/ELSE). That would produce an undecayable pack--an unstable antiform inside an unstable antiform. You don't want such things arising in casual code.

But with void-safety, everything works out:

>> if 1 = 2 ['b]
== \~,~\  ; antiform (void!)

>> 1000 + 20 if 1 = 2 ['b]
== \~[]~\  ; antiform (pack!) "heavy void"

>> 1000 + 20 ^ if 1 = 2 ['b]
== 1020

>> if 1 = 2 ['b] else [print "No Longer Afraid Of Ghosts!"]
No Longer Afraid Of Ghosts!

Speaking With Tics: Finally Resolved (?)

It's always been a frustrating question as to whether callsites should say for-each x [...]
or for-each 'x [...]. There seemed to be arguments on both sides.

But the needs of binding and the "loop iteration variable dialect" wound up forcing my hand. We needed the quote mark to mean "unbind", with absence meaning "apply binding from container".

I made peace with the answer...coming in right under the wire for 2025:

https://rebol.metaeducation.com/t/why-are-loop-variables-taken-literally/2625

This means that writing code that acts the traditional way that "people expect" gets easier, since the descent of binding is the default. And producing unbound material for spots which need it is easier, too.


Many Cool Features


There's A Lot More I Could Talk About

The source code itself has been evolving, and I've factored out a lot of the infrastructure that enables a C99 codebase with this depth and nuance to keep running (due to being able to build and validate aspects of it with C++). There's enough technique in there to give several talks about.

I keep trying to make sure that when it comes time for people to look under the hood, that what they see will be coherent. It's tough because there are so many novel ideas at work, and it's being written in such a low-level way. But I'm feeling the best about it that I have.


What's Not Going Well?

  • Because of just how many changes there are, I've let the tests fall off. There's hundreds of broken tests, which is bad. Where I think I'm headed is that "just because a test was written once, for a feature, that doesn't mean that feature is a priority or coming back." To get to a state of all tests passing again will have to mean saying goodbye to some ideas.

  • It's running slowly... but release builds are actually a lot faster than I thought. There's plenty of room for optimization, and also to see which of my heavy debug checks are causing more irritation than catching actual bugs.

  • I've mentioned how the HTTPS/TLS code has given way to just calling curl... as one person, I can't chase every moving target... and the cipher suites/etc. are just something that moves too fast and is too hard to get right even if you are keeping up. But there was a lot of learning in doing it, and the system evolved because of it.

  • No matter how good my work is, there seems to be no way to get the various ne'er-do-wells who've gone off to start their own Rebol projects to sit up and take note. That's a sort of existentially depressing thing about them. So thanks to everyone here for not being like them!

3 Likes

Happy New Year!!

Truly phenomenal year, @hostilefork. What a mad sprint to the finish line.

What does this year 2026 augur?

2 Likes

I want to get this out to people. Certainly want to bring @rgchris's Ren-C scripts up to date with the modern EXE, and get you back hacking on Query again.

As for other people: I'm still aiming for the starting target to be gaming--code golf puzzle challenge stuff. I think it can be very fun, and the Magical Statics point to the kinds of "a-ha" moments that I think the game could get people to.

But even if it's "just a game", it would have been bad to have put it out before having figured out even the things I've done this past week. I'm not sure how many more "foundational reversals" there will be. (I'm swapping the GROUP! and BLOCK! antiforms as we speak, and it's one of those "I'd feel remiss if that had been wrong" things.) It doesn't have to be a finished, I just have to have a certain baseline pinned down.

"A designer knows he has achieved perfection not when there is nothing left to add, but when there is nothing left to take away." -- Antoine de Saint-Exupery.

For PARSE to be usable on non-trivial data sizes, it has to be nativized. I've explained my strategy for that (one combinator per file, keep the usermode version in the same C file as comments so it can be maintained alongside the native, scrape the Rebol version out to make a module and then have ways of swapping or sporadically running the usermode versions in debug). It's been eye-opening to try honing the usermode code first though... so I'll keep doing that.

The "big ticket" items that have to be decided are:

For whatever reason, those are the things that are on my mind right now.

2 Likes

That's a huge amount of creative work. I missed the rebindable syntax idea, intuitively that seems full of possibility, very cool.

Best wishes and Happy New Year!

1 Like

Glad you appreciate it!

(I'm not sure how to address anyone who desn't think this "is Rebol" ... when it solves precisely the problems which were being contemplated in R3-Alpha, with clear and targeted solutions across the board.)

It's fun when things work. :wrench:

Maybe it will tempt you into revisiting some of your old code someday--or writing some new code.

It's going to be very powerful... and it should enable Redbol emulation to come back.

e.g. the Redbol lib would have definitions e.g. for QUOTED! to quote and bind... and for PATH! to analyze itself and rewrite as TUPLE! or CHAIN! if need be, then retrigger itself inline via an INLINER

Emulating historical Rebol and being able to do the %pdf-maker.r or similar would be pretty epic!

2 Likes