Future of the MATH Dialect

It is basically inevitable that people coming to Rebol will ask about its mathematical evaluation being left-to-right, instead of obeying the precedence order that they are used to. Red just had a new user try to file it as a bug... 3 days ago:

Incorrect Order of Arithmetic Operations · Issue #5276 · red/red · GitHub

What seemed to make sense to people like me and BrianH was that the core ship with a dialect called MATH. The concept was that math [1 + 2 * 3] would give 7 and not 9. Having something in the box seemed better than having the first line of argument being "you don't want the precedence you think you do".

But it turned out to be kind of hard to make pleasing. One key difficulty which @Brett and I fretted over at some point was that since Rebol isn't "psychic" regarding arity, does not know a-priori how much code an expression will consume:

math [1 + 2 * foo baz bar + 3]

; should it be...
[1 + (2 * (foo baz bar)) + 3]

; or perhaps...
[(1 + (2 * foo)) ((baz bar) + 3)]

; maybe...
[(1 + (2 * foo)) baz (bar + 3)]

It started to appear that the user would have to put anything that wasn't a number or a math operator in groups. There may be some heuristics which tolerate words that look up to numbers vs. functions, but it feels very slippery.

As I've said I hate to be dropping things, but MATH is something that Rebol programmers don't really want in the first place--and I don't think non-Rebol programmers would be satisfied by it. If it's included in the core that suggests support for it, and there are just too many things in play.

Here is an implementation that was previously included, by Gabriele:

; This MATH implementation is from Gabrielle Santilli circa 2001, found
; via http://www.rebol.org/ml-display-thread.r?m=rmlXJHS. It implements the
; much-requested (by new users) idea of * and / running before + and - in
; math expressions. Expanded to include functions.
;
math: func [
    {Process expression taking "usual" operator precedence into account.}

    expr [block!]
        {Block to evaluate}
    /only
        {Translate operators to their prefix calls, but don't execute}

    ; !!! This creation of static rules helps avoid creating those rules
    ; every time, but has the problem that the references to what should
    ; be locals are bound to statics as well (e.g. everything below which
    ; is assigned with BLANK! really should be relatively bound to the
    ; function, so that it will refer to the specific call.)  It's not
    ; technically obvious how to do that, not the least of the problem is
    ; that statics are currently a usermode feature...and injecting relative
    ; binding information into something that's not the function body itself
    ; isn't implemented.

    <static>

    slash (the /)

    expr-val (_)

    expr-op (_)

    expression  ([
        term (expr-val: term-val)
        opt some [
            ['+ (expr-op: 'add) | '- (expr-op: 'subtract)]
            term (expr-val: compose [(expr-op) (expr-val) (term-val)])
        ]
        <end>
    ])

    term-val (_)

    term-op (_)

    term ([
        pow (term-val: power-val)
        opt some [
            ['* (term-op: 'multiply) | slash (term-op: 'divide)]
            pow (term-val: compose [(term-op) (term-val) (power-val)])
        ]
    ])

    power-val (_)

    pow ([
        unary (power-val: unary-val)
        opt ['** unary (power-val: compose [power (power-val) (unary-val)])]
    ])

    unary-val (_)

    pre-uop (_)

    post-uop (_)

    unary ([
        (post-uop: pre-uop: [])
        opt ['- (pre-uop: 'negate)]
        primary
        opt ['! (post-uop: 'factorial)]
        (unary-val: compose [(post-uop) (pre-uop) (prim-val)])
    ])

    prim-val (_)

    primary ([
        set prim-val any-number!
        | set prim-val [word! | path!] (prim-val: reduce [prim-val])
            ; might be a funtion call, looking for arguments
            opt some [
                nested-expression (append prim-val take nested-expr-val)
            ]
        | ahead group! into nested-expression (prim-val: take nested-expr-val)
    ])

    p-recursion (_)

    nested-expr-val ([])

    save-vars (func [][
            p-recursion: reduce [
                :p-recursion :expr-val :expr-op :term-val :term-op :power-val :unary-val
                :pre-uop :post-uop :prim-val
            ]
        ])

    restore-vars (func [][
            set [
                p-recursion expr-val expr-op term-val term-op power-val unary-val
                pre-uop post-uop prim-val
            ] p-recursion
        ])

    nested-expression ([
            ;all of the static variables have to be saved
            (save-vars)
            expression
            (
                ; This rule can be recursively called as well,
                ; so result has to be passed via a stack
                insert nested-expr-val expr-val
                restore-vars
            )
            ; vars could be changed even it failed, so restore them and fail
            | (restore-vars) fail

    ])
][
    clear nested-expr-val
    let res: either parse3 expr expression [expr-val] [blank]

    either only [
        return res
    ][
        ret: reduce res
        all [
            1 = length of ret
            any-number? ret.1
        ] else [
            fail [
                unspaced ["Cannot be REDUCED to a number (" mold ret ")"]
                ":" mold res
            ]
        ]
        return ret.1
    ]
]
1 Like

Note: if you want to ask "why isn't Rebol psychic?" consider cases like:

mystery: func [a b] [return a * b]
tricky-negate: func [a] [mystery: :tricky-negate, return negate a]

1 + add tricky-negate 2 mystery 3 * 4

If a pre-pass tried to assume MYSTERY's arity and make decisions, those decisions might become invalid. Ren-C features like variadics make this worse, but the problem is all tied in with Rebol being as dynamic as it is.

Explicit grouping is not a terrible thing, when you consider that most languages require parentheses to delimit function calls. But it's certainly swimming upstream to be trying to ship MATH in the core.

Maybe this will change in the future, but Ren-C has bigger and more interesting issues to work on than trying to twist Rebol into a language that people who don't like Rebol would not want to use anyway.

1 Like