Skip to content

Commit ca2cc21

Browse files
committed
Lower Julia throws generically
1 parent cec28bd commit ca2cc21

8 files changed

Lines changed: 245 additions & 5 deletions

File tree

src/compiler/codegen/statements.jl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ function emit_statement!(ctx::CGCtx, @nospecialize(stmt), ssa_idx::Int, @nospeci
1515
tv = emit_join_tokens!(ctx, stmt)
1616
elseif stmt isa TokenResultNode
1717
tv = emit_token_result!(ctx, stmt)
18+
elseif stmt isa ThrowNode
19+
tv = emit_throw!(ctx, stmt)
1820
elseif stmt isa ReturnNode
1921
emit_return!(ctx, stmt)
2022
elseif stmt isa Expr
@@ -55,6 +57,13 @@ function emit_statement!(ctx::CGCtx, @nospecialize(stmt), ssa_idx::Int, @nospeci
5557
end
5658
end
5759

60+
function emit_throw!(ctx::CGCtx, node::ThrowNode)
61+
node.runtime || throw(IRError(node.message))
62+
cond = emit_value!(ctx, false)
63+
encode_AssertOp!(ctx.cb, cond.v, node.message)
64+
return nothing
65+
end
66+
5867
"""
5968
emit_return!(ctx, node::ReturnNode)
6069
@@ -112,4 +121,3 @@ function emit_token_result!(ctx::CGCtx, node::TokenResultNode)
112121
v === nothing && throw(IRError("TokenResultNode: no result token for memory op at SSA %$(node.mem_op_ssa)"))
113122
return CGVal(v, ctx.token_type, TokenType)
114123
end
115-

src/compiler/transform/dce.jl

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ function get_stmt_operands(@nospecialize(s))
6767
end
6868

6969
"""
70-
must_keep(s) -> Bool
70+
must_keep(inst) -> Bool
7171
7272
Check if a statement is side-effectful and must be kept as a root.
7373
@@ -79,6 +79,15 @@ efunc override are pure. Unknown calls are conservatively kept.
7979
Mirrors Python cuTile's `_must_keep` (dce.py:205-206) and Julia's compiler
8080
`stmt_effect_free` — both classify by per-instruction effect annotations.
8181
"""
82+
function must_keep(block::Block, inst::Instruction)
83+
# Preserve Julia's per-statement effect analysis. This covers removable
84+
# invokes and `:new` expressions that are not cuTile intrinsics, notably
85+
# exception construction made dead by `lower_throws!`.
86+
flag = inst[:flag]
87+
(flag & CC.IR_FLAGS_REMOVABLE) == CC.IR_FLAGS_REMOVABLE && return false
88+
return must_keep(block, inst[:stmt])
89+
end
90+
8291
function must_keep(block::Block, @nospecialize(s))
8392
# Token bookkeeping: no side effects
8493
s isa JoinTokensNode && return false
@@ -233,7 +242,7 @@ function _build_dataflow_graph!(graph::Dict{Any, Vector{Any}},
233242
graph[val] = deps
234243
end
235244

236-
if must_keep(block, s)
245+
if must_keep(block, inst)
237246
operands = get_stmt_operands(s)
238247
push!(roots, val)
239248
for op in operands
@@ -483,7 +492,7 @@ function _prune_block!(block::Block, live::Set{Any}, op_to_cf::Dict{UInt64, CFNo
483492

484493
else
485494
# Regular instruction: dead if not live and not must-keep
486-
if val live && !must_keep(block, s)
495+
if val live && !must_keep(block, inst)
487496
push!(to_delete, inst)
488497
changed = true
489498
end

src/compiler/transform/pipeline.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ derive per-operand `AssumePredicate` chains on demand via
272272
`op_predicates` (analysis/assume.jl).
273273
"""
274274
function run_passes!(sci::StructuredIRCode)
275+
lower_throws!(sci)
275276
canonicalize!(sci)
276277

277278
rewrite_patterns!(sci, PRINT_FUSION_RULES)

src/compiler/transform/throws.jl

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Julia throw lowering
2+
3+
"""Return `Some(x)` when `x` can be reconstructed without runtime values."""
4+
function throw_constant(sci::StructuredIRCode, def_index, @nospecialize(x))
5+
if x isa QuoteNode
6+
return Some(x.value)
7+
elseif x isa GlobalRef
8+
return IRStructurizer.const_value(sci, x)
9+
elseif x isa SSAValue
10+
inst = def(def_index, x)
11+
inst === nothing && return nothing
12+
call = resolve_call(inst.block, inst[:stmt])
13+
call === nothing && return nothing
14+
func, args = call
15+
if func === Core.tuple
16+
vals = Any[]
17+
for arg in args
18+
val = throw_constant(sci, def_index, arg)
19+
val === nothing && return nothing
20+
push!(vals, something(val))
21+
end
22+
return Some(Tuple(vals))
23+
elseif func === Intrinsics.format_string
24+
vals = Any[]
25+
for arg in args
26+
val = throw_constant(sci, def_index, arg)
27+
val === nothing && return nothing
28+
push!(vals, something(val))
29+
end
30+
return Some(string(vals...))
31+
end
32+
return nothing
33+
elseif x isa Union{Argument, SlotNumber, BlockArgument, Expr}
34+
return nothing
35+
else
36+
return Some(x)
37+
end
38+
end
39+
40+
function thrown_type(block::Block, @nospecialize(exception))
41+
if exception isa QuoteNode
42+
return typeof(exception.value)
43+
elseif !(exception isa Union{SSAValue, Argument, SlotNumber, BlockArgument, Expr, GlobalRef})
44+
return typeof(exception)
45+
end
46+
T = value_type(block, exception)
47+
T === nothing && return Exception
48+
return CC.widenconst(T)
49+
end
50+
51+
"""
52+
Best-effort reconstruction of an exception from its optimized constructor.
53+
Only constructors Julia marked removable are evaluated at compile time.
54+
"""
55+
function thrown_exception(sci::StructuredIRCode, def_index, block::Block,
56+
@nospecialize(exception))
57+
direct = throw_constant(sci, def_index, exception)
58+
direct !== nothing && something(direct) isa Exception && return something(direct)
59+
exception isa SSAValue || return nothing
60+
61+
inst = def(def_index, exception)
62+
inst === nothing && return nothing
63+
flag = inst[:flag]
64+
(flag & CC.IR_FLAGS_REMOVABLE) == CC.IR_FLAGS_REMOVABLE || return nothing
65+
66+
stmt = inst[:stmt]
67+
T = thrown_type(block, exception)
68+
T isa Type && T <: Exception || return nothing
69+
70+
args = if stmt isa Expr && stmt.head === :new
71+
stmt.args[2:end]
72+
else
73+
call = resolve_call(inst.block, stmt)
74+
call === nothing && return nothing
75+
func, call_args = call
76+
func === T || return nothing
77+
call_args
78+
end
79+
80+
vals = Any[]
81+
for arg in args
82+
val = throw_constant(sci, def_index, arg)
83+
val === nothing && return nothing
84+
push!(vals, something(val))
85+
end
86+
try
87+
return T(vals...)
88+
catch
89+
return nothing
90+
end
91+
end
92+
93+
function exception_message(sci::StructuredIRCode, def_index, block::Block,
94+
@nospecialize(exception))
95+
if thrown_type(block, exception) === MethodError && exception isa SSAValue
96+
inst = def(def_index, exception)
97+
if inst !== nothing
98+
stmt = inst[:stmt]
99+
args = if stmt isa Expr && stmt.head === :new
100+
stmt.args[2:end]
101+
else
102+
call = resolve_call(inst.block, stmt)
103+
call === nothing ? Any[] : last(call)
104+
end
105+
if length(args) >= 2 && args[2] isa SSAValue
106+
tuple_inst = def(def_index, args[2])
107+
tuple_call = tuple_inst === nothing ? nothing :
108+
resolve_call(tuple_inst.block, tuple_inst[:stmt])
109+
if tuple_call !== nothing && first(tuple_call) === Core.tuple
110+
return method_error_message(sci, def_index, block,
111+
Any[args[1]; last(tuple_call)])
112+
end
113+
end
114+
end
115+
end
116+
117+
ex = thrown_exception(sci, def_index, block, exception)
118+
if ex !== nothing
119+
if hasfield(typeof(ex), :msg) && isdefined(ex, :msg)
120+
msg = getfield(ex, :msg)
121+
msg isa AbstractString && return String(msg)
122+
end
123+
return sprint(showerror, ex)
124+
end
125+
126+
T = thrown_type(block, exception)
127+
return T isa Type && T <: Exception ? "$(T) was thrown" : "exception was thrown"
128+
end
129+
130+
function method_error_message(sci::StructuredIRCode, def_index, block::Block, args)
131+
isempty(args) && return "Unsupported function call during Tile IR compilation"
132+
func = throw_constant(sci, def_index, args[1])
133+
funcstr = func === nothing ? string(args[1]) : string(something(func))
134+
argtypes = Any[value_type(block, arg) for arg in args[2:end]]
135+
typestr = isempty(argtypes) ? "" : " with argument types ($(join(argtypes, ", ")))"
136+
return "Unsupported function call during Tile IR compilation: $funcstr$typestr has no Tile IR equivalent"
137+
end
138+
139+
"""
140+
lower_throws!(sci)
141+
142+
Canonicalize Julia throws before normal optimization and codegen. Throws nested
143+
in structured control flow remain runtime failures. A throw in the entry block
144+
is unavoidable after Julia's CFG simplification and becomes a collected
145+
compile-time diagnostic.
146+
"""
147+
function lower_throws!(sci::StructuredIRCode)
148+
def_index = defs(sci)
149+
for block in eachblock(sci)
150+
runtime = block !== sci.entry
151+
for inst in instructions(block)
152+
call = resolve_call(block, inst[:stmt])
153+
call === nothing && continue
154+
func, args = call
155+
message = if func === throw
156+
length(args) == 1 || continue
157+
exception_message(sci, def_index, block, args[1])
158+
elseif @static isdefined(Core, :throw_methoderror) && func === Core.throw_methoderror
159+
method_error_message(sci, def_index, block, args)
160+
else
161+
continue
162+
end
163+
inst[:stmt] = ThrowNode(message, runtime)
164+
inst[:flag] = CC.flags_for_effects(CC.EFFECTS_THROWS)
165+
end
166+
end
167+
return sci
168+
end

src/compiler/utils.jl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,32 @@ struct TokenResultNode
7272
mem_op_ssa::Int # SSA index of the memory operation that produced this token
7373
end
7474

75+
"""
76+
ThrowNode(message, runtime)
77+
78+
Canonical form for a Julia `throw`. Runtime throws lower to a failing Tile IR
79+
assertion. An unavoidable top-level throw becomes a compile-time diagnostic.
80+
The exception object is not retained as an operand, so DCE can remove its
81+
otherwise-dead construction.
82+
"""
83+
struct ThrowNode
84+
message::String
85+
runtime::Bool
86+
end
87+
7588
# walk_uses! extensions so that IRStructurizer's uses()/replace_uses! see
7689
# operands inside cuTile-specific IR nodes.
7790
IRStructurizer.walk_uses!(f, node::JoinTokensNode) =
7891
for i in 1:length(node.tokens); f(IRStructurizer.IndexedUseRef(node.tokens, i)); end
7992
IRStructurizer.walk_uses!(f, ::TokenResultNode) = nothing
8093
IRStructurizer.walk_uses!(f, ::MakeTokenNode) = nothing
94+
IRStructurizer.walk_uses!(f, ::ThrowNode) = nothing
8195

8296
# operands extensions for cuTile-specific IR nodes.
8397
operands(::Block, s::JoinTokensNode) = s.tokens
8498
operands(::Block, s::TokenResultNode) = Any[SSAValue(s.mem_op_ssa)]
8599
operands(::Block, ::MakeTokenNode) = Any[]
100+
operands(::Block, ::ThrowNode) = Any[]
86101

87102

88103
"""

src/cuTile.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ include("compiler/analysis/bounds.jl")
5353
include("compiler/analysis/assume.jl")
5454
include("compiler/transform/rewriter.jl")
5555
include("compiler/transform/rewrite.jl")
56+
include("compiler/transform/throws.jl")
5657
include("compiler/transform/canonicalize.jl")
5758
include("compiler/transform/control_flow.jl")
5859
include("compiler/transform/token_keys.jl")

test/codegen/integration.jl

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,38 @@ end
692692
end
693693
end
694694

695+
@testset "Julia throws" begin
696+
@testset "conditional throw lowers to a runtime assertion" begin
697+
@test @filecheck begin
698+
@check "assert {{.*}}x must be positive"
699+
code_tiled(Tuple{Int32}) do x
700+
x > Int32(0) || throw(ArgumentError("x must be positive"))
701+
return
702+
end
703+
end
704+
end
705+
706+
@testset "conditional singleton exception uses showerror" begin
707+
@test @filecheck begin
708+
@check "assert {{.*}}DivideError: integer division error"
709+
code_tiled(Tuple{Int32}) do x
710+
x != Int32(0) || throw(DivideError())
711+
return
712+
end
713+
end
714+
end
715+
716+
@testset "runtime-dependent message falls back to the exception type" begin
717+
@test @filecheck begin
718+
@check "assert {{.*}}ArgumentError was thrown"
719+
code_tiled(Tuple{Int32}) do x
720+
x > Int32(0) || throw(ArgumentError("invalid value $x"))
721+
return
722+
end
723+
end
724+
end
725+
end
726+
695727
@testset "method error detection" begin
696728
spec = ct.ArraySpec{1}(16, true)
697729

test/codegen/operations.jl

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -767,15 +767,21 @@ spec4d = ct.ArraySpec{4}(16, true)
767767
end
768768

769769
@testset "vec-vec throws error" begin
770-
@test_throws cuTile.CodegenErrors begin
770+
err = try
771771
code_tiled(Tuple{ct.TileArray{Float32,1,spec1d}, ct.TileArray{Float32,1,spec1d}}) do a, b
772772
bidx = ct.bid(1)
773773
tile_a = ct.load(a, bidx, (16,))
774774
tile_b = ct.load(b, bidx, (16,))
775775
tile_a * tile_b
776776
return
777777
end
778+
nothing
779+
catch e
780+
e
778781
end
782+
@test err isa cuTile.CodegenErrors
783+
@test only(err.errors).msg ==
784+
"Vector-vector multiplication is not supported. Use dot(a, b) for inner products, or reshape explicitly."
779785
end
780786

781787
@testset "4D batched matmul (2 batch dims)" begin

0 commit comments

Comments
 (0)