From 952dc0e161ff84857423d1cb9e1a386d6518826b Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 27 May 2026 21:32:45 +0300 Subject: [PATCH] ffi: drop legacy `name: ..T` variadic syntax MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parser hard-rejects the legacy `name: ..T` form with a one-line migration message pointing at the new `..name: []T` shape. The leading-`..` form is the one the lowering paths (`resolveParamType` / `packVariadicCallArgs`) treat as canonical post-issue-0049; leaving both forms accepted invited the same class of cross-module emit crashes any time a `..T`-form decl in stdlib crossed an import boundary. `specs.md` updated alongside: the Variadic Functions section now documents `..name: []T` as the surface form, with notes on homogeneous vs `[]Any` boxing and the `..` spread at call sites. Inline references to `args: ..Any` in §7 and §8 refreshed. --- specs.md | 24 ++++++++++++++++-------- src/parser.zig | 13 ++++++++----- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/specs.md b/specs.md index 06ae692..1f8274e 100644 --- a/specs.md +++ b/specs.md @@ -951,14 +951,22 @@ sum :: (a: $T, b: T) -> T { - Multiple type parameters are supported: `(a: $T, b: $U) -> T` ### Variadic Functions -Functions can accept a variable number of arguments using `..Type` syntax: +Functions can accept a variable number of arguments using `..name: []Type` syntax: ```sx -print :: (fmt: string, args: ..Any) { ... } +print :: (fmt: string, ..args: []Any) { ... } +path_join :: (..parts: []string) -> string { ... } ``` -- `..Any` means zero or more arguments, each boxed into `Any` (type tag + payload) -- The variadic parameter must be the last parameter -- At call sites, variadic arguments are automatically boxed: `print("x={}, y={}\n", x, y)` -- Inside the function body, `args` is accessed as a slice-like sequence +- The leading `..` marks the parameter as variadic; the declared type is the + slice the body sees (so `..parts: []string` makes `parts` a `[]string` inside). +- The variadic parameter must be the last positional parameter. +- For homogeneous element types (`[]s32`, `[]string`, ...), the call site packs the + trailing args into a stack-allocated `[N x T]` and passes a slice over it. +- For `[]Any`, each trailing arg is boxed into `Any` (type tag + payload) before + packing; `args[i]` reads back the boxed value. +- A `..` spread at the call site unpacks an existing slice/array into the variadic + tail: `sum(..arr)`. +- The heterogeneous comptime-pack form `..$args: []Type` binds per-position + comptime types — see "Variadic heterogeneous type packs" below. ### Type Inference - `::` bindings infer type from the right-hand side @@ -1663,7 +1671,7 @@ Built-in functions are declared in `std.sx` with the `#builtin` suffix, which te ### I/O - `out(str: string) -> void` — write a string to standard output -- `print(fmt: string, args: ..Any)` — formatted print. Parses `{}` placeholders in the format string and substitutes arguments. When all argument types are statically known, the compiler specializes the call at compile time (no `Any` boxing). +- `print(fmt: string, ..args: []Any)` — formatted print. Parses `{}` placeholders in the format string and substitutes arguments. When all argument types are statically known, the compiler specializes the call at compile time (no `Any` boxing). ### Math - `sqrt(x: $T) -> T` — square root (maps to LLVM intrinsic) @@ -1741,7 +1749,7 @@ response :: format("HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}", body.len, // response is a static string constant — no runtime allocation ``` -This works for any function, not just `format`. The mechanism is general: the VM compiles the function body (including `#insert` directives, variadic `..Any` args, and calls to other functions) and executes it entirely at compile time. If the VM encounters something it cannot evaluate (e.g., foreign function calls, unsupported operations), it silently falls through to runtime codegen. +This works for any function, not just `format`. The mechanism is general: the VM compiles the function body (including `#insert` directives, variadic `..args: []Any` args, and calls to other functions) and executes it entirely at compile time. If the VM encounters something it cannot evaluate (e.g., foreign function calls, unsupported operations), it silently falls through to runtime codegen. ### Build Configuration diff --git a/src/parser.zig b/src/parser.zig index cc99132..0f341d4 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -1631,12 +1631,15 @@ pub const Parser = struct { continue; } self.advance(); // consume ':' - // Old syntax keeps the variadic marker after the colon - // (`args: ..T`). Accept it even if a leading `..` already - // appeared — both forms set the same flag. + // The legacy `name: ..T` form is rejected — write `..name: []T`. + // Both forms used to live; the leading-`..` form is the one the + // language settled on (the AST shape downstream assumes the + // declared type IS the slice). Leaving the post-colon form + // accepted lets users land code that bypasses the new-form + // lowering paths (resolveParamType / packVariadicCallArgs) and + // hit cross-module emit crashes (issue-0049). Hard error here. if (self.current.tag == .dot_dot) { - is_variadic = true; - self.advance(); + return self.fail("legacy variadic syntax `name: ..T` is no longer supported — use `..name: []T` instead"); } const param_type = try self.parseTypeExpr(); var is_comptime_param = false;