We now have a clunky implementation of LET (when I say clunky, I mean it's basically like how SET-WORD!s were gathered before...so no clever virtual binding just yet). I explain in that pull request what the difference virtual binding would make would be; you would have to be -running- a let in order to see its impacts. Simple inert references in blocks would not be enough to cause memory to be allocated for a variable. It would have to be run, at which point there would be an allocation and then a "wave" of binding traveling along with the evaluation stream.
But the clunky method used for the moment is at least good enough to get us started moving away from SET-WORD! gathering, and assess the approach. What I want us to do is to slowly start turning FUNCTIONs into FUNC, and see what problems we find (other than the known issue with PARSE rules like copy x: to end not having a place to put a LET). Then someday we deprecate FUNCTION. And then someday after that, they become synonyms.
Here is a sample function, just to look at the BEFORE and AFTER (of a kind of klutzy piece of code used by the help system on derived functions, which is not particularly great to uphold as great code but it's a real thing to look at):
Before
dig-action-meta-fields: function [value [action!]] [
meta: meta-of :value else [
return make system/standard/action-meta [
description: _
return-type: _
return-note: _
parameter-types: make frame! :value
parameter-notes: make frame! :value
]
]
underlying: try ensure [<opt> action!] any [
select meta 'specializee
select meta 'adaptee
first try match block! select meta 'chainees
]
fields: try all [:underlying | dig-action-meta-fields :underlying]
inherit-frame: function [parent [<blank> frame!]] [
let child: make frame! :value
for-each param words of child [ ; `for-each param child` locks child
child/(param): maybe select parent param
]
return child
]
return make system/standard/action-meta [
description: try ensure [<opt> text!] any [
select meta 'description
copy try select fields 'description
]
return-type: try ensure [<opt> block!] any [
select meta 'return-type
copy try select fields 'return-type
]
return-note: try ensure [<opt> text!] any [
select meta 'return-note
copy try select fields 'return-note
]
parameter-types: try ensure [<opt> frame!] any [
select meta 'parameter-types
inherit-frame try select fields 'parameter-types
]
parameter-notes: try ensure [<opt> frame!] any [
select meta 'parameter-notes
inherit-frame try select fields 'parameter-notes
]
]
]
After
(Note: While the name is FUNC for the moment--to indicate suppression of auto-gathering--long term it could be FUNCTION as a synonym, under the current plan.)
dig-action-meta-fields: func [value [action!]] [
let meta: meta-of :value else [
return make system/standard/action-meta [
description: _
return-type: _
return-note: _
parameter-types: make frame! :value
parameter-notes: make frame! :value
]
]
let underlying: try ensure [<opt> action!] any [
select meta 'specializee
select meta 'adaptee
first try match block! select meta 'chainees
]
let fields: try all [:underlying | dig-action-meta-fields :underlying]
let inherit-frame: func [parent [<blank> frame!]] [
let child: make frame! :value
for-each param words of child [ ; `for-each param child` locks child
child/(param): maybe select parent param
]
return child
]
return make system/standard/action-meta [
description: try ensure [<opt> text!] any [
select meta 'description
copy try select fields 'description
]
return-type: try ensure [<opt> block!] any [
select meta 'return-type
copy try select fields 'return-type
]
return-note: try ensure [<opt> text!] any [
select meta 'return-note
copy try select fields 'return-note
]
parameter-types: try ensure [<opt> frame!] any [
select meta 'parameter-types
inherit-frame try select fields 'parameter-types
]
parameter-notes: try ensure [<opt> frame!] any [
select meta 'parameter-notes
inherit-frame try select fields 'parameter-notes
]
]
]
Observations
The auto-gathered version may seem cleaner to you, because you're not needing to mark the locals explicitly, but that comes at a cost.
What cost? Well, how big is the frame for the first case...?
>> words of make frame! :dig-action-meta-fields ; BEFORE
== [return value meta description return-type return-note parameter-types
parameter-notes underlying fields inherit-frame child]
...and the second case?
>> words of make frame! :dig-action-meta-fields ; AFTER
== [return value meta underlying fields inherit-frame child]
Here we have 4 unnecessary variables, and it's not just about wasted memory and processing cycles. This is why people who were skeptical of FUNCTION preferred FUNC and explicit locals. But it seems to me that LET offers the best of both worlds.
Like I say, this is not to say that an auto-gathering mechanism for SET-WORD! shouldn't exist. It's the kind of thing someone should be able to dream up here in the Minecraft-Of-Programming. But it's not what we want in the mezzanine and core implementation code. So we should let those who want it get it out of some third-party module or extension of some kind.
Thoughts on Unbinding
For reasons of safety, I'm wondering if any SET-WORD! in the body of the function be unbound if it isn't an argument, in the "wave" of a LET, or explicitly <with>
'd in the spec.
y: <global>
foo: func [x] [
y: x + 1
]
foo 10 ; error
bar: func [x <with> y] [
y: x + 1
]
bar 10 ; sets y to 11
In the design I'm thinking of, it would only unbind the SET-WORD!s. e.g.
y: <global>
baz: func [condition] [
print mold y
code: [y: 11]
if condition [do code]
]
baz false ; would print <global>
baz true ; would print <global> then error, y is unbound
Do people have any gut feelings on this? I feel like it would help catch bugs.