Rye Language

Thought I would pick apart some native ("BFunction") implementation code in Rye, just to get the gist.

A WHILE loop would be ideal, since I already contrasted R3-Alpha, Red, and Ren-C WHILE

But... uh, it turns out Rye doesn't have a normal WHILE (at least that I can find). It has produce\while ... which is arity-3 and is some kind of folding operation.

produce/while

Accepts a while condition, initial value and a block of code.
Does the block of code number times, injecting the number first
and then result of block.

Close enough. Here is a usage from Project Euler Problem 2

; 1. while with condition and a starting block
; 2. while's body: sums last 2 elements and concats result to block
; 3. last one is already greater than 4 million
; 4. filter to even numbers
; 5. sum them and print
; result: 4613732

produce\while { .last < 4000000 } { 1 2 }      ; [1]
{ :b .tail 2 |sum .concat* b }                 ; [2]
|head -1                                       ; [3]
|filter { .even }                              ; [4]
|sum .print                                    ; [5]

So... PRODUCE\WHILE is arity-3, and starts out with a seed block of { 1 2 } which is fed into the condition... and then the body. On successive calls the body's result is fed to the condition, and then to the body again. (The condition's result is discarded).

The first time it's fed into the body, it's observed that the last value in the block is < 4000000. So you have to read that sort of like:

 while (block -> [(last block) < 4000000]) 

Because there's a sort of "invisible left hand side" being fed into the block, you can either put an infix operation at the head of your block and pick it up implicitly, or use a "left set-word" as :b to capture it into a variable. Here the condition uses a leading dot to turn LAST into an operation that takes its argument from that sneaky left. But the body has to make a variable.

Okay, well, "de gustibus non est disputandum". :man_shrugging:

(I can't be too harsh and say I'd never use sneaky infix capture, because I have... though I'd not base a language on it: Persistence of Memory: The Console Enfix Trick. But @LkpPo got confused by it so I dropped it.)

On To The Go Code...

Here is Rye's "BFunction" for PRODUCE\WHILE, formatted slightly to fit on the page here:

"produce\\while": {
	Argsn: 3,
	Doc: "Accepts a while condition, initial value and a block of code."
         + " Does the block of code number times, injecting the number"
         + " first and then result of block.",
	Fn: func(
        ps *env.ProgramState,
        arg0 env.Object,
        arg1 env.Object, 
        arg2 env.Object,
        arg3 env.Object,
        arg4 env.Object
    ) env.Object {
		switch cond := arg0.(type) {
		case env.Block:
			switch bloc := arg2.(type) {
			case env.Block:
				acc := arg1
				ser := ps.Ser
				for {
					ps.Ser = cond.Series
					ps = EvalBlockInjMultiDialect(ps, acc, true)
					if ps.ErrorFlag {
						return ps.Res
					}
					if !util.IsTruthy(ps.Res) {
						ps.Ser.Reset()
						ps.Ser = ser
						return acc
					}
					ps.Ser.Reset()
					ps.Ser = bloc.Series
					ps = EvalBlockInjMultiDialect(ps, acc, true)
					if ps.ErrorFlag {
						return ps.Res
					}
					ps.Ser = ser
					ps.Ser.Reset()
					acc = ps.Res
				}
			default:
				return MakeArgError(
                   ps, 3, []env.Type{env.BlockType}, "produce\\while")
                )
			}
		default:
			return MakeArgError(
                ps, 1, []env.Type{env.IntegerType}, "produce\\while"
            )
		}
	},
},

So...

  • We know what arg0, arg1 and arg2 are, but I have no clue what arg3 and arg4 are supposed to be. Go apparently doesn't warn you about unused arguments.. I guess it's either there for interface compatibility with something else, or it's just accidental code.

  • As expected, it loops and makes two calls to the evaluator as it does so... one for the condition, and one for the body. When the condition isn't truthy any more, it's finished.

    • Makes a direct call to the evaluator, so not stackless. (My understanding is that Go itself has a conventional stack and is not stackless.)

    • Searching for stackless in the repository turns up a builtins_stackless.go but I don't understand how what's in it would apply to stacklessness.

  • Seems to bubble up errors out of the condition, and the body.

    • As I've said, Rye seems to be at least aware that handling arbitrary unknown abrupt failures emerging from arbitrary code is a dead end...especially in a language that can abruptly fail over a typo. You need to connect the errors to the function you just called, or it has to be a hard fail.

    • I'm not so sure about the meaningfulness of being willing to bubble errors up out of the condition and the body. Ren-C doesn't do that, e.g. a TRY cannot suppress an error from inside the while:

      ren-c>> block: []
      == []
      
      >> while [take block] [print "Not trappable"]
      ** Script Error: Can't TAKE, no value available (consider TRY TAKE)
      
      >> try while [take block] [print "Not trappable"]
      ** Script Error: Can't TAKE, no value available (consider TRY TAKE)
      

      I'll admit to not having a firm grasp on what the whole policy should be. But when errors can come from multiple places, I just get skeptical that you really know what you're handling. The body may make sense, but I'm pretty sure the condition is a bad idea.

EvalBlockInjMultiDialect()

Excerpting a bit here:

acc := arg1
ser := ps.Ser
for {
    ps.Ser = cond.Series
    ps = EvalBlockInjMultiDialect(ps, acc, true)
    if ps.ErrorFlag {
        return ps.Res
    }

All right, so acc is the accumulator, e.g. the thing that being sent into the block evaluations implicitly to be picked up as the left-hand side of an infix operator or assignment.

The "Inj" is presumably for "Inject" related to the fact that it's passing in this parameter to inject into the evaluation. The flag is for "injnow", so inject now. I don't know what injecting later would be like... and every call to EvalBlockInjMultiDialect() passes true.

func EvalBlockInjMultiDialect(
    ps *env.ProgramState, inj env.Object, injnow bool
) *env.ProgramState {
    switch ps.Dialect {
        case env.EyrDialect:
            return Eyr_EvalBlockInside(ps, inj, injnow)
        default:
            return EvalBlockInj(ps, inj, injnow)
    }
}

So the "ProgramState" can have the concept of being "in a dialect". Hence when something like WHILE calls EvalBlockInjMultiDialect() it might evaluate that block differently based on what dialect is in effect. And at the moment, the one dialect supported is Eyr...a sort of Reverse Polish math thing.

But the average case it calls the non-dialected EvalBlockInj()

Reading it... really the "inject now" is just due to the fact that there's no "Optional" state on the injected value to say there's no value. So as it evaluates each expression, it has to pass a pair of values: the left hand side, and whether the left hand side you got passed is meaningful.

I See Where The Left-Set-Word Motivation Comes From Now...

What it seems to come down to is that infix is a sunk cost, and it's an attempt to reuse that.

Hence the thinking here is: "so long as there have to be mechanics to feed a value from a previous evaluation in to the next anyway (for operations like +, *, etc)... why not use that already-existing mechanism to slipstream parameters into block evaluations? It's basically 'free'."

To me...that's the implementation-tail wagging the language-dog. :dog2:

It's a trick that obfuscates what's there, and breaks other tricks. For instance: Ren-C has the ability to ELIDE expressions so they vanish completely.

>> 1 + 2 elide print "like this"
like this
== 3

But if you are running a block that's been seeded with this value, and all your block says is [elide print "I think this block is vanishing"], you'd be leaving the residual of the thing you fed into that block.

If I sat down and looked...I imagine I could rattle off a bunch of other things it breaks. But abstractly speaking, I happen to like having constructs that can detect the "reality" of if there's anything to their left or not.

In the past I've had things like a mode-switching +, that became variadic with nothing on its left:

>> x: 1 + 2
== 3

>> x: (+ 1 2 3 4)
== 10

And so when this infix seeds evaluations with a value, anything like that would be broken too.

I'm empathetic to the idea that when you're sitting down and hacking on a language, you might find a trick like slipstreaming infix into a block evaluation, and think it solves your problem. But when you take off the implementation glasses--and look at what it really does in terms of distorting "what's there" in the code--I don't think it's a very empowering approach.