diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ecfb8f6..c23495b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,11 +30,12 @@ This release brings a lot of useful tools to write buzz code: LSP, formatter and ## Changed -- buzz binary now uses subcommands rather than options - - `buzz ` becomes `buzz run-script ` +- buzz binary now uses subcommands for tools and direct paths for execution + - `buzz ` runs a standalone script - `buzz -t ` becomes `buzz test ` - `buzz -f ` becomes `buzz format ` - - `buzz run` runs `src/main.buzz` from the current package + - `buzz ` runs `src/main.buzz` from a buzz package directory + - `buzz run` and `buzz run-script` were removed - `buzz init` and `buzz fetch` manage package scaffolding and dependencies - `buzz` will start the REPL - Extern libraries now must expose only one function which will be called by the compiler to lookup the functions of the library diff --git a/README.md b/README.md index 190bc269..07e7a53d 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,8 @@ A small/lightweight statically typed scripting language written in Zig 3. Build it `zig build -Doptimize=ReleaseSafe` 4. Have fun (with `BUZZ_PATH=./zig-out`) - `buzz` launches the REPL - - `buzz run-script` to run a lone script + - `buzz ` to run a lone script + - `buzz ` to run a buzz package - `buzz init` to start a buzz package ### Install @@ -62,4 +63,4 @@ sudo zig build -Doptimize=ReleaseSafe install -p /usr/local If you're usage if performance critical (game dev for example), you can build using `-Doptimize=ReleaseFast`. -Remember to modify PATH to include the `bin` directory where it is installed. For example, `export PATH=PATH:/home/xxx/.local/bin`. You can then run buzz with `buzz `. Or you can simply run `buzz` to start the REPL. +Remember to modify PATH to include the `bin` directory where it is installed. For example, `export PATH=PATH:/home/xxx/.local/bin`. You can then run buzz with `buzz ` or `buzz `. Or you can simply run `buzz` to start the REPL. diff --git a/examples/showcase.buzz b/examples/showcase.buzz new file mode 100644 index 00000000..efa97bf0 --- /dev/null +++ b/examples/showcase.buzz @@ -0,0 +1,40 @@ +import "buzz:std"; + +enum State { + todo, + doing, + done, +} + +object Task { + title: str, + points: int?, + state: State = .todo, + + fun score() => match (this.state) { + .done -> this.points ?? 0, + .doing, .todo -> 0 + }; +} + +fun scores(tasks: [Task]) > str *> int? { + foreach (task in tasks) { + _ = yield task.score(); + } + return "scored {tasks.len()} tasks"; +} + +fun main() > void { + final tasks: [Task] = [ + .{ title = "parser", points = 8, state = .done }, + .{ title = "lsp", points = 5, state = .doing }, + .{ title = "docs", points = 3 }, + ]; + + var total = 0; + foreach (score in &scores(tasks)) { + total += score ?? 0; + } + + std\print("completed points: {total}"); +} diff --git a/src/Ast.zig b/src/Ast.zig index 42e8ecb9..6fb7ba76 100644 --- a/src/Ast.zig +++ b/src/Ast.zig @@ -1426,6 +1426,453 @@ pub const Slice = struct { return value.* orelse Value.Void; } + + /// Context used to interpret terminal statements while analyzing a node. + pub const TerminalFlowContext = struct { + /// Loop whose reachable breaks and continues should be reported. + target_loop: ?Node.Index = null, + /// Nearest loop that receives unlabeled break and continue statements. + unlabeled_loop: ?Node.Index = null, + /// Whether an `out` statement exits the currently analyzed block expression. + out_exits: bool = false, + + /// Returns whether a break or continue destination reaches `target_loop`. + fn targetsLoop(self: TerminalFlowContext, destination: ?Node.Index) bool { + const target_loop = self.target_loop orelse return false; + + if (destination) |dest| { + return dest == target_loop; + } + + return self.unlabeled_loop != null and self.unlabeled_loop.? == target_loop; + } + }; + + /// Conservative summary of whether and how execution can leave an AST node. + pub const TerminalFlow = struct { + /// At least one reachable path continues after the analyzed node. + falls_through: bool = true, + /// At least one reachable path exits through a return statement. + returns: bool = false, + /// At least one reachable path exits through a throw statement. + throws: bool = false, + /// At least one reachable path exits the active block expression with `out`. + outs: bool = false, + /// At least one reachable path breaks to the active analysis target. + breaks: bool = false, + /// At least one reachable path continues to the active analysis target. + continues: bool = false, + + /// Returns whether the analyzed node cannot continue to the following statement. + pub fn terminal(self: TerminalFlow) bool { + return !self.falls_through; + } + + /// Merges terminal outcomes from another summary without changing fallthrough. + pub fn merge(self: *TerminalFlow, other: TerminalFlow) void { + self.returns = self.returns or other.returns; + self.throws = self.throws or other.throws; + self.outs = self.outs or other.outs; + self.breaks = self.breaks or other.breaks; + self.continues = self.continues or other.continues; + } + + /// Converts a block-expression-internal summary to the flow seen by its parent expression. + pub fn asExpression(self: TerminalFlow) TerminalFlow { + return .{ + .falls_through = self.falls_through or self.outs, + .returns = self.returns, + .throws = self.throws, + .breaks = self.breaks, + .continues = self.continues, + }; + } + }; + + /// Terminal flow plus the merged type of reachable block-expression `out` values. + pub const BlockExpressionFlow = struct { + terminal: TerminalFlow = .{}, + out_type: ?*obj.ObjTypeDef = null, + + /// Merges an `out` expression type into this summary. + fn mergeOutType(self: *BlockExpressionFlow, gc: *GC, type_def: ?*obj.ObjTypeDef) Error!void { + const incoming = type_def orelse return; + + if (self.out_type) |current| { + if (!current.eql(incoming)) { + self.out_type = gc.type_registry.any_type; + } + } else { + self.out_type = incoming; + } + } + + /// Merges flow outcomes from another summary without changing fallthrough. + fn merge(self: *BlockExpressionFlow, gc: *GC, other: BlockExpressionFlow) Error!void { + self.terminal.merge(other.terminal); + try self.mergeOutType(gc, other.out_type); + } + + /// Propagates labeled loop jumps that escape through a nested loop to an outer target loop. + fn mergeOuterLoopJumps( + self: *BlockExpressionFlow, + ast: Self.Slice, + allocator: std.mem.Allocator, + gc: *GC, + body: Node.Index, + loop_node: Node.Index, + ctx: TerminalFlowContext, + ) Error!void { + if (ctx.target_loop == null or ctx.target_loop.? == loop_node) { + return; + } + + const outer_flow = try ast.blockExpressionFlow( + allocator, + gc, + body, + .{ + .target_loop = ctx.target_loop, + .unlabeled_loop = loop_node, + .out_exits = ctx.out_exits, + }, + ); + self.terminal.breaks = self.terminal.breaks or outer_flow.terminal.breaks; + self.terminal.continues = self.terminal.continues or outer_flow.terminal.continues; + } + }; + + /// Computes terminal flow and reachable `out` value type for a single AST node. + fn blockExpressionFlow( + self: Self.Slice, + allocator: std.mem.Allocator, + gc: *GC, + node: Node.Index, + ctx: TerminalFlowContext, + ) Error!BlockExpressionFlow { + const tags = self.nodes.items(.tag); + const components = self.nodes.items(.components); + + return switch (tags[node]) { + .Return => .{ + .terminal = .{ + .falls_through = false, + .returns = true, + }, + }, + .Throw => .{ + .terminal = .{ + .falls_through = false, + .throws = true, + }, + }, + .Out => if (ctx.out_exits) .{ + .terminal = .{ + .falls_through = false, + .outs = true, + }, + .out_type = self.nodes.items(.type_def)[node], + } else .{}, + .Break => brk: { + break :brk .{ + .terminal = .{ + .falls_through = false, + .breaks = ctx.targetsLoop(components[node].Break.destination), + }, + }; + }, + .Continue => .{ + .terminal = .{ + .falls_through = false, + .continues = ctx.targetsLoop(components[node].Continue.destination), + }, + }, + .Block => try self.blockExpressionFlowStatements( + allocator, + gc, + components[node].Block, + ctx, + ), + .BlockExpression => expr: { + const inner = try self.blockExpressionFlowStatements( + allocator, + gc, + components[node].BlockExpression, + .{ + .target_loop = ctx.target_loop, + .unlabeled_loop = ctx.unlabeled_loop, + .out_exits = true, + }, + ); + + break :expr .{ + .terminal = inner.terminal.asExpression(), + }; + }, + .Expression => try self.blockExpressionFlow( + allocator, + gc, + components[node].Expression, + ctx, + ), + .If => try self.ifTerminalFlow(allocator, gc, node, ctx), + .Match => try self.matchTerminalFlow(allocator, gc, node, ctx), + .While => try self.whileTerminalFlow(allocator, gc, node, ctx), + .For => try self.forTerminalFlow(allocator, gc, node, ctx), + .DoUntil => try self.doUntilTerminalFlow(allocator, gc, node, ctx), + .Try => .{}, + else => .{}, + }; + } + + /// Computes terminal flow and reachable `out` value type for an ordered statement list. + pub fn blockExpressionFlowStatements( + self: Self.Slice, + allocator: std.mem.Allocator, + gc: *GC, + statements: []const Node.Index, + ctx: TerminalFlowContext, + ) Error!BlockExpressionFlow { + var result = BlockExpressionFlow{}; + var falls_through = true; + + for (statements) |statement| { + if (!falls_through) { + break; + } + + const statement_flow = try self.blockExpressionFlow( + allocator, + gc, + statement, + ctx, + ); + try result.merge(gc, statement_flow); + + falls_through = statement_flow.terminal.falls_through; + } + + result.terminal.falls_through = falls_through; + + return result; + } + + /// Returns a boolean value when a node is a compile-time boolean constant. + fn constantBoolean( + self: Self.Slice, + allocator: std.mem.Allocator, + gc: *GC, + node: Node.Index, + ) Error!?bool { + const type_def = self.nodes.items(.type_def)[node] orelse return null; + if (type_def.optional or type_def.def_type != .Boolean) { + return null; + } + + if (!try self.isConstant(allocator, node)) { + return null; + } + + return (try self.toValue(node, gc)).boolean(); + } + + /// Computes flow for an if node, folding constant conditions when possible. + fn ifTerminalFlow( + self: Self.Slice, + allocator: std.mem.Allocator, + gc: *GC, + node: Node.Index, + ctx: TerminalFlowContext, + ) Error!BlockExpressionFlow { + const components = self.nodes.items(.components)[node].If; + + if (components.unwrapped_identifier == null and components.casted_type == null) { + if (try self.constantBoolean(allocator, gc, components.condition)) |condition| { + if (condition) { + return try self.blockExpressionFlow(allocator, gc, components.body, ctx); + } + + if (components.else_branch) |else_branch| { + return try self.blockExpressionFlow(allocator, gc, else_branch, ctx); + } + + return .{}; + } + } + + var result = try self.blockExpressionFlow(allocator, gc, components.body, ctx); + + if (components.else_branch) |else_branch| { + const else_flow = try self.blockExpressionFlow(allocator, gc, else_branch, ctx); + result.terminal.falls_through = result.terminal.falls_through or else_flow.terminal.falls_through; + try result.merge(gc, else_flow); + } else { + result.terminal.falls_through = true; + } + + return result; + } + + /// Computes flow for a match node when all explicit branches can be inspected. + fn matchTerminalFlow( + self: Self.Slice, + allocator: std.mem.Allocator, + gc: *GC, + node: Node.Index, + ctx: TerminalFlowContext, + ) Error!BlockExpressionFlow { + const components = self.nodes.items(.components)[node].Match; + var result = BlockExpressionFlow{ + .terminal = .{ + .falls_through = components.else_branch == null, + }, + }; + + for (components.branches) |branch| { + const branch_flow = try self.blockExpressionFlow( + allocator, + gc, + branch.expression, + ctx, + ); + result.terminal.falls_through = result.terminal.falls_through or branch_flow.terminal.falls_through; + try result.merge(gc, branch_flow); + } + + if (components.else_branch) |else_branch| { + const else_flow = try self.blockExpressionFlow(allocator, gc, else_branch, ctx); + result.terminal.falls_through = result.terminal.falls_through or else_flow.terminal.falls_through; + try result.merge(gc, else_flow); + } + + return result; + } + + /// Computes flow for a while loop, including proven infinite loops without reachable breaks. + fn whileTerminalFlow( + self: Self.Slice, + allocator: std.mem.Allocator, + gc: *GC, + node: Node.Index, + ctx: TerminalFlowContext, + ) Error!BlockExpressionFlow { + const components = self.nodes.items(.components)[node].While; + + if (try self.constantBoolean(allocator, gc, components.condition)) |condition| { + if (!condition) { + return .{}; + } + } else { + return .{}; + } + + const body_flow = try self.blockExpressionFlow( + allocator, + gc, + components.body, + .{ + .target_loop = node, + .unlabeled_loop = node, + .out_exits = ctx.out_exits, + }, + ); + + var result = BlockExpressionFlow{ + .terminal = .{ + .falls_through = body_flow.terminal.breaks, + .returns = body_flow.terminal.returns, + .throws = body_flow.terminal.throws, + .outs = body_flow.terminal.outs, + }, + .out_type = body_flow.out_type, + }; + + try result.mergeOuterLoopJumps(self, allocator, gc, components.body, node, ctx); + + return result; + } + + /// Computes flow for a for loop, including constant-true loops without reachable breaks. + fn forTerminalFlow( + self: Self.Slice, + allocator: std.mem.Allocator, + gc: *GC, + node: Node.Index, + ctx: TerminalFlowContext, + ) Error!BlockExpressionFlow { + const components = self.nodes.items(.components)[node].For; + + if (try self.constantBoolean(allocator, gc, components.condition)) |condition| { + if (!condition) { + return .{}; + } + } else { + return .{}; + } + + const body_flow = try self.blockExpressionFlow( + allocator, + gc, + components.body, + .{ + .target_loop = node, + .unlabeled_loop = node, + .out_exits = ctx.out_exits, + }, + ); + + var result = BlockExpressionFlow{ + .terminal = .{ + .falls_through = body_flow.terminal.breaks, + .returns = body_flow.terminal.returns, + .throws = body_flow.terminal.throws, + .outs = body_flow.terminal.outs, + }, + .out_type = body_flow.out_type, + }; + + try result.mergeOuterLoopJumps(self, allocator, gc, components.body, node, ctx); + + return result; + } + + /// Computes flow for a do-until loop, accounting for the body running before the condition. + fn doUntilTerminalFlow( + self: Self.Slice, + allocator: std.mem.Allocator, + gc: *GC, + node: Node.Index, + ctx: TerminalFlowContext, + ) Error!BlockExpressionFlow { + const components = self.nodes.items(.components)[node].DoUntil; + const body_flow = try self.blockExpressionFlow( + allocator, + gc, + components.body, + .{ + .target_loop = node, + .unlabeled_loop = node, + .out_exits = ctx.out_exits, + }, + ); + const condition = try self.constantBoolean(allocator, gc, components.condition); + const condition_allows_exit = condition == null or condition.?; + + var result = BlockExpressionFlow{ + .terminal = .{ + .falls_through = body_flow.terminal.breaks or + (body_flow.terminal.falls_through and condition_allows_exit), + .returns = body_flow.terminal.returns, + .throws = body_flow.terminal.throws, + .outs = body_flow.terminal.outs, + }, + .out_type = body_flow.out_type, + }; + + try result.mergeOuterLoopJumps(self, allocator, gc, components.body, node, ctx); + + return result; + } }; pub fn init(allocator: std.mem.Allocator) Self { diff --git a/src/Codegen.zig b/src/Codegen.zig index 3b1504d7..8320ec30 100644 --- a/src/Codegen.zig +++ b/src/Codegen.zig @@ -35,7 +35,6 @@ pub const Frame = struct { function_node: Ast.Node.Index, function: ?*obj.ObjFunction = null, return_counts: bool = false, - return_emitted: bool = false, // Keep track of constants to avoid adding the same more than once to the chunk constants: std.AutoHashMapUnmanaged(Value, u24) = .empty, @@ -55,7 +54,17 @@ const NodeGen = *const fn ( self: *Self, node: Ast.Node.Index, breaks: ?*Breaks, -) Error!?*obj.ObjFunction; +) Error!GeneratedNode; + +const TerminalFlow = Ast.Slice.TerminalFlow; + +/// Result produced while generating an AST node. +const GeneratedNode = struct { + /// Function object produced by function-like nodes. + function: ?*obj.ObjFunction = null, + /// Terminal behavior observed while emitting bytecode for the node. + flow: TerminalFlow = .{}, +}; const Break = struct { ip: usize, // The op code will tell us if this is a continue or a break statement @@ -64,12 +73,20 @@ const Break = struct { const Breaks = std.ArrayList(Break); +/// Kind of pending loop jump being queried. +const LoopJumpKind = enum { + @"break", + @"continue", +}; + current: ?*Frame = null, ast: Ast.Slice = undefined, gc: *GC, flavor: RunFlavor, /// Jump to patch at end of current expression with a optional unwrapping in the middle of it opt_jumps: std.ArrayList(std.ArrayList(usize)) = .empty, +/// Jumps emitted by `out` statements to the end of the current block expression. +block_expression_out_jumps: std.ArrayList(std.ArrayList(usize)) = .empty, /// Used to generate error messages parser: *Parser, jit: ?*JIT, @@ -186,9 +203,9 @@ pub fn generate(self: *Self, ast: Ast.Slice) Error!?*obj.ObjFunction { self.reporter.last_error = null; self.reporter.panic_mode = false; - const function = self.generateNode(self.ast.root.?, null); + const result = try self.generateNode(self.ast.root.?, null); - return if (self.reporter.last_error != null) null else function; + return if (self.reporter.last_error != null) null else result.function; } pub fn emit(self: *Self, location: Ast.TokenIndex, code: u32) !void { @@ -428,9 +445,103 @@ fn endScope(self: *Self, node: Ast.Node.Index) Error!void { } } -fn generateNode(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +/// Closes the scopes attached to `node` while keeping the current stack top as an expression result. +fn endScopePreservingTop(self: *Self, node: Ast.Node.Index) Error!void { + const location = self.ast.nodes.items(.location)[node]; + + if (self.ast.nodes.items(.ends_scope)[node]) |closing| { + for (closing) |cls| { + try self.OP_SWAP(location, 0, 1); + try self.emitOpCode(location, cls.opcode); + try self.OP_DBG_LOCAL_EXIT(location, cls.slot); + } + } +} + +/// Emits the shared warning for a statement proven unreachable by control-flow analysis. +fn warnUnreachable(self: *Self, first_statement: Ast.Node.Index, last_statement: Ast.Node.Index) void { + self.reporter.warnFmt( + .unreachable_code, + self.ast.tokens.get( + self.ast.nodes.items(.location)[first_statement], + ), + self.ast.tokens.get( + self.ast.nodes.items(.end_location)[last_statement], + ), + "Code will never be reached", + .{}, + ); +} + +/// Returns whether pending loop jumps include a break or continue matching `loop_node`. +fn hasLoopJump(self: *Self, jumps: *const Breaks, loop_node: Ast.Node.Index, kind: LoopJumpKind, current: bool) bool { + for (jumps.items) |jump| { + const is_continue = is_it: { + const original = self.current.?.function.?.chunk.code.items[jump.ip]; + const instruction: u8 = @intCast(original >> 24); + break :is_it @as(Chunk.OpCode, @enumFromInt(instruction)) == .OP_LOOP; + }; + const matches_kind = switch (kind) { + .@"break" => !is_continue, + .@"continue" => is_continue, + }; + + const loop_jump_target_current = jump.label_node == null or jump.label_node.? == loop_node; + if (matches_kind and loop_jump_target_current == current) { + return true; + } + } + + return false; +} + +/// Builds the terminal-flow summary for a generated loop after current-loop jumps are known. +fn generatedLoopFlow( + self: *Self, + jumps: *const Breaks, + loop_node: Ast.Node.Index, + body_flow: TerminalFlow, + falls_through: bool, +) TerminalFlow { + return .{ + .falls_through = falls_through, + .returns = body_flow.returns, + .throws = body_flow.throws, + .outs = body_flow.outs, + .breaks = self.hasLoopJump(jumps, loop_node, .@"break", false), + .continues = self.hasLoopJump(jumps, loop_node, .@"continue", false), + }; +} + +/// Generates a statement list until generated terminal flow proves the remaining statements unreachable. +fn generateStatements( + self: *Self, + statements: []const Ast.Node.Index, + breaks: ?*Breaks, +) Error!TerminalFlow { + var result = TerminalFlow{}; + var falls_through = true; + + for (statements) |statement| { + if (!falls_through) { + self.warnUnreachable(statement, statements[statements.len - 1]); + break; + } + + const generated = try self.generateNode(statement, breaks); + result.merge(generated.flow); + + falls_through = generated.flow.falls_through; + } + + result.falls_through = falls_through; + + return result; +} + +fn generateNode(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { if (self.synchronize(node)) { - return null; + return .{}; } _ = try TypeChecker.check( @@ -445,10 +556,10 @@ fn generateNode(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj return generator(self, node, breaks); } - return null; + return .{}; } -fn generateAs(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateAs(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const locations = self.ast.nodes.items(.location); const node_location = locations[node]; const components = self.ast.nodes.items(.components)[node].As; @@ -479,10 +590,10 @@ fn generateAs(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.O try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateAsyncCall(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateAsyncCall(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const locations = self.ast.nodes.items(.location); const type_defs = self.ast.nodes.items(.type_def); const node_location = locations[node]; @@ -501,10 +612,10 @@ fn generateAsyncCall(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error! try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateBinary(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateBinary(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const components = self.ast.nodes.items(.components)[node].Binary; const locations = self.ast.nodes.items(.location); @@ -665,63 +776,87 @@ fn generateBinary(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*o try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateBlock(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { - const tags = self.ast.nodes.items(.tag); +fn generateBlock(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { + const flow = try self.generateStatements( + self.ast.nodes.items(.components)[node].Block, + breaks, + ); - var seen_return = false; - for (self.ast.nodes.items(.components)[node].Block) |statement| { - if (seen_return) { - self.reporter.warnFmt( - .code_after_return, - self.ast.tokens.get( - self.ast.nodes.items(.location)[statement], - ), - self.ast.tokens.get( - self.ast.nodes.items(.end_location)[statement], - ), - "Code after return statement will never be reached", - .{}, - ); + try self.patchOptJumps(node); + try self.endScope(node); - // No need to generate following statements - break; - } + return .{ + .flow = flow, + }; +} - _ = try self.generateNode(statement, breaks); +fn generateBlockExpression(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { + const location = self.ast.nodes.items(.location)[node]; - seen_return = !seen_return and tags[statement] == .Return; + try self.block_expression_out_jumps.append(self.gc.allocator, .empty); + errdefer { + var out_jumps = self.block_expression_out_jumps.pop().?; + out_jumps.deinit(self.gc.allocator); } - try self.patchOptJumps(node); - try self.endScope(node); + const flow = try self.generateStatements( + self.ast.nodes.items(.components)[node].BlockExpression, + breaks, + ); - return null; -} + var out_jumps = self.block_expression_out_jumps.pop().?; + defer out_jumps.deinit(self.gc.allocator); -fn generateBlockExpression(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { - for (self.ast.nodes.items(.components)[node].BlockExpression) |statement| { - _ = try self.generateNode(statement, breaks); - } + if (out_jumps.items.len > 0) { + const skip_out_postlude = try self.OP_JUMP(location); - try self.patchOptJumps(node); - try self.endScope(node); + for (out_jumps.items) |jump| { + self.patchJump(jump); + } - return null; + try self.patchOptJumps(node); + const end_jump = try self.OP_JUMP(location); + + self.patchJump(skip_out_postlude); + try self.endScope(node); + self.patchJump(end_jump); + } else { + try self.patchOptJumps(node); + try self.endScope(node); + } + + return .{ + .flow = flow.asExpression(), + }; } -fn generateOut(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateOut(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { _ = try self.generateNode(self.ast.nodes.items(.components)[node].Out, breaks); try self.patchOptJumps(node); - try self.endScope(node); - return null; + if (self.block_expression_out_jumps.items.len > 0) { + try self.endScopePreservingTop(node); + try self.block_expression_out_jumps.items[self.block_expression_out_jumps.items.len - 1].append( + self.gc.allocator, + try self.OP_JUMP(self.ast.nodes.items(.location)[node]), + ); + } else { + try self.endScope(node); + } + + return .{ + .flow = .{ + .falls_through = false, + .outs = true, + }, + }; } -fn generateBoolean(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.ObjFunction { +fn generateBoolean(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!GeneratedNode { try self.emitOpCode( self.ast.nodes.items(.location)[node], if (self.ast.nodes.items(.components)[node].Boolean) .OP_TRUE else .OP_FALSE, @@ -730,10 +865,10 @@ fn generateBoolean(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.O try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateBreak(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateBreak(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { // Close scope(s), then jump try self.endScope(node); try breaks.?.append( @@ -746,10 +881,15 @@ fn generateBreak(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*ob try self.patchOptJumps(node); - return null; + return .{ + .flow = .{ + .falls_through = false, + .breaks = true, + }, + }; } -fn generateContinue(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateContinue(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { // Close scope(s), then jump try self.endScope(node); try breaks.?.append( @@ -765,10 +905,15 @@ fn generateContinue(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!? try self.patchOptJumps(node); - return null; + return .{ + .flow = .{ + .falls_through = false, + .continues = true, + }, + }; } -fn generateCall(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateCall(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const type_defs = self.ast.nodes.items(.type_def); const locations = self.ast.nodes.items(.location); const end_locations = self.ast.nodes.items(.end_location); @@ -790,7 +935,7 @@ fn generateCall(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } // Find out if call is invoke or regular call @@ -1116,10 +1261,10 @@ fn generateCall(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateDot(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateDot(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const node_components = self.ast.nodes.items(.components); const type_defs = self.ast.nodes.items(.type_def); const locations = self.ast.nodes.items(.location); @@ -1341,10 +1486,10 @@ fn generateDot(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj. try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateDoUntil(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateDoUntil(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const locations = self.ast.nodes.items(.location); const node_components = self.ast.nodes.items(.components); const components = node_components[node].DoUntil; @@ -1354,7 +1499,7 @@ fn generateDoUntil(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?* var lbreaks = Breaks.empty; defer lbreaks.deinit(self.gc.allocator); - _ = try self.generateNode(components.body, &lbreaks); + const body_flow = (try self.generateNode(components.body, &lbreaks)).flow; _ = try self.generateNode(components.condition, &lbreaks); try self.OP_NOT(locations[node]); @@ -1377,10 +1522,27 @@ fn generateDoUntil(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?* try self.patchOptJumps(node); try self.endScope(node); - return null; + const condition_allows_exit = if (try self.ast.isConstant(self.gc.allocator, components.condition)) + (try self.ast.typeCheckAndToValue( + components.condition, + &self.reporter, + self.gc, + )).boolean() + else + true; + + return .{ + .flow = self.generatedLoopFlow( + &lbreaks, + node, + body_flow, + self.hasLoopJump(&lbreaks, node, .@"break", true) or + (body_flow.falls_through and condition_allows_exit), + ), + }; } -fn generateEnum(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.ObjFunction { +fn generateEnum(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!GeneratedNode { const locations = self.ast.nodes.items(.location); const node_components = self.ast.nodes.items(.components); const components = node_components[node].Enum; @@ -1402,10 +1564,10 @@ fn generateEnum(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.ObjF try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateAnonymousEnumCase(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.ObjFunction { +fn generateAnonymousEnumCase(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!GeneratedNode { const locations = self.ast.nodes.items(.location); const components = &self.ast.nodes.items(.components)[node].AnonymousEnumCase; const expected_case = self.ast.tokens.items(.lexeme)[components.case_name]; @@ -1418,7 +1580,7 @@ fn generateAnonymousEnumCase(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Err "Could not infer type for enum case.", ); - return null; + return .{}; } const enum_type_def = type_def.?.resolved_type.?.EnumInstance @@ -1463,10 +1625,10 @@ fn generateAnonymousEnumCase(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Err try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateExport(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateExport(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const components = self.ast.nodes.items(.components)[node].Export; if (components.declaration) |decl| { @@ -1476,10 +1638,10 @@ fn generateExport(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*o try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateExpression(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateExpression(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const locations = self.ast.nodes.items(.location); const end_locations = self.ast.nodes.items(.end_location); const components = self.ast.nodes.items(.components); @@ -1487,7 +1649,7 @@ fn generateExpression(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error const expr_node_type = self.ast.nodes.items(.tag)[expr]; const expr_type_def = self.ast.nodes.items(.type_def)[expr]; - _ = try self.generateNode(expr, breaks); + const generated = try self.generateNode(expr, breaks); try self.OP_POP(locations[node]); @@ -1515,10 +1677,12 @@ fn generateExpression(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{ + .flow = generated.flow, + }; } -fn generateFloat(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.ObjFunction { +fn generateFloat(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!GeneratedNode { try self.emitConstant( self.ast.nodes.items(.location)[node], try self.ast.typeCheckAndToValue( @@ -1531,10 +1695,10 @@ fn generateFloat(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.Obj try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateFor(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateFor(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const locations = self.ast.nodes.items(.location); const end_locations = self.ast.nodes.items(.end_location); const type_defs = self.ast.nodes.items(.type_def); @@ -1548,7 +1712,7 @@ fn generateFor(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj. )).boolean()) { try self.patchOptJumps(node); - return null; + return .{}; } for (components.init_declarations) |decl| { @@ -1599,7 +1763,7 @@ fn generateFor(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj. var lbreaks = Breaks.empty; defer lbreaks.deinit(self.gc.allocator); - _ = try self.generateNode(components.body, &lbreaks); + const body_flow = (try self.generateNode(components.body, &lbreaks)).flow; try self.emitLoop(locations[node], expr_loop); @@ -1620,10 +1784,26 @@ fn generateFor(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj. if (!is_wasm) self.patchTryOrJit(jit_jump); - return null; + const condition_true = if (try self.ast.isConstant(self.gc.allocator, components.condition)) + (try self.ast.typeCheckAndToValue( + components.condition, + &self.reporter, + self.gc, + )).boolean() + else + false; + + return .{ + .flow = self.generatedLoopFlow( + &lbreaks, + node, + body_flow, + !condition_true or self.hasLoopJump(&lbreaks, node, .@"break", true), + ), + }; } -fn generateForceUnwrap(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateForceUnwrap(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const locations = self.ast.nodes.items(.location); const components = self.ast.nodes.items(.components)[node].ForceUnwrap; @@ -1634,10 +1814,10 @@ fn generateForceUnwrap(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Erro try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateForEach(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateForEach(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const node_components = self.ast.nodes.items(.components); const locations = self.ast.nodes.items(.location); const type_defs = self.ast.nodes.items(.type_def); @@ -1668,7 +1848,7 @@ fn generateForEach(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?* else => self.reporter.last_error != null, }) { try self.patchOptJumps(node); - return null; + return .{}; } } } @@ -1717,7 +1897,7 @@ fn generateForEach(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?* var lbreaks = Breaks.empty; defer lbreaks.deinit(self.gc.allocator); - _ = try self.generateNode(components.body, &lbreaks); + const body_flow = (try self.generateNode(components.body, &lbreaks)).flow; try self.emitLoop(locations[node], loop_start); @@ -1743,10 +1923,17 @@ fn generateForEach(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?* if (!is_wasm) self.patchTryOrJit(jit_jump); - return null; + return .{ + .flow = self.generatedLoopFlow( + &lbreaks, + node, + body_flow, + true, + ), + }; } -fn generateFunction(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateFunction(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const node_components = self.ast.nodes.items(.components); const type_defs = self.ast.nodes.items(.type_def); const locations = self.ast.nodes.items(.location); @@ -1762,7 +1949,7 @@ fn generateFunction(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!? // If function is a test block and we're not testing/checking/etc. don't waste time generating the node if (self.flavor == .Run and function_type == .Test) { try self.emitOpCode(locations[node], .OP_NULL); - return null; + return .{}; } const enclosing = self.current; @@ -1840,12 +2027,16 @@ fn generateFunction(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!? self.current.?.function = try self.gc.allocateObject(function); // Generate function's body + var body_flow: ?TerminalFlow = null; if (components.body) |body| { - _ = try self.generateNode(body, breaks); + body_flow = (try self.generateNode(body, breaks)).flow; if (function_signature != null and function_signature.?.lambda) { try self.OP_RETURN(locations[body]); - self.current.?.return_emitted = true; + body_flow = .{ + .falls_through = false, + .returns = true, + }; } } @@ -1920,7 +2111,6 @@ fn generateFunction(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!? } else { try self.OP_VOID(locations[node]); try self.OP_RETURN(locations[node]); - self.current.?.return_emitted = true; } } else if (function_type == .Repl and components.body != null and @@ -1933,10 +2123,11 @@ fn generateFunction(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!? _ = self.current.?.function.?.chunk.locations.pop(); try self.emitReturn(locations[node]); - } else if (self.current.?.function.?.type_def.resolved_type.?.Function.return_type.def_type == .Void and !self.current.?.return_emitted) { - // TODO: detect if some branches of the function body miss a return statement + } else if (self.current.?.function.?.type_def.resolved_type.?.Function.return_type.def_type == .Void and + (body_flow == null or body_flow.?.falls_through)) + { try self.emitReturn(locations[node]); - } else if (!self.current.?.return_emitted) { + } else if (body_flow == null or body_flow.?.falls_through) { self.reporter.reportErrorAt( .missing_return, self.ast.tokens.get(locations[node]), @@ -1986,10 +2177,12 @@ fn generateFunction(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!? node_components[node].Function.function = current_function; - return current_function; + return .{ + .function = current_function, + }; } -fn generateFunDeclaration(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateFunDeclaration(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const node_components = self.ast.nodes.items(.components); const components = node_components[node].FunDeclaration; @@ -2006,10 +2199,10 @@ fn generateFunDeclaration(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) E try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateGenericResolve(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateGenericResolve(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { _ = try self.generateNode( self.ast.nodes.items(.components)[node].GenericResolve.expression, breaks, @@ -2018,10 +2211,10 @@ fn generateGenericResolve(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) E try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateGrouping(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateGrouping(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const components = self.ast.nodes.items(.components); const expr = components[node].Grouping; @@ -2030,10 +2223,10 @@ fn generateGrouping(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!? try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateIf(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateIf(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const type_defs = self.ast.nodes.items(.type_def); const locations = self.ast.nodes.items(.location); const node_components = self.ast.nodes.items(.components); @@ -2051,16 +2244,19 @@ fn generateIf(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.O self.gc, ); + var flow = TerminalFlow{}; if (condition.boolean()) { - _ = try self.generateNode(components.body, breaks); + flow = (try self.generateNode(components.body, breaks)).flow; } else if (components.else_branch) |else_branch| { - _ = try self.generateNode(else_branch, breaks); + flow = (try self.generateNode(else_branch, breaks)).flow; } try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{ + .flow = flow, + }; } _ = try self.generateNode(components.condition, breaks); @@ -2079,7 +2275,7 @@ fn generateIf(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.O const else_jump = try self.OP_JUMP_IF_FALSE(location); try self.OP_POP(location); - _ = try self.generateNode(components.body, breaks); + const body_flow = (try self.generateNode(components.body, breaks)).flow; const out_jump = try self.emitJump(location, .OP_JUMP); @@ -2090,8 +2286,11 @@ fn generateIf(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.O } try self.OP_POP(location); + var else_flow = TerminalFlow{}; if (components.else_branch) |else_branch| { - _ = try self.generateNode(else_branch, breaks); + else_flow = (try self.generateNode(else_branch, breaks)).flow; + } else { + else_flow.falls_through = true; } self.patchJump(out_jump); @@ -2099,7 +2298,13 @@ fn generateIf(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.O try self.patchOptJumps(node); try self.endScope(node); - return null; + var flow = body_flow; + flow.falls_through = body_flow.falls_through or else_flow.falls_through; + flow.merge(else_flow); + + return .{ + .flow = flow, + }; } fn matchSimpleEquality(self: *Self, condition: Ast.Node.Index, breaks: ?*Breaks) Error!void { @@ -2207,7 +2412,7 @@ fn matchTypeIsValue(self: *Self, condition: Ast.Node.Index, breaks: ?*Breaks) Er try self.OP_NOT(location); } -fn generateMatch(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateMatch(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const type_defs = self.ast.nodes.items(.type_def); const locations = self.ast.nodes.items(.location); const lexemes = self.ast.tokens.items(.lexeme); @@ -2219,6 +2424,9 @@ fn generateMatch(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*ob var out_jumps = try std.ArrayList(usize).initCapacity(self.gc.allocator, components.branches.len); defer out_jumps.deinit(self.gc.allocator); + var flow = TerminalFlow{ + .falls_through = components.else_branch == null, + }; // Keep tracks of covered enum cases var enum_cases = if (components.else_branch == null and @@ -2318,7 +2526,9 @@ fn generateMatch(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*ob } try self.OP_POP(branch.expression); // Pop comparison try self.OP_POP(branch.expression); // Pop value - _ = try self.generateNode(branch.expression, breaks); + const branch_flow = (try self.generateNode(branch.expression, breaks)).flow; + flow.falls_through = flow.falls_through or branch_flow.falls_through; + flow.merge(branch_flow); // Jump out of the match statement/expression try out_jumps.append( @@ -2337,7 +2547,9 @@ fn generateMatch(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*ob if (components.else_branch) |eb| { try self.OP_POP(locations[components.value]); - _ = try self.generateNode(eb, breaks); + const else_flow = (try self.generateNode(eb, breaks)).flow; + flow.falls_through = flow.falls_through or else_flow.falls_through; + flow.merge(else_flow); } else if (enum_cases != null and enum_cases.?.count() > 0) { const keys = enum_cases.?.keys(); @@ -2377,10 +2589,12 @@ fn generateMatch(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*ob try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{ + .flow = flow, + }; } -fn generateImport(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateImport(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const components = self.ast.nodes.items(.components)[node].Import; const location = self.ast.nodes.items(.location)[node]; @@ -2396,10 +2610,10 @@ fn generateImport(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*o try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateInteger(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.ObjFunction { +fn generateInteger(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!GeneratedNode { try self.emitConstant( self.ast.nodes.items(.location)[node], try self.ast.typeCheckAndToValue( @@ -2412,10 +2626,10 @@ fn generateInteger(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.O try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateIs(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateIs(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const components = self.ast.nodes.items(.components)[node].Is; const location = self.ast.nodes.items(.location)[node]; const constant = try self.ast.typeCheckAndToValue( @@ -2440,10 +2654,10 @@ fn generateIs(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.O try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateList(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateList(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const locations = self.ast.nodes.items(.location); const components = self.ast.nodes.items(.components)[node].List; const type_defs = self.ast.nodes.items(.type_def); @@ -2467,10 +2681,10 @@ fn generateList(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateMap(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateMap(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const locations = self.ast.nodes.items(.location); const components = self.ast.nodes.items(.components)[node].Map; const type_defs = self.ast.nodes.items(.type_def); @@ -2495,10 +2709,10 @@ fn generateMap(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj. try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateNamedVariable(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateNamedVariable(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const components = self.ast.nodes.items(.components)[node].NamedVariable; const locations = self.ast.nodes.items(.location); const type_defs = self.ast.nodes.items(.type_def); @@ -2598,19 +2812,19 @@ fn generateNamedVariable(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Er try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateNull(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.ObjFunction { +fn generateNull(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!GeneratedNode { try self.OP_NULL(self.ast.nodes.items(.location)[node]); try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateObjectDeclaration(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateObjectDeclaration(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const locations = self.ast.nodes.items(.location); const type_defs = self.ast.nodes.items(.type_def); const lexemes = self.ast.tokens.items(.lexeme); @@ -2674,7 +2888,7 @@ fn generateObjectDeclaration(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } /// Finds the declared object global backing an inferred anonymous object init. @@ -2722,7 +2936,7 @@ fn objectGlobalSlotForInstanceType(self: *Self, type_def: *obj.ObjTypeDef) ?u24 return null; } -fn generateObjectInit(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateObjectInit(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const locations = self.ast.nodes.items(.location); const type_defs = self.ast.nodes.items(.type_def); const lexemes = self.ast.tokens.items(.lexeme); @@ -2782,10 +2996,10 @@ fn generateObjectInit(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generatePattern(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.ObjFunction { +fn generatePattern(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!GeneratedNode { try self.emitConstant( self.ast.nodes.items(.location)[node], try self.ast.typeCheckAndToValue( @@ -2798,10 +3012,10 @@ fn generatePattern(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.O try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateProtocolDeclaration(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.ObjFunction { +fn generateProtocolDeclaration(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!GeneratedNode { const location = self.ast.nodes.items(.location)[node]; const components = self.ast.nodes.items(.components)[node].ProtocolDeclaration; const type_def = self.ast.nodes.items(.type_def)[node].?; @@ -2816,10 +3030,10 @@ fn generateProtocolDeclaration(self: *Self, node: Ast.Node.Index, _: ?*Breaks) E try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateRange(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateRange(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const components = self.ast.nodes.items(.components)[node].Range; const locations = self.ast.nodes.items(.location); @@ -2831,10 +3045,10 @@ fn generateRange(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*ob try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateResolve(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateResolve(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const fiber = self.ast.nodes.items(.components)[node].Resolve; const locations = self.ast.nodes.items(.location); @@ -2845,10 +3059,10 @@ fn generateResolve(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?* try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateResume(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateResume(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const fiber = self.ast.nodes.items(.components)[node].Resume; const locations = self.ast.nodes.items(.location); @@ -2859,17 +3073,13 @@ fn generateResume(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*o try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateReturn(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateReturn(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const components = self.ast.nodes.items(.components)[node].Return; const locations = self.ast.nodes.items(.location); - if (components.unconditional) { - self.current.?.return_emitted = true; - } - if (components.value) |value| { _ = try self.generateNode(value, breaks); } else { @@ -2881,10 +3091,15 @@ fn generateReturn(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*o try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{ + .flow = .{ + .falls_through = false, + .returns = true, + }, + }; } -fn generateString(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateString(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const location = self.ast.nodes.items(.location)[node]; const type_defs = self.ast.nodes.items(.type_def); const elements = self.ast.nodes.items(.components)[node].String; @@ -2895,7 +3110,7 @@ fn generateString(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*o try self.endScope(node); - return null; + return .{}; } for (elements, 0..) |element, index| { @@ -2914,10 +3129,10 @@ fn generateString(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*o try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateStringLiteral(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.ObjFunction { +fn generateStringLiteral(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!GeneratedNode { try self.emitConstant( self.ast.nodes.items(.location)[node], self.ast.nodes.items(.components)[node].StringLiteral.literal.toValue(), @@ -2926,10 +3141,10 @@ fn generateStringLiteral(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!? try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateSubscript(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateSubscript(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const locations = self.ast.nodes.items(.location); const location = locations[node]; const type_defs = self.ast.nodes.items(.type_def); @@ -3020,10 +3235,10 @@ fn generateSubscript(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error! try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateTry(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateTry(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const components = self.ast.nodes.items(.components)[node].Try; const type_defs = self.ast.nodes.items(.type_def); const locations = self.ast.nodes.items(.location); @@ -3146,19 +3361,15 @@ fn generateTry(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj. try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateThrow(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateThrow(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const components = self.ast.nodes.items(.components)[node].Throw; const type_defs = self.ast.nodes.items(.type_def); const location = self.ast.nodes.items(.location)[node]; const end_locations = self.ast.nodes.items(.end_location); - if (components.unconditional) { - self.current.?.return_emitted = true; - } - const expression_type_def = type_defs[components.expression].?; const current_error_types = self.current.?.function.?.type_def.resolved_type.?.Function.error_types; @@ -3202,10 +3413,15 @@ fn generateThrow(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*ob try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{ + .flow = .{ + .falls_through = false, + .throws = true, + }, + }; } -fn generateTypeExpression(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.ObjFunction { +fn generateTypeExpression(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!GeneratedNode { const node_components = self.ast.nodes.items(.components); const type_defs = self.ast.nodes.items(.type_def); @@ -3217,10 +3433,10 @@ fn generateTypeExpression(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error! try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateTypeOfExpression(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateTypeOfExpression(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { _ = try self.generateNode(self.ast.nodes.items(.components)[node].TypeOfExpression, breaks); try self.OP_TYPEOF(self.ast.nodes.items(.location)[node]); @@ -3228,10 +3444,10 @@ fn generateTypeOfExpression(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateUnary(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateUnary(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const components = self.ast.nodes.items(.components)[node].Unary; const location = self.ast.nodes.items(.location)[node]; const expression_type_def = self.ast.nodes.items(.type_def)[components.expression].?; @@ -3251,10 +3467,10 @@ fn generateUnary(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*ob try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateUnwrap(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateUnwrap(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const locations = self.ast.nodes.items(.location); const location = locations[node]; const components = self.ast.nodes.items(.components)[node].Unwrap; @@ -3281,10 +3497,10 @@ fn generateUnwrap(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*o try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateVarDeclaration(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateVarDeclaration(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const components = self.ast.nodes.items(.components)[node].VarDeclaration; const locations = self.ast.nodes.items(.location); const location = locations[node]; @@ -3314,19 +3530,19 @@ fn generateVarDeclaration(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) E try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateVoid(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.ObjFunction { +fn generateVoid(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!GeneratedNode { try self.OP_VOID(self.ast.nodes.items(.location)[node]); try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateWhile(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateWhile(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const components = self.ast.nodes.items(.components)[node].While; const locations = self.ast.nodes.items(.location); const location = locations[node]; @@ -3340,7 +3556,7 @@ fn generateWhile(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*ob try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } const loop_start: usize = self.currentCode(); @@ -3356,7 +3572,7 @@ fn generateWhile(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*ob var while_breaks = Breaks.empty; defer while_breaks.deinit(self.gc.allocator); - _ = try self.generateNode(components.body, &while_breaks); + const body_flow = (try self.generateNode(components.body, &while_breaks)).flow; try self.emitLoop(location, loop_start); self.patchJump(exit_jump); @@ -3376,10 +3592,26 @@ fn generateWhile(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*ob if (!is_wasm) self.patchTryOrJit(jit_jump); - return null; + const condition_true = if (try self.ast.isConstant(self.gc.allocator, components.condition)) + (try self.ast.typeCheckAndToValue( + components.condition, + &self.reporter, + self.gc, + )).boolean() + else + false; + + return .{ + .flow = self.generatedLoopFlow( + &while_breaks, + node, + body_flow, + !condition_true or self.hasLoopJump(&while_breaks, node, .@"break", true), + ), + }; } -fn generateYield(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { +fn generateYield(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!GeneratedNode { const expression = self.ast.nodes.items(.components)[node].Yield; const locations = self.ast.nodes.items(.location); const location = locations[node]; @@ -3391,12 +3623,12 @@ fn generateYield(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*ob try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } -fn generateZdef(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.ObjFunction { +fn generateZdef(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!GeneratedNode { if (is_wasm) { - return null; + return .{}; } const components = self.ast.nodes.items(.components)[node].Zdef; @@ -3431,7 +3663,7 @@ fn generateZdef(self: *Self, node: Ast.Node.Index, _: ?*Breaks) Error!?*obj.ObjF try self.patchOptJumps(node); try self.endScope(node); - return null; + return .{}; } fn OP_DBG_LOCAL_ENTER(self: *Self, location: Ast.TokenIndex, slot: u8, name: Value) !void { diff --git a/src/Jit.zig b/src/Jit.zig index 5cb1da8b..a6cd742b 100644 --- a/src/Jit.zig +++ b/src/Jit.zig @@ -93,6 +93,11 @@ const State = struct { /// Label to jump to when continuing a loop whithout a label continue_label: m.MIR_insn_t = null, + /// Label reached by `out` statements in the current block expression. + block_expression_out_label: m.MIR_insn_t = null, + /// Register holding the value produced by `out` in the current block expression. + block_expression_out_value: ?m.MIR_reg_t = null, + breaks_label: Breaks = .empty, args_buffer: [255]m.MIR_op_t = undefined, @@ -878,7 +883,7 @@ fn generateNode(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { .VarDeclaration => try self.generateVarDeclaration(node), .Block => try self.generateBlock(node), .BlockExpression => try self.generateBlockExpression(node), - .Out => try self.generateNode(components[node].Out), + .Out => try self.generateOut(node), .Call => try self.generateCall(node), .NamedVariable => try self.generateNamedVariable(node), .Return => try self.generateReturn(node), @@ -920,46 +925,10 @@ fn generateNode(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { }, }; - if (tag != .Break and tag != .Continue) { + if (tag != .Break and tag != .Continue and tag != .Out) { // Patch opt jumps if needed if (self.state.?.ast.nodes.items(.patch_opt_jumps)[node]) { - std.debug.assert(self.state.?.opt_jumps.items.len > 0); - - var opt_jump = self.state.?.opt_jumps.pop().?; - const out_label = m.MIR_new_label(self.ctx); - - // We reached here, means nothing was null, set the alloca with the value and use it has the node return value - self.MOV( - m.MIR_new_reg_op(self.ctx, opt_jump.alloca), - value.?, - ); - - self.JMP(out_label); - - // Patch opt blocks with the branching - for (opt_jump.current_insn.items) |current_insn| { - m.MIR_insert_insn_after( - self.ctx, - self.state.?.function.?, - current_insn, - m.MIR_new_insn_arr( - self.ctx, - @intFromEnum(m.MIR_Instruction.BEQ), - 3, - &.{ - m.MIR_new_label_op(self.ctx, out_label), - m.MIR_new_reg_op(self.ctx, opt_jump.alloca), - m.MIR_new_uint_op(self.ctx, Value.Null.val), - }, - ), - ); - } - - self.append(out_label); - - value = m.MIR_new_reg_op(self.ctx, opt_jump.alloca); - - opt_jump.deinit(self.gc.allocator); + value = try self.patchOptJumps(node, value.?); } // Close scope if needed if (constant == null or tag != .Range) { // Range creates locals for its limits, but we don't push anything if its constant @@ -970,6 +939,48 @@ fn generateNode(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { return value; } +/// Patches pending optional-chain jumps and returns the register containing the resolved value. +fn patchOptJumps(self: *Self, node: Ast.Node.Index, value: m.MIR_op_t) !m.MIR_op_t { + std.debug.assert(self.state.?.ast.nodes.items(.patch_opt_jumps)[node]); + std.debug.assert(self.state.?.opt_jumps.items.len > 0); + + var opt_jump = self.state.?.opt_jumps.pop().?; + const out_label = m.MIR_new_label(self.ctx); + + // We reached here, means nothing was null, set the alloca with the value and use it as the node return value. + self.MOV( + m.MIR_new_reg_op(self.ctx, opt_jump.alloca), + value, + ); + + self.JMP(out_label); + + // Patch opt blocks with the branching. + for (opt_jump.current_insn.items) |current_insn| { + m.MIR_insert_insn_after( + self.ctx, + self.state.?.function.?, + current_insn, + m.MIR_new_insn_arr( + self.ctx, + @intFromEnum(m.MIR_Instruction.BEQ), + 3, + &.{ + m.MIR_new_label_op(self.ctx, out_label), + m.MIR_new_reg_op(self.ctx, opt_jump.alloca), + m.MIR_new_uint_op(self.ctx, Value.Null.val), + }, + ), + ); + } + + self.append(out_label); + + opt_jump.deinit(self.gc.allocator); + + return m.MIR_new_reg_op(self.ctx, opt_jump.alloca); +} + fn closeScope(self: *Self, node: Ast.Node.Index) !void { if (self.state.?.ast.nodes.items(.ends_scope)[node]) |closing| { for (closing) |close| { @@ -5496,16 +5507,57 @@ fn generateBlock(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { fn generateBlockExpression(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { const statements = self.state.?.ast.nodes.items(.components)[node].BlockExpression; + const out_label = m.MIR_new_label(self.ctx); + const out_value = try self.REG("block_expression_out", m.MIR_T_I64); + + const previous_out_label = self.state.?.block_expression_out_label; + const previous_out_value = self.state.?.block_expression_out_value; + self.state.?.block_expression_out_label = out_label; + self.state.?.block_expression_out_value = out_value; + defer { + self.state.?.block_expression_out_label = previous_out_label; + self.state.?.block_expression_out_value = previous_out_value; + } + + // The typechecker guarantees that value-typed block expressions reach an `out`. + // Keeping a void fallback lets terminal return/throw-only paths compile without producing a null MIR operand. + self.MOV( + m.MIR_new_reg_op(self.ctx, out_value), + m.MIR_new_uint_op(self.ctx, Value.Void.val), + ); - var out_statement: ?m.MIR_op_t = null; for (statements) |statement| { - out_statement = try self.generateNode(statement); + _ = try self.generateNode(statement); } - return if (statements.len > 0 and self.state.?.ast.nodes.items(.tag)[statements[statements.len - 1]] == .Out) - out_statement.? - else - null; + self.append(out_label); + + return m.MIR_new_reg_op(self.ctx, out_value); +} + +/// Generates `out` as an early exit from the innermost block expression. +fn generateOut(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { + var value = (try self.generateNode(self.state.?.ast.nodes.items(.components)[node].Out)).?; + + if (self.state.?.ast.nodes.items(.patch_opt_jumps)[node]) { + value = try self.patchOptJumps(node, value); + } + + if (self.state.?.block_expression_out_label) |out_label| { + const out_value = self.state.?.block_expression_out_value.?; + const result = m.MIR_new_reg_op(self.ctx, out_value); + + // Store before closing the scope so `out` can safely use locals from the block expression. + self.MOV(result, value); + try self.closeScope(node); + self.JMP(out_label); + + return result; + } + + try self.closeScope(node); + + return value; } fn generateFunDeclaration(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { diff --git a/src/Package.zig b/src/Package.zig index f8a42f91..fcf36cb6 100644 --- a/src/Package.zig +++ b/src/Package.zig @@ -1934,7 +1934,7 @@ pub fn init(process: std.process.Init) !void { try stdout.interface.print( \\ - \\🎉 Buzz package created. Run `buzz run` to try it. + \\🎉 Buzz package created. Run `buzz .` to try it. \\. \\├── build.zig \\├── {s} diff --git a/src/Parser.zig b/src/Parser.zig index 328b05a3..b9bd6f4c 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -7232,60 +7232,50 @@ fn blockExpression(self: *Self, _: bool) Error!Ast.Node.Index { try self.consume(.LeftBrace, "Expected `{` at start of block expression"); try self.beginScope(null); + const previous_block_expression = self.current.?.in_block_expression; self.current.?.in_block_expression = self.current.?.scope_depth; + errdefer self.current.?.in_block_expression = previous_block_expression; var statements = std.ArrayList(Ast.Node.Index).empty; - var out: ?Ast.Node.Index = null; while (!self.check(.RightBrace) and !self.check(.Eof)) { if (try self.declarationOrStatement(null)) |stmt| { try statements.append(self.gc.allocator, stmt); - - if (self.ast.nodes.items(.tag)[stmt] == .Out) { - if (out != null) { - self.reportErrorAtNode( - .syntax, - stmt, - "Only one `out` statement is allowed in block expression", - .{}, - ); - } - - out = stmt; - } } } - if (out != null and statements.getLastOrNull() != out) { - if (statements.getLastOrNull()) |stmt| { - self.reportErrorAtNode( - .syntax, - stmt, - "Last block expression statement must be `out`", - .{}, - ); - } else { - const location = self.ast.tokens.get(self.current_token.? - 1); - self.reporter.reportErrorAt( - .syntax, - location, - location, - "Last block expression statement must be `out`", - ); - } + const flow = try self.ast.slice().blockExpressionFlowStatements( + self.gc.allocator, + self.gc, + statements.items, + .{ + .out_exits = true, + }, + ); + + if (flow.terminal.outs and flow.terminal.falls_through) { + const location = self.ast.tokens.get(self.current_token.? - 1); + self.reporter.reportErrorAt( + .syntax, + location, + location, + "All block expression paths must end with `out`", + ); } try self.consume(.RightBrace, "Expected `}` at end of block expression"); - self.current.?.in_block_expression = null; + self.current.?.in_block_expression = previous_block_expression; return try self.ast.appendNode( .{ .tag = .BlockExpression, .location = start_location, .end_location = self.current_token.? - 1, - .type_def = if (out) |o| - self.ast.nodes.items(.type_def)[o] + .type_def = if (flow.terminal.outs) + flow.out_type orelse self.gc.type_registry.any_type + else if (flow.terminal.terminal()) + self.gc.type_registry.any_type else self.gc.type_registry.void_type, .components = .{ @@ -10552,14 +10542,6 @@ fn outStatement(self: *Self) Error!Ast.Node.Index { location, "`out` statement is only allowed inside a block expression", ); - } else if (self.current.?.scope_depth != self.current.?.in_block_expression.?) { - const location = self.ast.tokens.get(start_location); - self.reporter.reportErrorAt( - .syntax, - location, - location, - "`out` statement must be the last statement of a block expression", - ); } const expr = try self.expression(false); @@ -10572,6 +10554,10 @@ fn outStatement(self: *Self) Error!Ast.Node.Index { .location = start_location, .end_location = self.current_token.? - 1, .type_def = self.ast.nodes.items(.type_def)[expr], + .ends_scope = if (self.current.?.in_block_expression) |scope_depth| + try self.closeScope(scope_depth) + else + null, .components = .{ .Out = expr, }, diff --git a/src/Reporter.zig b/src/Reporter.zig index 52caaf37..444b68b4 100644 --- a/src/Reporter.zig +++ b/src/Reporter.zig @@ -141,6 +141,7 @@ pub const Error = enum(u8) { unexhaustive_match = 105, match_condition_type = 106, match_duplicate_condition = 107, + unreachable_code = 108, }; pub const ReportKind = enum { diff --git a/src/Runner.zig b/src/Runner.zig index 711bd6ea..4e78dc8f 100644 --- a/src/Runner.zig +++ b/src/Runner.zig @@ -138,7 +138,7 @@ fn resolveRootDir( } // If the entry point lives under a `src` directory, its parent is the - // package root. This lets `buzz run src/main.buzz` work without `-r`. + // package root. This lets `buzz src/main.buzz` work without `-r`. if (std.fs.path.dirname(absolute_file_path)) |dir| { var it = std.fs.path.componentIterator(dir); var maybe_component = it.last(); diff --git a/src/lsp.zig b/src/lsp.zig index 638dcd4d..3ae136c0 100644 --- a/src/lsp.zig +++ b/src/lsp.zig @@ -1776,6 +1776,11 @@ const Handler = struct { for (doc.errors) |report| { for (report.items) |item| { if (std.mem.eql(u8, item.location.script_name, doc.uri)) { + const tags: ?[]const lsp.types.Diagnostic.Tag = if (report.error_type == .unreachable_code) + &.{.Unnecessary} + else + null; + try diags.append( self.allocator, .{ @@ -1795,6 +1800,7 @@ const Handler = struct { .hint => .Hint, }, .message = item.message, + .tags = tags, }, ); } diff --git a/src/main.zig b/src/main.zig index e039d5a5..4457a892 100644 --- a/src/main.zig +++ b/src/main.zig @@ -41,8 +41,6 @@ const SubCommand = enum { format, help, init, - run, - @"run-script", version, }; @@ -53,14 +51,12 @@ const command_summaries = .{ .format = .{ .name = "format", .description = "Format a buzz script." }, .help = .{ .name = "help", .description = "Show global or command-specific help." }, .init = .{ .name = "init", .description = "Create a minimal buzz package in the current directory." }, - .run = .{ .name = "run", .description = "Run src/main.buzz from the current package." }, - .@"run-script" = .{ .name = "run-script", .description = "Run a standalone buzz script by path." }, .@"test" = .{ .name = "test", .description = "Run tests from a buzz script." }, .version = .{ .name = "version", .description = "Print buzz version information." }, }; const main_params = clap.parseParamsComptime( - \\ + \\ ); const test_params = clap.parseParamsComptime( @@ -82,15 +78,10 @@ const format_params = clap.parseParamsComptime( \\ Script to format ); -const run_params = clap.parseParamsComptime( - \\... Arguments to pass to src/main.buzz -); - -const run_script_params = clap.parseParamsComptime( +const direct_run_params = clap.parseParamsComptime( \\-L, --library ... Add search path for external libraries \\-r, --root-dir Root dir for package resolution - \\ Script to run - \\... Arguments to pass to the script + \\ File or package directory to run ); const help_params = clap.parseParamsComptime( @@ -101,10 +92,6 @@ const fetch_params = clap.parseParamsComptime( \\-m, --manifest Path to manifest file (defaults to `./manifest.buzz`) ); -const main_parsers = .{ - .command = clap.parsers.enumeration(SubCommand), -}; - pub fn main(provided_init: Init) u8 { if (is_wasm) unreachable; @@ -134,74 +121,94 @@ pub fn main(provided_init: Init) u8 { _ = arg_iter.next(); + var args = std.ArrayList([]const u8).empty; + defer { + for (args.items) |arg| { + allocator.free(arg); + } + args.deinit(allocator); + } + + while (arg_iter.next()) |arg| { + const owned_arg = allocator.dupe(u8, arg) catch { + stderr.interface.writeAll("Could not allocate command line arguments\n") catch {}; + return 1; + }; + args.append(allocator, owned_arg) catch { + allocator.free(owned_arg); + stderr.interface.writeAll("Could not allocate command line arguments\n") catch {}; + return 1; + }; + } + var diag = clap.Diagnostic{}; - var res = clap.parseEx( - clap.Help, - &main_params, - main_parsers, - &arg_iter, - .{ - .allocator = allocator, - .diagnostic = &diag, - // Stop parsing after we read the subcommand - .terminating_positional = 0, - }, - ) catch |err| { - // Report useful error and exit - diag.report(&stderr.interface, err) catch {}; - return 1; - }; - defer res.deinit(); - // No arguments, we run the REPL - if (res.positionals[0]) |command| { + // No arguments, we run the REPL. + if (args.items.len == 0) { + repl(init, allocator) catch { + return 1; + }; + + return 0; + } + + if (std.meta.stringToEnum(SubCommand, args.items[0])) |command| { return switch (command) { - .@"test" => run( - init, - allocator, - command, - clap.parseEx( - clap.Help, - &test_params, - clap.parsers.default, - &arg_iter, - .{ - .allocator = allocator, - .diagnostic = &diag, + .@"test" => { + var sub_arg_iter = clap.args.SliceIterator{ .args = args.items[1..] }; + + return run( + init, + allocator, + command, + clap.parseEx( + clap.Help, + &test_params, + clap.parsers.default, + &sub_arg_iter, + .{ + .allocator = allocator, + .diagnostic = &diag, + }, + ) catch |err| { + // Report useful error and exit + diag.report(&stderr.interface, err) catch {}; + return 1; }, - ) catch |err| { - // Report useful error and exit - diag.report(&stderr.interface, err) catch {}; - return 1; - }, - .{}, - ), - .check => run( - init, - allocator, - command, - clap.parseEx( - clap.Help, - &check_params, - clap.parsers.default, - &arg_iter, - .{ - .allocator = allocator, - .diagnostic = &diag, + .{}, + ); + }, + .check => { + var sub_arg_iter = clap.args.SliceIterator{ .args = args.items[1..] }; + + return run( + init, + allocator, + command, + clap.parseEx( + clap.Help, + &check_params, + clap.parsers.default, + &sub_arg_iter, + .{ + .allocator = allocator, + .diagnostic = &diag, + }, + ) catch |err| { + // Report useful error and exit + diag.report(&stderr.interface, err) catch {}; + return 1; }, - ) catch |err| { - // Report useful error and exit - diag.report(&stderr.interface, err) catch {}; - return 1; - }, - .{}, - ), + .{}, + ); + }, .format => { + var sub_arg_iter = clap.args.SliceIterator{ .args = args.items[1..] }; const sub_res = clap.parseEx( clap.Help, &format_params, clap.parsers.default, - &arg_iter, + &sub_arg_iter, .{ .allocator = allocator, .diagnostic = &diag, @@ -232,82 +239,13 @@ pub fn main(provided_init: Init) u8 { }, ); }, - .run => { - const sub_res = clap.parseEx( - clap.Help, - &run_params, - clap.parsers.default, - &arg_iter, - .{ - .allocator = allocator, - .diagnostic = &diag, - }, - ) catch |err| { - // Report useful error and exit - diag.report(&stderr.interface, err) catch {}; - return 1; - }; - - std.Io.Dir.cwd().access(init.io, Package.MANIFEST, .{ .read = true }) catch |err| { - stderr.interface.print( - "Could not find `{s}` in current directory: {s}\n", - .{ - Package.MANIFEST, - @errorName(err), - }, - ) catch @panic("Could not check package manifest"); - return 1; - }; - - var perf: ?Perf = if (BuildOptions.show_perf) Perf.init(init.io) else null; - defer if (perf) |*p| p.report(); - - var runner: Runner = undefined; - runner.init( - init, - allocator, - .Run, - null, - if (perf) |*p| p else null, - ) catch { - return 1; - }; - defer runner.deinit(); - - return runner.runFile( - ".", - "src/main.buzz", - sub_res.positionals[0], - ) catch { - return 1; - }; - }, - .@"run-script" => run( - init, - allocator, - command, - clap.parseEx( - clap.Help, - &run_script_params, - clap.parsers.default, - &arg_iter, - .{ - .allocator = allocator, - .diagnostic = &diag, - }, - ) catch |err| { - // Report useful error and exit - diag.report(&stderr.interface, err) catch {}; - return 1; - }, - .{}, - ), .fetch => { + var sub_arg_iter = clap.args.SliceIterator{ .args = args.items[1..] }; const sub_res = clap.parseEx( clap.Help, &fetch_params, clap.parsers.default, - &arg_iter, + &sub_arg_iter, .{ .allocator = allocator, .diagnostic = &diag, @@ -397,11 +335,12 @@ pub fn main(provided_init: Init) u8 { return 1; }, .help => { + var sub_arg_iter = clap.args.SliceIterator{ .args = args.items[1..] }; const sub_res = clap.parseEx( clap.Help, &help_params, clap.parsers.default, - &arg_iter, + &sub_arg_iter, .{ .allocator = allocator, .diagnostic = &diag, @@ -425,13 +364,123 @@ pub fn main(provided_init: Init) u8 { return 0; }, }; - } else { - repl(init, allocator) catch { - return 1; - }; } - return 0; + return runDirect(init, allocator, args.items); +} + +/// Runs either a standalone buzz file or a package directory passed directly to the CLI. +fn runDirect( + init: Init, + allocator: Allocator, + args: []const []const u8, +) u8 { + var stderr = io.stderrWriter(init.io); + var diag = clap.Diagnostic{}; + var arg_iter = clap.args.SliceIterator{ .args = args }; + const res = clap.parseEx( + clap.Help, + &direct_run_params, + clap.parsers.default, + &arg_iter, + .{ + .allocator = allocator, + .diagnostic = &diag, + // Once the run target is known, all remaining tokens belong to the script. + .terminating_positional = 0, + }, + ) catch |err| { + diag.report(&stderr.interface, err) catch {}; + return 1; + }; + + const target = res.positionals[0] orelse { + stderr.interface.writeAll("Missing file or package directory to run\n") catch {}; + return 1; + }; + const script_args = args[arg_iter.index..]; + + if (res.args.library.len > 0) { + var list = std.ArrayList([]const u8).empty; + + for (res.args.library) |path| { + list.append(allocator, path) catch return 1; + } + + Parser.user_library_paths = list.toOwnedSlice(allocator) catch return 1; + } + + const stat = std.Io.Dir.cwd().statFile(init.io, target, .{}) catch |err| { + stderr.interface.print( + "Could not access `{s}`: {s}\n", + .{ + target, + @errorName(err), + }, + ) catch @panic("Could not stat run target"); + return 1; + }; + + const root_dir, const file_name = switch (stat.kind) { + .file => .{ res.args.@"root-dir", target }, + .directory => directory_entry: { + const manifest_path = std.fs.path.join(allocator, &.{ target, Package.MANIFEST }) catch { + stderr.interface.writeAll("Could not allocate package manifest path\n") catch {}; + return 1; + }; + defer allocator.free(manifest_path); + + std.Io.Dir.cwd().access(init.io, manifest_path, .{ .read = true }) catch |err| { + stderr.interface.print( + "Could not find `{s}` in `{s}`: {s}\n", + .{ + Package.MANIFEST, + target, + @errorName(err), + }, + ) catch @panic("Could not check package manifest"); + return 1; + }; + + const entry_point = std.fs.path.join(allocator, &.{ target, "src", "main.buzz" }) catch { + stderr.interface.writeAll("Could not allocate package entry point path\n") catch {}; + return 1; + }; + + break :directory_entry .{ target, entry_point }; + }, + else => { + stderr.interface.print( + "`{s}` is not a buzz file or package directory\n", + .{target}, + ) catch @panic("Could not report invalid run target"); + return 1; + }, + }; + defer if (stat.kind == .directory) allocator.free(file_name); + + var perf: ?Perf = if (BuildOptions.show_perf) Perf.init(init.io) else null; + defer if (perf) |*p| p.report(); + + var runner: Runner = undefined; + runner.init( + init, + allocator, + .Run, + null, + if (perf) |*p| p else null, + ) catch { + return 1; + }; + defer runner.deinit(); + + return runner.runFile( + root_dir, + file_name, + script_args, + ) catch { + return 1; + }; } fn initPackage(init: Init) u8 { @@ -469,12 +518,6 @@ fn run( sub_res: anytype, renderer_options: Renderer.Options, ) u8 { - if (command == .@"run-script" and sub_res.positionals[0] == null) { - var stderr = io.stderrWriter(init.io); - stderr.interface.writeAll("Missing script to run\n") catch {}; - return 1; - } - var perf: ?Perf = if (BuildOptions.show_perf) Perf.init(init.io) else null; defer if (perf) |*p| p.report(); @@ -486,7 +529,6 @@ fn run( .@"test" => .Test, .check => .Check, .format => .Fmt, - .run, .@"run-script" => .Run, else => unreachable, }, null, @@ -579,44 +621,6 @@ fn help(init: Init, stderr: *std.Io.Writer, subcommand_opt: ?[]const u8) u8 { .spacing_between_parameters = 0, }, ) catch return 1; - } else if (std.mem.eql(u8, subcommand, "run")) { - clap.usage( - stderr, - clap.Help, - &run_params, - ) catch return 1; - - io.print(init.io, "\n\n", .{}); - - clap.help( - stderr, - clap.Help, - &run_params, - .{ - .description_on_new_line = false, - .description_indent = 4, - .spacing_between_parameters = 0, - }, - ) catch return 1; - } else if (std.mem.eql(u8, subcommand, "run-script")) { - clap.usage( - stderr, - clap.Help, - &run_script_params, - ) catch return 1; - - io.print(init.io, "\n\n", .{}); - - clap.help( - stderr, - clap.Help, - &run_script_params, - .{ - .description_on_new_line = false, - .description_indent = 4, - .spacing_between_parameters = 0, - }, - ) catch return 1; } else if (std.mem.eql(u8, subcommand, "fetch")) { clap.usage( stderr, @@ -678,6 +682,7 @@ fn help(init: Init, stderr: *std.Io.Writer, subcommand_opt: ?[]const u8) u8 { }); } io.print(init.io, "\nUse `buzz help ` for command-specific help.\n", .{}); + io.print(init.io, "Run a script with `buzz ` or a package with `buzz `.\n", .{}); } return 0; diff --git a/tests/behavior/terminal-flow.buzz b/tests/behavior/terminal-flow.buzz new file mode 100644 index 00000000..d9627ecf --- /dev/null +++ b/tests/behavior/terminal-flow.buzz @@ -0,0 +1,76 @@ +import "buzz:std"; + +/// Returns from both branches of an if/else statement. +fun ifElseReturn(flag: bool) > int { + if (flag) { + return 1; + } else { + return 2; + } +} + +/// Either returns a value or throws from the function. +fun returnOrThrow(flag: bool) > int !> str { + if (flag) { + return 3; + } else { + throw "failed"; + } +} + +/// Throws instead of producing the declared return type. +fun throwInsteadOfReturn() > int !> str { + throw "failed"; +} + +/// Never falls through because the loop is statically infinite. +fun infiniteInsteadOfReturn() > int { + while (true) {} +} + +/// Exits an outer constant loop through a labeled break inside a nested loop. +fun labeledBreakOuterLoopFallthrough() > int { + var value = 0; + while (true) :outer { + while (true) { + value = 10; + break outer; + } + } + + return value; +} + +/// Produces a block-expression value from both if/else branches. +fun blockExpressionBranchOut(flag: bool) > int { + return from { + if (flag) { + out 4; + } else { + out 5; + } + }; +} + +/// Produces a block-expression value from either an early out or a fallback out. +fun blockExpressionFallbackOut(flag: bool) > int { + return from { + if (flag) { + out 6; + } + out 7; + }; +} + +test "terminal flow" { + std\assert(ifElseReturn(true) == 1); + std\assert(ifElseReturn(false) == 2); + std\assert(returnOrThrow(true) == 3); + std\assert((returnOrThrow(false) catch 8) == 8); + std\assert((throwInsteadOfReturn() catch 9) == 9); + std\assert(labeledBreakOuterLoopFallthrough() == 10); + std\assert(blockExpressionBranchOut(true) == 4); + std\assert(blockExpressionBranchOut(false) == 5); + std\assert(blockExpressionFallbackOut(true) == 6); + std\assert(blockExpressionFallbackOut(false) == 7); +} diff --git a/tests/compile_errors/block-expression-partial-out.buzz b/tests/compile_errors/block-expression-partial-out.buzz new file mode 100644 index 00000000..d1f4060d --- /dev/null +++ b/tests/compile_errors/block-expression-partial-out.buzz @@ -0,0 +1,10 @@ +// All block expression paths must end with `out` +test "partial block expression out" { + final flag = true; + + _ = from { + if (flag) { + out 1; + } + }; +} diff --git a/tests/compile_errors/early-return.buzz b/tests/compile_errors/early-return.buzz index e070358e..31145683 100644 --- a/tests/compile_errors/early-return.buzz +++ b/tests/compile_errors/early-return.buzz @@ -1,4 +1,4 @@ -// Code after return statement will never be reached +// Code will never be reached import "buzz:std"; test "Early return" { @@ -7,4 +7,4 @@ test "Early return" { return; std\print("Guess I was"); -} \ No newline at end of file +} diff --git a/tests/compile_errors/early-throw.buzz b/tests/compile_errors/early-throw.buzz new file mode 100644 index 00000000..a24cfb69 --- /dev/null +++ b/tests/compile_errors/early-throw.buzz @@ -0,0 +1,11 @@ +// Code will never be reached +/// Throws before reaching the following return statement. +fun fail() > int !> str { + throw "failed"; + + return 1; +} + +test "early throw" { + _ = fail() catch 0; +} diff --git a/tests/compile_errors/labeled-continue-terminal-flow.buzz b/tests/compile_errors/labeled-continue-terminal-flow.buzz new file mode 100644 index 00000000..9252ebe0 --- /dev/null +++ b/tests/compile_errors/labeled-continue-terminal-flow.buzz @@ -0,0 +1,13 @@ +// Code will never be reached +/// Keeps a labeled continue to an outer loop from making an inner do-until fall through. +fun labeledContinueDoesNotExposeLaterBreak() > int { + while (true) :outer { + do { + continue outer; + } until (true) + + break outer; + } +} + +test "labeled continue terminal flow" {} diff --git a/tests/compile_errors/missing-return-one-branch.buzz b/tests/compile_errors/missing-return-one-branch.buzz new file mode 100644 index 00000000..3dc0195f --- /dev/null +++ b/tests/compile_errors/missing-return-one-branch.buzz @@ -0,0 +1,9 @@ +// Missing return statement +/// Returns from only one branch, so the function can still fall through. +fun value(flag: bool) > int { + if (flag) { + return 1; + } +} + +test "missing return one branch" {} diff --git a/tests/compile_errors/multiple-out.buzz b/tests/compile_errors/multiple-out.buzz index e7f7bbe2..ac22e30f 100644 --- a/tests/compile_errors/multiple-out.buzz +++ b/tests/compile_errors/multiple-out.buzz @@ -1,7 +1,7 @@ -// Only one `out` statement is allowed in block expression +// Code will never be reached test "multiple out statements" { from { out "one"; out "two"; }; -} \ No newline at end of file +} diff --git a/tests/compile_errors/out-last-statement.buzz b/tests/compile_errors/out-last-statement.buzz index 4311c76e..c1399d3c 100644 --- a/tests/compile_errors/out-last-statement.buzz +++ b/tests/compile_errors/out-last-statement.buzz @@ -1,8 +1,7 @@ -// Last block expression statement must be `out` +// Code will never be reached test "out must be last statement" { from { out "i should be last"; - "hello world"; }; -} \ No newline at end of file +} diff --git a/tests/compile_errors/terminal-break-unreachable.buzz b/tests/compile_errors/terminal-break-unreachable.buzz new file mode 100644 index 00000000..9773228f --- /dev/null +++ b/tests/compile_errors/terminal-break-unreachable.buzz @@ -0,0 +1,11 @@ +// Code will never be reached +/// Warns after a break statement exits the surrounding loop. +fun unreachableAfterBreak() { + while (true) { + break; + + _ = "unreachable"; + } +} + +test "terminal break unreachable" {} diff --git a/tests/compile_errors/terminal-continue-unreachable.buzz b/tests/compile_errors/terminal-continue-unreachable.buzz new file mode 100644 index 00000000..216b22f9 --- /dev/null +++ b/tests/compile_errors/terminal-continue-unreachable.buzz @@ -0,0 +1,11 @@ +// Code will never be reached +/// Warns after a continue statement starts the next loop iteration. +fun unreachableAfterContinue() { + while (true) { + continue; + + _ = "unreachable"; + } +} + +test "terminal continue unreachable" {} diff --git a/tests/compile_errors/terminal-if-unreachable.buzz b/tests/compile_errors/terminal-if-unreachable.buzz new file mode 100644 index 00000000..bc667468 --- /dev/null +++ b/tests/compile_errors/terminal-if-unreachable.buzz @@ -0,0 +1,17 @@ +// Code will never be reached +import "buzz:std"; + +/// Terminates in both branches, making the following return unreachable. +fun value(flag: bool) > int !> str { + if (flag) { + return 1; + } else { + throw "failed"; + } + + return 2; +} + +test "terminal if unreachable" { + std\assert(value(true) == 1); +} diff --git a/tests/compile_errors/terminal-loop-unreachable.buzz b/tests/compile_errors/terminal-loop-unreachable.buzz new file mode 100644 index 00000000..63557e12 --- /dev/null +++ b/tests/compile_errors/terminal-loop-unreachable.buzz @@ -0,0 +1,9 @@ +// Code will never be reached +/// Never falls through because the loop condition is a constant true. +fun forever() > int { + while (true) {} + + return 1; +} + +test "terminal loop unreachable" {}