You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found by code review on PR #2954 (#2865), and recorded there in docs/compliance/jq/limitations.md. #2865 makes a module's own includes work
by wrapping each exported def's body in the dependencies it reaches. The wrap
nests inside that def's own scope, so a name left free in a dependency can
be captured by the def's own name or by one of its parameters.
Not a regression: every shape below needs a transitive include, which did not
work at all before #2865 (main answers g/0 is not defined / k/0 is not defined for each). These are residual gaps in the new functionality.
Repro
All rows confirmed live against jq 1.7.1 and PR #2954's head.
1. The self-(name, arity) exclusion strands a dependency another dependency calls
mid's body calls c directly, so the dependency c is excluded to leave the
self-recursive call binding to itself — but the kept dependency g still needs
it, and its c then resolves outward to mid's own c.
2. A parameter satisfies a name a dependency left undefined, swallowing a compile error
h's own g is correctly the parameter; k's g should still be the
dependency, and is not.
Root cause
ModuleLoader::load_and_bind_module / visible_deps_for
(src/bin/succinctly/jq_runner.rs). Binding is wrap_defs(body, deps), which
nests the dependency block inside the Expr::FuncDef for the def itself — so
that def's name (for self-recursion) and its parameters are enclosing binders
for every dependency in the block. Real jq binds each module's block in its own
scope and only then links it, so nothing of the caller is in scope for it.
The two exclusions in visible_deps_for are the current, partial mitigation:
they keep the def's own recursion and its parameters working for names the body
uses directly. They cannot help a name reached only through another
dependency, and the cases pull in opposite directions — excluding strands the
other dependency (row 1), keeping it would shadow the def's own binding.
Suggested fix direction
Two shapes:
Targeted renaming. When a dependency has to be excluded, give it a fresh
internal name instead of dropping it, and rewrite free calls to the original
name inside the other kept dependency bodies — a scope-aware walk that
stops at any nested def of the same (name, arity) and at parameters
binding it. Contained, and fixes rows 1 and 3; row 2 needs (3) below as well.
#2865 delivers transitive loading and matched every ordinary shape probed
(dependencies, siblings, parameters, arity splits, self-recursion, patterns,
namespaces, diamonds, chains, $__loc__, cycles); this is the boundary of the
mechanism it uses, and needs either a scope boundary (#2951) or the narrower
rename above rather than another exclusion rule. #2950 is the shadow-candidate
half of the same module-scope story.
Severity: Low
Summary
Found by code review on PR #2954 (#2865), and recorded there in
docs/compliance/jq/limitations.md. #2865 makes a module's ownincludes workby wrapping each exported def's body in the dependencies it reaches. The wrap
nests inside that def's own scope, so a name left free in a dependency can
be captured by the def's own name or by one of its parameters.
Not a regression: every shape below needs a transitive
include, which did notwork at all before #2865 (
mainanswersg/0 is not defined/k/0 is not definedfor each). These are residual gaps in the new functionality.Repro
All rows confirmed live against jq 1.7.1 and PR #2954's head.
1. The self-(name, arity) exclusion strands a dependency another dependency calls
mid's body callscdirectly, so the dependencycis excluded to leave theself-recursive call binding to itself — but the kept dependency
gstill needsit, and its
cthen resolves outward tomid's ownc.2. A parameter satisfies a name a dependency left undefined, swallowing a compile error
The worse half of the family: a program jq rejects produces an answer.
3. A parameter captures a dependency reached only indirectly
h's owngis correctly the parameter;k'sgshould still be thedependency, and is not.
Root cause
ModuleLoader::load_and_bind_module/visible_deps_for(
src/bin/succinctly/jq_runner.rs). Binding iswrap_defs(body, deps), whichnests the dependency block inside the
Expr::FuncDeffor the def itself — sothat def's name (for self-recursion) and its parameters are enclosing binders
for every dependency in the block. Real jq binds each module's block in its own
scope and only then links it, so nothing of the caller is in scope for it.
The two exclusions in
visible_deps_forare the current, partial mitigation:they keep the def's own recursion and its parameters working for names the body
uses directly. They cannot help a name reached only through another
dependency, and the cases pull in opposite directions — excluding strands the
other dependency (row 1), keeping it would shadow the def's own binding.
Suggested fix direction
Two shapes:
internal name instead of dropping it, and rewrite free calls to the original
name inside the other kept dependency bodies — a scope-aware walk that
stops at any nested
defof the same (name, arity) and at parametersbinding it. Contained, and fixes rows 1 and 3; row 2 needs (3) below as well.
sealed scope the wrap would not nest inside the caller at all, and the whole
family goes away. Subsumes (1).
b/0 is not definedagainst the module's own file the way jq does. That is what turns row 2 back
into a compile error rather than something a caller can accidentally satisfy,
and it also interacts with jq: a module's own source is parsed with no shadow-candidate seeding, so a transitively included def can't shadow a builtin inside that module #2950 (a module's source is parsed with no
shadow-candidate seeding).
Why this is separate from #2865, #2950, #2951
#2865 delivers transitive loading and matched every ordinary shape probed
(dependencies, siblings, parameters, arity splits, self-recursion, patterns,
namespaces, diamonds, chains,
$__loc__, cycles); this is the boundary of themechanism it uses, and needs either a scope boundary (#2951) or the narrower
rename above rather than another exclusion rule. #2950 is the shadow-candidate
half of the same module-scope story.