From 9ba8190b8a276d1c28bbd06522c05171a94a8183 Mon Sep 17 00:00:00 2001 From: Penelope Yong Date: Sun, 22 Feb 2026 21:48:34 +0000 Subject: [PATCH 1/6] Fix throw_undef_if_not error --- src/copyable_task.jl | 39 ++++++++++++++++++++++++++++++++++++--- test/copyable_task.jl | 30 ++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/copyable_task.jl b/src/copyable_task.jl index 5d46e241..409bf700 100644 --- a/src/copyable_task.jl +++ b/src/copyable_task.jl @@ -99,6 +99,40 @@ function _throw_ir_error(@nospecialize(sig::Type{<:Tuple})) throw(ArgumentError(msg)) end +""" + _generate_ir(optimise::Bool, f, args...; kwargs...) + +Returns `(original_ir, transformed_ir)` for the call `f(args...; kwargs...)`. + +The first element is the original `IRCode` that Julia generates for the call. The second is +the transformed `IRCode` that Libtask would use to implement the `produce`/`consume` +interface in a `TapedTask`. + +`optimise` by default is `true`; it controls whether the transformed IR (i.e. for the +`TapedTask`) is optimised or not. Apart from inspecting the effects of optimisation, setting +`optimise` to `false` can also be useful for debugging, because the optimisation pass will +also perform verification, and will error if the IR is malformed. + +This is intended purely as a debugging tool, and is not exported. +""" +function _generate_ir(optimise::Bool, fargs...; kwargs...) + all_args = isempty(kwargs) ? fargs : (Core.kwcall, getfield(kwargs, :data), fargs...) + sig = typeof(all_args) + ir_results = Base.code_ircode_by_type(sig) + if isempty(ir_results) + _throw_ir_error(sig) + end + original_ir = ir_results[1][1] + seed_id!() + bb, _, _ = derive_copyable_task_ir(BBCode(original_ir)) + transformed_ir = if optimise + optimise_ir!(IRCode(bb)) + else + IRCode(bb) + end + return original_ir, transformed_ir +end + """ build_callable(sig::Type{<:Tuple}) @@ -929,7 +963,8 @@ function derive_copyable_task_ir(ir::BBCode)::Tuple{BBCode,Tuple,Vector{Any}} if Meta.isexpr(stmt, :invoke) || Meta.isexpr(stmt, :call) || Meta.isexpr(stmt, :new) || - Meta.isexpr(stmt, :foreigncall) + Meta.isexpr(stmt, :foreigncall)|| + Meta.isexpr(stmt, :throw_undef_if_not) # Find any `ID`s and replace them with calls to read whatever is stored # in the `Ref`s that they are associated to. @@ -962,8 +997,6 @@ function derive_copyable_task_ir(ir::BBCode)::Tuple{BBCode,Tuple,Vector{Any}} push!(inst_pairs, (id, inst)) elseif Meta.isexpr(stmt, :gc_preserve_end) push!(inst_pairs, (id, inst)) - elseif Meta.isexpr(stmt, :throw_undef_if_not) - push!(inst_pairs, (id, inst)) elseif stmt isa Nothing push!(inst_pairs, (id, inst)) elseif stmt isa GlobalRef diff --git a/test/copyable_task.jl b/test/copyable_task.jl index 545783ed..cbbb5ef9 100644 --- a/test/copyable_task.jl +++ b/test/copyable_task.jl @@ -373,6 +373,36 @@ using Test # This used to error @test (consume(t); true) end + + # Regression test for https://github.com/TuringLang/Libtask.jl/issues/204 + @testset "throw_undef_if_not handling" begin + function tuin_g(y) + produce(y + 1) + end + Libtask.might_produce(::Type{Tuple{typeof(tuin_g),Int}}) = true + + function tuin_f(x) + if x == 1 + y = 2 + end + # `g` must produce. + # Also, there must be at least two calls to `produce` in the function. + tuin_g(y) + tuin_g(y) + return 1 + end + + # TapedTask construction used to error. + t = TapedTask(nothing, tuin_f, 1) + # Check that it returns the right things. + @test consume(t) == 3 + @test consume(t) == 3 + @test consume(t) === nothing + + # This is bogus, but we should check that it has the right behaviour. + t = TapedTask(nothing, tuin_f, 2) + @test_throws UndefVarError consume(t) + end end end # module From 5cc36451e6fd76745675d09b4cdd3e34c11a24bf Mon Sep 17 00:00:00 2001 From: Penelope Yong Date: Sun, 22 Feb 2026 21:53:59 +0000 Subject: [PATCH 2/6] Changelog --- HISTORY.md | 8 ++++++++ Project.toml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 611f0359..75de0a65 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,11 @@ +# 0.9.13 + +Fix a bug where SSA registers in `throw_undef_if_not` expressions were not being correctly handled. +This affected `TapedTask`s where the underlying function had variables that were conditionally defined. + +Also adds an internal function, `Libtask._generate_ir`, which is useful for debugging. +This is not part of the public API; however if you are developing Libtask it is a useful function for quickly inspecting the IR that Libtask generates, both pre- and post-optimisation. + # 0.9.12 Added a new method, `Libtask.get_taped_globals(tt::TapedTask)`. diff --git a/Project.toml b/Project.toml index 36633d31..04993379 100644 --- a/Project.toml +++ b/Project.toml @@ -3,7 +3,7 @@ uuid = "6f1fad26-d15e-5dc8-ae53-837a1d7b8c9f" license = "MIT" desc = "Tape based task copying in Turing" repo = "https://github.com/TuringLang/Libtask.jl.git" -version = "0.9.12" +version = "0.9.13" [deps] MistyClosures = "dbe65cb8-6be2-42dd-bbc5-4196aaced4f4" From 8f96b26ad71262f63d2f935ccd2a68aa0379782d Mon Sep 17 00:00:00 2001 From: Penelope Yong Date: Sun, 22 Feb 2026 22:02:07 +0000 Subject: [PATCH 3/6] Add docstring for generate_ir --- docs/src/internals.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/internals.md b/docs/src/internals.md index 5ab5060d..92b5e0cf 100644 --- a/docs/src/internals.md +++ b/docs/src/internals.md @@ -1,6 +1,7 @@ # Internals ```@docs; canonical=true +Libtask._generate_ir Libtask.produce_value Libtask.is_produce_stmt Libtask.stmt_might_produce From f343291de30f33c9e55c00ef1205ac24690f39ed Mon Sep 17 00:00:00 2001 From: Penelope Yong Date: Sun, 22 Feb 2026 22:02:43 +0000 Subject: [PATCH 4/6] remove underscore prefix --- HISTORY.md | 2 +- docs/src/internals.md | 2 +- src/copyable_task.jl | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 75de0a65..7d6af24f 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -3,7 +3,7 @@ Fix a bug where SSA registers in `throw_undef_if_not` expressions were not being correctly handled. This affected `TapedTask`s where the underlying function had variables that were conditionally defined. -Also adds an internal function, `Libtask._generate_ir`, which is useful for debugging. +Also adds an internal function, `Libtask.generate_ir`, which is useful for debugging. This is not part of the public API; however if you are developing Libtask it is a useful function for quickly inspecting the IR that Libtask generates, both pre- and post-optimisation. # 0.9.12 diff --git a/docs/src/internals.md b/docs/src/internals.md index 92b5e0cf..1311d099 100644 --- a/docs/src/internals.md +++ b/docs/src/internals.md @@ -1,7 +1,7 @@ # Internals ```@docs; canonical=true -Libtask._generate_ir +Libtask.generate_ir Libtask.produce_value Libtask.is_produce_stmt Libtask.stmt_might_produce diff --git a/src/copyable_task.jl b/src/copyable_task.jl index 409bf700..84b82781 100644 --- a/src/copyable_task.jl +++ b/src/copyable_task.jl @@ -100,7 +100,7 @@ function _throw_ir_error(@nospecialize(sig::Type{<:Tuple})) end """ - _generate_ir(optimise::Bool, f, args...; kwargs...) + generate_ir(optimise::Bool, f, args...; kwargs...) Returns `(original_ir, transformed_ir)` for the call `f(args...; kwargs...)`. @@ -115,7 +115,7 @@ also perform verification, and will error if the IR is malformed. This is intended purely as a debugging tool, and is not exported. """ -function _generate_ir(optimise::Bool, fargs...; kwargs...) +function generate_ir(optimise::Bool, fargs...; kwargs...) all_args = isempty(kwargs) ? fargs : (Core.kwcall, getfield(kwargs, :data), fargs...) sig = typeof(all_args) ir_results = Base.code_ircode_by_type(sig) From 7f6d6d23cc7c231ed52249c37653bf605e81564f Mon Sep 17 00:00:00 2001 From: Penelope Yong Date: Sun, 22 Feb 2026 22:02:50 +0000 Subject: [PATCH 5/6] Format --- src/copyable_task.jl | 2 +- test/copyable_task.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/copyable_task.jl b/src/copyable_task.jl index 84b82781..e8ac1e33 100644 --- a/src/copyable_task.jl +++ b/src/copyable_task.jl @@ -963,7 +963,7 @@ function derive_copyable_task_ir(ir::BBCode)::Tuple{BBCode,Tuple,Vector{Any}} if Meta.isexpr(stmt, :invoke) || Meta.isexpr(stmt, :call) || Meta.isexpr(stmt, :new) || - Meta.isexpr(stmt, :foreigncall)|| + Meta.isexpr(stmt, :foreigncall) || Meta.isexpr(stmt, :throw_undef_if_not) # Find any `ID`s and replace them with calls to read whatever is stored diff --git a/test/copyable_task.jl b/test/copyable_task.jl index cbbb5ef9..185aabc3 100644 --- a/test/copyable_task.jl +++ b/test/copyable_task.jl @@ -377,7 +377,7 @@ using Test # Regression test for https://github.com/TuringLang/Libtask.jl/issues/204 @testset "throw_undef_if_not handling" begin function tuin_g(y) - produce(y + 1) + return produce(y + 1) end Libtask.might_produce(::Type{Tuple{typeof(tuin_g),Int}}) = true From 21feae7ebccc4a711b7e012d78db6011daebf330 Mon Sep 17 00:00:00 2001 From: Penelope Yong Date: Sun, 22 Feb 2026 22:05:14 +0000 Subject: [PATCH 6/6] fix docs --- src/copyable_task.jl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/copyable_task.jl b/src/copyable_task.jl index e8ac1e33..521b0467 100644 --- a/src/copyable_task.jl +++ b/src/copyable_task.jl @@ -108,10 +108,11 @@ The first element is the original `IRCode` that Julia generates for the call. Th the transformed `IRCode` that Libtask would use to implement the `produce`/`consume` interface in a `TapedTask`. -`optimise` by default is `true`; it controls whether the transformed IR (i.e. for the -`TapedTask`) is optimised or not. Apart from inspecting the effects of optimisation, setting -`optimise` to `false` can also be useful for debugging, because the optimisation pass will -also perform verification, and will error if the IR is malformed. +`optimise` controls whether the transformed IR (i.e. for the `TapedTask`) is optimised or +not. Apart from inspecting the effects of optimisation, setting `optimise` to `false` can +also be useful for debugging, because the optimisation pass will also perform verification, +and will error if the IR is malformed (which can happen if Libtask's transformation pass has +bugs). This is intended purely as a debugging tool, and is not exported. """