Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion src/desugaring.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2153,6 +2153,60 @@ function strip_decls!(ctx, stmts, declkind, declmeta, ex)
end
end

# Extract the declarable name from a function signature
# Returns the bare identifier that should be declared, or nothing if the
# name is qualified (e.g., A.B.f)
# This follows the same logic as expand_function_def for consistency.
function extract_decl_name_from_funclike(ex)
@assert kind(ex) == K"function"
name = ex[1]

# Strip `where` clauses and type annotation (same as expand_function_def)
while kind(name) == K"where" && numchildren(name) >= 1
name = name[1]
end
if kind(name) == K"::"
if numchildren(name) == 2
name = name[1]
else
# Invalid signature
return nothing
end
end

if kind(name) == K"call"
name = name[1]
elseif kind(name) == K"tuple"
# Anonymous function syntax `function (x,y) ... end` (no declaration)
return nothing
else # Bad function definition
return nothing
end

# Extract bare function name
if kind(name) == K"::"
# Self argument is specified by user: function (f::T)() ...
if numchildren(name) == 2
# function (f::T)() ... => f is the declarable name
return name[1]
else
# function (::T)() ... => anonymous, no declaration
return nothing
end
elseif kind(name) == K"Placeholder"
return nothing # Anonymous function
elseif is_invalid_func_name(name)
return nothing # Invalid name like `ccall` or `cglobal`
elseif is_identifier_like(name)
return name # Normal function: function f() ...
elseif kind(name) == K"." && numchildren(name) == 2 && kind(name[2]) == K"Symbol"
# Qualified name: function A.B.f() ...
# Don't add declaration for qualified names
return nothing
end
return nothing
end

# Separate decls and assignments (which require re-expansion)
# local x, (y=2), z ==> local x; local z; y = 2
function expand_decls(ctx, ex)
Expand All @@ -2169,6 +2223,16 @@ function expand_decls(ctx, ex)
push!(stmts, expand_assignment(ctx, @ast ctx binding [kb lhs binding[2]]))
elseif is_sym_decl(binding)
strip_decls!(ctx, stmts, declkind, declmeta, binding)
elseif kb === K"function"
# Handle function definitions within global/local
# e.g., `global function f() ... end` or `let x=1; global g()=x; end`
name = extract_decl_name_from_funclike(binding)
if !isnothing(name) && is_identifier_like(name)
# Add the declaration for the name
push!(stmts, @ast ctx binding [declkind name])
end
# Expand the function definition
push!(stmts, expand_forms_2(ctx, binding))
else
throw(LoweringError(ex, "invalid syntax in variable declaration"))
end
Expand Down Expand Up @@ -3035,7 +3099,7 @@ function expand_function_def(ctx, ex, docs, rewrite_call=identity, rewrite_body=
push!(sig_stmts, @ast(ctx, ex, [K"curly" "Tuple"::K"core" arg_types[2:i]...]))
end
sig_type = @ast ctx ex [K"where"
[K"curly" "Union"::K"core" sig_stmts...]
[K"curly" "Union"::K"core" sig_stmts...]
[K"_typevars" [K"block" typevar_names...] [K"block"]]
]
out = @ast ctx docs [K"block"
Expand Down
89 changes: 89 additions & 0 deletions test/decls.jl
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,93 @@ end
# Unsupported for now
@test_throws LoweringError JuliaLowering.include_string(test_mod, "const a,b,c = 1,2,3")

@testset "global function in let" begin
# Basic case: short form function syntax
@test JuliaLowering.include_string(test_mod, """
let x = 42
global getx1() = x
end
""") == test_mod.getx1
@test test_mod.getx1() === 42

# Long form function syntax
@test JuliaLowering.include_string(test_mod, """
let y = 100
global function gety1()
y
end
end
""") == test_mod.gety1
@test test_mod.gety1() === 100

# Multiple global functions in same let
@test JuliaLowering.include_string(test_mod, """
let val = 7
global getval1() = val
global setval1(v) = (val = v)
end
""") == test_mod.setval1
@test test_mod.getval1() === 7
test_mod.setval1(20)
@test test_mod.getval1() === 20

# Type-qualified function
JuliaLowering.include_string(test_mod, """
struct TestCallable1 end
let x = 99
global (::TestCallable1)() = x
end
""")
@test test_mod.TestCallable1()() === 99

# Function with where clause
@test JuliaLowering.include_string(test_mod, """
let data = [1,2,3]
global getdata1(::Type{T}) where T = T.(data)
end
""") == test_mod.getdata1
@test test_mod.getdata1(Float64) == [1.0, 2.0, 3.0]
end

@testset "local function declaration" begin
@test JuliaLowering.include_string(test_mod, """
let
local f() = 42
f()
end
""") === 42

# Local function should not leak out
@test !Base.isdefinedglobal(test_mod, :f)
end

# Qualified names should work (no declaration added)
@testset "qualified function names" begin
JuliaLowering.include_string(test_mod, """
module TestMod1
f() = 1
end
""")

@test JuliaLowering.include_string(test_mod, """
global TestMod1.f(x::Int) = x + 1
""") === nothing
@test test_mod.TestMod1.f(5) === 6

@test JuliaLowering.include_string(test_mod, """
let
global TestMod1.f(x::Float64) = x + 1
end
""") === nothing
@test test_mod.TestMod1.f(5.0) === 6.0
end

# Error cases
@test_throws LoweringError JuliaLowering.include_string(test_mod, """
let
local func(x) = x
global func(x) = x
end
""")

end
124 changes: 124 additions & 0 deletions test/decls_ir.jl
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,127 @@ function f()
# └────┘ ── type declarations for global variables must be at top level, not inside a function
end

########################################
# Global function in let block (short form)
let x = 42
global getx() = x
end
#---------------------
1 42
2 (= slot₁/x (call core.Box))
3 slot₁/x
4 (call core.setfield! %₃ :contents %₁)
5 (global TestMod.getx)
6 latestworld
7 (method TestMod.getx)
8 latestworld
9 TestMod.getx
10 (call core.Typeof %₉)
11 (call core.svec %₁₀)
12 (call core.svec)
13 SourceLocation::2:12
14 (call core.svec %₁₁ %₁₂ %₁₃)
15 --- code_info
slots: [slot₁/#self#(!read) slot₂/x(!read)]
1 (captured_local 1)
2 (call core.isdefined %₁ :contents)
3 (gotoifnot %₂ label₅)
4 (goto label₇)
5 (newvar slot₂/x)
6 slot₂/x
7 (call core.getfield %₁ :contents)
8 (return %₇)
16 slot₁/x
17 (call core.svec %₁₆)
18 (call JuliaLowering.replace_captured_locals! %₁₅ %₁₇)
19 --- method core.nothing %₁₄ %₁₈
20 latestworld
21 TestMod.getx
22 (return %₂₁)

########################################
# Global function with where clause in let
let data = [1,2,3]
global getdata(::Type{T}) where T = T.(data)
end
#---------------------
1 (call top.vect 1 2 3)
2 (= slot₁/data (call core.Box))
3 slot₁/data
4 (call core.setfield! %₃ :contents %₁)
5 (global TestMod.getdata)
6 latestworld
7 (method TestMod.getdata)
8 latestworld
9 (= slot₂/T (call core.TypeVar :T))
10 TestMod.getdata
11 (call core.Typeof %₁₀)
12 TestMod.Type
13 slot₂/T
14 (call core.apply_type %₁₂ %₁₃)
15 (call core.svec %₁₁ %₁₄)
16 slot₂/T
17 (call core.svec %₁₆)
18 SourceLocation::2:12
19 (call core.svec %₁₅ %₁₇ %₁₈)
20 --- code_info
slots: [slot₁/#self#(!read) slot₂/_(!read) slot₃/data(!read)]
1 static_parameter₁
2 (captured_local 1)
3 (call core.isdefined %₂ :contents)
4 (gotoifnot %₃ label₆)
5 (goto label₈)
6 (newvar slot₃/data)
7 slot₃/data
8 (call core.getfield %₂ :contents)
9 (call top.broadcasted %₁ %₈)
10 (call top.materialize %₉)
11 (return %₁₀)
21 slot₁/data
22 (call core.svec %₂₁)
23 (call JuliaLowering.replace_captured_locals! %₂₀ %₂₂)
24 --- method core.nothing %₁₉ %₂₃
25 latestworld
26 TestMod.getdata
27 (return %₂₆)

########################################
# Local function in let block
let
local f() = 42
f()
end
#---------------------
1 (call core.svec)
2 (call core.svec)
3 (call JuliaLowering.eval_closure_type TestMod :#f##0 %₁ %₂)
4 latestworld
5 TestMod.#f##0
6 (new %₅)
7 (= slot₁/f %₆)
8 TestMod.#f##0
9 (call core.svec %₈)
10 (call core.svec)
11 SourceLocation::2:11
12 (call core.svec %₉ %₁₀ %₁₁)
13 --- method core.nothing %₁₂
slots: [slot₁/#self#(!read)]
1 (return 42)
14 latestworld
15 slot₁/f
16 (call %₁₅)
17 (return %₁₆)

########################################
# Error: conflicting local and global declarations
let
local func(x) = x
global func(x) = x
end
#---------------------
LoweringError:
let
local func(x) = x
global func(x) = x
# └──────────┘ ── Variable `func` declared both local and global
end
Loading