Having unrefined fun: REDBOL-FUNC & REDBOL-FUNCTION


#1

I have implemented Pure and Refined: Simplifying Refinements to One or Zero Args. It's so much better in so many ways...usermode and internal...that it's essentially a bug that Rebol ever did it any other way.

Despite being a significant change, it's pretty easy to update client code to. The majority of refinements don't actually take arguments in the first place--so there's nothing you need to do for those. If a refinement took an argument but didn't give a datatype for it, you now have to...it's the type block's presence that indicates it needs an argument at all. But maybe rather than just slapping [any-value!] on, it would be a good time to actually annotate with what the expected types are.

Because there's a goal to be able to emulate Rebol2/Red style in usermode, I decided that before committing the change I would update the Redbol emulation. This would process a function defined in the historical way to a new spec and body for the new rules. (Though note that Redbol will be an extension, and fundamental transformations like this would be rewritten as natives--in whole or in part.)

Nothing too fancy...it transforms the ANY-WORD! refinement arguments into SET-WORD!s, and then when the body runs it moves the value from the refinement into the local and sets the refinement's variable to true or false. A more complex one that allowed multiple refinement arguments would have to be variadic...I'm interested in the workings of that but it's low priority considering everything else.

Technique-wise, what I really like is how smooth it's getting to add set-ness/get-ness/lit-ness with COMPOSE:

insert body compose/deep [
    (argument): :(refinement)
    if not blank? :(refinement) [(refinement): true]
]

REFINEMENT is a PATH! in this case (as /foo is a path). While such paths with blanks at the head are inert, their GET-PATH! and SET-PATH! forms are active, so /foo: acts just like foo: and :/foo acts just like :foo. Scenarios like this one motivate that.

Another thing that I'm getting comfortable with is the no-op status of NULL when used with things like the value to APPEND or the value to KEEP. Historically I had some mixed feelings about it, but as NULL has become the true "non-thing" parallel to what NONE! was trying to signal, it would be a real waste to not be able to use its non-thing-ness. And when you're doing operations like a REPLACE in a block and want to actually replace with nothing, a BLANK! won't do. When you take that to its logical conclusion you realize that something like if keep match text! value [...] is a good thing. VOID! is there to fill in the gaps if you want to have an "ornery" error-triggering value.

Anyway, this is just an example of how the state of everyday coding is evolving for the better. It's a non-trivial function to write, but can be written without feeling in the dark about edge cases.

This includes the other parts of the transformation, like dealing with /extern on function, or making it so you can mutate the body by tweaking the <const> marker out of the spec for the body parameter. It's actually working in practice, and I just ran the Rebol2-ish script to build hostilefork.com with it!

rewrite-spec-and-body: function [
    spec "(modified)" [block!]
    body "(modified)" [block!]
][
    ; R3-Alpha didn't implement the Rebol2 `func [[throw catch] x y][...]`
    ; but it didn't error on the block in the first position.  It just
    ; ignored it.  For now, do the same in the emulation.
    ;
    if block? first spec [take spec]  ; skip Rebol2's [throw]

    spool-descriptions-and-locals: does [
        while [match [text! set-word!] first spec] [
            spec: my next
        ]
    ]

    while [not tail? spec] [
        refinement: try match path! spec/1

        ; Refinements with multiple arguments are no longer allowed, and
        ; there weren't many of those so it's not a big deal.  But there
        ; are *many* instances of the non-refinement usage of /LOCAL.
        ; These translate in Ren-C to the <local> tag.
        ;
        if refinement = lit /local [
            change spec <local>
            refinement: _
        ]

        spec: my next
        if not refinement [continue]

        if tail? spec [break]
        spool-descriptions-and-locals
        if tail? spec [break]

        if not argument: match [word! lit-word! get-word!] spec/1 [
            continue  ; refinement didn't take args, so leave it alone
        ]
        take spec ; don't want argument between refinement + type block

        if not tail? spec [spool-descriptions-and-locals]

        ; may be at tail, if so need the [any-value!] injection

        if types: match block! first spec [  ; explicit arg types
            spec: my next
        ]
        else [
            insert/only spec [any-value!]  ; old refinement-arg default
        ]

        append spec as set-word! argument  ; SET-WORD! in specs are locals

        ; Take the value of the refinement and assign it to the argument
        ; name that was in the spec.  Then set refinement to true/blank.
        ;
        ; (Rebol2 missing refinements are #[none], or #[true] if present
        ; Red missing refinements are #[false], or #[true] if present
        ; Rebol2 and Red arguments to unused refinements are #[none]
        ; Since there's no agreement, Redbol goes with the Rebol2 way,
        ; since NONE! is closer to Ren-C's BLANK! for unused refinements.)

        insert body compose/deep [
            (argument): :(refinement)
            if not blank? :(refinement) [(refinement): true]
        ]

        if tail? spec [break]
        spool-descriptions-and-locals
        if tail? spec [break]

        if extra: match any-word! first spec [
            fail [
                {Refinement} refinement {can't take more than one}
                {argument in the Redbol emulation, so} extra {must be}
                {done some other way.  (We should be *able* to do}
                {it via variadics, but woul be much more involved.)}
            ]
        ]
    ]

    spec: head spec  ; At tail, so seek head for any debugging!

    ; We don't go to an effort to provide a non-definitional return.  But
    ; add support for an EXIT that's a synonym for returning void.
    ;
    insert body [
        exit: specialize 'return [set/any (lit value:) void]
    ]
    append spec [<local> exit]  ; FUNC needs it (function doesn't...)
]

; If a Ren-C function suspects it is running code that may happen more than
; once (e.g. a loop or function body) it marks that parameter `<const>`.
; That prevents casual mutations.
;
 ; !!! See notes in RESKINNED for why an ADAPT must be used (for now)

func-nonconst: reskinned [
     body [block!]  ; no <const> tag
] adapt :func []

function-nonconst: reskinned [
    body [block!]  ; no <const> tag
] adapt :function []


redbol-func: function [
    return: [action!]
    spec [block!]
    body [block!]
][
    spec: copy spec
    body: copy body
    rewrite-spec-and-body spec body

    return func-nonconst spec body
]

redbol-function: function [
    return: [action!]
    spec [block!]
    body [block!]
    /with [object! block! map!]  ; from R3-Alpha, not adopted by Red
    /extern [block!]  ; from R3-Alpha, adopted by Red
][
    if block? with [with: make object! with]

    spec: copy spec
    body: copy body
    rewrite-spec-and-body spec body

    ; The shift in Ren-C is to remove the refinements from FUNCTION, and
    ; put everything into the spec dialect...marked with <tags>
    ;
    if with [
        append spec compose [<in> (with)]  ; <in> replaces /WITH
    ]
    if extern [
        append spec compose [<with> ((extern))]  ; <with> replaces /EXTERN
    ]

    return function-nonconst spec body
]

Pure and Refined: Simplifying Refinements to One or Zero Args