Skip to content

Conversation

@jpco
Copy link
Collaborator

@jpco jpco commented Jan 6, 2026

Fixes #239.

Es' historical behavior with $0 is to bind it, dynamically, when invoking a lambda invoked via a function name. This creates confusion, as most obviously demonstrated by

; `{echo echo $0}
%backquote

but also illustrated by a number of other examples in #239.

This PR changes the binding of $0 from dynamic to lexical, such that when we look up a function, we bind $0 to the function name within the lexical scope of the looked-up definition. This creates good, intuitive behavior in basically all cases, particularly with respect to functions which take other code as arguments. It also removes the behavioral differences between fn-x = @ {} and fn-x = $&noreturn @ {}. $0 in settor functions also uses this new behavior.

$0 is still a bit unpredictable, as functions invoked as $fn-X do not get $0 set to X. But within the body of a function, you're at least now ~guaranteed to have $0 set to the function's name, the script you're running, or the es binary.

QUESTION: some hook functions are not invoked in a way where $0 is set to the function name.

; fn %home {result $0}
: ~/git/es-fork; echo ~
es

Is this unexpected? Should we scan through these cases and replace varlookups with fnlookups? It is inconsistent that the syntax-expanded hook functions like %backquote get $0 set to the function name but shell-lookup hook functions like %home don't.

jpco added 4 commits January 5, 2026 19:21
This binding is created when a function is looked up by name.  Works for
settor functions as well.  Should be a minimal performance impact, as
functions tend to be short lists (typically a single lambda term) and
this implementation appends the arguments to the call in the same pass.
@jpco
Copy link
Collaborator Author

jpco commented Jan 14, 2026

Found a sufficiently pathological example that gives me a little pause. The idea is that the function x, when given an argument, "saves" that argument in the function definition, and then when given no arguments, pops the first saved argument from the queue and evaluates it.

Granted, it's a very strange function, but I also don't know if this behavior is the most reasonable way to handle it.

#!/usr/local/bin/es

fn x {
	# echo ' > calling x' $*
	if {~ $#fn-x <={%count (1 $*)}} {
		let ((f a) = $fn-x) {
			fn-x = $f $a(2 ...)
			$a(1)
		}
	} {
		fn-x = $fn-x(1) $*
	}
}

x {echo $0 one}
x {echo $0 two} {echo $0 three}

x
x
x

echo done

When run with current es at HEAD, this does

; es ~/testme.es
x one
x two
x three
done

Not so surprising. When run with this PR's es, it does

; ./es testme.es
x one
testme.es two
testme.es three
done

More surprising.

@jpco
Copy link
Collaborator Author

jpco commented Jan 15, 2026

Okay, I figured out what's going on with my script above and I think that the current implementation is basically fine, the script is just pathological and inconsistent. A super minimal repro is here:

; cat demo.es 
fn-x = @ {echo $*} {cmd1}
x {cmd2}
fn-y = @ {echo $fn-y(2 ...) $*($#fn-y ...)} {cmd1}
y {cmd2}
; . demo.es 
%closure(0=x){cmd1} {cmd2}
{cmd1} {cmd2}

When we refer to {cmd1} via $* in x, where it comes from looking up x via fnlookup, it has $0 bound. However, when we pull {cmd1} straight from $fn-y in y, then it doesn't have $0 bound. My script above is inconsistent because in one case we're looking up the saved thunks via $fn-x in (f a) = $fn-x, and in the other case we're looking up the saved thunks via $* in fn-x = $fn-x(1) $*. If we change one or the other, we can make the behavior consistent -- and we can control whether we have $0 bound or not.

I think this is all basically reasonable on the shell's part; we just found a sharp edge. In theory we could bind $0 in $fn-x lookups, but then $fn-x is inconsistent with %whatis x and var x, and things get a bit messy in other ways.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Was dynamic scoping of $0 a mistake?

1 participant