The %rebol.r Boot File From Rebol 1.0

While we don't have the C source code for Rebol 1.0, we do have the %rebol.r initialization file:

Rebol 1.0.2 Initialization File (October 7, 1998) · GitHub

It would seem that if there were an ability to pack source code in with the executable, they would have done it...given that the Quick Start says "do not modify rebol.r. If you accidentally do modify rebol.r, reinstall it."

Hence this is probably the entire portion of Rebol 1.0 that's written in Rebol, e.g. the whole "Mezzanine".

It says:

;;; Note: Code in rebol.r runs in the system context.  The system
;;; context [has] all the built in bindings of the user context, but also
;;; has extra bindings to allow rebol to be bootstrapped.  Many of the 
;;; rebol functions available in user code are actually written in
;;; terms of simpler rebol natives, or in terms of special
;;; system natives.

;;; REBOL reserves the right to change the system natives at any time, 
;;; so you shouldn't depend on them for portable code.

Remarks On Contents, In No Particular Order

FUNC Definition

func: make function! [args body] [make function! :args :body]

I found it a bit interesting that the User Guide talked about how FUNC was defined, as an illustrative example, of an important thing for users to know about. (It turns out there's a brief mention in the Rebol2 User's Guide, but the Rebol 1.0 Guide writes it up twice, probably on accident.)

It's worth pointing out that there's big questions even in this seemingly simple definition. Such as, should a function copy its arguments or body? What should this do?

 body: [print "Hello"]
 foo: func [] body
 append body [print "Goodbye"]
 bar: func [] body

Does foo print just "Hello", or does it print "Hello" and "Goodbye"?

I don't yet know what Rebol1 did, but Rebol2's MAKE FUNCTION! would not copy the body. So FUNC would do a deep copy as the "higher level" operator, before passing it to MAKE FUNCTION!.

But during bootstrap, it used a definition of FUNC that didn't copy the body, for performance reasons...which it switched over to the copying implementation at the end of boot. :roll_eyes:

Some version of these crazy optimizations are on the table for future Ren-C.

PRINT, PRIN, PROBE

write-block-or-element:
    make function! [port element] [
             do
                if block? :element [:write-block] else [:form-to-port]
                :port
                :element]

write-block: func [port block] [    ; !!! needs work
    foreach element :block [form-to-port :port :element form-to-port :port " "]
]

prin: func [value] [
    if block? :value [write-block output-port reduce :value]
    else [form-to-port output-port :value]
    exit
]

print: func [value] [
    prin :value
    linefeed-port output-port
    exit
]

probe: func [value] [
     prin " PROBE --> "
     send output-port :value
     linefeed-port output-port
     :value
 ]

Weird. (and prints a space after every element, so you get a space at the end of the line vs. just delimited between, etc.)

At least one interesting aspect of this is to see the rigid "EXIT" at the end to make sure that PRIN and PRINT don't leak a result on accident. Things like this feel like a vindication of Ren-C's requirement to use a RETURN in order to give back a result from FUNC (but not LAMBDA).

IS? as TO-LOGIC

is?: func [value] [not not :value]

This was called TRUE? in Rebol2, and I very much disliked the ambiguity of that vs. testing to see if a value was = #[true] the LOGIC! literal.

I wrote something up about how DID could be the opposite of NOT (which even goes together as DIDN'T for DID NOT). But due to some various shades of meaning the current state is that it means THEN? and DIDN'T means ELSE? as prefix tests for the trigger conditions that would run THEN or ELSE. It needs thought.

Anyway, interesting to see the choice of IS? here.

A Recursive Folding ANY and ALL :interrobang:

any: func [block] [
    eval-one block
        make function! [value rest] [
            if not value [any rest]
            else [value]
        ]
        make function! [value] [value]
]

all: func [block] [
    eval-one block
        make function! [value rest] [
            if is? value [all rest]
            else [false]
        ]
        make function! [value] [value]
]

So this is based on a function called EVAL-ONE, that takes a list and two functions. It isn't defined in %rebol.r and isn't in the reference guide either. But it's a right fold with early termination.

One can definitely imagine the Joe Marshall and Carl friction on this ("why are you making all these usermode functions and calls, why not just use a loop?").

While there's a time and a place for this, I do think that if you are starting to push out into the usermode layers and finding this mentality is driving it...you're going to end up with something that isn't hitting the mark that Rebol was aiming at.

Why Is PICK So Weird?

pick: func [series index] [
  do make function! [offset] [
    if (:offset + index? :series) <= 1
        [none]
    else [do make function! [disp] [
            if (length? :disp) = 0 
               [none]
            else
               [&peek :disp 0]
            ] skip :series if :offset < 0 [:offset] else [:offset - 1]
         ]
    ] if logic? :index [if :index [1] else [2]] else [:index]
]

My guess here is that the pattern:

do make function! [arg] [...code with arg...] value-for-arg

...is probably some holdover from before USE existed. Or maybe USE is just an abstraction built on functions, and so it's done this way for optimization. I dunno.

Poor-Man's EXPORT

;;; These functions can be defined in terms of system natives that are
;;; not available in the user context.  Since we made the functions in 
;;; this context, the values of the words in the body are relative to
;;; this context.  But we place the functions in the user context so
;;; that the users can call them.  This allows the user to call the
;;; system natives through a defined API in a controlled manner.

user-functions: [
    dir? [file] [do func [info] [info/dir?] info? :file]
    size? [file] [do func [info] [info/size] info? :file]
    ...
]

foreach [name args body] user-functions [
    context-set user-context name func args body
]

So the comment says what's going on here, it's the attempt to push functions out into the user context when they're implemented in terms of functions that aren't available in the user context. I'm not sure what's not available (these implementations of DIR? and FILE? are based on INFO?, is that not exported to the user context?)

As far as I know, there's nothing like this in Rebol2 (there's no separate user-context from a system-context, is there?) Interesting if that was something that disappeared in Rebol2 and came back in Rebol3.

More Modularization: EVAL-REDUCE Takes Context

if not none? REBOL/script [
    if exists? REBOL/script [
        do make function! [] [
                top-level-continuation: :return
                if not REBOL/silent [
                        linefeed
                        prin "Loading script "
                        print REBOL/script]
                eval-reduce [do REBOL/script] user-context
                ]
        ]
    ]

Rebol1 seems to have been working with modularization ideas, because even during startup, the script you pass on the command line is run via something called EVAL-REDUCE that takes a parameter of where to do the evaluation.

So definitely a shame that Rebol2 seems to have moved away from the idea that evaluations needed to be done in a context.

CATCH is defined in terms of CATCH-FUNC

We know from the user-functions exporting that this:

catch [word block] [catch-func func reduce [word] :block]

Is actually:

catch: func [word block] [catch-func func reduce [word] :block]

Since there's no type checking, there's a :BLOCK GET-WORD! just to be sure it's not a function, I guess? And then it's FUNC's job to do a check in its implementation. But then, why not :WORD just to be sure WORD! isn't a function you're calling? (I like pointing this out, due to Ren-C's better answers to this issue...avoiding the "pox of documenting what you don't know")

So the idea of using functions as proxies for "virtual binding" is the old way. What's going on here is that the block contains code that wants to be bound to whatever the throw construct is, and so that block is made the body of a function, that you call and pass the thing you want bound to that name as the argument. (COLLECT+KEEP worked this way). But it's undesirable, because it means you've lost the fluidity of having the currency of a structural BLOCK!...replaced with the black box of a function just because you wanted to bind something.

I'm pretty sure this CATCH mechanic (being called a "continuation") is stackful and can't do anything too bizarre, but I'd like an executable to try and ensure that.

1 Like

Exception Handling

do-with-exception-handler: make function! [handler body] [
    do make function! [outside-value inside-value] [
        shield [outside-value: :exception-handler-stack
                exception-handler-stack: :inside-value]
               :body
               [inside-value: :exception-handler-stack
                exception-handler-stack: :outside-value]
        ] :exception-handler-stack 
          stack-push :exception-handler-stack :handler
    ]

So DO-WITH-EXCEPTION-HANDLER is defined in %rebol.r, but not used by it (e.g. it's not how the REPL traps errors). Hence we don't see any usages of the exception handling functions.

But what we do see is that it depends on a native called SHIELD, which is in the user guide:

shield before-block main-block after-block

This appears to be a convenience of some kind, to let you register a handler to keep you from having to go around SHIELD-ing all your individual calls.

Read/Eval/Print Loop (REPL)

The REPL is initialized by a call to INIT-REPL, which passes in some messages and ports, and a context to do evaluations in:

init-repl
    [linefeed prin "REBOL top level."]         ; start-message
    [linefeed prin "Returning to top level."]  ; resume-message
    ">> "                                      ; prompt
    user-context                               ; repl-context
    input-port                                 ; repl-input-port
    output-port                                ; repl-output-port

The meat of INIT-REPL is in this code that it passes to PUSH-REPL, which I've

do make function! [form] [
     write-block-or-element :repl-output-port (
         shield [&trace repl-trace nearest-repl]  ; before-block
                [eval-reduce :form :repl-context]  ; main-block
                [&trace false]  ; after-block
     )
] (port-read :repl-input-port)

If we were to make this a bit less obtuse:

form: port-read :repl-input-port  ; READ

result: shield [&trace repl-trace nearest-repl] 
               [eval-reduce :form :repl-context]  ; EVAL
               [&trace false]

write-block-or-element :repl-output-port :result  ; PRINT

So I gather the SHIELD is there to make sure that if code was being traced and there was an error, you don't wind up tracing the REPL itself. It doesn't actually catch errors or throws, it just seems to make sure your AFTER-BLOCK code runs. :confused:

I don't see anything about printing error messages, and there's nothing in the user's guide about an ERROR! type, so it seems like errors aren't values.

So what probably happens when there's an error is that the interpreter just prints it. I don't know what the SHIELD's result is... NONE?

It would be easier to understand the situation with errors or uncaught throws with a working interpreter.

Ctrl-C Handling

;;; The repl-driver drives the console interaction.  The argument
;;; 'once' is assumed to do a single interaction.  We capture a return 
;;; continuation and then turn on the interrupts before calling
;;; 'once'.  If an error or control C happens, control is transfered
;;; to the captured continuation.  Since this continuation is captured 
;;; in a (dynamic) context with interrupts turned off, the interrupts
;;; are disabled while iterating around the loop (so the loop cannot
;;; be broken by a lucky interrupt).

So at least this has something in common with Ren-C, which is the implementation of the REPL in usermode...and being sensitive to questions like not wanting you to be able to Ctrl-C the REPL implementation itself. Ren-C also disables the Ctrl-C while inside the console's implementation.

(Though interestingly, you can Ctrl-C if you're in console skin code...a misbehaving skin where you've customized your I/O can be canceled, and it will fall back on the default skin.)

There's a definition exported to the user context:

 halt: func [] [top-level-continuation none]

Which this does the same thing as Ctrl-C. I still kind of want to understand the limits of what is possible with these "throws" that are being called continuations, and just how arbitrarily you can throw the program around.

Anyway, it's still a bit murky what some of it is doing, but by-and-large I pretty much understand it.

1 Like