favor return over return _ (void return); newline/closing terminates

This commit is contained in:
2026-07-14 19:07:37 +02:00
parent eac5b32738
commit 471896b48a
19 changed files with 92266 additions and 94923 deletions
+2 -2
View File
@@ -89,9 +89,9 @@ fields. `_` is not a keyword member name.
- boolean `if` / `else if` / `else`, braceless single-statement branches, and optional parenthesized conditions
- `while` loops with optional post-iteration update clauses
- `for` loops over ranges, arrays, slices, and pointers-to-arrays with copy captures, pointer captures `|@item|`, and optional `usize` index captures
- `break`, `continue`, labeled `break :label`, labeled `continue :label`, and labeled plain blocks
- `break`, `continue`, labeled `break :label`, labeled `continue :label`, and labeled plain blocks; `break :label` can cross nested scopes to exit a labeled block
- bare block scopes, `defer`, and fallible-function `errdefer` with optional error capture; cleanup is block-scoped and LIFO
- value blocks, value `if`, value loops, value `match`, `yield`, and labeled `yield :label value`
- bare void `return`, same-line `return value`, value blocks, value `if`, value loops, value `match`, and strictly value-producing `yield value` / `yield :label value`
- `match` statements/expressions over enums, tagged unions, and scalars, including exhaustiveness checks, payload captures, pointer payload captures, multi-pattern arms, and scalar range patterns
- fallible `try`, fallback `catch`, and `catch |e| { ... }` handler blocks
- direct `return match ...` and `yield match ...` value-control-flow operands
+4 -2
View File
@@ -733,8 +733,8 @@
(needs string building), struct field defaults to drop `&[]` on empty lists
29. fix bugs (implemented)
- `return _` was already the supported empty return for void functions; the original
bare-`return` report was stale
- bare `return` is the empty return for void functions; `yield` always requires a
same-line, non-void value because `break` handles valueless scope exits
- catch value blocks may end by returning from the function instead of yielding when
every path exits
- implicit-conversion diagnostics render source-level composite and named types instead
@@ -834,6 +834,8 @@
- add `debug.print` function making use of `std/io` to print values to the console
- this may either require native variadic arguments or a tuple value to like zig's approach (consider pros and cons)
38. consider renaming `ptr_cast` to `ptrcast`
## A word on unchecked casts
For casts that bypass safety checks, Honey provides builtin functions:
+32 -3
View File
@@ -7516,7 +7516,7 @@ build_block :: proc(
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
})
} else if !types.is_void(ctx.result) {
id := source.add(checker.diagnostics, statement.span, "'return _' is only valid in a void function")
id := source.add(checker.diagnostics, statement.span, "non-void function must return a value")
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
@@ -8118,6 +8118,16 @@ build_block :: proc(
} else {
yielded = build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, target.slot_type, ctx.pkg, ctx.file)
}
if yielded != hir.INVALID_EXPR && types.is_void(checker.module.exprs[yielded].type) {
id := source.add(checker.diagnostics, statement.span, "'yield' expression must produce a non-void value")
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
local = hir.INVALID_LOCAL, diagnostic = id,
})
ctx.problematic^ = true
continue
}
yielded = resolve_loop_slot(ctx, target, yielded, checker.module.exprs[yielded].type if yielded != hir.INVALID_EXPR else types.INVALID, statement.span)
if target.slot == hir.INVALID_LOCAL || yielded == hir.INVALID_EXPR {
id := source.add(checker.diagnostics, statement.span,
@@ -8291,9 +8301,19 @@ build_value_block :: proc(
checker, yield_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
expected, ctx.pkg, ctx.file,
)
value_type = checker.module.exprs[value].type
}
value_type = checker.module.exprs[value].type
if is_runtime_type(checker, expected) {
if types.is_void(value_type) {
id := source.add(checker.diagnostics, yield_stmt.span, "'yield' expression must produce a non-void value")
value = invalid_hir_expr(checker, yield_stmt.span, id)
value_type = types.INVALID
ctx.problematic^ = true
} else if types.is_void(expected) {
id := source.add(checker.diagnostics, yield_stmt.span, "void value context must fall through instead of yielding")
value = invalid_hir_expr(checker, yield_stmt.span, id)
value_type = types.INVALID
ctx.problematic^ = true
} else if is_runtime_type(checker, expected) {
value = coerce_expr(checker, value, expected, yield_stmt.span)
value_type = checker.module.exprs[value].type
}
@@ -9521,6 +9541,15 @@ build_value_loop :: proc(
// The fall-through value initializes the slot before the loop (loop captures are
// out of scope here), so the loop completing leaves it as the result.
fall_value := build_expr(checker, fall_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, target.slot_type, ctx.pkg, ctx.file)
if fall_value != hir.INVALID_EXPR && types.is_void(checker.module.exprs[fall_value].type) {
for s in loop_block {
append(body, s)
}
delete(loop_block, checker.allocator)
id := source.add(checker.diagnostics, fall_stmt.span, "'yield' expression must produce a non-void value")
ctx.problematic^ = true
return invalid_hir_expr(checker, fall_stmt.span, id), types.INVALID
}
fall_value = resolve_loop_slot(ctx, &target, fall_value, checker.module.exprs[fall_value].type if fall_value != hir.INVALID_EXPR else types.INVALID, fall_stmt.span)
if target.slot == hir.INVALID_LOCAL || fall_value == hir.INVALID_EXPR {
for s in loop_block {
+8 -1
View File
@@ -2463,12 +2463,19 @@ ct_exec_statements :: proc(
ok = ct_fail(state, .Not_Comptime, statement.span, "'yield' is only valid in a comptime value block")
} else if statement.value_control_flow {
flow, ok = ct_exec_statements(state, statement.body, true, depth+1)
if ok && flow.kind == .Yield && types.is_void(state.values[flow.value].type) {
ok = ct_fail(state, .Not_Comptime, statement.span, "'yield' expression must produce a non-void value")
}
} else {
value, expr_flow, expr_ok := ct_eval_expr(state, statement.expr, types.INVALID, depth+1)
ok = expr_ok
flow = expr_flow
if ok && flow.kind == .Normal {
flow = ct_flow(.Yield, value, statement.label)
if types.is_void(state.values[value].type) {
ok = ct_fail(state, .Not_Comptime, statement.span, "'yield' expression must produce a non-void value")
} else {
flow = ct_flow(.Yield, value, statement.label)
}
}
}
case .If:
+17 -10
View File
@@ -1282,14 +1282,11 @@ finish_statement :: proc(parser: ^Parser, allow_closing_brace := false) -> sourc
parse_return :: proc(parser: ^Parser) -> ast.Stmt_Id {
start := advance(parser)
skip_newlines(parser)
if current(parser).kind == .Underscore {
end := advance(parser)
if current(parser).kind == .Newline || current(parser).kind == .Right_Brace || current(parser).kind == .Eof {
id := ast.stmt_id(len(parser.module.statements))
append(&parser.module.statements, ast.Stmt{
kind=.Return,
span=span_from(start.span, end.span),
name=end.symbol,
span=start.span,
expr=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
@@ -1321,12 +1318,10 @@ parse_return :: proc(parser: ^Parser) -> ast.Stmt_Id {
return id
}
// `yield <expr>` supplies the value of the enclosing value block. The checker
// only accepts it as the final statement of a value block (a `{ ... }` on the
// right of a declaration/assignment); it is the block analogue of `return`.
// `yield <expr>` supplies a non-void value to an enclosing value construct.
// The expression must start on the same line; `break` handles valueless exits.
parse_yield :: proc(parser: ^Parser) -> ast.Stmt_Id {
start := advance(parser) // consume 'yield'
skip_newlines(parser)
// `yield :blk x` targets the loop labeled `blk`; a bare `yield x` targets
// the directly-enclosing value block / if branch. No expression starts with
// ':', so a leading colon is unambiguously a label.
@@ -1335,9 +1330,21 @@ parse_yield :: proc(parser: ^Parser) -> ast.Stmt_Id {
if name, name_ok := allow(parser, .Identifier); name_ok {
label = name.symbol
} else {
source.add(parser.diagnostics, current(parser).span, "expected a loop label after ':'")
source.add(parser.diagnostics, current(parser).span, "expected a yield target label after ':'")
}
}
if current(parser).kind == .Newline || current(parser).kind == .Right_Brace || current(parser).kind == .Eof {
expr := invalid_expr(parser, start.span, "'yield' must produce a value")
id := ast.stmt_id(len(parser.module.statements))
append(&parser.module.statements, ast.Stmt{
kind=.Yield,
span=start.span,
label=label,
expr=expr,
diagnostic=source.INVALID_DIAGNOSTIC,
})
return id
}
if cf, is_cf := parse_value_control_flow(parser); is_cf {
cf_span := parser.module.statements[cf].span
body := make([]ast.Stmt_Id, 1, parser.module.allocator)
+91 -1
View File
@@ -2289,7 +2289,7 @@ return_sink_and_unconsumed_values_have_distinct_hir :: proc(t: ^testing.T) {
return 1
}
done func() void {
return _
return
}
main func() void {
done()
@@ -2327,6 +2327,96 @@ main func() void {
testing.expect_value(t, hir_module.statements[main.body[2]].kind, hir.Stmt_Kind.Trap)
}
@(test)
bare_returns_and_strict_yields :: proc(t: ^testing.T) {
text := `Failure :: enum { bad }
noop func() void {}
newline_return func() void {
return
}
inline_return func() void { return }
fallible_return func() void ! Failure { return }
split_return func() i32 {
return
1
}
old_return func() void { return _ }
missing_yield func() i32 {
value :: {
yield
1
}
return value
}
missing_labeled_yield func() i32 {
value :: block: {
yield :block
}
return value
}
void_yield func() i32 {
value :: { yield noop() }
return value
}
sink_yield func() i32 {
value :: { yield _ }
return value
}
void_context func() void {
fallible_return() catch |_| { yield 1 }
}
bad_comptime :: ${ yield noop() }
main func() void {
newline_return()
inline_return()
fallible_return() catch |_| {}
_ = split_return()
old_return()
_ = missing_yield()
_ = missing_labeled_yield()
_ = void_yield()
_ = sink_yield()
void_context()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, ast_module.statements[ast_module.functions[1].body[0]].expr, ast.INVALID_EXPR)
testing.expect_value(t, ast_module.statements[ast_module.functions[2].body[0]].expr, ast.INVALID_EXPR)
testing.expect_value(t, ast_module.statements[ast_module.functions[3].body[0]].expr, ast.INVALID_EXPR)
testing.expect_value(t, len(ast_module.functions[4].body), 2)
missing_value := false
non_void_function := false
void_yields := 0
sink_read := false
void_context := false
for diagnostic in diagnostics.items {
missing_value = missing_value || strings.contains(diagnostic.message, "'yield' must produce a value")
non_void_function = non_void_function || strings.contains(diagnostic.message, "non-void function must return a value")
if strings.contains(diagnostic.message, "'yield' expression must produce a non-void value") {
void_yields += 1
}
sink_read = sink_read || strings.contains(diagnostic.message, "'_' is a write-only sink and cannot be read")
void_context = void_context || strings.contains(diagnostic.message, "void value context must fall through instead of yielding")
}
testing.expect(t, missing_value)
testing.expect(t, non_void_function)
testing.expect(t, void_yields >= 2)
testing.expect(t, sink_read)
testing.expect(t, void_context)
}
@(test)
unused_locals_and_params_warn_without_traps :: proc(t: ^testing.T) {
text := `warn_only func(value i32, unused i32) i32 {
-3
View File
@@ -23,8 +23,6 @@ _fail_allocator mem.Allocator :: mem.Allocator {
vtable = &_fail_vtable,
}
_noop func() void {}
run func() i32 ! mem.AllocError {
values std.ArrayList(i32) = arraylist.init(mem.c_allocator)
defer arraylist.deinit(&values)
@@ -65,7 +63,6 @@ run func() i32 ! mem.AllocError {
failed_as_expected bool = false
arraylist.append(&failed, 1) catch |_| {
failed_as_expected = true
yield _noop()
}
if (failed_as_expected == false or failed.items.len != 0 or failed.capacity != 0) return 9
arraylist.deinit(&failed)
+14
View File
@@ -266,6 +266,19 @@ stmt_block_escape func() i32 {
return hits # 4 + 1000 (defer) = 1004
}
# A labeled block can also be exited through an ordinary nested block.
stmt_block_nested func() i32 {
hits i32 = 0
outer: {
{
hits = 1
break :outer
}
hits = 100 # skipped by break :outer
}
return hits
}
# Item B: a `none` yielded before a concrete `yield :blk` that references a block local.
lblock_local func() i32 {
r :: blk: {
@@ -319,6 +332,7 @@ main func() i32 {
if (stmt_block(0) != 2) return 132
if (stmt_block_escape() != 1004) return 133
if (lblock_local() != 9) return 134
if (stmt_block_nested() != 1) return 135
return 42
}
+3 -3
View File
@@ -25,7 +25,7 @@ deinit func($T type, list @mut ArrayList(T)) void {
reserve func($T type, list @mut ArrayList(T), minimum_capacity usize) void ! mem.AllocError {
if minimum_capacity <= list.capacity {
return _
return
}
new_capacity usize = 8
@@ -48,7 +48,7 @@ reserve func($T type, list @mut ArrayList(T), minimum_capacity usize) void ! mem
}
list.items = grown.ptr[..length]
list.capacity = new_capacity
return _
return
}
append func($T type, list @mut ArrayList(T), value T) void ! mem.AllocError {
@@ -59,7 +59,7 @@ append func($T type, list @mut ArrayList(T), value T) void ! mem.AllocError {
try reserve(list, length + 1)
list.items = list.items.ptr[..length + 1]
list.items[length] = value
return _
return
}
clear func($T type, list @mut ArrayList(T)) void {
+1 -1
View File
@@ -71,7 +71,7 @@ write_all func(writer Writer, bytes []u8) void ! WriteError {
}
offset += count
}
return _
return
}
_system_read func(_ ?*mut anyopaque, stream ReadStream, buffer []mut u8) usize ! ReadError {
+2 -2
View File
@@ -44,7 +44,7 @@ _append func(tokens @mut std.ArrayList(Token), kind Kind, start, end usize) void
length = end - start,
kind = kind,
})
return _
return
}
lex func(source []u8, tokens @mut std.ArrayList(Token)) void ! mem.AllocError {
@@ -92,7 +92,7 @@ lex func(source []u8, tokens @mut std.ArrayList(Token)) void ! mem.AllocError {
}
}
try _append(tokens, .eof, cursor, cursor)
return _
return
}
_kind_name func(kind Kind) *c_char {
+3 -3
View File
@@ -25,7 +25,7 @@ deinit func($T type, list @mut ArrayList(T)) void {
reserve func($T type, list @mut ArrayList(T), minimum_capacity usize) void ! mem.AllocError {
if minimum_capacity <= list.capacity {
return _
return
}
new_capacity usize = 8
@@ -48,7 +48,7 @@ reserve func($T type, list @mut ArrayList(T), minimum_capacity usize) void ! mem
}
list.items = grown.ptr[..length]
list.capacity = new_capacity
return _
return
}
append func($T type, list @mut ArrayList(T), value T) void ! mem.AllocError {
@@ -59,7 +59,7 @@ append func($T type, list @mut ArrayList(T), value T) void ! mem.AllocError {
try reserve(list, length + 1)
list.items = list.items.ptr[..length + 1]
list.items[length] = value
return _
return
}
clear func($T type, list @mut ArrayList(T)) void {
+1 -1
View File
@@ -71,7 +71,7 @@ write_all func(writer Writer, bytes []u8) void ! WriteError {
}
offset += count
}
return _
return
}
_system_read func(_ ?*mut anyopaque, stream ReadStream, buffer []mut u8) usize ! ReadError {
+1 -1
View File
@@ -6,7 +6,7 @@ main func() void {
data :: mem.alloc(u8, allocator, 24) catch |_| {
_ = c.printf("Failed to allocate memory\n")
return _
return
}
defer mem.free(u8, allocator, data)
+3 -5
View File
@@ -262,15 +262,13 @@ module.exports = grammar({
expression_statement: $ => $.expression,
return_statement: $ => seq(
return_statement: $ => prec.right(seq(
'return',
repeat($._newline),
field('value', $._value),
),
optional(field('value', $._value)),
)),
yield_statement: $ => seq(
'yield',
repeat($._newline),
optional(seq(':', field('label', $.identifier))),
field('value', $._value),
),
+26 -28
View File
@@ -1714,28 +1714,33 @@
"name": "expression"
},
"return_statement": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "return"
},
{
"type": "REPEAT",
"content": {
"type": "SYMBOL",
"name": "_newline"
"type": "PREC_RIGHT",
"value": 0,
"content": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "return"
},
{
"type": "CHOICE",
"members": [
{
"type": "FIELD",
"name": "value",
"content": {
"type": "SYMBOL",
"name": "_value"
}
},
{
"type": "BLANK"
}
]
}
},
{
"type": "FIELD",
"name": "value",
"content": {
"type": "SYMBOL",
"name": "_value"
}
}
]
]
}
},
"yield_statement": {
"type": "SEQ",
@@ -1744,13 +1749,6 @@
"type": "STRING",
"value": "yield"
},
{
"type": "REPEAT",
"content": {
"type": "SYMBOL",
"name": "_newline"
}
},
{
"type": "CHOICE",
"members": [
+1 -1
View File
@@ -1667,7 +1667,7 @@
"fields": {
"value": {
"multiple": false,
"required": true,
"required": false,
"types": [
{
"type": "block",
+92004 -94856
View File
File diff suppressed because it is too large Load Diff
@@ -154,3 +154,56 @@ work func() i32 ! Failure {
(return_statement
(expression
(integer)))))))
==================
Bare return and value yield
==================
done func() void {
return
}
inline func() void { return }
choose func() i32 {
result :: { yield 1 }
return result
}
---
(source_file
(function_declaration
(identifier)
(parameter_list)
(type
(builtin_type))
(block
(statement
(return_statement))))
(function_declaration
(identifier)
(parameter_list)
(type
(builtin_type))
(block
(statement
(return_statement))))
(function_declaration
(identifier)
(parameter_list)
(type
(builtin_type))
(block
(statement
(constant_declaration
(identifier)
(block
(statement
(yield_statement
(expression
(integer)))))))
(statement
(return_statement
(expression
(identifier)))))))