Dissecting the ASSERT Dialect

Another dialect to talk about while discussing the question of whether the evaluator can accrue state across evaluation steps is ASSERT.

The idea of ASSERT is to let you put one expression after the other, and have it tested for "truthiness".

ASSERT needs to do stepwise evaluation, test the result, and ideally report the expression that failed.

Historical Brittleness

Since assert accepts multiple expressions, clipping it in the error makes sense so that you only see the expression that failed.

>> assert [1 = 1 2 = 3 4 = 4]
** Script error: assertion failed for: [2 = 3]

So it has to copy the expression out to put in the error. But for some reason, it only copies an arbitrary three items from the expression:

r3-alpha>> assert [empty? first first [[[a]]]]  
** Script error: assertion failed for: [empty? first first]

This raises a number of questions about error reporting...as to whether this kind of copying makes sense in the first place, or if there should be some common services to help be more informative when providing the "near" information...to implicate the start of an expression instead of the end.

But as it's referring to the block, what if the block is modified?

r3-alpha>> block: [not empty? clear block]

r3-alpha>> assert block
** Script error: assertion failed for: []

This is something Ren-C helps with, by locking the array during evaluation:

ren-c >> block: [not empty? clear block]
== [not empty? clear block]

ren-c>> assert block
** Access Error: series has temporary read-only hold for iteration
** Where: clear evaluate while _ assert console
** Near: [*** empty? clear block **]
** Line: 1

Invisibility in Ren-C

A difference with Ren-C is that you can put an assert anywhere and it won't count against the evaluation:

 ren-c>> all [1 = 1, assert [2 = 2], 10 + 20]
 == 30

 ren-c>> any [1 = 2, assert [2 = 2], 10 + 20]
 == 30

R3-Alpha can't have it both ways...the behavior has to fall on the side of making assert return something either truthy or falsey:

r3-alpha>> all [1 = 1 assert [2 = 2] 10 + 20] 
== 30  ; because the assert returned true

r3-alpha>> any [1 = 2 assert [2 = 2] 10 + 20]
== true  ; because the assert returned true

Implementation Needs

This gives an example of an abstraction that wants to be able to:

  • record a position
  • perform an evaluation step
  • decide it doesn't like the evaluation result, and implicate the position it previously recorded

I started the discussion about "state accumulation" with LET. But let's talk about something like MACRO, which throws a more obvious wrench into this situation.

For instance, let's imagine:

macroA: enfix macro [] [return [+ 2 =]]
macroB: macro [] [return [3 10 =]]

assert [1 macroA macroB 20]

The full expression being processed in practice would be assert [1 + 2 = 3 10 = 20] which should fail on the 10 = 20.

Here we have not just a desire to take single steps across a virtualized block, but also a desire to produce meaningful error messages.

How to deal with this kind of situation? We're in a position where we can probably get the evaluator to make the code work for a plain DO. But when it comes to giving errors and single stepping, what parts are involved is not clear. Is this something that should be forbidden, because the evaluator state is not entirely capturable in terms of the input block's positions? If not forbidden, what sort of interface and mitigation would it need?

Note: Rebol2 and R3-Alpha also had a /TYPE refinement for alternating words and types:

r3-alpha>> age: 37
r3-alpha>> name: "Bob"

r3-alpha>> assert/type [age integer! name string!]
== true

That's not really all that interesting, as Ren-C has ensure, which also passes through the value if you want it to:

ren-c>> ensure integer! age
== 37

ren-c>> ensure string! name
== "Bob"

So the /TYPE refinement was omitted from Ren-C's assert.