Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ DLFP8Types = "0.1.0"
EnumX = "1.0"
GPUArrays = "11"
GPUToolbox = "3"
IRStructurizer = "0.6.2"
IRStructurizer = "0.6.3"
LMDB = "3"
Microfloats = "0.2"
PrecompileTools = "1"
Expand Down
11 changes: 8 additions & 3 deletions src/compiler/codegen/control_flow.jl
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ function emit_if_op!(ctx::CGCtx, op::IfOp, @nospecialize(parent_result_type), ss
if parent_result_type !== Nothing
Ts = parent_result_type <: Tuple ? collect(parent_result_type.parameters) :
Any[parent_result_type]
result_types, result_poisoned = collect_result_types!(ctx, Ts; context="`if`/`else` result")
result_types, result_poisoned = collect_result_types!(
ctx, (T for T in Ts if !is_empty_tile_type(T)); context="`if`/`else` result")
end
carry_errors = carry_mark+1:length(ctx.errors)

Expand All @@ -130,7 +131,9 @@ function emit_if_op!(ctx::CGCtx, op::IfOp, @nospecialize(parent_result_type), ss
results = encode_IfOp!(then_body, else_body, cb, result_types, cond_tv.v)
prune_derived_errors!(ctx, carry_errors)

ctx.values[ssa_idx] = CGVal(results, parent_result_type)
ctx.values[ssa_idx] = is_empty_tile_type(parent_result_type) ?
ghost_value(parent_result_type) :
CGVal(results, parent_result_type)
result_poisoned && push!(ctx.poisoned, ssa_idx)
end

Expand Down Expand Up @@ -443,8 +446,10 @@ function emit_loop_getfield!(ctx::CGCtx, args::Vector{Any})
ref_cgval.v isa Vector{Value} || return nothing

field_idx = args[2]::Int
v = ref_cgval.v[field_idx]
elem_type = ref_cgval.jltype.parameters[field_idx]
is_empty_tile_type(elem_type) && return ghost_value(elem_type)
value_idx = count(i -> !is_empty_tile_type(ref_cgval.jltype.parameters[i]), 1:field_idx)
v = ref_cgval.v[value_idx]
type_id = tile_type_for_julia!(ctx, elem_type)
shape = RowMajorShape(extract_tile_shape(elem_type))
CGVal(v, type_id, elem_type, shape)
Expand Down
19 changes: 17 additions & 2 deletions src/compiler/codegen/kernel.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ function emit_kernel!(writer::BytecodeWriter, func_buf::Vector{UInt8},

# Validate non-ghost, non-const argument types are concrete
for (i, argtype) in enumerate(sci.argtypes)
is_ghost_type(CC.widenconst(argtype)) && continue
argtype_unwrapped = CC.widenconst(argtype)
is_empty_tile_type(argtype_unwrapped) &&
throw(IRError("kernel argument $i has no Tile IR representation (inferred `$argtype_unwrapped`)"))
is_ghost_type(argtype_unwrapped) && continue
is_const_arg(i) && continue
require_concrete_type(argtype, "kernel argument $i")
end
Expand Down Expand Up @@ -78,8 +81,16 @@ function emit_kernel!(writer::BytecodeWriter, func_buf::Vector{UInt8},

# Return types
result_types = TypeId[]
return_type_ok = true
if rettype !== Nothing && rettype !== Union{}
push!(result_types, tile_type_for_julia!(ctx, rettype))
# Emit the body before diagnosing an unsupported return so a more
# specific intrinsic error can surface first.
result_type = tile_type_for_julia!(ctx, rettype; throw_error=false)
if result_type === nothing
return_type_ok = false
else
push!(result_types, result_type)
end
end

# Create entry hints if provided
Expand Down Expand Up @@ -188,6 +199,10 @@ function emit_kernel!(writer::BytecodeWriter, func_buf::Vector{UInt8},
# Emit the structured IR (uses original Julia SSA indices everywhere)
emit_block!(ctx, ctx.sci.entry)

if !return_type_ok && isempty(ctx.errors)
record_error!(ctx, "kernel return has no Tile IR representation (inferred `$(CC.widenconst(rettype))`)")
end

# Cover the function-definition line (Julia's codegen does this at the
# prologue; the per-statement coverage effects only cover body lines).
record_definition_coverage!(ctx)
Expand Down
14 changes: 11 additions & 3 deletions src/compiler/codegen/values.jl
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ function get_constant(ctx::CGCtx, @nospecialize(ref))
return nothing
end

# Tile constructors fold to opaque handle constants. Empty tiles are compiler
# ghosts; a constant nonempty handle cannot name a Tile IR value.
function emit_value!(ctx::CGCtx, val::Tile)
T = typeof(val)
is_empty_tile_type(T) && return ghost_value(T, val)
throw(IRError("constant tile handle without a Tile IR value: $T"))
end

# Symbols are compile-time only values
emit_value!(ctx::CGCtx, val::Symbol) = ghost_value(Symbol, val)

Expand All @@ -140,7 +148,7 @@ emit_value!(ctx::CGCtx, @nospecialize(val::Type)) = ghost_value(Type{val}, val)
# Undef values (dead-code branches, unexported loop slots) -> zero constant
function emit_value!(ctx::CGCtx, undef::Undef)
T = CC.widenconst(undef.type)
is_ghost_type(T) && return ghost_value(T)
(is_ghost_type(T) || is_empty_tile_type(T)) && return ghost_value(T)
type_id = tile_type_for_julia!(ctx, T)
elem_type = T <: Tile ? eltype(T) : T
bytes = constant_to_bytes(zero(elem_type), elem_type)
Expand Down Expand Up @@ -171,8 +179,8 @@ end
function emit_constant!(ctx::CGCtx, @nospecialize(value), @nospecialize(result_type))
result_type_unwrapped = CC.widenconst(result_type)

# Ghost types have no runtime representation
if is_ghost_type(result_type_unwrapped)
# Ghost types and empty tiles have no runtime representation
if is_ghost_type(result_type_unwrapped) || is_empty_tile_type(result_type_unwrapped)
return ghost_value(result_type_unwrapped)
end

Expand Down
21 changes: 18 additions & 3 deletions src/compiler/interpreter.jl
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ CC.may_discard_trees(::cuTileInterpreter) = false
# Returns nothing when no override applies (fallback).
tfunc(𝕃, @nospecialize(f), @nospecialize args...) = nothing

# Keep invalid zero-volume intrinsic calls visible to codegen. A concrete
# override lets inference fold or discard the call before its emitter can
# diagnose the invalid shape.
function checked_tfunc(𝕃, @nospecialize(f), @nospecialize args...)
rt = tfunc(𝕃, f, args...)
contains_empty_tile(rt) ? nothing : rt
end

function contains_empty_tile(@nospecialize(T))
T isa DataType || return false
is_empty_tile_type(T) && return true
T <: Tuple && return any(contains_empty_tile, T.parameters)
false
end

# Per-intrinsic effect overrides.
# Returns nothing when no override applies (fallback).
efunc(@nospecialize(f), effects::CC.Effects) = nothing
Expand Down Expand Up @@ -172,7 +187,7 @@ end
sv::CC.InferenceState, max_methods::Int)
is_intr = isintrinsic(f)
𝕃 = CC.typeinf_lattice(interp)
rt_override = tfunc(𝕃, f, arginfo.argtypes[2:end]...)
rt_override = checked_tfunc(𝕃, f, arginfo.argtypes[2:end]...)
subprog = _infer_subprogram(interp, f, arginfo, si, vtypes, sv)
!is_intr && rt_override === nothing && subprog === nothing && return result
wrapped = CC.Future{CC.CallMeta}()
Expand Down Expand Up @@ -200,7 +215,7 @@ elseif isdefined(CC, :Future) # 1.12–1.13
sv::CC.InferenceState, max_methods::Int)
is_intr = isintrinsic(f)
𝕃 = CC.typeinf_lattice(interp)
rt_override = tfunc(𝕃, f, arginfo.argtypes[2:end]...)
rt_override = checked_tfunc(𝕃, f, arginfo.argtypes[2:end]...)
subprog = _infer_subprogram(interp, f, arginfo, si, nothing, sv)
!is_intr && rt_override === nothing && subprog === nothing && return result
wrapped = CC.Future{CC.CallMeta}()
Expand Down Expand Up @@ -229,7 +244,7 @@ else # 1.11: synchronous, edges auto-tracked via stmt_edges
_infer_subprogram(interp, f, arginfo, si, nothing, sv) # side-effect only
is_intr = isintrinsic(f)
𝕃 = CC.typeinf_lattice(interp)
rt_override = tfunc(𝕃, f, arginfo.argtypes[2:end]...)
rt_override = checked_tfunc(𝕃, f, arginfo.argtypes[2:end]...)
rt = rt_override !== nothing ? rt_override : result.rt
efunc_override = is_intr ? efunc(f, result.effects) : nothing
effects = efunc_override !== nothing ? efunc_override : result.effects
Expand Down
5 changes: 3 additions & 2 deletions src/compiler/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,7 @@ function tile_type_for_julia!(tt::TypeTable, @nospecialize(T::Type))
if !(shape_param isa Tuple)
throw(IRError("Tile shape must be a tuple, got: $shape_param"))
end
is_empty_tile_type(T) && return nothing
elem_dtype = lookup_dtype!(tt, eltype(T))
shape = RowMajorShape(ColMajorShape(shape_param))
return tile_type!(tt, elem_dtype, shape)
Expand Down Expand Up @@ -741,9 +742,9 @@ function is_ghost_type(@nospecialize(T))
end
end

function is_empty_tile_type(T)
"""Check whether `T` is a concrete tile with a zero extent."""
is_empty_tile_type(@nospecialize(T)) =
T isa DataType && T <: Tile && isconcretetype(T) && 0 in size(T)
end

"""
flat_field_count(T) -> Int
Expand Down
22 changes: 7 additions & 15 deletions src/language/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -317,21 +317,13 @@ end
Represents a tile of data with element type `T` and static shape `Shape`.
Shape is a tuple type encoding the tile dimensions (e.g. `Tuple{16, 32}`).

This is a compile-time abstraction - at runtime in kernel code, tiles are
represented as Tile IR values. The struct exists to enable proper type
inference and operator dispatch.

Note: This is a mutable struct (despite having no fields) to prevent Julia's
optimizer from treating it as a singleton. Each Tile instance represents a
distinct Tile IR value, and we need SSA references to be preserved rather
than being replaced with constant QuoteNodes.
"""
mutable struct Tile{T, Shape}
# Inner constructor that's never actually called at runtime
function Tile{T, Shape}() where {T, Shape}
new{T, Shape}()
end
end
This is an opaque handle like `Ptr`: tile data exists only as Tile IR values.
The byte payload is never read; it keeps concrete tile types from becoming
singletons so inference preserves distinct tile SSA values.
"""
primitive type Tile{T, Shape} 8 end

(::Type{Tile{T, Shape}})() where {T, Shape} = reinterpret(Tile{T, Shape}, 0x00)

"""
Tile(val::T) -> Tile{T, Tuple{}}
Expand Down
30 changes: 30 additions & 0 deletions test/codegen/operations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1335,6 +1335,36 @@ spec4d = ct.ArraySpec{4}(16, true)
end
end

@testset "invalid zero-volume intrinsic results" begin
for f in (
a -> (ct.load(a, ct.bid(1), (0,)); nothing),
a -> ct.load(a, ct.bid(1), (0,)),
)
@test_throws "tile dimension 1 must be positive" code_tiled(
f, Tuple{ct.TileArray{Float32,1,spec1d}})
end
end

@testset "zero-volume tile boundaries" begin
@test_throws "kernel return has no Tile IR representation" code_tiled(
devnull, () -> Float32[], Tuple{})

@test_throws "kernel argument 2 has no Tile IR representation" code_tiled(
devnull, x -> nothing, Tuple{ct.Tile{Float32, Tuple{0}}})

code_tiled(devnull, c -> begin
Base.donotdelete(c ? Float32[] : Float32[])
nothing
end, Tuple{Bool})

code_tiled(devnull, c -> begin
empty, index = c ? (Float32[], ct.bid(1)) : (Float32[], ct.bid(1))
Base.donotdelete(empty)
Base.donotdelete(index)
nothing
end, Tuple{Bool})
end

#=========================================================================
8.4 Conversions
=========================================================================#
Expand Down
9 changes: 9 additions & 0 deletions test/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ end
@test length(ct.Tile{Float32, Tuple{16}}) == 16
@test length(ct.Tile{Float32, Tuple{16, 32}}) == 512

for Shape in (Tuple{4}, Tuple{4, 0, 8})
T = ct.Tile{Float32, Shape}
@test typeof(T()) === T
@test isbitstype(T)
@test !Base.issingletontype(T)
@test sizeof(T) == 1
@test fieldcount(T) == 0
end

# similar_type tests
@test ct.similar_type(ct.Tile{Float32, Tuple{16}}, Float64) == ct.Tile{Float64, Tuple{16}}
@test ct.similar_type(ct.Tile{Float32, Tuple{16}}, Int32, (8, 8)) == ct.Tile{Int32, Tuple{8, 8}}
Expand Down