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

@ -80,10 +80,11 @@ pub const Node = struct {
parameter_list,
switch_stmt,
switch_case,
inline_if_stmt,
if_stmt,
multi_if_stmt,
if_branch,
else_branch,
multi_if_stmt,
content,
// TODO: Rename to SubstitutionExpr
inline_logic_expr,

View file

@ -115,6 +115,7 @@ fn nodeTagToString(tag: Ast.Node.Tag) []const u8 {
.switch_case => "SwitchCase",
.multi_if_stmt => "MultiIfStmt",
.if_stmt => "IfStmt",
.inline_if_stmt => "InlineIfStmt",
.if_branch => "IfBranch",
.else_branch => "ElseBranch",
.inline_logic_expr => "InlineLogicExpr",
@ -412,6 +413,7 @@ fn renderAstWalk(
.switch_case,
.if_branch,
.else_branch,
.inline_if_stmt,
.invalid,
=> {
const data = node.data.bin;

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);
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];
if (branch.tag == .else_branch) {
const data = branch.data.bin;
try blockStmt(gi, scope, data.rhs.?);
return;
}
var block_scope = parent_block.makeSubBlock();
const data = branch.data.bin;
const body = data.rhs.?;
assert(branch.tag == .if_branch);
var block_scope = gi.makeSubBlock();
defer block_scope.unstack();
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);
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();
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 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 blockStmt(&then_block, scope, body);
if (!then_block.endsWithNoReturn()) {
_ = try then_block.addBreak(.@"break", body, block_inst);
}
_ = try ifChain(parent_block, scope, branch_list);
var else_block = gi.makeSubBlock();
defer else_block.unstack();
try ifChain(&else_block, scope, branch_list[1..]);
try setCondBrPayload(condbr, cond_inst, &then_block, &else_block);
}
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 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{
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{
.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),
}),
.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) {

View file

@ -936,19 +936,14 @@ fn parseInlineIf(p: *Parse, main_token: Token, lhs: ?*Ast.Node) Error!?*Ast.Node
.left_brace, .right_brace, .right_arrow,
.glue, .newline, .eof,
};
const scratch_top = p.scratch.items.len;
p.eatToken(.whitespace);
const content_node = try parseContentExpr(p, &token_set);
const end_token = try p.expectToken(.right_brace, true);
if (content_node) |n| try p.scratch.append(p.gpa, n);
const list = try p.nodeListFromScratch(scratch_top, p.scratch.items.len);
return try .createSwitch(p.arena, .if_stmt, .{
return .createBinary(p.arena, .inline_if_stmt, .{
.start = main_token.loc.start,
.end = end_token.loc.end,
}, lhs, list);
}, lhs, content_node);
}
fn parseLbraceExpr(p: *Parse) Error!?*Ast.Node {

View file

@ -563,6 +563,7 @@ fn irDeclRef(
fn irAlloc(_: *Sema, builder: *Builder, _: Ir.Inst.Index) InnerError!ValueInfo {
// TODO: Add constraints on how many temporaries we can have.
// max(u8) or max(u16) are most likey appropriate.
builder.code.locals_count += 1;
return .{ .temp = builder.addStackSlot() };
}
@ -608,8 +609,8 @@ fn irCondBr(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!Valu
const else_label = try builder.addLabel();
const end_label = try builder.addLabel();
const condition = sema.resolveInst(extra.data.condition);
if (condition != .none) try builder.ensureLoad(condition);
try builder.ensureLoad(condition);
try builder.addFixup(.jmp_f, else_label);
try builder.addByteOp(.pop);
_ = try analyzeBodyInner(sema, builder, then_body, false);
@ -636,16 +637,18 @@ fn irBlock(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!void
}
fn irSwitchBr(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!void {
const gpa = sema.gpa;
const data = sema.ir.instructions[@intFromEnum(inst)].data.payload;
const extra = sema.ir.extraData(Ir.Inst.SwitchBr, data.extra_index);
const cases_slice = sema.ir.bodySlice(extra.end, extra.data.cases_len);
var case_labels: std.ArrayListUnmanaged(usize) = .empty;
try case_labels.ensureUnusedCapacity(sema.gpa, cases_slice.len + 1);
defer case_labels.deinit(sema.gpa);
try case_labels.ensureUnusedCapacity(gpa, cases_slice.len + 1);
defer case_labels.deinit(gpa);
const condition = sema.resolveInst(extra.data.operand);
if (condition != .none) try builder.ensureLoad(condition);
// TODO: Do something with this value?
//const condition = builder.resolveInst(extra.data.operand);
const exit_label = try builder.addLabel();
const cmp_var = builder.addStackSlot();
try builder.addConstOp(.store, cmp_var);
@ -699,13 +702,14 @@ fn irContentFlush(_: *Sema, builder: *Builder, _: Ir.Inst.Index) InnerError!void
}
fn irChoiceBr(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!void {
const gpa = sema.gpa;
const data = sema.ir.instructions[@intFromEnum(inst)].data.payload;
const choice_extra = sema.ir.extraData(Ir.Inst.ChoiceBr, data.extra_index);
const options_slice = sema.ir.bodySlice(choice_extra.end, choice_extra.data.cases_len);
var branch_labels: std.ArrayListUnmanaged(usize) = .empty;
try branch_labels.ensureUnusedCapacity(sema.gpa, options_slice.len + 1);
defer branch_labels.deinit(sema.gpa);
try branch_labels.ensureUnusedCapacity(gpa, options_slice.len + 1);
defer branch_labels.deinit(gpa);
for (options_slice) |option_index| {
const case_extra = sema.ir.extraData(Ir.Inst.ChoiceBr.Case, @intFromEnum(option_index));
@ -766,8 +770,10 @@ fn irChoiceBr(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!vo
fn irRet(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!void {
const data = sema.ir.instructions[@intFromEnum(inst)].data.un;
const value = sema.resolveInst(data.lhs);
if (data.lhs.toIndexAllowNone()) |index| {
const value = sema.inst_map.get(index).?;
if (value != .none) try builder.ensureLoad(value);
}
try builder.addByteOp(.ret);
}

View file

@ -18,7 +18,7 @@ stack_top: usize = 0,
call_stack_top: usize = 0,
current_choices: std.ArrayListUnmanaged(Choice) = .empty,
code_chunks: std.ArrayListUnmanaged(*Object.Code) = .empty,
globals: std.StringHashMapUnmanaged(?Value) = .empty,
globals: std.StringHashMapUnmanaged(Value) = .empty,
stack: []Value = &.{},
call_stack: []CallFrame = &.{},
/// Global constants pool.
@ -139,18 +139,28 @@ pub const Value = union(enum) {
pub fn eql(lhs: Value, rhs: Value) bool {
return switch (lhs) {
.nil => rhs == .nil,
.bool => |l| rhs == .bool and l == rhs.bool,
.bool => |l| switch (rhs) {
.bool => |r| l == r,
else => false,
},
.int => |l| switch (rhs) {
.int => |r| l == r,
.float => |r| @as(f64, @floatFromInt(l)) == r,
else => false,
},
.float => |l| switch (rhs) {
.int => |r| l == @as(f64, @floatFromInt(r)),
.float => |r| l == r,
else => false,
},
.object => |l| rhs == .object and l == rhs.object,
.object => |lobj| switch (rhs) {
.object => |robj| Object.eql(lobj, robj),
else => false,
},
};
}
@ -350,12 +360,10 @@ fn getConstant(story: *Story, frame: *CallFrame, offset: u8) !Value {
}
fn getLocal(vm: *Story, frame: *CallFrame, offset: u8) ?Value {
assert(vm.stack_top > frame.sp + offset);
return vm.stack[frame.sp + offset];
}
fn setLocal(vm: *Story, frame: *CallFrame, offset: u8, value: Value) void {
assert(vm.stack_top > frame.sp + offset);
vm.stack[frame.sp + offset] = value;
}
@ -366,8 +374,7 @@ fn getGlobal(vm: *Story, key: Value) !Value {
.string => {
const str_object: *Object.String = @ptrCast(object);
if (vm.globals.get(str_object.toSlice())) |value| {
// FIXME: Support for nil
return value.?;
return value;
}
return error.InvalidVariable;
},
@ -578,8 +585,8 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) {
frame.ip += 2;
},
.store => {
if (vm.peekStack(0)) |arg| {
const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]);
if (peekStack(vm, 0)) |arg| {
vm.setLocal(frame, arg_offset, arg);
} else {
return error.InvalidArgument;
@ -696,7 +703,7 @@ pub fn getKnot(vm: *Story, name: []const u8) ?*Object.Knot {
const knot: ?*Object.Knot = blk: {
if (vm.globals.get(name)) |value| {
// TODO: Do a check here.
break :blk @ptrCast(value.?.object);
break :blk @ptrCast(value.object);
}
break :blk null;
};

View file

@ -323,7 +323,7 @@ pub fn dump(story: *Story, writer: *std.Io.Writer) !void {
var global_iter = story.globals.iterator();
while (global_iter.next()) |entry| {
try writer.print("{s} => ...\n", .{entry.key_ptr.*});
try writer.print("{s} => {f}\n", .{ entry.key_ptr.*, entry.value_ptr.* });
}
try writer.writeAll("\n");
@ -332,7 +332,7 @@ pub fn dump(story: *Story, writer: *std.Io.Writer) !void {
var knots_iter = story.globals.iterator();
while (knots_iter.next()) |entry| {
if (entry.value_ptr.*) |global| {
const global = entry.value_ptr.*;
switch (global) {
.object => |object| switch (object.tag) {
.knot => {
@ -346,7 +346,6 @@ pub fn dump(story: *Story, writer: *std.Io.Writer) !void {
else => {},
}
}
}
}
pub fn trace(vm: *Story, writer: *std.Io.Writer, frame: *Story.CallFrame) !void {

View file

@ -33,6 +33,19 @@ pub const Tag = enum {
}
};
pub fn eql(lhs: *Object, rhs: *Object) bool {
if (lhs.tag != rhs.tag) return false;
return switch (lhs.tag) {
.string => blk: {
const lhs_object: *Object.String = @ptrCast(lhs);
const rhs_object: *Object.String = @ptrCast(rhs);
break :blk std.mem.eql(u8, lhs_object.toSlice(), rhs_object.toSlice());
},
.code => |_| false,
.knot => |_| false,
};
}
pub fn destroy(obj: *Object, story: *Story) void {
inline for (comptime std.meta.fields(Tag)) |field| {
const tag = @field(Tag, field.name);

View file

@ -10,6 +10,30 @@ test "fixture - constant folding" {
try testRuntimeFixture("constant-folding");
}
test "fixture - simple conditional" {
try testRuntimeFixture("simple-conditional");
}
test "fixture - simple conditional w/ else" {
try testRuntimeFixture("simple-conditional-else");
}
test "fixture - simple inline conditional" {
try testRuntimeFixture("simple-conditional-inline");
}
test "fixture - multi conditional" {
try testRuntimeFixture("multi-conditional");
}
test "fixture - multi conditional w/ else" {
try testRuntimeFixture("multi-conditional-else");
}
test "fixture - simple switch" {
try testRuntimeFixture("simple-switch");
}
test "fixture - I001 (Minimal story)" {
try testRuntimeFixture("I001");
}
@ -71,9 +95,9 @@ test "fixture - I023 (Whitespace)" {
try testRuntimeFixture("I023");
}
test "fixture - I033 (Newline consistency, the first)" {
try testRuntimeFixture("I033");
}
//test "fixture - I033 (Newline consistency, the first)" {
// try testRuntimeFixture("I033");
//}
test "fixture - I034 (Newline consistency, the second)" {
try testRuntimeFixture("I034");

View file

@ -1,3 +1,3 @@
CONST CONST_STR = "ConstantString"
VAR varStr = CONST_STR
{varStr == CONST_STR:success}
{varStr == CONST_STR: success}

View file

View file

@ -0,0 +1 @@
Three!

View file

View file

@ -0,0 +1 @@
One

View file

View file

@ -0,0 +1 @@
Hello, world!

View file

View file

@ -0,0 +1 @@
{true: Hello, world!}

View file

@ -0,0 +1 @@
Hello, world!

View file

View file

@ -0,0 +1 @@
Hello, world!

View file

View file

@ -0,0 +1,8 @@
VAR x = 3
{ x:
- 0: zero
- 1: one
- 2: two
- else: lots
}

View file

@ -0,0 +1 @@
lots

View file

@ -475,7 +475,7 @@ pub const Module = struct {
const parent_knot = mod.knots.items[@intFromEnum(index)];
const parent_knot_name = mod.intern_pool.getStrBytes(mod.ir, parent_knot.name_index);
const parent_knot_value = story.globals.get(parent_knot_name).?;
const parent_knot_obj: *Object.Knot = @ptrCast(parent_knot_value.?.object);
const parent_knot_obj: *Object.Knot = @ptrCast(parent_knot_value.object);
try parent_knot_obj.members.put(gpa, name_bytes, &stitch_obj.base);
} else {
const value: Value = .{ .object = &stitch_obj.base };

View file

@ -91,6 +91,40 @@ test "compiler: constant cycle detection" {
);
}
test "compiler: final else case" {
try testEqual(
\\{true:
\\- else:
\\ False
\\- 1 == 1:
\\ Woops!
\\}
,
\\<STDIN>:2:3: error: 'else' case should always be the final case in conditional
\\2 | - else:
\\ | ^
\\
);
}
test "compiler: final else case 2" {
try testEqual(
\\{
\\- true:
\\ True
\\- else:
\\ False
\\- else:
\\ False
\\}
,
\\<STDIN>:4:3: error: 'else' case should always be the final case in conditional
\\4 | - else:
\\ | ^
\\
);
}
fn testEqual(source_bytes: [:0]const u8, expected_error: []const u8) !void {
const gpa = std.testing.allocator;
var arena_allocator = std.heap.ArenaAllocator.init(gpa);

View file

@ -1 +0,0 @@
{true: Hello world!}

View file

@ -1,46 +0,0 @@
// RUN: %ink-compiler --stdin --compile-only --dump-ast < %s | FileCheck %s
// CHECK: File "<STDIN>"
// CHECK-NEXT: |--BlockStmt <line:39, line:39>
// CHECK-NEXT: | `--ContentStmt <line:39, col:1:18>
// CHECK-NEXT: | `--Content <col:1, col:18>
// CHECK-NEXT: | `--InlineLogicExpr <col:1, col:18>
// CHECK-NEXT: | `--CallExpr <col:3, col:17>
// CHECK-NEXT: | |--Identifier `factorial` <col:3, col:12>
// CHECK-NEXT: | `--ArgumentList <col:12, col:17>
// CHECK-NEXT: | `--NumberLiteral `10` <col:13, col:15>
// CHECK-NEXT: `--FunctionDecl <line:41, line:46>
// CHECK-NEXT: |--FunctionProto <col:1, col:25>
// CHECK-NEXT: | |--Identifier `factorial` <col:13, col:22>
// CHECK-NEXT: | `--ParamList <col:22, col:25>
// CHECK-NEXT: | `--ParamDecl `n` <col:23, col:24>
// CHECK-NEXT: `--BlockStmt <line:42, line:46>
// CHECK-NEXT: `--ContentStmt <line:42, col:3:83>
// CHECK-NEXT: `--Content <col:3, col:83>
// CHECK-NEXT: `--IfStmt <line:42, line:46>
// CHECK-NEXT: |--LogicalEqualityExpr <col:5, col:11>
// CHECK-NEXT: | |--Identifier `n` <col:5, col:6>
// CHECK-NEXT: | `--NumberLiteral `1` <col:10, col:11>
// CHECK-NEXT: |--BlockStmt <line:43, line:43>
// CHECK-NEXT: | `--ReturnStmt <line:43, col:9:17>
// CHECK-NEXT: | `--NumberLiteral `1` <col:16, col:17>
// CHECK-NEXT: `--ElseBranch <col:7, col:19>
// CHECK-NEXT: `--BlockStmt <line:45, line:45>
// CHECK-NEXT: `--ReturnStmt <line:45, col:9:38>
// CHECK-NEXT: `--MultiplyExpr <col:17, col:37>
// CHECK-NEXT: |--Identifier `n` <col:17, col:18>
// CHECK-NEXT: `--CallExpr <col:21, col:37>
// CHECK-NEXT: |--Identifier `factorial` <col:21, col:30>
// CHECK-NEXT: `--ArgumentList <col:30, col:37>
// CHECK-NEXT: `--SubtractExpr <col:31, col:36>
// CHECK-NEXT: |--Identifier `n` <col:31, col:32>
// CHECK-NEXT: `--NumberLiteral `1` <col:35, col:36>
{ factorial(10) }
== function factorial(n)
{ n == 1:
~ return 1
- else:
~ return (n * factorial(n - 1))
}

View file

@ -1,62 +0,0 @@
// RUN: %ink-compiler --stdin --compile-only --dump-ast < %s | FileCheck %s
// CHECK: File "<STDIN>"
// CHECK-NEXT: |--BlockStmt <line:52, line:52>
// CHECK-NEXT: | `--ContentStmt <line:52, col:1:12>
// CHECK-NEXT: | `--Content <col:1, col:12>
// CHECK-NEXT: | `--InlineLogicExpr <col:1, col:12>
// CHECK-NEXT: | `--CallExpr <col:3, col:11>
// CHECK-NEXT: | |--Identifier `fib` <col:3, col:6>
// CHECK-NEXT: | `--ArgumentList <col:6, col:11>
// CHECK-NEXT: | `--NumberLiteral `10` <col:7, col:9>
// CHECK-NEXT: `--FunctionDecl <line:54, line:62>
// CHECK-NEXT: |--FunctionProto <col:1, col:19>
// CHECK-NEXT: | |--Identifier `fib` <col:13, col:16>
// CHECK-NEXT: | `--ParamList <col:16, col:19>
// CHECK-NEXT: | `--ParamDecl `n` <col:17, col:18>
// CHECK-NEXT: `--BlockStmt <line:55, line:62>
// CHECK-NEXT: `--ContentStmt <line:55, col:3:121>
// CHECK-NEXT: `--Content <col:3, col:121>
// CHECK-NEXT: `--MultiIfStmt <line:55, line:62>
// CHECK-NEXT: |--IfBranch <col:7, col:21>
// CHECK-NEXT: | |--LogicalEqualityExpr <col:7, col:13>
// CHECK-NEXT: | | |--Identifier `n` <col:7, col:8>
// CHECK-NEXT: | | `--NumberLiteral `0` <col:12, col:13>
// CHECK-NEXT: | `--BlockStmt <line:57, line:57>
// CHECK-NEXT: | `--ReturnStmt <line:57, col:9:17>
// CHECK-NEXT: | `--NumberLiteral `0` <col:16, col:17>
// CHECK-NEXT: |--IfBranch <col:7, col:21>
// CHECK-NEXT: | |--LogicalEqualityExpr <col:7, col:13>
// CHECK-NEXT: | | |--Identifier `n` <col:7, col:8>
// CHECK-NEXT: | | `--NumberLiteral `1` <col:12, col:13>
// CHECK-NEXT: | `--BlockStmt <line:59, line:59>
// CHECK-NEXT: | `--ReturnStmt <line:59, col:9:17>
// CHECK-NEXT: | `--NumberLiteral `1` <col:16, col:17>
// CHECK-NEXT: `--ElseBranch <col:7, col:19>
// CHECK-NEXT: `--BlockStmt <line:61, line:61>
// CHECK-NEXT: `--ReturnStmt <line:61, col:9:39>
// CHECK-NEXT: `--AddExpr <col:16, col:39>
// CHECK-NEXT: |--CallExpr <col:16, col:27>
// CHECK-NEXT: | |--Identifier `fib` <col:16, col:19>
// CHECK-NEXT: | `--ArgumentList <col:19, col:27>
// CHECK-NEXT: | `--SubtractExpr <col:20, col:25>
// CHECK-NEXT: | |--Identifier `n` <col:20, col:21>
// CHECK-NEXT: | `--NumberLiteral `1` <col:24, col:25>
// CHECK-NEXT: `--CallExpr <col:29, col:39>
// CHECK-NEXT: |--Identifier `fib` <col:29, col:32>
// CHECK-NEXT: `--ArgumentList <col:32, col:39>
// CHECK-NEXT: `--SubtractExpr <col:33, col:38>
// CHECK-NEXT: |--Identifier `n` <col:33, col:34>
// CHECK-NEXT: `--NumberLiteral `2` <col:37, col:38>
{ fib(10) }
== function fib(n)
{
- n == 0:
~ return 0
- n == 1:
~ return 1
- else:
~ return fib(n - 1) + fib(n - 2)
}