Skip to content

Commit 728f90c

Browse files
aviateskclaude
andcommitted
JuliaLowering: Set is_ambiguous_local for toplevel-assigned
globals in begin blocks When a variable is assigned at toplevel within a `begin` block and then reassigned inside a permeable scope (for/while/try) in the same block, the inner assignment was creating a plain local without setting `is_ambiguous_local`. This happened because the variable was already resolved as `:global` (from the toplevel assignment), so scope resolution took the `b.kind === :global` path instead of the `b === nothing` path where `is_ambiguous_local` was originally set. Now the `b.kind === :global` fallback also sets `is_ambiguous_local` when the scope is permeable and the variable is in `soft_assignable_globals`. The tests directly inspect `BindingInfo.is_ambiguous_local` via `resolve_scopes` rather than testing observable runtime behavior, because `is_ambiguous_local` is currently metadata only — the runtime warning that flisp emitted for this case has not yet been ported to JuliaLowering. The flag's primary consumer is JETLS, which uses it to emit editor diagnostics. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a0083b4 commit 728f90c

File tree

2 files changed

+74
-1
lines changed

2 files changed

+74
-1
lines changed

JuliaLowering/src/scope_analysis.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,8 @@ function enter_scope!(ctx, ex)
320320
# assign-existing-global if this is an explicit global that
321321
# isn't at top level, or if the soft scope exception applies
322322
else
323-
declare_in_scope!(ctx, scope, ex, :local)
323+
declare_in_scope!(ctx, scope, ex, :local;
324+
is_ambiguous_local = scope.is_permeable && vk in ctx.soft_assignable_globals)
324325
end
325326
elseif b.kind === :static_parameter
326327
throw(LoweringError(ex, "cannot overwrite a static parameter"))

JuliaLowering/test/scopes.jl

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,78 @@ end
343343
end
344344
end
345345

346+
# When a toplevel assignment precedes a permeable scope (for/while/try) within
347+
# the same toplevel thunk (e.g. inside a begin block), the inner assignment
348+
# should be marked as ambiguous_local, just like the case where the global
349+
# already existed before the expression was lowered.
350+
@testset "is_ambiguous_local for toplevel-assigned globals in begin blocks" begin
351+
function resolve_and_get_bindings(mod::Module, ex)
352+
est = JuliaLowering.expr_to_est(ex)
353+
ctx1, ex1 = JuliaLowering.expand_forms_1(
354+
mod, est, false, Base.get_world_counter())
355+
ctx2, ex2 = JuliaLowering.expand_forms_2(ctx1, ex1)
356+
ctx3, _ex3 = JuliaLowering.resolve_scopes(ctx2, ex2)
357+
return ctx3.bindings.info
358+
end
359+
360+
# Assignment in for loop within begin block after toplevel assignment
361+
let bindings = resolve_and_get_bindings(Module(), quote
362+
x = 1
363+
for _ = 1:10
364+
x = 2
365+
end
366+
end)
367+
ambiguous = filter(b -> b.name == "x" && b.is_ambiguous_local, bindings)
368+
@test length(ambiguous) == 1
369+
end
370+
371+
# while loop
372+
let bindings = resolve_and_get_bindings(Module(), quote
373+
x = 1
374+
while true
375+
x = 2
376+
break
377+
end
378+
end)
379+
ambiguous = filter(b -> b.name == "x" && b.is_ambiguous_local, bindings)
380+
@test length(ambiguous) == 1
381+
end
382+
383+
# No ambiguity inside a function (hard scope)
384+
let bindings = resolve_and_get_bindings(Module(), quote
385+
x = 1
386+
function f()
387+
for _ = 1:10
388+
x = 2
389+
end
390+
end
391+
end)
392+
ambiguous = filter(b -> b.name == "x" && b.is_ambiguous_local, bindings)
393+
@test isempty(ambiguous)
394+
end
395+
396+
# No ambiguity when variable is not assigned at toplevel first
397+
let bindings = resolve_and_get_bindings(Module(), quote
398+
for _ = 1:10
399+
y = 2
400+
end
401+
end)
402+
ambiguous = filter(b -> b.name == "y" && b.is_ambiguous_local, bindings)
403+
@test isempty(ambiguous)
404+
end
405+
406+
# Explicit `global` should not produce ambiguous local
407+
let bindings = resolve_and_get_bindings(Module(), quote
408+
x = 1
409+
for _ = 1:10
410+
global x = 2
411+
end
412+
end)
413+
ambiguous = filter(b -> b.name == "x" && b.is_ambiguous_local, bindings)
414+
@test isempty(ambiguous)
415+
end
416+
end
417+
346418
# Note: Certain flisp (un)hygiene behaviour is yet to be implemented.
347419
# In flisp, with no escaping:
348420
# - Top-level functions are unhygienic and declared in the macro's module

0 commit comments

Comments
 (0)