Sticky SET-WORD! Binding Problem In MAKE OBJECT!

Trying to use the policy that bindings don't override, I ran into a bug with MAKE OBJECT! when a SET-WORD! is carrying a pre-existing binding.

What happens is that a new field is made (cued by seeing a top-level SET-WORD!) but then when the body is executed it uses the old binding... so the newly created field remains unassigned, and the old value is updated.

>> obj: make object! [x: 10]
== make object! [
    x: 10
]

>> block: compose [(bind 'x: obj) <new>]
== [x: <new>]

>> obj2: make object! block
== make object! [
    x: ~
]

>> obj
== make object! [
    x: <new>
]

This hints at a class of hard-to-reason-about cases. The more you use material with hardened bindings, the more you'll see them come up.

(I hit the problem in the whitespace dialect code, which is still using mutable binding when it probably should not... but, it gives a good example of a problem that happens if you do... and if we're living in the non-binding-overriding world.)

For the moment, I think I'm going to say that MAKE OBJECT! will error if any of the SET-WORD!s in the top level already have a binding.

  • Binding can't serve two masters. If the code were more complex than the above, it's not clear to say that the original binding didn't mean what it said in terms of intending a specific assignment... vs wanting to be overridden.

  • I don't want fundamentals like this to use mutable binding, and to get this to work virtually at one-level of depth would require a tricky bind instruction that I don't want to deal with right now.

I'd say it's the first "major" problem I've seen in practice from a policy of not overriding binding (in the sense that it's fairly hard to argue "well, maybe you wanted that").

Well… my view is that, with this binding policy, rebinding individual words becomes a much less common thing to do. So I do think it’s possible to argue in this case that bind 'x: obj is an indicator that you want to do something ‘weird’. In any case, making this situation an error seems like the best solution to me.

This raises the question of how often constructs should UNBIND material they get vs. error.

The case in question was with UPARSE's EMIT.

 >> label1: 'name

 >> parse [foo 1 2 3 bar 4 5] [collect [
        some keep gather [
            emit (label1): word!, emit list: collect some keep integer!]
        ]
    ]]
 == [
    make object! [
        name: 'foo
        list: [1 2 3]
    ]
    make object! [
       name: 'bar
       list: [4 5]
  ]

(Note: I think this motivates ACCUMULATE INTEGER! as a synonym for COLLECT SOME KEEP INTEGER! and ACCUMULATE GATHER as a synonym for COLLECT SOME KEEP GATHER)

What EMIT does is adds a SET-WORD! and a META of the value it gets to a block:

 [name: 'foo list: '[1 2 3]]

Then it runs MAKE OBJECT! on the result.

What was happening was that the incoming WORD! here (name) was bound. So the SET-WORD! in that block was bound.

  • If EMIT doesn't report the error it goes into the MAKE OBJECT! and will error. Error locality is better if it reports an error.

  • It could also just remove the binding and keep going.

    • Yet removing the binding may gloss over potential misunderstandings you had by passing something with a binding in.

Implication For Casual Creation of Bound Items

I'm observing you need to be wary of the style:

for-each item block [
    item: in block item
    ...
]

Because if you do any insertions of that item into code, they will be bound. You probably should wait to do the IN until you need to do a lookup, and work with unbound items as long as possible...

I haven't actually made it error yet.

But had another occurrence, now triggered by the SPREAD COMPOSE hardening the bindings of what it spreads:

>> spec: load %some-file.r
== [field1: if prop1 [...] field2: 20]  ; some list, mentions external props

>> insert spec spread compose [prop1: (...) prop2: (...)]

>> obj: make object! spec
** Error: prop1 is ~ isotope

So here we got our spec as:

[prop1: (...) prop2: (...) field1: if prop1 [...] field2: 20]

But when the prop1 assignment is done, it's to whatever context of assignment in the pre-spread block was... due to the SPREAD propagating bindings.

So you have to stop it from doing that, e.g.

insert spec spread bindable compose [prop1: (...) prop2: (...)]

This is sort of an epicycle of strangeness, where the bootstrap code has to run in a Ren-C that is many years old... so it's a workaround for a workaround. But worth noting patterns of "things that seem like they should work" that don't...

(UPDATE: I think the SPREAD of binding is not the right strategy...)