refactor: canonical failable syntax (T, !) — remove the bare -> T ! sugar
The trailing-`!`-after-the-value-type spelling (`-> T !`, `-> Tuple(A,B) !`) was a
redundant second way to write a failable return that the parser folded into the
same AST as the parenthesized `(T, !)` / `(A, B, !)` result list. Remove it so
there is ONE canonical spelling: the error channel always rides as the last slot
of the parenthesized list.
- parser: `parseFnReturnType` no longer folds a trailing `!` after a value type —
it rejects it with a located diagnostic ("a failable return is written `(T, !)`
… not `T !`"). This one chokepoint covers fn declarations, lambdas, fn-pointer
types `(A) -> R`, and closure types `Closure(A) -> R`. The error-ONLY `-> !` /
`-> !ErrSet` form is unaffected (parsed by parseTypeExpr as an error_type_expr).
- migrated every usage to canonical form across library/ + examples/ + issues/ +
tests/: `-> T !E` → `-> (T, !E)`; the value-carrying `-> Tuple(A, B) !` (which
FLATTENED to a multi-value failable) → `-> (A, B, !)`, preserving behavior. A
genuine single-tuple-value failable stays `-> (Tuple(A,B), !)`.
- parser unit tests: the "bare form folds" tests become "bare form is rejected";
canonical-form parse tests retained.
- docs: specs.md §12 + scattered refs and readme.md updated to the `(T, !)` form.
Behavior-preserving (the bare form was sugar for the same AST). Adversarial review
confirmed: rejection complete across all positions, every canonical form works on
both success/error paths, error-only `-> !` intact, no crashes. Full suite green
(unit tests + 850 corpus examples).
This commit is contained in:
@@ -876,7 +876,7 @@ pub const TupleTypeExpr = struct {
|
||||
/// rejects it anywhere else), and its result is consumed only by destructuring
|
||||
/// (`a, b := f()`), never bound to a single value. Same shape as a tuple type so
|
||||
/// the resolver can reuse the field-resolution path. The single-value `(T, !)`
|
||||
/// (one value + error) is NOT this — it is a plain failable, `-> T !`.
|
||||
/// (one value + error) is NOT this — it is a plain failable.
|
||||
pub const ReturnTypeExpr = struct {
|
||||
field_types: []const *Node,
|
||||
field_names: ?[]const []const u8, // null for positional
|
||||
|
||||
@@ -341,36 +341,21 @@ test "parser: .(x) is a 1-tuple, .() is empty" {
|
||||
try std.testing.expectEqual(@as(usize, 0), v2.data.tuple_literal.elements.len);
|
||||
}
|
||||
|
||||
// `-> T !` folds to the same `(T, !)` representation: tuple_type_expr whose
|
||||
// last field is an error_type_expr.
|
||||
test "parser: -> T ! folds to (T, !) tuple_type_expr" {
|
||||
// The legacy bare trailing-`!` spelling `-> T !` was removed — the canonical
|
||||
// failable result list is `-> (T, !)`. The bare form is now a parse error.
|
||||
test "parser: legacy bare `-> T !` is rejected" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () -> i64 ! { 0 }");
|
||||
const root = try parser.parse();
|
||||
const rt = root.data.root.decls[0].data.fn_decl.return_type.?;
|
||||
try std.testing.expect(rt.data == .tuple_type_expr);
|
||||
const fields = rt.data.tuple_type_expr.field_types;
|
||||
try std.testing.expectEqual(@as(usize, 2), fields.len);
|
||||
try std.testing.expect(fields[0].data == .type_expr);
|
||||
try std.testing.expect(fields[1].data == .error_type_expr);
|
||||
try std.testing.expect(fields[1].data.error_type_expr.name == null);
|
||||
try std.testing.expectError(error.ParseError, parser.parse());
|
||||
}
|
||||
|
||||
// `-> Tuple(T1, T2) !` flattens to (T1, T2, !).
|
||||
test "parser: -> Tuple(A, B) ! flattens to (A, B, !)" {
|
||||
// Likewise the legacy `-> Tuple(A, B) !` spelling — write `-> (A, B, !)`.
|
||||
test "parser: legacy bare `-> Tuple(A, B) !` is rejected" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () -> Tuple(i64, i32) !ParseErr { 0 }");
|
||||
const root = try parser.parse();
|
||||
const rt = root.data.root.decls[0].data.fn_decl.return_type.?;
|
||||
try std.testing.expect(rt.data == .tuple_type_expr);
|
||||
const fields = rt.data.tuple_type_expr.field_types;
|
||||
try std.testing.expectEqual(@as(usize, 3), fields.len);
|
||||
try std.testing.expect(fields[0].data == .type_expr);
|
||||
try std.testing.expect(fields[1].data == .type_expr);
|
||||
try std.testing.expect(fields[2].data == .error_type_expr);
|
||||
try std.testing.expectEqualStrings("ParseErr", fields[2].data.error_type_expr.name.?);
|
||||
try std.testing.expectError(error.ParseError, parser.parse());
|
||||
}
|
||||
|
||||
// `-> !` (void + error) stays a bare error_type_expr — the trailing-`!` fold
|
||||
|
||||
114
src/parser.zig
114
src/parser.zig
@@ -431,66 +431,27 @@ pub const Parser = struct {
|
||||
return self.fail("expected ':', '=', ';', or 'extern' after type annotation");
|
||||
}
|
||||
|
||||
/// Parse a function/method/lambda return type, folding a trailing `!`
|
||||
/// error channel that sits OUTSIDE the value type into the same
|
||||
/// representation the inline `(T, !)` form produces.
|
||||
/// Parse a function/method/lambda/closure/fn-pointer return type.
|
||||
///
|
||||
/// `-> T !` ⇒ tuple_type_expr { [T, error_type_expr] } (== `(T, !)`)
|
||||
/// `-> Tuple(A, B) !` ⇒ tuple_type_expr { [A, B, error_type_expr] } (== `(A, B, !)`)
|
||||
/// `-> !` ⇒ error_type_expr (bare; handled by parseTypeExpr directly)
|
||||
/// The canonical failable / multi-return spelling wraps the result list in
|
||||
/// parens: `-> (T, !)`, `-> (A, B, !)`, `-> (x: A, y: B, !)` — the error
|
||||
/// channel is the last slot. A bare `-> !` (error-only, no value) is parsed
|
||||
/// by `parseTypeExpr` as an `error_type_expr` and is unaffected.
|
||||
///
|
||||
/// The old inline `(T, !)` / `(T1, T2, !)` forms keep working unchanged —
|
||||
/// this only ADDS the trailing-`!`-after-the-type spelling.
|
||||
/// The legacy trailing-`!`-after-the-value-type spelling (`-> T !`,
|
||||
/// `-> Tuple(A, B) !`) is REJECTED — it was a redundant second spelling of
|
||||
/// `(T, !)` / `(A, B, !)` and is no longer accepted.
|
||||
fn parseFnReturnType(self: *Parser) anyerror!*Node {
|
||||
const start = self.current.loc.start;
|
||||
const ty = try self.parseTypeExpr();
|
||||
|
||||
// A trailing `!` (optionally `!Named`) after the return TYPE denotes the
|
||||
// error channel sitting OUTSIDE the value type. A bare `-> !` is already
|
||||
// an error_type_expr (no value), so a `!` after one would be a doubled
|
||||
// error channel — leave it for the normal "unexpected token" path.
|
||||
if (self.current.tag != .bang or ty.data == .error_type_expr) return ty;
|
||||
|
||||
self.advance(); // skip '!'
|
||||
var set_name: ?[]const u8 = null;
|
||||
if (self.current.tag == .identifier) {
|
||||
set_name = self.tokenSlice(self.current);
|
||||
self.advance();
|
||||
// A trailing `!` after a VALUE return type is the removed legacy
|
||||
// spelling. (`-> !` already parsed to an error_type_expr above, so a
|
||||
// `!` after one would be a doubled channel — leave that to the normal
|
||||
// "unexpected token" path.)
|
||||
if (self.current.tag == .bang and ty.data != .error_type_expr) {
|
||||
return self.fail("a failable return is written `(T, !)` — or `(A, B, !)` for multiple values — not `T !`");
|
||||
}
|
||||
const err_node = try self.createNode(start, .{ .error_type_expr = .{ .name = set_name } });
|
||||
|
||||
// Build the value+error result list. If the value type is itself a
|
||||
// tuple — `Tuple(A, B)` (positional) or `Tuple(x: A, y: B)` (named) —
|
||||
// flatten its fields and append the error channel, so `-> Tuple(A,B) !`
|
||||
// is identical to `(A, B, !)` and `-> Tuple(x: A, y: B) !` keeps the
|
||||
// `x`/`y` names on the flattened value fields. The value-return lowering
|
||||
// inserts the value slots FLAT into the result tuple, so the result type
|
||||
// must list the value fields flat too — wrapping a named tuple as
|
||||
// `{ {A,B}, err }` would miscompile the flat 2-tuple insert. Otherwise
|
||||
// wrap the single value as `(T, !)`.
|
||||
var fields = std.ArrayList(*Node).empty;
|
||||
var names = std.ArrayList([]const u8).empty;
|
||||
var has_names = false;
|
||||
if (ty.data == .tuple_type_expr) {
|
||||
const tt = ty.data.tuple_type_expr;
|
||||
for (tt.field_types) |f| try fields.append(self.allocator, f);
|
||||
if (tt.field_names) |fn_names| {
|
||||
has_names = true;
|
||||
for (fn_names) |nm| try names.append(self.allocator, nm);
|
||||
// The trailing error channel needs a placeholder name so the
|
||||
// names slice stays 1:1 with field_types. It is identified by
|
||||
// position (last field), never by this name (see
|
||||
// lower/error.zig errorChannelOf).
|
||||
try names.append(self.allocator, "!");
|
||||
}
|
||||
} else {
|
||||
try fields.append(self.allocator, ty);
|
||||
}
|
||||
try fields.append(self.allocator, err_node);
|
||||
return try self.createNode(start, .{ .tuple_type_expr = .{
|
||||
.field_types = try fields.toOwnedSlice(self.allocator),
|
||||
.field_names = if (has_names) try names.toOwnedSlice(self.allocator) else null,
|
||||
} });
|
||||
return ty;
|
||||
}
|
||||
|
||||
fn parseTypeExpr(self: *Parser) anyerror!*Node {
|
||||
@@ -675,10 +636,9 @@ pub const Parser = struct {
|
||||
}
|
||||
try self.expect(.r_paren);
|
||||
if (self.current.tag == .arrow) {
|
||||
// '->' present: function type. Accept a trailing `!`/`!Named`
|
||||
// error channel after the return type (`(i64) -> i64 !E`), folded
|
||||
// to the SAME `(T, !)` / `(A, B, !)` representation the inline form
|
||||
// produces — the old `-> (T, !)` spelling keeps working too.
|
||||
// '->' present: function type. A failable return is the canonical
|
||||
// parenthesized list `(i64) -> (i64, !E)` (parseFnReturnType
|
||||
// rejects the bare `-> i64 !E` spelling).
|
||||
self.advance(); // skip '->'
|
||||
const return_type = try self.parseFnReturnType();
|
||||
const abi = try self.parseOptionalAbi();
|
||||
@@ -848,10 +808,9 @@ pub const Parser = struct {
|
||||
var return_type: ?*Node = null;
|
||||
if (self.current.tag == .arrow) {
|
||||
self.advance();
|
||||
// Accept a trailing `!`/`!Named` error channel after the
|
||||
// closure return type (`Closure(i64) -> i64 !E`, `... -> T !`),
|
||||
// folded to the same `(T, !)` / `(A, B, !)` representation; the
|
||||
// old `-> (T, !)` form keeps working.
|
||||
// A failable closure return is the canonical parenthesized
|
||||
// list `Closure(i64) -> (i64, !E)` (parseFnReturnType rejects
|
||||
// the bare `Closure(i64) -> i64 !E` spelling).
|
||||
return_type = try self.parseFnReturnType();
|
||||
}
|
||||
return try self.createNode(start, .{ .closure_type_expr = .{
|
||||
@@ -5020,8 +4979,8 @@ test "parse bare failable return: named `!Foo`" {
|
||||
try std.testing.expectEqualStrings("ParseErr", rt.data.error_type_expr.name.?);
|
||||
}
|
||||
|
||||
test "parse failable with inferred `!` (new `-> T !` form)" {
|
||||
const source = "f :: () -> i32 ! { 0; }";
|
||||
test "parse single-value failable `-> (T, !)`" {
|
||||
const source = "f :: () -> (i32, !) { 0; }";
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), source);
|
||||
@@ -5036,20 +4995,28 @@ test "parse failable with inferred `!` (new `-> T !` form)" {
|
||||
try std.testing.expect(fields[1].data.error_type_expr.name == null);
|
||||
}
|
||||
|
||||
test "parse failable with named `!Foo` (new `-> Tuple(...) !` form)" {
|
||||
const source = "f :: () -> Tuple(i32, i64) !ParseErr { 0; }";
|
||||
test "parse multi-value named failable `-> (A, B, !Foo)`" {
|
||||
const source = "f :: () -> (i32, i64, !ParseErr) { 0; }";
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), source);
|
||||
const root = try parser.parse();
|
||||
const rt = root.data.root.decls[0].data.fn_decl.return_type.?;
|
||||
try std.testing.expect(rt.data == .tuple_type_expr);
|
||||
const fields = rt.data.tuple_type_expr.field_types;
|
||||
try std.testing.expect(rt.data == .return_type_expr or rt.data == .tuple_type_expr);
|
||||
const fields = if (rt.data == .return_type_expr) rt.data.return_type_expr.field_types else rt.data.tuple_type_expr.field_types;
|
||||
try std.testing.expectEqual(@as(usize, 3), fields.len);
|
||||
try std.testing.expect(fields[2].data == .error_type_expr);
|
||||
try std.testing.expectEqualStrings("ParseErr", fields[2].data.error_type_expr.name.?);
|
||||
}
|
||||
|
||||
test "parse legacy bare failable `-> T !` is rejected" {
|
||||
const source = "f :: () -> i32 ! { 0; }";
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), source);
|
||||
try std.testing.expectError(error.ParseError, parser.parse());
|
||||
}
|
||||
|
||||
test "parse old bare-paren failable `-> (!, i32)` is rejected" {
|
||||
const source = "f :: () -> (!, i32) { 0; }";
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
@@ -5077,11 +5044,10 @@ test "round-trip print: error-set decl" {
|
||||
try std.testing.expectEqualStrings(source, aw.writer.toArrayList().items);
|
||||
}
|
||||
|
||||
test "print: failable result list with pointer + named error folds to tuple repr" {
|
||||
// New `-> T !` form: a single value + named error channel folds to the SAME
|
||||
// internal `tuple_type_expr` the old `(*Handle, !IoErr)` spelling produced,
|
||||
// so printType still renders the canonical tuple representation.
|
||||
const source = "open :: () -> *Handle !IoErr { 0; }";
|
||||
test "print: failable result list with pointer + named error renders canonically" {
|
||||
// A single value + named error channel `(*Handle, !IoErr)` renders back as
|
||||
// the canonical parenthesized result list.
|
||||
const source = "open :: () -> (*Handle, !IoErr) { 0; }";
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), source);
|
||||
@@ -5431,7 +5397,7 @@ test "E0.3 or value-terminator: parse(s) or 0" {
|
||||
|
||||
test "E0.3 full failable function parses end-to-end (all E0 forms)" {
|
||||
const source =
|
||||
\\parse :: (s: string) -> i32 !ParseErr {
|
||||
\\parse :: (s: string) -> (i32, !ParseErr) {
|
||||
\\ onfail (e) { cleanup(s); }
|
||||
\\ v := try inner(s) or 0;
|
||||
\\ w := other(s) catch (e2) { return 0; };
|
||||
|
||||
Reference in New Issue
Block a user