From b748a3ff043f4026792458cb3728f10162b87882 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Sat, 4 Oct 2025 06:03:03 +0900 Subject: [PATCH] Add support for `global`/`local` declarations for functions Allow function definitions with `global` and `local` declarations, enabling patterns like ``` let x = 1 global f() = x end ``` Fixes c42f/JuliaLowering.jl#28. --- src/desugaring.jl | 66 +++++++++++++++++++++++- test/decls.jl | 89 +++++++++++++++++++++++++++++++++ test/decls_ir.jl | 124 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 278 insertions(+), 1 deletion(-) diff --git a/src/desugaring.jl b/src/desugaring.jl index 84ccbac9..c66a8352 100644 --- a/src/desugaring.jl +++ b/src/desugaring.jl @@ -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) @@ -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 @@ -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" diff --git a/test/decls.jl b/test/decls.jl index 50aa98a5..16fc808b 100644 --- a/test/decls.jl +++ b/test/decls.jl @@ -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 diff --git a/test/decls_ir.jl b/test/decls_ir.jl index 155754cd..781c5217 100644 --- a/test/decls_ir.jl +++ b/test/decls_ir.jl @@ -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