fix: broken conditionals

This commit is contained in:
Brett Broadhurst 2026-03-30 05:44:35 -06:00
parent b231e66b49
commit 97a43f63eb
Failed to generate hash of commit
33 changed files with 358 additions and 313 deletions

View file

@ -195,7 +195,6 @@ pub fn generate(gpa: std.mem.Allocator, tree: *const Ast) !Ir {
astgen.extra.items.len += reserved_extra_count;
const fatal = if (tree.errors.len == 0) fatal: {
// TODO: Make sure this is never null.
file(&block, &file_scope, tree.root) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.SemanticError => break :fatal true,
@ -599,18 +598,15 @@ fn setCondBrPayload(
const astgen = then_block.astgen;
const then_body = then_block.instructionsSliceUpto(else_block);
const else_body = else_block.instructionsSlice();
const then_body_len = then_body.len;
const else_body_len = else_body.len;
const extra_len =
@typeInfo(Ir.Inst.CondBr).@"struct".fields.len + then_body_len + else_body_len;
const extra_len = @typeInfo(Ir.Inst.CondBr).@"struct".fields.len + then_body.len + else_body.len;
try astgen.extra.ensureUnusedCapacity(astgen.gpa, extra_len);
const inst_data = &astgen.instructions.items[@intFromEnum(condbr)].data;
inst_data.payload.extra_index = astgen.addExtraAssumeCapacity(
Ir.Inst.CondBr{
.condition = cond,
.then_body_len = @intCast(then_body_len),
.else_body_len = @intCast(else_body_len),
.then_body_len = @intCast(then_body.len),
.else_body_len = @intCast(else_body.len),
},
);
@ -776,229 +772,289 @@ fn expr(gi: *GenIr, scope: *Scope, optional_node: ?*const Ast.Node) InnerError!I
.else_branch => unreachable, // Handled in switchStmt, multiIfStmt, and ifStmt
.content => unreachable,
.inline_logic_expr => unreachable,
.inline_if_stmt => unreachable,
.invalid => unreachable,
}
}
fn exprStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!Ir.Inst.Ref {
if (node.data.bin.lhs) |n| {
return expr(gi, scope, n);
}
const data = node.data.bin;
if (data.lhs) |lhs| return expr(gi, scope, lhs);
return .none;
}
fn inlineLogicExpr(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!Ir.Inst.Ref {
if (node.data.bin.lhs) |lhs| {
return expr(gi, scope, lhs);
}
const data = node.data.bin;
if (data.lhs) |lhs| return expr(gi, scope, lhs);
return .none;
}
fn validateSwitchProngs(gen: *GenIr, stmt_node: *const Ast.Node) InnerError!void {
const astgen = gen.astgen;
var stmt_has_block: bool = false;
var stmt_has_else: bool = false;
const case_list = stmt_node.data.switch_stmt.cases;
const last_prong = case_list[case_list.len - 1];
for (case_list) |case_stmt| {
switch (case_stmt.tag) {
.block_stmt => stmt_has_block = true,
.switch_case, .if_branch => {
if (stmt_has_block) {
//return gen.fail(.expected_else, case_stmt);
}
},
.else_branch => {
if (case_stmt != last_prong) {
return fail(astgen, case_stmt, "invalid else stmt", .{});
}
if (stmt_has_else) {
return fail(astgen, case_stmt, "duplicate else stmt", .{});
}
stmt_has_else = true;
},
inline else => |tag| @panic("Unexpected node " ++ @tagName(tag)),
}
}
}
fn inlineIfStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!Ir.Inst.Ref {
const gpa = gi.astgen.gpa;
const data = node.data.bin;
const cond_expr = data.lhs.?;
fn ifStmt(
parent_block: *GenIr,
scope: *Scope,
stmt_node: *const Ast.Node,
) InnerError!Ir.Inst.Ref {
const astgen = parent_block.astgen;
const cond_expr = stmt_node.data.switch_stmt.condition_expr.?;
try validateSwitchProngs(parent_block, stmt_node);
const case_list = stmt_node.data.switch_stmt.cases;
const then_node = case_list[0];
const last_prong = case_list[case_list.len - 1];
var block_scope = parent_block.makeSubBlock();
var block_scope = gi.makeSubBlock();
defer block_scope.unstack();
const cond_inst = try expr(&block_scope, scope, cond_expr);
const condbr = try block_scope.addCondBr(.condbr);
const block = try parent_block.makePayloadNode(.block);
try block_scope.setBlockBody(block); // unstacks block
try parent_block.instructions.append(astgen.gpa, block);
const block = try gi.makePayloadNode(.block);
var then_block = parent_block.makeSubBlock();
try block_scope.setBlockBody(block);
try gi.instructions.append(gpa, block);
var then_block = gi.makeSubBlock();
defer then_block.unstack();
try blockStmt(&then_block, scope, then_node);
_ = try then_block.addBreak(.@"break", then_node, block);
if (data.rhs) |rhs| {
// TODO: Revisit this. This isn't quite correct.
switch (rhs.tag) {
.content => _ = try contentExpr(&then_block, scope, rhs),
inline else => |tag| @panic("Unexpected node type: " ++ @tagName(tag)),
}
}
if (!then_block.endsWithNoReturn()) {
_ = try then_block.addBreak(.@"break", node, block);
}
var else_block = parent_block.makeSubBlock();
var else_block = gi.makeSubBlock();
defer else_block.unstack();
if (then_node == last_prong) {
_ = try else_block.addBreak(.@"break", then_node, block);
_ = try else_block.addBreak(.@"break", node, block);
try setCondBrPayload(condbr, cond_inst, &then_block, &else_block);
return condbr.toRef();
}
fn validateIfCases(astgen: *AstGen, cases: []const *Ast.Node) InnerError!void {
assert(cases.len != 0);
var seen_else = false;
for (cases, 0..) |case_node, i| {
switch (case_node.tag) {
.block_stmt => {
if (i != 0)
return fail(astgen, case_node, "unexpected block in conditional prong list", .{});
},
.if_branch => {
if (seen_else)
return fail(astgen, case_node, "branch after else is unreachable", .{});
},
.else_branch => {
if (i != cases.len - 1)
return fail(astgen, case_node, "'else' case should always be the final case in conditional", .{});
if (seen_else)
return fail(astgen, case_node, "duplicate else branch", .{});
seen_else = true;
},
else => return fail(astgen, case_node, "unexpected node in conditional prong list", .{}),
}
}
}
fn ifStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!Ir.Inst.Ref {
const astgen = gi.astgen;
const gpa = astgen.gpa;
const data = node.data.switch_stmt;
const cases = data.cases;
const cond_expr = data.condition_expr.?;
try validateIfCases(astgen, cases);
var block_scope = gi.makeSubBlock();
defer block_scope.unstack();
const cond_inst = try expr(&block_scope, scope, cond_expr);
const condbr = try block_scope.addCondBr(.condbr);
const block = try gi.makePayloadNode(.block);
try block_scope.setBlockBody(block);
try gi.instructions.append(gpa, block);
var then_block = gi.makeSubBlock();
defer then_block.unstack();
const then_body = switch (cases[0].tag) {
.block_stmt => cases[0],
.if_branch => cases[0].data.bin.rhs.?,
else => unreachable,
};
try blockStmt(&then_block, scope, then_body);
if (!then_block.endsWithNoReturn()) {
_ = try then_block.addBreak(.@"break", then_body, block);
}
var else_block = gi.makeSubBlock();
defer else_block.unstack();
const else_case = if (cases.len > 1) cases[cases.len - 1] else null;
if (else_case) |else_stmt| {
if (else_stmt.tag == .else_branch) {
const else_body = else_stmt.data.bin.rhs.?;
try blockStmt(&else_block, scope, else_body);
if (!else_block.endsWithNoReturn()) {
_ = try else_block.addBreak(.@"break", else_body, block);
}
}
} else {
const block_node = last_prong.data.bin.rhs.?;
try blockStmt(&else_block, scope, block_node);
_ = try else_block.addBreak(.@"break", then_body, block);
}
try setCondBrPayload(condbr, cond_inst, &then_block, &else_block);
return condbr.toRef();
}
fn ifChain(
parent_block: *GenIr,
scope: *Scope,
branch_list: []const *Ast.Node,
) InnerError!Ir.Inst.Ref {
const gpa = parent_block.astgen.gpa;
if (branch_list.len == 0) return @enumFromInt(0);
if (branch_list[0].data.bin.lhs == null) {
const body_node = branch_list[0].data.bin.rhs.?;
try blockStmt(parent_block, scope, body_node);
return @enumFromInt(0);
}
var block_scope = parent_block.makeSubBlock();
defer block_scope.unstack();
fn ifChain(gi: *GenIr, scope: *Scope, branch_list: []const *Ast.Node) InnerError!void {
const gpa = gi.astgen.gpa;
if (branch_list.len == 0) return;
const branch = branch_list[0];
const cond_expr = branch.data.bin.lhs.?;
const body_node = branch.data.bin.rhs.?;
const cond_inst = try expr(&block_scope, scope, cond_expr);
if (branch.tag == .else_branch) {
const data = branch.data.bin;
try blockStmt(gi, scope, data.rhs.?);
return;
}
const data = branch.data.bin;
const body = data.rhs.?;
assert(branch.tag == .if_branch);
var block_scope = gi.makeSubBlock();
defer block_scope.unstack();
const cond_inst = try expr(&block_scope, scope, data.lhs.?);
const condbr = try block_scope.addCondBr(.condbr);
const block_inst = try parent_block.makePayloadNode(.block);
const block_inst = try gi.makePayloadNode(.block);
try block_scope.setBlockBody(block_inst);
try parent_block.instructions.append(gpa, block_inst);
try gi.instructions.append(gpa, block_inst);
var then_block = parent_block.makeSubBlock();
var then_block = gi.makeSubBlock();
defer then_block.unstack();
try blockStmt(&then_block, scope, body_node);
_ = try then_block.addBreak(.@"break", body_node, block_inst);
var else_block = parent_block.makeSubBlock();
try blockStmt(&then_block, scope, body);
if (!then_block.endsWithNoReturn()) {
_ = try then_block.addBreak(.@"break", body, block_inst);
}
var else_block = gi.makeSubBlock();
defer else_block.unstack();
const next_branches = branch_list[1..];
_ = try ifChain(parent_block, scope, next_branches);
_ = try else_block.addBreak(.@"break", body_node, block_inst);
try ifChain(&else_block, scope, branch_list[1..]);
try setCondBrPayload(condbr, cond_inst, &then_block, &else_block);
return @enumFromInt(0);
}
fn multiIfStmt(
parent_block: *GenIr,
scope: *Scope,
stmt_node: *const Ast.Node,
) InnerError!Ir.Inst.Ref {
try validateSwitchProngs(parent_block, stmt_node);
const branch_list = stmt_node.data.switch_stmt.cases;
if (branch_list[0].data.bin.lhs == null) {
const branch = branch_list[0];
const body_node = branch.data.bin.rhs.?;
try blockStmt(parent_block, scope, body_node);
return .none;
}
_ = try ifChain(parent_block, scope, branch_list);
fn multiIfStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!Ir.Inst.Ref {
const data = node.data.switch_stmt;
try validateIfCases(gi.astgen, data.cases);
try ifChain(gi, scope, data.cases);
return .none;
}
fn switchStmt(
parent_block: *GenIr,
scope: *Scope,
stmt_node: *const Ast.Node,
) InnerError!Ir.Inst.Ref {
const astgen = parent_block.astgen;
fn switchStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!Ir.Inst.Ref {
const astgen = gi.astgen;
const gpa = astgen.gpa;
const switch_stmt = stmt_node.data.switch_stmt;
const data = node.data.switch_stmt;
const cases = data.cases;
var seen_else = false;
try validateSwitchProngs(parent_block, stmt_node);
assert(cases.len > 0);
for (cases, 0..) |case_node, i| {
const is_last = i == cases.len - 1;
switch (case_node.tag) {
.switch_case => {
if (seen_else)
return fail(astgen, case_node, "case after else is unreachable", .{});
},
.else_branch => {
if (!is_last)
return fail(astgen, case_node, "'else' case should always be the final case in conditional", .{});
if (seen_else)
return fail(astgen, case_node, "duplicate else branch", .{});
seen_else = true;
},
else => return fail(astgen, case_node, "unexpected node in switch prong list", .{}),
}
}
const cond_inst = try expr(gi, scope, data.condition_expr);
const switch_br = try gi.makePayloadNode(.switch_br);
const cond_inst = try expr(parent_block, scope, switch_stmt.condition_expr);
const switch_br = try parent_block.makePayloadNode(.switch_br);
var case_indexes: std.ArrayListUnmanaged(u32) = .empty;
try case_indexes.ensureUnusedCapacity(gpa, switch_stmt.cases.len);
try case_indexes.ensureUnusedCapacity(gpa, cases.len);
defer case_indexes.deinit(gpa);
// TODO: Length checks.
const switch_cases = switch_stmt.cases[0 .. switch_stmt.cases.len - 1];
const switch_cases = if (seen_else) cases[0 .. cases.len - 1] else cases;
for (switch_cases) |case_stmt| {
// TODO: Maybe make this non-nullable
const case_expr = case_stmt.data.bin.lhs.?;
assert(case_stmt.tag == .switch_case);
const case_data = case_stmt.data.bin;
const case_expr = case_data.lhs.?;
const operand: Ir.Inst.Ref = switch (case_expr.tag) {
.number_literal => try numberLiteral(parent_block, case_expr),
.number_literal => try numberLiteral(gi, case_expr),
.true_literal => .bool_true,
.false_literal => .bool_false,
else => return fail(astgen, case_expr, "invalid switch case", .{}),
else => return fail(astgen, case_expr, "invalid switch case operand", .{}),
};
var case_block = parent_block.makeSubBlock();
var case_block = gi.makeSubBlock();
defer case_block.unstack();
_ = try blockStmt(&case_block, scope, case_stmt.data.bin.rhs.?);
_ = try case_block.addBreak(.@"break", case_stmt, switch_br);
_ = try blockStmt(&case_block, scope, case_data.rhs.?);
if (!case_block.endsWithNoReturn()) {
_ = try case_block.addBreak(.@"break", case_stmt, switch_br);
}
const body = case_block.instructionsSlice();
const case_extra_len = @typeInfo(Ir.Inst.SwitchBr.Case).@"struct".fields.len + body.len;
try astgen.extra.ensureUnusedCapacity(gpa, case_extra_len);
const extra_index = astgen.addExtraAssumeCapacity(
Ir.Inst.SwitchBr.Case{
.operand = operand,
.body_len = @intCast(body.len),
},
);
const extra_index = astgen.addExtraAssumeCapacity(Ir.Inst.SwitchBr.Case{
.operand = operand,
.body_len = @intCast(body.len),
});
astgen.appendBlockBody(body);
case_indexes.appendAssumeCapacity(extra_index);
}
try parent_block.instructions.append(gpa, switch_br);
try gi.instructions.append(gpa, switch_br);
const else_branch = switch_stmt.cases[switch_stmt.cases.len - 1];
var case_block = parent_block.makeSubBlock();
defer case_block.unstack();
_ = try blockStmt(&case_block, scope, else_branch.data.bin.rhs.?);
_ = try case_block.addBreak(.@"break", else_branch, switch_br);
var else_block = gi.makeSubBlock();
defer else_block.unstack();
const else_body = case_block.instructionsSlice();
if (seen_else) {
const else_branch = cases[cases.len - 1];
assert(else_branch.tag == .else_branch);
const else_data = else_branch.data.bin;
_ = try blockStmt(&else_block, scope, else_data.rhs.?);
if (!else_block.endsWithNoReturn()) {
_ = try else_block.addBreak(.@"break", else_branch, switch_br);
}
} else {
_ = try else_block.addBreak(.@"break", node, switch_br);
}
const else_body = else_block.instructionsSlice();
const extra_len =
@typeInfo(Ir.Inst.SwitchBr).@"struct".fields.len + case_indexes.items.len + else_body.len;
@typeInfo(Ir.Inst.SwitchBr).@"struct".fields.len +
case_indexes.items.len +
else_body.len;
try astgen.extra.ensureUnusedCapacity(gpa, extra_len);
astgen.instructions.items[@intFromEnum(switch_br)].data.payload = .{
.extra_index = astgen.addExtraAssumeCapacity(
Ir.Inst.SwitchBr{
.operand = cond_inst,
.cases_len = @intCast(switch_cases.len),
.else_body_len = @intCast(else_body.len),
},
),
.src_offset = @intCast(stmt_node.loc.start),
.extra_index = astgen.addExtraAssumeCapacity(Ir.Inst.SwitchBr{
.operand = cond_inst,
.cases_len = @intCast(switch_cases.len),
.else_body_len = @intCast(else_body.len),
}),
.src_offset = @intCast(node.loc.start),
};
astgen.extra.appendSliceAssumeCapacity(case_indexes.items[0..]);
astgen.extra.appendSliceAssumeCapacity(case_indexes.items);
astgen.appendBlockBody(else_body);
return switch_br.toRef();
}
fn contentExpr(block: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!Ir.Inst.Ref {
// FIXME: This is a placeholder until we figure out what this function should be returning.
// TODO: Make sure that this is not nullable.
const data = node.data.list;
for (data.items) |child_node| {
switch (child_node.tag) {
@ -1013,6 +1069,7 @@ fn contentExpr(block: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!I
.if_stmt => _ = try ifStmt(block, scope, child_node),
.multi_if_stmt => _ = try multiIfStmt(block, scope, child_node),
.switch_stmt => _ = try switchStmt(block, scope, child_node),
.inline_if_stmt => _ = try inlineIfStmt(block, scope, child_node),
else => unreachable,
}
}
@ -1031,7 +1088,6 @@ fn assignStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!void
const expr_node = node.data.bin.rhs.?;
const name_str = try astgen.strFromNode(identifier_node);
// TODO: Support globals as well
if (scope.lookup(name_str.index)) |decl| {
const expr_result = try expr(gi, scope, expr_node);
_ = try gi.addBinaryNode(.store, decl.inst_index.toRef(), expr_result);
@ -1356,13 +1412,9 @@ fn blockInner(gi: *GenIr, parent_scope: *Scope, stmt_list: []*Ast.Node) !void {
inline else => |e| @panic("Unexpected node: " ++ @tagName(e)),
};
}
if (!gi.endsWithNoReturn()) {
_ = try gi.addUnaryNode(.implicit_ret, .none);
}
}
fn blockStmt(block: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!void {
// TODO: Make sure that this value is concrete to omit check.
const data = node.data.list;
try blockInner(block, scope, data.items);
}
@ -1383,6 +1435,9 @@ fn defaultBlock(
const knot_inst = try decl_scope.makePayloadNode(.decl_knot);
try blockInner(&decl_scope, scope, data.items);
if (!decl_scope.endsWithNoReturn()) {
_ = try decl_scope.addUnaryNode(.implicit_ret, .none);
}
var stub_scope = decl_scope.makeSubBlock();
defer stub_scope.unstack();
@ -1437,6 +1492,9 @@ fn stitchDeclInner(
} else {
try blockInner(&decl_block, scope, &.{});
}
if (!decl_block.endsWithNoReturn()) {
_ = try decl_block.addUnaryNode(.implicit_ret, .none);
}
const decl_str = try astgen.strFromNode(identifier_node);
try setDeclStitchPayload(stitch_inst, &decl_block);
@ -1494,7 +1552,8 @@ fn functionDeclInner(
}
if (body_node) |body| {
try blockStmt(&decl_block, scope, body);
} else {
}
if (!decl_block.endsWithNoReturn()) {
_ = try decl_block.addUnaryNode(.implicit_ret, .none);
}
@ -1591,7 +1650,6 @@ fn file(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!void {
var file_scope = gi.makeSubBlock();
defer file_scope.unstack();
// TODO: Make sure this is non-nullable.
if (data.items.len > 0) {
const first_child = data.items[0];
if (first_child.tag == .block_stmt) {