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...)

So it occurs to me that this could be of benefit when dealing with the sticky SET-WORD! binding problem. When you're enumerating an OBJECT!, you could have to request for the keys to be bound to the object:

 for-each [$key val] obj [...]  ; key is bound to object

 for-each [key val] obj [...]  ; key is unbound

So, let's imagine you tried to copy an object via COLLECT of some code with a FOR-EACH, and you wrote:

 make object! collect [
     for-each [key ^val] obj [
         keep spread compose [^(key): (lift ^val)]
     ]
 ]

(That's a fairly general solution, though it doesn't cover things like when an object's fields are "truly unset"... and if a field has type checking/etc. you'd have to use a different mechanic.)

ANYWAY... if that key had a binding, then the SET-WORD you got from (key): would have a binding. That's an easy mistake to make (and good to error if you make it).

I think defaulting to not having a binding, and having you say [$key ^val] to get a binding, looks like a good idea.

But I'm not sure how this generalizes. I was hoping that things like FOR-EACH were generalized on making calls to a generator. My thinking about how object enumeration worked would be that it would return a PACK!, such that if you just said for-each 'key it knew to give you just the first element of the ~[key value]~ pack, instead of giving you the value as the next key in the loop. That seemed to work... but now we have to have some kind of channel to communicate if we want the generator to bind things, and that applies to every potential element of a pack... (?) :frowning:

Passing the variables to the generator doesn't feel right, at best it should either offer a :BIND or not. Maybe $[key ^val] is what you'd have to use, to say "I want the answer bound" and it understands that only applies to the keys. :frowning: That's not very good.

I'll have to think about it. Anyway, it's desirable to be able to ask the key to be bound to the object or not.