presidentbeef / brat

Brat is a little language for people who don't like to be told what to do.
https://www.brat-lang.info/
93 stars 6 forks source link

BUG? blocks can partially defer execution #39

Open mantacid opened 1 month ago

mantacid commented 1 month ago

I'm trying to make a lisp-like let operator, I discovered unexpected behavior in how blocks are executed.

## the let method will allow for lexical scope.
lexical = new
lexical.let = {bindings, body |
  0.to (bindings.keys.length - 1) {i|
    lexical.add_method "#{bindings.keys[i]}" {|bindings[bindings.keys[i]]}
  }
  lexical.with_this {invoke -> body}
  0.to (bindings.keys.length - 1) {i|
    lexical.del_method "#{bindings.keys[i]}"
  }
}

## test the above code
a = 10
lexical.let [a:1, b:1] {p a + b}

Expected behavior: either execution of the {p a + b} block is deferred, and thus prints 2, or it is executed immediately and throws an error saying that b is undefined (since in that scope it is).

Observed Behavior: the function prints 11 as if a used the value from outside the lexical object, but waited to get the value for b.

Is this unintended behavior? Or is there some rule I'm missing?

presidentbeef commented 3 weeks ago

The block definitely grabbing the a from the scope where the block is defined. All functions are closures, so I guess this is expected?

There doesn't seem to be a way of avoiding this. Probably one of many problems with this language...

mantacid commented 2 weeks ago

From my understanding of functional programming, closures don't change their behavior based on an external state. Rather, they have to be passed an argument for that value to be used. The entire reason functional programs need closures is because there is no global mutable state, and closures offer a shared mutable state in place of that global state. While brat is object oriented, and thus has global mutable state, proper closures shouldn't be affected by that state.

Perhaps it could be fixed by prefixing the compiled Lua variable names with a unique id, such that brat variables with the same name don't compile to have the same name in lua if they have different scopes.

For example: a global brat variable "foo" and a brat variable named "foo" in a closure compile like so:

foo in global  --> _g_temp1
foo in closure --> _c1_temp1
mantacid commented 2 weeks ago

I have misremembered what a closure is, that's my bad. Turns out closures persist things in their lexical scope even when called outside said scope. So the closures in brat are using the scope in which they are defined as their lexical scope. The refusal to override the "a" variable thus makes sense, as the value of a from the global scope is being stored in the closure, while the value of "b" is being inherited from the lexical scope. This doesn't explain why the "a" variable isn't being overridden in the lexical scope, but it at least explains the observed behavior.