lang: inline for element form over packs — multi-iterable parity
This commit is contained in:
63
examples/0545-packs-inline-for-element.sx
Normal file
63
examples/0545-packs-inline-for-element.sx
Normal file
@@ -0,0 +1,63 @@
|
||||
// `inline for` element form over a pack — multi-iterable parity with the
|
||||
// runtime for-loop. Position 0 drives the unroll count (a pack's arity or a
|
||||
// bounded range's span); trailing iterables pair with it. A pack capture is
|
||||
// the concrete per-position element viewed through the constraint protocol
|
||||
// (same semantics as `xs[i]`); a range capture is a comptime cursor.
|
||||
//
|
||||
// inline for xs (x) — element form
|
||||
// inline for xs, 0.. (x, i) — element + paired index
|
||||
// inline for 0..xs.len, xs (i, x) — range driver, trailing pack
|
||||
// inline for xs { } — captureless; N=0 unrolls nothing
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
Show :: protocol { show :: () -> string; }
|
||||
IntBox :: struct { v: s64; }
|
||||
StrBox :: struct { s: string; }
|
||||
impl Show for IntBox { show :: (self: *IntBox) -> string { int_to_string(self.v) } }
|
||||
impl Show for StrBox { show :: (self: *StrBox) -> string { self.s } }
|
||||
|
||||
bare :: (..xs: Show) {
|
||||
inline for xs (x) {
|
||||
print("bare: {}\n", x.show());
|
||||
}
|
||||
}
|
||||
elem_and_index :: (..xs: Show) {
|
||||
inline for xs, 0.. (x, i) {
|
||||
print("{}: {}\n", i, x.show());
|
||||
}
|
||||
}
|
||||
range_driver :: (..xs: Show) {
|
||||
inline for 0..xs.len, xs (i, x) {
|
||||
print("r{}: {}\n", i, x.show());
|
||||
}
|
||||
}
|
||||
offset_index :: (..xs: Show) {
|
||||
inline for xs, 10.. (x, i) {
|
||||
print("{} -> {}\n", i, x.show());
|
||||
}
|
||||
}
|
||||
captureless :: (..xs: Show) {
|
||||
n := 0;
|
||||
inline for xs { n += 1; }
|
||||
print("ran {}\n", n);
|
||||
}
|
||||
value_pos :: (..xs: Show) {
|
||||
inline for xs (x) {
|
||||
print("val: {}\n", x);
|
||||
}
|
||||
}
|
||||
empty :: (..xs: Show) {
|
||||
inline for xs (x) { print("never\n"); }
|
||||
print("empty ok\n");
|
||||
}
|
||||
|
||||
main :: () {
|
||||
bare(IntBox.{ v = 7 }, StrBox.{ s = "hi" });
|
||||
elem_and_index(IntBox.{ v = 7 }, StrBox.{ s = "hi" });
|
||||
range_driver(IntBox.{ v = 1 }, StrBox.{ s = "two" });
|
||||
offset_index(StrBox.{ s = "x" }, StrBox.{ s = "y" });
|
||||
captureless(IntBox.{ v = 0 }, IntBox.{ v = 0 }, IntBox.{ v = 0 });
|
||||
value_pos(IntBox.{ v = 42 });
|
||||
empty();
|
||||
}
|
||||
31
examples/1164-diagnostics-inline-for-pack-rejections.sx
Normal file
31
examples/1164-diagnostics-inline-for-pack-rejections.sx
Normal file
@@ -0,0 +1,31 @@
|
||||
// `inline for` pack rejections: (1) a pack-element capture exposes only the
|
||||
// constraint protocol's interface (same rule as `xs[i]`, example 0530);
|
||||
// (2) a pack element cannot be captured by reference (`(*x)` — an element is
|
||||
// an AST-substituted call arg, no storage); (3) a trailing pack shorter than
|
||||
// the driving iterable; (4) a non-pack, non-range iterable.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
Show :: protocol { show :: () -> string; }
|
||||
IntBox :: struct { v: s64; }
|
||||
impl Show for IntBox { show :: (self: *IntBox) -> string { int_to_string(self.v) } }
|
||||
|
||||
leak :: (..xs: Show) {
|
||||
inline for xs (x) {
|
||||
print("{}\n", x.v);
|
||||
}
|
||||
}
|
||||
borrow :: (..xs: Show) {
|
||||
inline for xs (*x) { }
|
||||
}
|
||||
short :: (..xs: Show) {
|
||||
inline for 0..5, xs (i, x) { }
|
||||
}
|
||||
|
||||
main :: () {
|
||||
leak(IntBox.{ v = 5 });
|
||||
borrow(IntBox.{ v = 5 });
|
||||
short(IntBox.{ v = 1 }, IntBox.{ v = 2 });
|
||||
arr := .[1, 2, 3];
|
||||
inline for arr (x) { }
|
||||
}
|
||||
@@ -37,7 +37,7 @@ error: pack 'xs' has no runtime value — a pack is comptime-only and can't be u
|
||||
17 | iter :: (..xs: Show) -> void { for xs (x) { _ = x; } } // D: runtime iterate
|
||||
| ^^
|
||||
|
||||
help: to iterate at comptime use `inline for 0..xs.len (i)`; for a runtime loop declare it as `..xs: []P` (a protocol slice) instead of a pack
|
||||
help: to iterate at comptime use `inline for xs (x)` (or `inline for 0..xs.len (i)` for the index); for a runtime loop declare it as `..xs: []P` (a protocol slice) instead of a pack
|
||||
|
|
||||
17 | iter :: (..xs: Show) -> void { for xs (x) { _ = x; } } // D: runtime iterate
|
||||
| ^^
|
||||
|
||||
1
examples/expected/0545-packs-inline-for-element.exit
Normal file
1
examples/expected/0545-packs-inline-for-element.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
examples/expected/0545-packs-inline-for-element.stderr
Normal file
1
examples/expected/0545-packs-inline-for-element.stderr
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
11
examples/expected/0545-packs-inline-for-element.stdout
Normal file
11
examples/expected/0545-packs-inline-for-element.stdout
Normal file
@@ -0,0 +1,11 @@
|
||||
bare: 7
|
||||
bare: hi
|
||||
0: 7
|
||||
1: hi
|
||||
r0: 1
|
||||
r1: two
|
||||
10 -> x
|
||||
11 -> y
|
||||
ran 3
|
||||
val: IntBox{v: 42}
|
||||
empty ok
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,23 @@
|
||||
error: 'v' is not part of protocol 'Show' — a pack element exposes only the protocol's interface
|
||||
--> examples/1164-diagnostics-inline-for-pack-rejections.sx:15:23
|
||||
|
|
||||
15 | print("{}\n", x.v);
|
||||
| ^^^
|
||||
|
||||
error: a pack element cannot be captured by reference
|
||||
--> examples/1164-diagnostics-inline-for-pack-rejections.sx:19:21
|
||||
|
|
||||
19 | inline for xs (*x) { }
|
||||
| ^
|
||||
|
||||
error: inline for: pack 'xs' has 2 elements but the unroll is 5 iterations
|
||||
--> examples/1164-diagnostics-inline-for-pack-rejections.sx:22:22
|
||||
|
|
||||
22 | inline for 0..5, xs (i, x) { }
|
||||
| ^^
|
||||
|
||||
error: inline for: each iterable must be a comptime range or a pack — `inline for 0..N (i) { }` / `inline for xs (x) { }`
|
||||
--> examples/1164-diagnostics-inline-for-pack-rejections.sx:30:16
|
||||
|
|
||||
30 | inline for arr (x) { }
|
||||
| ^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
12
specs.md
12
specs.md
@@ -1343,6 +1343,8 @@ may do, regardless of the concrete arg types at any particular call site.
|
||||
| Length | `xs.len` | comptime int (field-style, not `len(xs)`) |
|
||||
| Index | `xs[i]` | i-th element; `i` must be comptime |
|
||||
| Comptime unroll (index) | `inline for 0..xs.len (i) { ... }` | unrolled loop; cursor `i` is a comptime constant per iteration; not `#for` |
|
||||
| Comptime unroll (element) | `inline for xs (x) { ... }` | unrolled loop; `x` is the concrete i-th element, viewed through the constraint protocol (≡ `xs[i]`) |
|
||||
| Comptime unroll (element + index) | `inline for xs, 0.. (x, i) { ... }` | multi-iterable parity with the runtime `for`: position 0 drives the count, a trailing open range pairs the cursor |
|
||||
| Projection | `xs.field` | see "Pack projection" |
|
||||
| Spread → call args | `..xs` / `..xs.field` | expands to N positional args |
|
||||
| Spread → tuple value | `(..xs)` / `(..xs.field)` | materializes a tuple |
|
||||
@@ -1395,8 +1397,9 @@ suggestion:
|
||||
variadic `..xs: []P` (a runtime slice) instead of a pack `..xs: P`;
|
||||
- returning it (`return xs;`) → return a tuple `(..xs)` (and make the return
|
||||
type that tuple);
|
||||
- iterating it (`for xs (x)`, `xs[runtime_i]`) → `inline for 0..xs.len (i)`
|
||||
for a comptime unroll, or take `..xs: []P` for a runtime loop.
|
||||
- iterating it (`for xs (x)`, `xs[runtime_i]`) → `inline for xs (x)` (or
|
||||
`inline for 0..xs.len (i)` for the index) for a comptime unroll, or take
|
||||
`..xs: []P` for a runtime loop.
|
||||
|
||||
The recurring runtime escape hatch is the **slice-of-protocol variadic**
|
||||
`..xs: []P` (see "Variadic Functions"): it is the runtime, protocol-erased
|
||||
@@ -2093,7 +2096,10 @@ for xs, ys (x, y) { } // parallel (zip) iteration
|
||||
for 1..=5, 0.. (a, b) { } // a: 1..5, b: 0..4 (end inferred)
|
||||
for a4, b4, 100.. (p, q, k) { } // any number of positions
|
||||
for xs (x) => sum += x; // arrow body
|
||||
inline for 0..n (i) { } // comptime-unrolled single bounded range
|
||||
inline for 0..n (i) { } // comptime unroll; first range bounded
|
||||
inline for xs, 0.. (x, i) { } // comptime unroll over a PACK: x = the
|
||||
// concrete i-th element (see "Variadic
|
||||
// Heterogeneous Type Packs")
|
||||
```
|
||||
|
||||
**Range bound markers.** Each side of `..` takes an optional marker — `=`
|
||||
|
||||
@@ -265,6 +265,9 @@ pub const ExprTyper = struct {
|
||||
.identifier => |id| {
|
||||
if (self.l.scope) |scope| {
|
||||
if (scope.lookup(id.name)) |binding| {
|
||||
// `inline for xs (x)` element capture — type as the
|
||||
// synthesized `xs[<i>]` it aliases.
|
||||
if (binding.pack_elem) |elem| return self.l.inferExprType(elem);
|
||||
return binding.ty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,12 @@ pub const Binding = struct {
|
||||
ty: TypeId,
|
||||
is_alloca: bool, // true if ref is a pointer that needs load
|
||||
is_ref_capture: bool = false, // `for xs: (*x)` — `ref` is `*elem`; auto-deref in value positions
|
||||
/// `inline for xs (x)` element capture: `x` is an AST ALIAS for the
|
||||
/// synthesized `xs[<i>]` of the current unroll iteration (`ref` is
|
||||
/// `.none`). Identifier consumers substitute this node, so the capture
|
||||
/// inherits the full pack-element semantics — concrete-arg substitution,
|
||||
/// typing, and the interface-only constraint check.
|
||||
pack_elem: ?*ast.Node = null,
|
||||
};
|
||||
|
||||
// `init` / `deinit` / `put` are pub so collaborator unit tests (e.g.
|
||||
|
||||
@@ -467,54 +467,125 @@ pub fn lowerFor(self: *Lowering, fe: *const ast.ForExpr) Ref {
|
||||
return self.builder.constInt(0, .void);
|
||||
}
|
||||
|
||||
/// Comptime-unrolled `inline for start..end (i) { }`. A single bounded range
|
||||
/// with comptime-known bounds. The body is lowered once per value with the
|
||||
/// cursor bound as an `int_val` comptime constant, so `xs[i]` over a pack
|
||||
/// substitutes the concrete per-position argument each iteration.
|
||||
/// Comptime-unrolled `inline for`. Iterables are comptime ranges and/or
|
||||
/// PACKS, mirroring the runtime multi-iterable contract: position 0 drives
|
||||
/// the iteration count (a pack's arity, or a bounded range's span) and
|
||||
/// trailing range bounds are ignored. Per iteration the body is lowered
|
||||
/// once; a range capture binds as an `int_val` comptime constant (so
|
||||
/// `xs[i]` substitutes the concrete per-position argument), and a pack
|
||||
/// capture binds as an AST alias for the synthesized `xs[<i>]`
|
||||
/// (`Binding.pack_elem`), inheriting full pack-element semantics —
|
||||
/// substitution, typing, and the interface-only constraint check.
|
||||
///
|
||||
/// inline for 0..xs.len (i) { xs[i].show(); } // index form
|
||||
/// inline for xs (x) { x.show(); } // element form
|
||||
/// inline for xs, 0.. (x, i) { ... } // element + index
|
||||
pub fn lowerInlineRangeFor(self: *Lowering, fe: *const ast.ForExpr) Ref {
|
||||
const it = fe.iterables[0];
|
||||
if (fe.iterables.len != 1 or !it.is_range or it.range_end == null) {
|
||||
if (self.diagnostics) |d| d.addFmt(.err, it.expr.span, "inline for: a single bounded range is required — `inline for 0..N (i) {{ }}`", .{});
|
||||
return self.builder.constInt(0, .void);
|
||||
}
|
||||
var start = self.evalComptimeInt(it.expr) orelse {
|
||||
if (self.diagnostics) |d| d.addFmt(.err, it.expr.span, "inline for: range start is not a compile-time integer", .{});
|
||||
return self.builder.constInt(0, .void);
|
||||
const IterClass = union(enum) {
|
||||
range: i64, // comptime start value
|
||||
pack: []const u8, // pack name
|
||||
};
|
||||
if (it.start_exclusive) start += 1;
|
||||
var end = self.evalComptimeInt(it.range_end.?) orelse {
|
||||
if (self.diagnostics) |d| d.addFmt(.err, it.range_end.?.span, "inline for: range end is not a compile-time integer", .{});
|
||||
return self.builder.constInt(0, .void);
|
||||
};
|
||||
if (it.end_inclusive) end += 1;
|
||||
const capture_name = if (fe.captures.len > 0) fe.captures[0].name else "";
|
||||
var classes = std.ArrayList(IterClass).empty;
|
||||
defer classes.deinit(self.alloc);
|
||||
var count: i64 = 0;
|
||||
|
||||
var i: i64 = start;
|
||||
while (i < end) : (i += 1) {
|
||||
for (fe.iterables, 0..) |it, idx| {
|
||||
if (it.is_range) {
|
||||
var start = self.evalComptimeInt(it.expr) orelse {
|
||||
if (self.diagnostics) |d| d.addFmt(.err, it.expr.span, "inline for: range start is not a compile-time integer", .{});
|
||||
return self.builder.constInt(0, .void);
|
||||
};
|
||||
if (it.start_exclusive) start += 1;
|
||||
if (idx == 0) {
|
||||
const end_node = it.range_end orelse {
|
||||
if (self.diagnostics) |d| d.addFmt(.err, it.expr.span, "inline for: the first range must be bounded — `inline for 0..N (i) {{ }}`", .{});
|
||||
return self.builder.constInt(0, .void);
|
||||
};
|
||||
var end = self.evalComptimeInt(end_node) orelse {
|
||||
if (self.diagnostics) |d| d.addFmt(.err, end_node.span, "inline for: range end is not a compile-time integer", .{});
|
||||
return self.builder.constInt(0, .void);
|
||||
};
|
||||
if (it.end_inclusive) end += 1;
|
||||
count = end - start;
|
||||
}
|
||||
classes.append(self.alloc, .{ .range = start }) catch unreachable;
|
||||
} else if (it.expr.data == .identifier and self.isPackName(it.expr.data.identifier.name)) {
|
||||
const name = it.expr.data.identifier.name;
|
||||
const len: i64 = if (self.pack_param_count) |ppc| @intCast(ppc.get(name) orelse 0) else 0;
|
||||
if (idx == 0) {
|
||||
count = len;
|
||||
} else if (len < count) {
|
||||
if (self.diagnostics) |d| d.addFmt(.err, it.expr.span, "inline for: pack '{s}' has {} element{s} but the unroll is {} iterations", .{
|
||||
name, len, if (len == 1) @as([]const u8, "") else @as([]const u8, "s"), count,
|
||||
});
|
||||
return self.builder.constInt(0, .void);
|
||||
}
|
||||
classes.append(self.alloc, .{ .pack = name }) catch unreachable;
|
||||
} else {
|
||||
if (self.diagnostics) |d| d.addFmt(.err, it.expr.span, "inline for: each iterable must be a comptime range or a pack — `inline for 0..N (i) {{ }}` / `inline for xs (x) {{ }}`", .{});
|
||||
return self.builder.constInt(0, .void);
|
||||
}
|
||||
}
|
||||
|
||||
// `(*x)` on a pack element: there is no storage to borrow — an element
|
||||
// is an AST-substituted call argument.
|
||||
for (fe.captures, 0..) |cap, ci| {
|
||||
if (cap.by_ref and ci < classes.items.len and classes.items[ci] == .pack) {
|
||||
const sp = cap.span orelse fe.iterables[ci].expr.span;
|
||||
if (self.diagnostics) |d| d.addFmt(.err, sp, "a pack element cannot be captured by reference", .{});
|
||||
return self.builder.constInt(0, .void);
|
||||
}
|
||||
}
|
||||
|
||||
const CursorSave = struct { name: []const u8, had_prev: bool, prev: ComptimeValue };
|
||||
|
||||
var i: i64 = 0;
|
||||
while (i < count) : (i += 1) {
|
||||
var body_scope = Scope.init(self.alloc, self.scope);
|
||||
const old_scope = self.scope;
|
||||
self.scope = &body_scope;
|
||||
|
||||
// Bind the cursor both as a runtime value (constInt, for uses like
|
||||
// `print(i)`) and as a comptime constant (for `xs[i]` substitution).
|
||||
var had_prev = false;
|
||||
var prev: ComptimeValue = undefined;
|
||||
if (capture_name.len > 0) {
|
||||
body_scope.put(capture_name, .{ .ref = self.builder.constInt(i, .s64), .ty = .s64, .is_alloca = false });
|
||||
if (self.comptime_constants.get(capture_name)) |p| {
|
||||
had_prev = true;
|
||||
prev = p;
|
||||
var saves = std.ArrayList(CursorSave).empty;
|
||||
defer saves.deinit(self.alloc);
|
||||
|
||||
for (fe.captures, 0..) |cap, ci| {
|
||||
if (cap.name.len == 0) continue;
|
||||
switch (classes.items[ci]) {
|
||||
.range => |start| {
|
||||
// Bind the cursor both as a runtime value (constInt, for
|
||||
// uses like `print(i)`) and as a comptime constant (for
|
||||
// `xs[i]` substitution).
|
||||
const v = start + i;
|
||||
body_scope.put(cap.name, .{ .ref = self.builder.constInt(v, .s64), .ty = .s64, .is_alloca = false });
|
||||
var save = CursorSave{ .name = cap.name, .had_prev = false, .prev = undefined };
|
||||
if (self.comptime_constants.get(cap.name)) |p| {
|
||||
save.had_prev = true;
|
||||
save.prev = p;
|
||||
}
|
||||
saves.append(self.alloc, save) catch {};
|
||||
self.comptime_constants.put(cap.name, .{ .int_val = v }) catch {};
|
||||
},
|
||||
.pack => |pack_name| {
|
||||
const span = fe.iterables[ci].expr.span;
|
||||
const id_node = self.alloc.create(Node) catch break;
|
||||
id_node.* = .{ .span = span, .data = .{ .identifier = .{ .name = pack_name } } };
|
||||
const idx_node = self.alloc.create(Node) catch break;
|
||||
idx_node.* = .{ .span = span, .data = .{ .int_literal = .{ .value = i } } };
|
||||
const elem_node = self.alloc.create(Node) catch break;
|
||||
elem_node.* = .{ .span = span, .data = .{ .index_expr = .{ .object = id_node, .index = idx_node } } };
|
||||
const elem_ty = self.inferExprType(elem_node);
|
||||
body_scope.put(cap.name, .{ .ref = Ref.none, .ty = elem_ty, .is_alloca = false, .pack_elem = elem_node });
|
||||
},
|
||||
}
|
||||
self.comptime_constants.put(capture_name, .{ .int_val = i }) catch {};
|
||||
}
|
||||
|
||||
self.lowerBlock(fe.body);
|
||||
|
||||
if (capture_name.len > 0) {
|
||||
if (had_prev) {
|
||||
self.comptime_constants.put(capture_name, prev) catch {};
|
||||
for (saves.items) |save| {
|
||||
if (save.had_prev) {
|
||||
self.comptime_constants.put(save.name, save.prev) catch {};
|
||||
} else {
|
||||
_ = self.comptime_constants.remove(capture_name);
|
||||
_ = self.comptime_constants.remove(save.name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -407,6 +407,22 @@ pub fn resolveFieldType(self: *Lowering, ty: TypeId, field: []const u8) TypeId {
|
||||
}
|
||||
|
||||
pub fn lowerFieldAccess(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) Ref {
|
||||
// `inline for xs (x)` element capture as the receiver: re-enter with the
|
||||
// synthesized `xs[<i>]` as the object, so every pack-element rule below
|
||||
// (interface-only constraint check, projection, substitution) sees the
|
||||
// canonical `xs[i].<field>` shape.
|
||||
if (fa.object.data == .identifier) {
|
||||
if (self.scope) |scope| {
|
||||
if (scope.lookup(fa.object.data.identifier.name)) |binding| {
|
||||
if (binding.pack_elem) |elem| {
|
||||
var patched = fa.*;
|
||||
patched.object = elem;
|
||||
return self.lowerFieldAccess(&patched, span);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `error.X` — an error-tag literal. The `error` keyword in expression
|
||||
// position parses as identifier "error" (E0.2), so `error.X` is a
|
||||
// field access we intercept here. `error` is reserved, so this is
|
||||
@@ -1602,6 +1618,9 @@ pub fn lowerExpr(self: *Lowering, node: *const Node) Ref {
|
||||
}
|
||||
if (self.scope) |scope| {
|
||||
if (scope.lookup(id.name)) |binding| {
|
||||
// `inline for xs (x)` element capture — lower the
|
||||
// synthesized `xs[<i>]` it aliases.
|
||||
if (binding.pack_elem) |elem| break :blk self.lowerExpr(elem);
|
||||
if (binding.is_alloca) {
|
||||
break :blk self.builder.load(binding.ref, binding.ty);
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ pub fn diagPackAsValue(self: *Lowering, name: []const u8, span: ast.Span, kind:
|
||||
.storage => d.addHelpFmt(id, span, null, "to store it, materialize a tuple: `(..{s})`", .{name}),
|
||||
.call_arg => d.addHelpFmt(id, span, null, "to pass it to a `[]Any`/`[]P` parameter, materialize it with `xx {s}`", .{name}),
|
||||
.return_value => d.addHelpFmt(id, span, null, "to return it, return a tuple `(..{s})` and make the return type that tuple", .{name}),
|
||||
.runtime_iter => d.addHelpFmt(id, span, null, "to iterate at comptime use `inline for 0..{s}.len (i)`; for a runtime loop declare it as `..{s}: []P` (a protocol slice) instead of a pack", .{ name, name }),
|
||||
.runtime_iter => d.addHelpFmt(id, span, null, "to iterate at comptime use `inline for {s} (x)` (or `inline for 0..{s}.len (i)` for the index); for a runtime loop declare it as `..{s}: []P` (a protocol slice) instead of a pack", .{ name, name, name }),
|
||||
.generic => d.addHelpFmt(id, span, null, "materialize a tuple `(..{s})` to store it, or `xx {s}` to convert it to an expected `[]Any`/`[]P` slice", .{ name, name }),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user