feat(lang): block value requires no trailing ; (Rust-style)
A block's value is now its last statement ONLY when that statement is a trailing expression with no `;`. A trailing `;` discards the value, leaving the block void. This makes value-vs-statement explicit and lets the compiler reject "this block was supposed to produce a value". Compiler: - Parser records `Block.produces_value` (last stmt is a no-`;` trailing expression) + `Block.discarded_semi` (the `;` that discarded a value), via `expectSemicolonAfter`. A trailing expression before `}` may now omit its `;` (previously a parse error). Match-arm and else-arm bodies are built value-producing regardless of the arm `;` (arms are exempt — the `;` is an arm terminator). - Lowering: `lowerBlockValue` / the block-expr path / `inferExprType` respect `produces_value`. A value-position block that discards its value is a hard error (`lowerValueBody` for function bodies; the value-context `.block` path for if/else branches, `catch` bodies, value bindings, match arms). Pure-failable `-> !` bodies (value rides the error channel) and a value-if whose branches are void are handled without false errors. - `defer`/`onfail` cleanup bodies lower as statements (void), so a trailing `;` there is fine. Migration (behavior-preserving — output unchanged): - stdlib + ~210 examples: dropped the trailing `;` on value-position last expressions. `format` now ends with an explicit `#insert "return result;"` (it relied on `#insert`-as-block-value, which `;` discards). - Two `main :: () -> s32` examples that relied on the old silent default-return got an explicit trailing `0`. - Rejection snapshots 0412 / 1013 regenerated (their quoted source lines lost a `;`); the diagnostics themselves are unchanged. Docs/tests: specs.md "Block values" section; examples 0040 (rules) + 0041 (rejection); 3 parser unit tests. Filed issue 0066 (pre-existing match-arm negated-literal phi-width quirk, surfaced not caused here). Gates: zig build, zig build test, run_examples.sh -> 343 passed, cross_compile.sh -> 7 passed (also refreshed its stale example names).
This commit is contained in:
115
src/parser.zig
115
src/parser.zig
@@ -37,6 +37,18 @@ pub const Parser = struct {
|
||||
/// already exiting, so there is nothing to propagate to. E1.7 extends this
|
||||
/// to the full {try, return, break, continue} set.
|
||||
in_defer_body: bool = false,
|
||||
/// Set by `expectSemicolonAfter` for the statement just parsed: true when the
|
||||
/// statement is a trailing value (an expression / block-form with NO `;`),
|
||||
/// false when a `;` terminated it (value discarded). `parseBlock` reads it
|
||||
/// after the last statement to set `Block.produces_value`. Reset at the top
|
||||
/// of `parseStmt` so non-expression statements (decls, return, …) leave it
|
||||
/// false.
|
||||
last_stmt_produces_value: bool = false,
|
||||
/// Span of the `;` that discarded the just-parsed statement's value, when
|
||||
/// that statement was an expression terminated by `;` (so the value could
|
||||
/// have been kept by dropping it). Null when the statement kept its value or
|
||||
/// wasn't a value expression. Read by `parseBlock` into `Block.discarded_semi`.
|
||||
last_stmt_semi_loc: ?ast.Span = null,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, source: [:0]const u8) Parser {
|
||||
var lexer = Lexer.init(source);
|
||||
@@ -1401,7 +1413,7 @@ pub const Parser = struct {
|
||||
try self.expect(.semicolon);
|
||||
const stmts = try self.allocator.alloc(*Node, 1);
|
||||
stmts[0] = expr_node;
|
||||
const block_node = try self.createNode(expr_node.span.start, .{ .block = .{ .stmts = stmts } });
|
||||
const block_node = try self.createNode(expr_node.span.start, .{ .block = .{ .stmts = stmts, .produces_value = true } });
|
||||
try members.append(self.allocator, .{ .method = .{
|
||||
.name = member_name,
|
||||
.params = &.{},
|
||||
@@ -1500,7 +1512,7 @@ pub const Parser = struct {
|
||||
try self.expect(.semicolon);
|
||||
const stmts = try self.allocator.alloc(*Node, 1);
|
||||
stmts[0] = expr;
|
||||
body_node = try self.createNode(expr.span.start, .{ .block = .{ .stmts = stmts } });
|
||||
body_node = try self.createNode(expr.span.start, .{ .block = .{ .stmts = stmts, .produces_value = true } });
|
||||
} else {
|
||||
try self.expect(.semicolon);
|
||||
}
|
||||
@@ -1911,7 +1923,7 @@ pub const Parser = struct {
|
||||
const stmts = try self.allocator.alloc(*Node, 1);
|
||||
stmts[0] = expr;
|
||||
const block_start = expr.span.start;
|
||||
const block = try self.createNode(block_start, .{ .block = .{ .stmts = stmts } });
|
||||
const block = try self.createNode(block_start, .{ .block = .{ .stmts = stmts, .produces_value = true } });
|
||||
break :blk block;
|
||||
} else try self.parseBlock();
|
||||
|
||||
@@ -1932,33 +1944,54 @@ pub const Parser = struct {
|
||||
const start = self.current.loc.start;
|
||||
try self.expect(.l_brace);
|
||||
var stmts = std.ArrayList(*Node).empty;
|
||||
var produces_value = false;
|
||||
var discarded_semi: ?ast.Span = null;
|
||||
while (self.current.tag != .r_brace and self.current.tag != .eof) {
|
||||
const stmt = try self.parseStmt();
|
||||
try stmts.append(self.allocator, stmt);
|
||||
// The block's value-ness is its LAST statement's value-ness.
|
||||
produces_value = self.last_stmt_produces_value;
|
||||
// A discarding `;` is only meaningful when the block has no value.
|
||||
discarded_semi = if (produces_value) null else self.last_stmt_semi_loc;
|
||||
}
|
||||
try self.expect(.r_brace);
|
||||
return try self.createNode(start, .{ .block = .{ .stmts = try stmts.toOwnedSlice(self.allocator) } });
|
||||
return try self.createNode(start, .{ .block = .{ .stmts = try stmts.toOwnedSlice(self.allocator), .produces_value = produces_value, .discarded_semi = discarded_semi } });
|
||||
}
|
||||
|
||||
/// Block-form if/match/while/bare blocks don't require trailing semicolon.
|
||||
/// Consume the terminator after an expression/block-form statement and
|
||||
/// record whether the statement is a trailing VALUE (no `;`) or a discarded
|
||||
/// statement (`;`). A trailing `;` always discards; otherwise the statement
|
||||
/// is the (potential) block value — allowed when it is block-form (where
|
||||
/// `;` is optional) or when it is the last thing before `}`.
|
||||
fn expectSemicolonAfter(self: *Parser, expr: *Node) anyerror!void {
|
||||
const needs_semi = switch (expr.data) {
|
||||
.if_expr => |ie| ie.is_inline,
|
||||
.match_expr => false,
|
||||
.while_expr => false,
|
||||
.for_expr => false,
|
||||
.block => false,
|
||||
.jni_env_block => false,
|
||||
else => true,
|
||||
const block_form = switch (expr.data) {
|
||||
.if_expr => |ie| !ie.is_inline,
|
||||
.match_expr, .while_expr, .for_expr, .block, .jni_env_block => true,
|
||||
else => false,
|
||||
};
|
||||
if (needs_semi) {
|
||||
try self.expect(.semicolon);
|
||||
} else if (self.current.tag == .semicolon) {
|
||||
self.advance(); // consume optional ;
|
||||
if (self.current.tag == .semicolon) {
|
||||
self.last_stmt_semi_loc = .{ .start = self.current.loc.start, .end = self.current.loc.end };
|
||||
self.advance(); // explicit terminator → value discarded
|
||||
self.last_stmt_produces_value = false;
|
||||
} else if (block_form or self.current.tag == .r_brace) {
|
||||
// Block-form statements never require `;`; a plain expression may
|
||||
// omit it only as the trailing value before `}`. Either way this
|
||||
// statement is the block's value (and discards nothing — clear any
|
||||
// stale semi location from a nested statement).
|
||||
self.last_stmt_produces_value = true;
|
||||
self.last_stmt_semi_loc = null;
|
||||
} else {
|
||||
try self.expect(.semicolon); // emits "expected ;"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parseStmt(self: *Parser) anyerror!*Node {
|
||||
// Default: a statement discards its value unless `expectSemicolonAfter`
|
||||
// marks it a trailing value (no `;`). Non-expression statements (decls,
|
||||
// return/raise, break/continue, defer/onfail) never set it, so they
|
||||
// correctly leave the enclosing block value-less.
|
||||
self.last_stmt_produces_value = false;
|
||||
self.last_stmt_semi_loc = null;
|
||||
// Check if this is a declaration (IDENT followed by ::, :=, or : type)
|
||||
if (self.isIdentLike()) {
|
||||
const saved_lexer = self.lexer;
|
||||
@@ -3138,7 +3171,10 @@ pub const Parser = struct {
|
||||
self.advance();
|
||||
const expr = try self.parseExpr();
|
||||
try self.expect(.semicolon);
|
||||
const body = try self.createNode(arm_start, .{ .block = .{ .stmts = try self.allocator.dupe(*Node, &.{expr}) } });
|
||||
// Arm bodies are value-producing regardless of the arm `;` (the
|
||||
// `;` is an arm terminator, not a value-discard — match arms are
|
||||
// exempt from the block trailing-`;` rule).
|
||||
const body = try self.createNode(arm_start, .{ .block = .{ .stmts = try self.allocator.dupe(*Node, &.{expr}), .produces_value = true } });
|
||||
try arms.append(self.allocator, .{ .pattern = pattern, .body = body, .is_break = false, .capture = capture });
|
||||
} else {
|
||||
const stmts_start = self.current.loc.start;
|
||||
@@ -3146,7 +3182,10 @@ pub const Parser = struct {
|
||||
while (self.current.tag != .kw_case and self.current.tag != .kw_else and self.current.tag != .r_brace and self.current.tag != .eof) {
|
||||
try stmts.append(self.allocator, try self.parseStmt());
|
||||
}
|
||||
const body = try self.createNode(stmts_start, .{ .block = .{ .stmts = try stmts.toOwnedSlice(self.allocator) } });
|
||||
// Arm exempt from the trailing-`;` rule (see above); the wrapper
|
||||
// yields its last statement's value — which, for a braced-block
|
||||
// arm body, still respects that inner block's own flag.
|
||||
const body = try self.createNode(stmts_start, .{ .block = .{ .stmts = try stmts.toOwnedSlice(self.allocator), .produces_value = true } });
|
||||
try arms.append(self.allocator, .{ .pattern = pattern, .body = body, .is_break = false, .capture = capture });
|
||||
}
|
||||
}
|
||||
@@ -3159,7 +3198,7 @@ pub const Parser = struct {
|
||||
while (self.current.tag != .r_brace and self.current.tag != .eof) {
|
||||
try stmts.append(self.allocator, try self.parseStmt());
|
||||
}
|
||||
const body = try self.createNode(else_start, .{ .block = .{ .stmts = try stmts.toOwnedSlice(self.allocator) } });
|
||||
const body = try self.createNode(else_start, .{ .block = .{ .stmts = try stmts.toOwnedSlice(self.allocator), .produces_value = true } });
|
||||
try arms.append(self.allocator, .{ .pattern = null, .body = body, .is_break = false });
|
||||
}
|
||||
try self.expect(.r_brace);
|
||||
@@ -3692,6 +3731,42 @@ test "parse minimal main" {
|
||||
try std.testing.expectEqual(@as(i64, 42), body.data.block.stmts[0].data.int_literal.value);
|
||||
}
|
||||
|
||||
test "block value: trailing expr without `;` produces a value" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () -> s32 { 42 }");
|
||||
const root = try parser.parse();
|
||||
const body = root.data.root.decls[0].data.fn_decl.body;
|
||||
try std.testing.expect(body.data.block.produces_value);
|
||||
try std.testing.expect(body.data.block.discarded_semi == null);
|
||||
}
|
||||
|
||||
test "block value: trailing `;` discards the value" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () -> s32 { 42; }");
|
||||
const root = try parser.parse();
|
||||
const body = root.data.root.decls[0].data.fn_decl.body;
|
||||
try std.testing.expect(!body.data.block.produces_value);
|
||||
try std.testing.expect(body.data.block.discarded_semi != null);
|
||||
}
|
||||
|
||||
test "block value: match arms are exempt (keep `;`, still produce a value)" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: (n: s32) -> s32 { if n == { case 1: 5; else: 0; } }");
|
||||
const root = try parser.parse();
|
||||
const body = root.data.root.decls[0].data.fn_decl.body;
|
||||
// Function body's trailing match has no `;` → the body is a value.
|
||||
try std.testing.expect(body.data.block.produces_value);
|
||||
const match = body.data.block.stmts[0];
|
||||
try std.testing.expect(match.data == .match_expr);
|
||||
// Each arm body (built with `;`) is still value-producing (exempt).
|
||||
for (match.data.match_expr.arms) |arm| {
|
||||
try std.testing.expect(arm.body.data.block.produces_value);
|
||||
}
|
||||
}
|
||||
|
||||
test "parse #run const binding" {
|
||||
const source = "x :: #run compute(5);";
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
|
||||
Reference in New Issue
Block a user