From e699d2185e542af165193792da4ad92ddcdbdbc1 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Sat, 15 Aug 2026 21:07:17 -0700 Subject: [PATCH 01/10] simd diagnostics --- src/check_builtin.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/check_builtin.cpp b/src/check_builtin.cpp index 4a59a59b7..50c679c7b 100644 --- a/src/check_builtin.cpp +++ b/src/check_builtin.cpp @@ -1787,7 +1787,7 @@ gb_internal bool check_builtin_simd_operation(CheckerContext *c, Operand *operan check_expr(c, &offset, ce->args[1]); if (offset.mode == Addressing_Invalid) return false; convert_to_typed(c, &offset, t_i64); if (!is_type_integer(offset.type) || offset.mode != Addressing_Constant) { - error(offset.expr, "'%.*s' expected a constant integer offset"); + error(offset.expr, "'%.*s' expected a constant integer offset", LIT(builtin_name)); return false; } check_assignment(c, &offset, t_i64, builtin_name); @@ -1917,7 +1917,7 @@ gb_internal bool check_builtin_simd_operation(CheckerContext *c, Operand *operan i64 max_count = 64; if (count > max_count) { - error(ce->proc, "'%.*s' exceeds the maximum #simd count %lld, got %lld", cast(long long)max_count, cast(long long)count); + error(ce->proc, "'%.*s' exceeds the maximum #simd count %lld, got %lld", LIT(builtin_name), cast(long long)max_count, cast(long long)count); return false; } From b2f2b653c57fc961e9ba721ca524621357548655 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Sat, 15 Aug 2026 21:18:36 -0700 Subject: [PATCH 02/10] add pow2 guard on interleave --- src/check_builtin.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/check_builtin.cpp b/src/check_builtin.cpp index 50c679c7b..290481c54 100644 --- a/src/check_builtin.cpp +++ b/src/check_builtin.cpp @@ -1920,6 +1920,12 @@ gb_internal bool check_builtin_simd_operation(CheckerContext *c, Operand *operan error(ce->proc, "'%.*s' exceeds the maximum #simd count %lld, got %lld", LIT(builtin_name), cast(long long)max_count, cast(long long)count); return false; } + // the lane count is the operand width times the argument count, so it is a power + // of two only when the argument count is + if (!is_power_of_two(count)) { + error(ce->proc, "'%.*s' must produce a power of two #simd count, got %lld", LIT(builtin_name), cast(long long)count); + return false; + } operand->type = alloc_type_simd_vector(count, elem); operand->mode = Addressing_Value; From 4ceba928c76307f588485201d447bd3e641d564c Mon Sep 17 00:00:00 2001 From: kalsprite Date: Sat, 15 Aug 2026 21:28:43 -0700 Subject: [PATCH 03/10] fix swizzle and shuffle max size bypass --- src/check_builtin.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/check_builtin.cpp b/src/check_builtin.cpp index 290481c54..457252354 100644 --- a/src/check_builtin.cpp +++ b/src/check_builtin.cpp @@ -1530,6 +1530,12 @@ gb_internal bool check_builtin_simd_operation(CheckerContext *c, Operand *operan return false; } + // the result is as wide as the index list, which may be twice the operand width + if (arg_count > SIMD_ELEMENT_COUNT_MAX) { + error(call, "'%.*s' constructs a #simd vector beyond the maximum element count of %d, got %lld", LIT(builtin_name), SIMD_ELEMENT_COUNT_MAX, cast(long long)arg_count); + return false; + } + operand->mode = Addressing_Value; operand->type = alloc_type_simd_vector(arg_count, elem); return true; @@ -1915,7 +1921,7 @@ gb_internal bool check_builtin_simd_operation(CheckerContext *c, Operand *operan i64 base_count = get_array_type_count(x.type); i64 count = base_count * cast(i64)ce->args.count; - i64 max_count = 64; + i64 max_count = SIMD_ELEMENT_COUNT_MAX; if (count > max_count) { error(ce->proc, "'%.*s' exceeds the maximum #simd count %lld, got %lld", LIT(builtin_name), cast(long long)max_count, cast(long long)count); return false; @@ -3517,6 +3523,11 @@ gb_internal bool check_builtin_procedure(CheckerContext *c, Operand *operand, As return false; } + if (is_type_simd_vector(type) && arg_count > SIMD_ELEMENT_COUNT_MAX) { + error(call, "'swizzle' constructs a #simd vector beyond the maximum element count of %d, got %lld", SIMD_ELEMENT_COUNT_MAX, cast(long long)arg_count); + return false; + } + operand->type = determine_swizzle_array_type(original_type, type_hint, arg_count); break; } From 0243398b19f5a24529dbf23b6a6134be5b7e3984 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Sat, 15 Aug 2026 21:52:07 -0700 Subject: [PATCH 04/10] swizzle arg guard --- src/check_builtin.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/check_builtin.cpp b/src/check_builtin.cpp index 457252354..5d9c6ac08 100644 --- a/src/check_builtin.cpp +++ b/src/check_builtin.cpp @@ -3500,10 +3500,8 @@ gb_internal bool check_builtin_procedure(CheckerContext *c, Operand *operand, As arg_count++; } - if (false && arg_count > max_count) { - error(call, "Too many 'swizzle' indices, %td > %td", arg_count, max_count); - return false; - } else if (arg_count < 2) { + // No upper bound on the index count + if (arg_count < 2) { error(call, "Not enough 'swizzle' indices, %td < 2", arg_count); return false; } From 69fb453e518197626a1899124c5c452d1ce9b449 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Sat, 15 Aug 2026 22:02:06 -0700 Subject: [PATCH 05/10] redecl type; pairwise guard --- core/simd/simd.odin | 2 +- src/check_builtin.cpp | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/core/simd/simd.odin b/core/simd/simd.odin index fcbceb81a..7d61f523a 100644 --- a/core/simd/simd.odin +++ b/core/simd/simd.odin @@ -2899,7 +2899,7 @@ abs_diff :: #force_inline proc "contextless" (a, b: $T/#simd[$LANES]$E) -> T whe } pairwise_add :: intrinsics.simd_pairwise_add -pairwise_sub :: intrinsics.simd_pairwise_add +pairwise_sub :: intrinsics.simd_pairwise_sub interleave :: intrinsics.simd_interleave deinterleave :: intrinsics.simd_deinterleave \ No newline at end of file diff --git a/src/check_builtin.cpp b/src/check_builtin.cpp index 5d9c6ac08..ed35a928a 100644 --- a/src/check_builtin.cpp +++ b/src/check_builtin.cpp @@ -949,6 +949,16 @@ gb_internal bool check_builtin_simd_operation(CheckerContext *c, Operand *operan // don't return } + if (id == BuiltinProc_simd_pairwise_add || id == BuiltinProc_simd_pairwise_sub) { + i64 lanes = get_array_type_count(x.type); + if (lanes % 2 != 0) { + gbString xs = type_to_string(x.type); + error(x.expr, "'%.*s' expected a #simd type with an even lane count, got '%s'", LIT(builtin_name), xs); + gb_string_free(xs); + return false; + } + } + operand->mode = Addressing_Value; operand->type = x.type; return true; From e53fc6a8796728cd1634cdfbeff9a7b747e5c104 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Sat, 15 Aug 2026 22:23:15 -0700 Subject: [PATCH 06/10] simd [?] type fix; count recovery --- src/check_expr.cpp | 34 ++++- src/check_type.cpp | 10 +- tests/internal/test_simd_lane_counts.odin | 172 ++++++++++++++++++++++ 3 files changed, 213 insertions(+), 3 deletions(-) create mode 100644 tests/internal/test_simd_lane_counts.odin diff --git a/src/check_expr.cpp b/src/check_expr.cpp index ec276884a..96772a778 100644 --- a/src/check_expr.cpp +++ b/src/check_expr.cpp @@ -10688,7 +10688,25 @@ gb_internal ExprKind check_compound_literal(CheckerContext *c, Operand *o, Ast * if (count != nullptr) { if (count->kind == Ast_UnaryExpr && count->UnaryExpr.op.kind == Token_Question) { - type = alloc_type_array(check_type(c, type_expr->ArrayType.elem), -1); + Type *elem = check_type(c, type_expr->ArrayType.elem); + + bool is_simd_tag = false; + if (type_expr->ArrayType.tag != nullptr) { + GB_ASSERT(type_expr->ArrayType.tag->kind == Ast_BasicDirective); + is_simd_tag = type_expr->ArrayType.tag->BasicDirective.name.string == "simd"; + } + if (is_simd_tag) { + if (!is_type_valid_vector_elem(elem) && !is_type_polymorphic(elem)) { + gbString str = type_to_string(elem); + error(type_expr->ArrayType.elem, "Invalid element type for #simd, expected an integer, float, boolean, or 'rawptr' with no specific endianness, got '%s'", str); + gb_string_free(str); + type = alloc_type_array(elem, -1); + } else { + type = alloc_type_simd_vector(-1, elem); + } + } else { + type = alloc_type_array(elem, -1); + } is_to_be_determined_array_count = true; } } else { @@ -10912,7 +10930,9 @@ gb_internal ExprKind check_compound_literal(CheckerContext *c, Operand *o, Ast * } else if (t->kind == Type_SimdVector) { elem_type = t->SimdVector.elem; context_name = str_lit("simd vector literal"); - max_type_count = t->SimdVector.count; + if (!is_to_be_determined_array_count) { + max_type_count = t->SimdVector.count; + } } else if (t->kind == Type_Matrix) { elem_type = t->Matrix.elem; context_name = str_lit("matrix literal"); @@ -11088,6 +11108,16 @@ gb_internal ExprKind check_compound_literal(CheckerContext *c, Operand *o, Ast * error(node, "Expected %lld values for this array literal, got %lld", cast(long long)t->Array.count, cast(long long)max); } } + } else if (t->kind == Type_SimdVector) { + // the length laws cannot be applied until the literal has supplied the count + if (is_to_be_determined_array_count) { + t->SimdVector.count = max; + if (max < 1 || !is_power_of_two(max)) { + error(node, "Invalid length for #simd, expected a power of two length, got '%lld'", cast(long long)max); + } else if (max > SIMD_ELEMENT_COUNT_MAX) { + error(node, "#simd support a maximum element count of %d, got %lld", SIMD_ELEMENT_COUNT_MAX, cast(long long)max); + } + } } else if (t->kind == Type_Struct) { GB_ASSERT(t->Struct.soa_kind == StructSoa_Fixed); if (is_to_be_determined_array_count) { diff --git a/src/check_type.cpp b/src/check_type.cpp index 14da12340..0fdf6d6b9 100644 --- a/src/check_type.cpp +++ b/src/check_type.cpp @@ -3538,9 +3538,12 @@ gb_internal void check_array_type_internal(CheckerContext *ctx, Ast *e, Type **t return; } + // Track user input and recovery value seperate, since both could be '0' + bool count_recovered = false; if (count < 0) { error(at->count, "? can only be used in conjunction with compound literals"); count = 0; + count_recovered = true; } @@ -3562,7 +3565,12 @@ gb_internal void check_array_type_internal(CheckerContext *ctx, Ast *e, Type **t // Ignore } else if (count < 1 || !is_power_of_two(count)) { *type = alloc_type_array(elem, count, generic_type); - if (ctx->disallow_polymorphic_return_types && count == 0) { + if (count_recovered) { + return; + } + // a polymorphic value used as the count is still unresolved while the + // signature is checked and reads as 0; only a written count is constant + if (ctx->disallow_polymorphic_return_types && o.mode != Addressing_Constant) { return; } error(at->count, "Invalid length for #simd, expected a power of two length, got '%lld'", cast(long long)count); diff --git a/tests/internal/test_simd_lane_counts.odin b/tests/internal/test_simd_lane_counts.odin new file mode 100644 index 000000000..1d06790f0 --- /dev/null +++ b/tests/internal/test_simd_lane_counts.odin @@ -0,0 +1,172 @@ +package test_internal + +import "base:intrinsics" +import "core:simd" +import "core:testing" + +// `simd_interleave` derives its lane count as operand width times argument count, so it is a +// power of two only when the argument count is. The rejection of the rest cannot be asserted +// from a test -- it is a compile error -- so what is pinned here is the other side: the widths +// that must keep working, and the values they carry. + +@(test) +simd_interleave_power_of_two_widths :: proc(t: ^testing.T) { + a: #simd[4]i32 = {1, 2, 3, 4} + b: #simd[4]i32 = {5, 6, 7, 8} + c: #simd[4]i32 = {9, 10, 11, 12} + d: #simd[4]i32 = {13, 14, 15, 16} + + two := intrinsics.simd_interleave(a, b) + testing.expect_value(t, len(two), 8) + testing.expect_value(t, simd_extract_i32(two, 0), 1) + testing.expect_value(t, simd_extract_i32(two, 1), 5) + testing.expect_value(t, simd_extract_i32(two, 2), 2) + testing.expect_value(t, simd_extract_i32(two, 3), 6) + + four := intrinsics.simd_interleave(a, b, c, d) + testing.expect_value(t, len(four), 16) + testing.expect_value(t, simd_extract_i32(four, 0), 1) + testing.expect_value(t, simd_extract_i32(four, 1), 5) + testing.expect_value(t, simd_extract_i32(four, 2), 9) + testing.expect_value(t, simd_extract_i32(four, 3), 13) + + // a single argument is a power of two count as well, and must not be caught by the guard + one := intrinsics.simd_interleave(a) + testing.expect_value(t, len(one), 4) + + x, y := intrinsics.simd_deinterleave(two, 2) + testing.expect_value(t, len(x), 4) + testing.expect_value(t, len(y), 4) + testing.expect_value(t, simd_extract_i32(x, 0), 1) + testing.expect_value(t, simd_extract_i32(y, 0), 5) +} + +simd_extract_i32 :: #force_inline proc(v: $V/#simd[$N]i32, $I: int) -> i32 { + return intrinsics.simd_extract(v, I) +} + +// `simd_shuffle` and `swizzle` size their result from the index list, which can be twice the +// operand width, so both can construct a `#simd` wider than the syntax accepts. What is pinned +// here is the boundary that must keep working: exactly SIMD_ELEMENT_COUNT_MAX lanes, and a +// `swizzle` over a plain array, which is not bound by the `#simd` limit at all. + +@(test) +simd_construction_at_the_element_count_max :: proc(t: ^testing.T) { + a: #simd[32]i32 = 1 + at_max := intrinsics.simd_shuffle(a, a, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63) + testing.expect_value(t, len(at_max), 64) + + b: #simd[8]i32 = {1, 2, 3, 4, 5, 6, 7, 8} + under := intrinsics.simd_shuffle(b, b, 0, 1, 2, 3, 8, 9, 10, 11) + testing.expect_value(t, len(under), 8) + testing.expect_value(t, simd_extract_i32(under, 4), 1) + + sw := swizzle(b, 7, 6, 5, 4, 3, 2, 1, 0) + testing.expect_value(t, len(sw), 8) + testing.expect_value(t, simd_extract_i32(sw, 0), 8) + + // a plain array is not a #simd vector, so the element-count limit must not reach it + arr: [96]i32 + arr[95] = 7 + asw := swizzle(arr, 95, 0, 1) + testing.expect_value(t, len(asw), 3) + testing.expect_value(t, asw[0], 7) + + d, e: #simd[32]i32 + ilv := intrinsics.simd_interleave(d, e) + testing.expect_value(t, len(ilv), 64) +} + +// A swizzle may repeat indices to produce a result wider than its operand. `core:crypto` +// depends on it -- chacha20's simd256 path doubles a 4-lane state with eight indices -- so +// there is deliberately no upper bound on the index count. + +@(test) +swizzle_may_widen_its_operand :: proc(t: ^testing.T) { + v: [2]f32 = {1, 2} + testing.expect_value(t, swizzle(v, 0, 0, 0, 0), [4]f32{1, 1, 1, 1}) + testing.expect_value(t, swizzle(v, 0, 1, 0, 1), [4]f32{1, 2, 1, 2}) + + a: [3]i32 = {7, 8, 9} + testing.expect_value(t, swizzle(a, 2, 2, 2, 2, 2, 2), [6]i32{9, 9, 9, 9, 9, 9}) + + // the same shape on a #simd vector: four lanes widened to eight + q: #simd[4]u32 = {1, 2, 3, 4} + w := swizzle(q, 0, 1, 2, 3, 0, 1, 2, 3) + testing.expect_value(t, len(w), 8) + testing.expect_value(t, intrinsics.simd_extract(w, 4), u32(1)) + testing.expect_value(t, intrinsics.simd_extract(w, 7), u32(4)) +} + +// A pairwise operation folds adjacent lanes within each operand, so it needs an even lane +// count -- `base:intrinsics` declares `LANES % 2 == 0`. At one lane it has nothing to pair +// with and silently switches to combining the two operands instead, which is why that width +// is rejected rather than defined. + +@(test) +simd_pairwise_folds_adjacent_lanes :: proc(t: ^testing.T) { + a: #simd[2]i32 = {10, 3} + b: #simd[2]i32 = {20, 4} + + add := intrinsics.simd_pairwise_add(a, b) + testing.expect_value(t, intrinsics.simd_extract(add, 0), i32(13)) + testing.expect_value(t, intrinsics.simd_extract(add, 1), i32(24)) + + sub := intrinsics.simd_pairwise_sub(a, b) + testing.expect_value(t, intrinsics.simd_extract(sub, 0), i32(7)) + testing.expect_value(t, intrinsics.simd_extract(sub, 1), i32(16)) + + c: #simd[4]f32 = {1, 2, 3, 4} + d: #simd[4]f32 = {5, 6, 7, 8} + f := intrinsics.simd_pairwise_add(c, d) + testing.expect_value(t, intrinsics.simd_extract(f, 0), f32(3)) + testing.expect_value(t, intrinsics.simd_extract(f, 1), f32(7)) + + // the other six builtins sharing this arm take a single lane, and must stay unaffected + o: #simd[1]i32 = 7 + p: #simd[1]i32 = 2 + testing.expect_value(t, intrinsics.simd_extract(intrinsics.simd_add(o, p), 0), i32(9)) + testing.expect_value(t, intrinsics.simd_extract(intrinsics.simd_max(o, p), 0), i32(7)) +} + +// `#simd[?]T{...}` The inferred form must agree with the explicit one. + +@(test) +simd_inferred_length_from_a_compound_literal :: proc(t: ^testing.T) { + a := #simd[?]i32{1, 2, 3, 4} + testing.expect_value(t, len(a), 4) + testing.expect_value(t, intrinsics.type_is_simd_vector(type_of(a)), true) + testing.expect_value(t, typeid_of(type_of(a)), typeid_of(#simd[4]i32)) + testing.expect_value(t, intrinsics.simd_extract(a, 0), i32(1)) + testing.expect_value(t, intrinsics.simd_extract(a, 3), i32(4)) + + b := #simd[?]f32{1.5, 2.5} + testing.expect_value(t, len(b), 2) + testing.expect_value(t, typeid_of(type_of(b)), typeid_of(#simd[2]f32)) + testing.expect_value(t, intrinsics.simd_extract(b, 1), f32(2.5)) + + // the inferred vector must work with the intrinsics, which is the point of the tag + testing.expect_value(t, intrinsics.simd_extract(intrinsics.simd_add(a, a), 3), i32(8)) + + // a plain `[?]` array is unaffected + c := [?]i32{1, 2, 3} + testing.expect_value(t, len(c), 3) + testing.expect_value(t, intrinsics.type_is_array(type_of(c)), true) +} + +// `core:simd` aliased `pairwise_sub` to `simd_pairwise_add`, so it silently added +@(test) +simd_pairwise_aliases_are_distinct :: proc(t: ^testing.T) { + a: #simd[2]i32 = {10, 3} + b: #simd[2]i32 = {20, 4} + + add := simd.pairwise_add(a, b) + sub := simd.pairwise_sub(a, b) + testing.expect_value(t, intrinsics.simd_extract(add, 0), i32(13)) + testing.expect_value(t, intrinsics.simd_extract(sub, 0), i32(7)) + testing.expect_value(t, intrinsics.simd_extract(sub, 1), i32(16)) +} From 5f0251fec24dc72eb4331ce4043ff75033fc6e77 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Sat, 15 Aug 2026 22:44:52 -0700 Subject: [PATCH 07/10] 5791,6748 --- src/check_builtin.cpp | 13 +++------ tests/internal/test_simd_lane_counts.odin | 33 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/check_builtin.cpp b/src/check_builtin.cpp index ed35a928a..f67c06dcb 100644 --- a/src/check_builtin.cpp +++ b/src/check_builtin.cpp @@ -1305,10 +1305,6 @@ gb_internal bool check_builtin_simd_operation(CheckerContext *c, Operand *operan if (!check_index_value(c, x.type, false, ce->args[1], max_count, &value)) { return false; } - if (max_count < 0) { - error(ce->args[1], "'%.*s' expected a constant integer index, got '%lld'", LIT(builtin_name), cast(long long)value); - return false; - } operand->mode = Addressing_Value; operand->type = elem; @@ -1330,10 +1326,6 @@ gb_internal bool check_builtin_simd_operation(CheckerContext *c, Operand *operan if (!check_index_value(c, x.type, false, ce->args[1], max_count, &value)) { return false; } - if (max_count < 0) { - error(ce->args[1], "'%.*s' expected a constant integer index, got '%lld'", LIT(builtin_name), cast(long long)value); - return false; - } Operand y = {}; check_expr_with_type_hint(c, &y, ce->args[2], elem); if (y.mode == Addressing_Invalid) return false; @@ -1801,12 +1793,13 @@ gb_internal bool check_builtin_simd_operation(CheckerContext *c, Operand *operan } Operand offset = {}; check_expr(c, &offset, ce->args[1]); if (offset.mode == Addressing_Invalid) return false; - convert_to_typed(c, &offset, t_i64); + // `base:intrinsics` declares the offset as `int` and does not mark it #any_int + convert_to_typed(c, &offset, t_int); if (!is_type_integer(offset.type) || offset.mode != Addressing_Constant) { error(offset.expr, "'%.*s' expected a constant integer offset", LIT(builtin_name)); return false; } - check_assignment(c, &offset, t_i64, builtin_name); + check_assignment(c, &offset, t_int, builtin_name); operand->type = x.type; operand->mode = Addressing_Value; diff --git a/tests/internal/test_simd_lane_counts.odin b/tests/internal/test_simd_lane_counts.odin index 1d06790f0..5f5ff6e49 100644 --- a/tests/internal/test_simd_lane_counts.odin +++ b/tests/internal/test_simd_lane_counts.odin @@ -170,3 +170,36 @@ simd_pairwise_aliases_are_distinct :: proc(t: ^testing.T) { testing.expect_value(t, intrinsics.simd_extract(sub, 0), i32(7)) testing.expect_value(t, intrinsics.simd_extract(sub, 1), i32(16)) } + +// The rotate offset is declared `$offset: int`; the checker demanded `i64`, so the declared +// spelling did not compile. Other integer types stay rejected -- the declaration is not #any_int. + +@(test) +simd_lanes_rotate_offset_is_int :: proc(t: ^testing.T) { + rot :: proc(v: #simd[4]u32, $offset: int) -> #simd[4]u32 { + return intrinsics.simd_lanes_rotate_right(v, offset) + } + + v: #simd[4]u32 = {0, 1, 2, 3} + r := rot(v, 1) + testing.expect_value(t, intrinsics.simd_extract(r, 0), u32(3)) + testing.expect_value(t, intrinsics.simd_extract(r, 1), u32(0)) + + l := intrinsics.simd_lanes_rotate_left(v, 1) + testing.expect_value(t, intrinsics.simd_extract(l, 0), u32(1)) + testing.expect_value(t, intrinsics.simd_extract(l, 3), u32(0)) +} + +// `simd_extract` / `simd_replace` declare a plain `idx: uint`, so a runtime index is allowed and +// lowers to a dynamic extractelement. Bounds are enforced only where the index is constant. + +@(test) +simd_extract_accepts_a_runtime_index :: proc(t: ^testing.T) { + v: #simd[4]u32 = {10, 20, 30, 40} + i := 2 + testing.expect_value(t, intrinsics.simd_extract(v, i), u32(30)) + + w := intrinsics.simd_replace(v, i, u32(99)) + testing.expect_value(t, intrinsics.simd_extract(w, 2), u32(99)) + testing.expect_value(t, intrinsics.simd_extract(w, 0), u32(10)) +} From d52aff5c4b4fee17113a110f22b9cde43e453009 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Sat, 15 Aug 2026 23:53:54 -0700 Subject: [PATCH 08/10] impl riffle for llvm<=20 fallback on interleave --- src/llvm_backend_proc.cpp | 43 ++++++++++++++++++++--- tests/internal/test_simd_lane_counts.odin | 6 ++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/llvm_backend_proc.cpp b/src/llvm_backend_proc.cpp index be1b3724f..011219c08 100644 --- a/src/llvm_backend_proc.cpp +++ b/src/llvm_backend_proc.cpp @@ -1563,12 +1563,45 @@ gb_internal lbValue lb_build_builtin_simd_proc(lbProcedure *p, Ast *expr, TypeAn args[i] = arg.value; } - gbString name = gb_string_make(heap_allocator(), ""); - name = gb_string_append_fmt(name, "llvm.vector.interleave%d", n); - defer (gb_string_free(name)); + // `llvm.vector.interleave` for N > 2 is only usable on recent LLVM: it does not + // exist at all on 20, and where it does exist a backend may still have no pattern + // for it (x86 on 20 fails to select). Measured working on 22, so take it there. + #if LLVM_VERSION_MAJOR >= 22 + if (n > 2) { + gbString name = gb_string_make(heap_allocator(), ""); + name = gb_string_append_fmt(name, "llvm.vector.interleave%d", n); + defer (gb_string_free(name)); - LLVMTypeRef types[1] = {lb_type(m, tv.type)}; - res.value = lb_call_intrinsic(p, name, args, n, types, gb_count_of(types)); + LLVMTypeRef types[1] = {lb_type(m, tv.type)}; + res.value = lb_call_intrinsic(p, name, args, n, types, gb_count_of(types)); + return res; + } + #endif + + // otherwise build the same permutation from interleave2, which every supported LLVM + // has. The operand count is a power of two (the checker requires the lane count to + // be one), and pairing each operand with the one a half-step away is what makes the + // orders agree: interleave4(a,b,c,d) == interleave2(interleave2(a,c), interleave2(b,d)). + LLVMTypeRef elem_type = LLVMGetElementType(LLVMTypeOf(args[0])); + unsigned width = LLVMGetVectorSize(LLVMTypeOf(args[0])); + + LLVMValueRef *cur = args; + for (int count = n; count > 1; /**/) { + int half = count/2; + width *= 2; + + LLVMTypeRef types[1] = {LLVMVectorType(elem_type, width)}; + LLVMValueRef *next = temporary_alloc_array(half); + for (int i = 0; i < half; i++) { + LLVMValueRef pair[2] = {cur[i], cur[i+half]}; + next[i] = lb_call_intrinsic(p, "llvm.vector.interleave2", pair, 2, types, gb_count_of(types)); + } + + cur = next; + count = half; + } + + res.value = cur[0]; return res; } diff --git a/tests/internal/test_simd_lane_counts.odin b/tests/internal/test_simd_lane_counts.odin index 5f5ff6e49..c6ad57051 100644 --- a/tests/internal/test_simd_lane_counts.odin +++ b/tests/internal/test_simd_lane_counts.odin @@ -9,6 +9,9 @@ import "core:testing" // from a test -- it is a compile error -- so what is pinned here is the other side: the widths // that must keep working, and the values they carry. +// More than two operands take one of two lowerings, `llvm.vector.interleave` on a LLVM 21+, +// a riffle of `interleave2` elsewhere. The lane order and width are both asserted. + @(test) simd_interleave_power_of_two_widths :: proc(t: ^testing.T) { a: #simd[4]i32 = {1, 2, 3, 4} @@ -29,6 +32,9 @@ simd_interleave_power_of_two_widths :: proc(t: ^testing.T) { testing.expect_value(t, simd_extract_i32(four, 1), 5) testing.expect_value(t, simd_extract_i32(four, 2), 9) testing.expect_value(t, simd_extract_i32(four, 3), 13) + testing.expect_value(t, simd_extract_i32(four, 4), 2) + testing.expect_value(t, simd_extract_i32(four, 7), 14) + testing.expect_value(t, simd_extract_i32(four, 15), 16) // a single argument is a power of two count as well, and must not be caught by the guard one := intrinsics.simd_interleave(a) From 95c4004239a8e13da425e538c446bc52884a294d Mon Sep 17 00:00:00 2001 From: kalsprite Date: Sun, 16 Aug 2026 00:16:21 -0700 Subject: [PATCH 09/10] switch to shuffle intrinsic since #simd is pow2 --- src/llvm_backend_proc.cpp | 43 ++++++++++------------- tests/internal/test_simd_lane_counts.odin | 4 +-- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/src/llvm_backend_proc.cpp b/src/llvm_backend_proc.cpp index 011219c08..2ca2594c5 100644 --- a/src/llvm_backend_proc.cpp +++ b/src/llvm_backend_proc.cpp @@ -1563,38 +1563,31 @@ gb_internal lbValue lb_build_builtin_simd_proc(lbProcedure *p, Ast *expr, TypeAn args[i] = arg.value; } - // `llvm.vector.interleave` for N > 2 is only usable on recent LLVM: it does not - // exist at all on 20, and where it does exist a backend may still have no pattern - // for it (x86 on 20 fails to select). Measured working on 22, so take it there. - #if LLVM_VERSION_MAJOR >= 22 - if (n > 2) { - gbString name = gb_string_make(heap_allocator(), ""); - name = gb_string_append_fmt(name, "llvm.vector.interleave%d", n); - defer (gb_string_free(name)); - - LLVMTypeRef types[1] = {lb_type(m, tv.type)}; - res.value = lb_call_intrinsic(p, name, args, n, types, gb_count_of(types)); - return res; - } - #endif - - // otherwise build the same permutation from interleave2, which every supported LLVM - // has. The operand count is a power of two (the checker requires the lane count to - // be one), and pairing each operand with the one a half-step away is what makes the - // orders agree: interleave4(a,b,c,d) == interleave2(interleave2(a,c), interleave2(b,d)). - LLVMTypeRef elem_type = LLVMGetElementType(LLVMTypeOf(args[0])); - unsigned width = LLVMGetVectorSize(LLVMTypeOf(args[0])); + // `llvm.vector.interleave` is not usable across the supported targets: N > 2 does + // not exist before LLVM 22. Riscv & Darwin AMD64 has no `interleave2` either. + // A shuffle is the one primitive every target has, and it expresses a two-way + // interleave directly. + // + // The operand count is a power of two. The result is a riffle: pairing each operand + // with the one a half-step away is what makes the order come out right, as + // interleave4(a,b,c,d) == interleave2(interleave2(a,c), interleave2(b,d)). + LLVMTypeRef llvm_u32 = lb_type(m, t_u32); LLVMValueRef *cur = args; for (int count = n; count > 1; /**/) { int half = count/2; - width *= 2; + unsigned width = LLVMGetVectorSize(LLVMTypeOf(cur[0])); + + LLVMValueRef *mask = temporary_alloc_array(2*width); + for (unsigned i = 0; i < width; i++) { + mask[2*i + 0] = LLVMConstInt(llvm_u32, i, false); + mask[2*i + 1] = LLVMConstInt(llvm_u32, width+i, false); + } + LLVMValueRef mask_value = LLVMConstVector(mask, 2*width); - LLVMTypeRef types[1] = {LLVMVectorType(elem_type, width)}; LLVMValueRef *next = temporary_alloc_array(half); for (int i = 0; i < half; i++) { - LLVMValueRef pair[2] = {cur[i], cur[i+half]}; - next[i] = lb_call_intrinsic(p, "llvm.vector.interleave2", pair, 2, types, gb_count_of(types)); + next[i] = LLVMBuildShuffleVector(p->builder, cur[i], cur[i+half], mask_value, ""); } cur = next; diff --git a/tests/internal/test_simd_lane_counts.odin b/tests/internal/test_simd_lane_counts.odin index c6ad57051..2d6bea312 100644 --- a/tests/internal/test_simd_lane_counts.odin +++ b/tests/internal/test_simd_lane_counts.odin @@ -9,8 +9,8 @@ import "core:testing" // from a test -- it is a compile error -- so what is pinned here is the other side: the widths // that must keep working, and the values they carry. -// More than two operands take one of two lowerings, `llvm.vector.interleave` on a LLVM 21+, -// a riffle of `interleave2` elsewhere. The lane order and width are both asserted. +// More than two operands lower to a riffle of two-way shuffles rather than one intrinsic, so +// the lane ORDER is asserted and not just the width. @(test) simd_interleave_power_of_two_widths :: proc(t: ^testing.T) { From f7b734c82b3ca99bf8b7eea48126dacd082d28aa Mon Sep 17 00:00:00 2001 From: kalsprite Date: Sun, 16 Aug 2026 00:24:09 -0700 Subject: [PATCH 10/10] deinterleave -> shuffle --- src/llvm_backend_proc.cpp | 25 ++++++++++++---- tests/internal/test_simd_lane_counts.odin | 35 +++++++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/llvm_backend_proc.cpp b/src/llvm_backend_proc.cpp index 2ca2594c5..6da9314c5 100644 --- a/src/llvm_backend_proc.cpp +++ b/src/llvm_backend_proc.cpp @@ -1601,7 +1601,6 @@ gb_internal lbValue lb_build_builtin_simd_proc(lbProcedure *p, Ast *expr, TypeAn case BuiltinProc_simd_deinterleave: { lbValue arg0 = lb_build_expr(p, ce->args[0]); - LLVMTypeRef types[1] = {lb_type(m, arg0.type)}; GB_ASSERT(ce->args[1]->tav.value.kind == ExactValue_Integer); int n = cast(int)exact_value_to_i64(ce->args[1]->tav.value); @@ -1611,11 +1610,27 @@ gb_internal lbValue lb_build_builtin_simd_proc(lbProcedure *p, Ast *expr, TypeAn return res; } - gbString name = gb_string_make(heap_allocator(), ""); - name = gb_string_append_fmt(name, "llvm.vector.deinterleave%d", n); - defer (gb_string_free(name)); + // `llvm.vector.deinterleave` for N > 2 cannot be selected or legalized on most + // targets, only arm64 takes it. The split is done with shuffles, same as + // `simd_interleave`. Output `j` is the input strided by N starting at lane `j`. + LLVMTypeRef llvm_u32 = lb_type(m, t_u32); + LLVMTypeRef vector_type = LLVMTypeOf(arg0.value); + LLVMValueRef undef = LLVMGetUndef(vector_type); - res.value = lb_call_intrinsic(p, name, &arg0.value, 1, types, gb_count_of(types)); + unsigned width = LLVMGetVectorSize(vector_type); + unsigned part = width/cast(unsigned)n; + + LLVMValueRef agg = LLVMGetUndef(lb_type(m, tv.type)); + LLVMValueRef *mask = temporary_alloc_array(part); + for (int j = 0; j < n; j++) { + for (unsigned i = 0; i < part; i++) { + mask[i] = LLVMConstInt(llvm_u32, i*cast(unsigned)n + cast(unsigned)j, false); + } + LLVMValueRef lanes = LLVMBuildShuffleVector(p->builder, arg0.value, undef, LLVMConstVector(mask, part), ""); + agg = LLVMBuildInsertValue(p->builder, agg, lanes, cast(unsigned)j, ""); + } + + res.value = agg; return res; } } diff --git a/tests/internal/test_simd_lane_counts.odin b/tests/internal/test_simd_lane_counts.odin index 2d6bea312..3931fc065 100644 --- a/tests/internal/test_simd_lane_counts.odin +++ b/tests/internal/test_simd_lane_counts.odin @@ -47,6 +47,41 @@ simd_interleave_power_of_two_widths :: proc(t: ^testing.T) { testing.expect_value(t, simd_extract_i32(y, 0), 5) } +// `simd_deinterleave` splits by stride, so output `j` is the input taken every N lanes from +// lane `j`. N > 2 lowers to shuffles for the same reason interleave does, only arm64 could +// select the intrinsic. The round trip is asserted, not just the widths. + +@(test) +simd_deinterleave_splits_by_stride :: proc(t: ^testing.T) { + v: #simd[16]i32 + for i in 0..<16 { + v = intrinsics.simd_replace(v, i, i32(i)) + } + + a2, b2 := intrinsics.simd_deinterleave(v, 2) + testing.expect_value(t, len(a2), 8) + testing.expect_value(t, simd_extract_i32(a2, 0), 0) + testing.expect_value(t, simd_extract_i32(a2, 1), 2) + testing.expect_value(t, simd_extract_i32(b2, 0), 1) + testing.expect_value(t, simd_extract_i32(b2, 7), 15) + + a4, b4, c4, d4 := intrinsics.simd_deinterleave(v, 4) + testing.expect_value(t, len(a4), 4) + testing.expect_value(t, simd_extract_i32(a4, 0), 0) + testing.expect_value(t, simd_extract_i32(b4, 0), 1) + testing.expect_value(t, simd_extract_i32(c4, 0), 2) + testing.expect_value(t, simd_extract_i32(d4, 0), 3) + testing.expect_value(t, simd_extract_i32(a4, 3), 12) + testing.expect_value(t, simd_extract_i32(d4, 3), 15) + + // interleave is the inverse, so the pair must round trip + r := intrinsics.simd_interleave(a4, b4, c4, d4) + testing.expect_value(t, len(r), 16) + testing.expect_value(t, simd_extract_i32(r, 0), 0) + testing.expect_value(t, simd_extract_i32(r, 7), 7) + testing.expect_value(t, simd_extract_i32(r, 15), 15) +} + simd_extract_i32 :: #force_inline proc(v: $V/#simd[$N]i32, $I: int) -> i32 { return intrinsics.simd_extract(v, I) }