stage2: fix while loops

also start to form a plan for how inline while loops will work
This commit is contained in:
Andrew Kelley 2021-03-23 21:37:10 -07:00
parent bf7c3e9355
commit 13ced07f23
5 changed files with 158 additions and 125 deletions

View File

@ -700,14 +700,7 @@ pub const Scope = struct {
/// It is shared among all the blocks in an inline or comptime called
/// function.
pub const Inlining = struct {
/// Shared state among the entire inline/comptime call stack.
shared: *Shared,
merges: Merges,
pub const Shared = struct {
caller: ?*Fn,
branch_count: u32,
};
};
pub const Merges = struct {
@ -2015,6 +2008,7 @@ fn astgenAndSemaDecl(mod: *Module, decl: *Decl) !bool {
.inst_map = try analysis_arena.allocator.alloc(*ir.Inst, code.instructions.len),
.owner_decl = decl,
.func = null,
.owner_func = null,
.param_inst_list = &.{},
};
var block_scope: Scope.Block = .{
@ -2236,6 +2230,7 @@ fn astgenAndSemaFn(
.inst_map = try fn_type_scope_arena.allocator.alloc(*ir.Inst, fn_type_code.instructions.len),
.owner_decl = decl,
.func = null,
.owner_func = null,
.param_inst_list = &.{},
};
var block_scope: Scope.Block = .{
@ -2544,6 +2539,7 @@ fn astgenAndSemaVarDecl(
.inst_map = try gen_scope_arena.allocator.alloc(*ir.Inst, code.instructions.len),
.owner_decl = decl,
.func = null,
.owner_func = null,
.param_inst_list = &.{},
};
var block_scope: Scope.Block = .{
@ -2608,6 +2604,7 @@ fn astgenAndSemaVarDecl(
.inst_map = try type_scope_arena.allocator.alloc(*ir.Inst, code.instructions.len),
.owner_decl = decl,
.func = null,
.owner_func = null,
.param_inst_list = &.{},
};
var block_scope: Scope.Block = .{
@ -3192,6 +3189,7 @@ pub fn analyzeFnBody(mod: *Module, decl: *Decl, func: *Fn) !void {
.inst_map = try mod.gpa.alloc(*ir.Inst, func.zir.instructions.len),
.owner_decl = decl,
.func = func,
.owner_func = func,
.param_inst_list = param_inst_list,
};
defer mod.gpa.free(sema.inst_map);
@ -3681,20 +3679,11 @@ pub fn failWithOwnedErrorMsg(mod: *Module, scope: *Scope, err_msg: *ErrorMsg) In
switch (scope.tag) {
.block => {
const block = scope.cast(Scope.Block).?;
if (block.inlining) |inlining| {
if (inlining.shared.caller) |func| {
func.state = .sema_failure;
} else {
block.sema.owner_decl.analysis = .sema_failure;
block.sema.owner_decl.generation = mod.generation;
}
if (block.sema.owner_func) |func| {
func.state = .sema_failure;
} else {
if (block.sema.func) |func| {
func.state = .sema_failure;
} else {
block.sema.owner_decl.analysis = .sema_failure;
block.sema.owner_decl.generation = mod.generation;
}
block.sema.owner_decl.analysis = .sema_failure;
block.sema.owner_decl.generation = mod.generation;
}
mod.failed_decls.putAssumeCapacityNoClobber(block.sema.owner_decl, err_msg);
},

View File

@ -17,6 +17,12 @@ inst_map: []*Inst,
/// and `src_decl` of `Scope.Block` is the `Decl` of the callee.
/// This `Decl` owns the arena memory of this `Sema`.
owner_decl: *Decl,
/// For an inline or comptime function call, this will be the root parent function
/// which contains the callsite. Corresponds to `owner_decl`.
owner_func: ?*Module.Fn,
/// The function this ZIR code is the body of, according to the source code.
/// This starts out the same as `owner_func` and then diverges in the case of
/// an inline or comptime function call.
func: ?*Module.Fn,
/// For now, TZIR requires arg instructions to be the first N instructions in the
/// TZIR code. We store references here for the purpose of `resolveInst`.
@ -26,6 +32,7 @@ func: ?*Module.Fn,
/// > param_count: u32
param_inst_list: []const *ir.Inst,
branch_quota: u32 = 1000,
branch_count: u32 = 0,
/// This field is updated when a new source location becomes active, so that
/// instructions which do not have explicitly mapped source locations still have
/// access to the source location set by the previous instruction which did
@ -86,6 +93,7 @@ pub fn analyzeBody(sema: *Sema, block: *Scope.Block, body: []const zir.Inst.Inde
const map = block.sema.inst_map;
const tags = block.sema.code.instructions.items(.tag);
const datas = block.sema.code.instructions.items(.data);
// We use a while(true) loop here to avoid a redundant way of breaking out of
// the loop. The only way to break out of the loop is with a `noreturn`
@ -178,6 +186,7 @@ pub fn analyzeBody(sema: *Sema, block: *Scope.Block, body: []const zir.Inst.Inde
.is_non_null_ptr => try sema.zirIsNullPtr(block, inst, true),
.is_null => try sema.zirIsNull(block, inst, false),
.is_null_ptr => try sema.zirIsNullPtr(block, inst, false),
.loop => try sema.zirLoop(block, inst),
.merge_error_sets => try sema.zirMergeErrorSets(block, inst),
.mod_rem => try sema.zirArithmetic(block, inst),
.mul => try sema.zirArithmetic(block, inst),
@ -225,7 +234,7 @@ pub fn analyzeBody(sema: *Sema, block: *Scope.Block, body: []const zir.Inst.Inde
.ret_node => return sema.zirRetNode(block, inst),
.ret_tok => return sema.zirRetTok(block, inst, false),
.@"unreachable" => return sema.zirUnreachable(block, inst),
.loop => return sema.zirLoop(block, inst),
.repeat => return sema.zirRepeat(block, inst),
// Instructions that we know can *never* be noreturn based solely on
// their tag. We avoid needlessly checking if they are noreturn and
@ -276,6 +285,14 @@ pub fn analyzeBody(sema: *Sema, block: *Scope.Block, body: []const zir.Inst.Inde
try sema.zirResolveInferredAlloc(block, inst);
continue;
},
// Special case: send comptime control flow back to the beginning of this block.
.repeat_inline => {
const src: LazySrcLoc = .{ .node_offset = datas[inst].node };
try sema.emitBackwardBranch(block, src);
i = 0;
continue;
},
};
if (map[inst].ty.isNoReturn())
return always_noreturn;
@ -764,14 +781,50 @@ fn zirCompileLog(sema: *Sema, block: *Scope.Block, inst: zir.Inst.Index) InnerEr
}
}
fn zirLoop(sema: *Sema, parent_block: *Scope.Block, inst: zir.Inst.Index) InnerError!zir.Inst.Ref {
fn zirRepeat(sema: *Sema, block: *Scope.Block, inst: zir.Inst.Index) InnerError!zir.Inst.Ref {
const tracy = trace(@src());
defer tracy.end();
const src_node = sema.code.instructions.items(.data)[inst].node;
const src: LazySrcLoc = .{ .node_offset = src_node };
try sema.requireRuntimeBlock(block, src);
return always_noreturn;
}
fn zirLoop(sema: *Sema, parent_block: *Scope.Block, inst: zir.Inst.Index) InnerError!*Inst {
const tracy = trace(@src());
defer tracy.end();
const inst_data = sema.code.instructions.items(.data)[inst].pl_node;
const src = inst_data.src();
const extra = sema.code.extraData(zir.Inst.MultiOp, inst_data.payload_index);
const body = sema.code.extra[extra.end..][0..extra.data.operands_len];
const extra = sema.code.extraData(zir.Inst.Block, inst_data.payload_index);
const body = sema.code.extra[extra.end..][0..extra.data.body_len];
// TZIR expects a block outside the loop block too.
const block_inst = try sema.arena.create(Inst.Block);
block_inst.* = .{
.base = .{
.tag = Inst.Block.base_tag,
.ty = undefined,
.src = src,
},
.body = undefined,
};
var child_block = parent_block.makeSubBlock();
child_block.label = Scope.Block.Label{
.zir_block = inst,
.merges = .{
.results = .{},
.br_list = .{},
.block_inst = block_inst,
},
};
const merges = &child_block.label.?.merges;
defer child_block.instructions.deinit(sema.gpa);
defer merges.results.deinit(sema.gpa);
defer merges.br_list.deinit(sema.gpa);
// Reserve space for a Loop instruction so that generated Break instructions can
// point to it, even if it doesn't end up getting used because the code ends up being
@ -786,23 +839,17 @@ fn zirLoop(sema: *Sema, parent_block: *Scope.Block, inst: zir.Inst.Index) InnerE
.body = undefined,
};
var child_block: Scope.Block = .{
.parent = parent_block,
.sema = sema,
.src_decl = parent_block.src_decl,
.instructions = .{},
.inlining = parent_block.inlining,
.is_comptime = parent_block.is_comptime,
};
defer child_block.instructions.deinit(sema.gpa);
var loop_block = child_block.makeSubBlock();
defer loop_block.instructions.deinit(sema.gpa);
_ = try sema.analyzeBody(&child_block, body);
_ = try sema.analyzeBody(&loop_block, body);
// Loop repetition is implied so the last instruction may or may not be a noreturn instruction.
try parent_block.instructions.append(sema.gpa, &loop_inst.base);
loop_inst.body = .{ .instructions = try sema.arena.dupe(*Inst, child_block.instructions.items) };
return always_noreturn;
try child_block.instructions.append(sema.gpa, &loop_inst.base);
loop_inst.body = .{ .instructions = try sema.arena.dupe(*Inst, loop_block.instructions.items) };
return sema.analyzeBlockBody(parent_block, &child_block, merges);
}
fn zirBlock(
@ -1160,16 +1207,9 @@ fn analyzeCall(
},
.body = undefined,
};
// If this is the top of the inline/comptime call stack, we use this data.
// Otherwise we pass on the shared data from the parent scope.
var shared_inlining: Scope.Block.Inlining.Shared = .{
.branch_count = 0,
.caller = sema.func,
};
// This one is shared among sub-blocks within the same callee, but not
// shared among the entire inline/comptime call stack.
var inlining: Scope.Block.Inlining = .{
.shared = if (block.inlining) |inlining| inlining.shared else &shared_inlining,
.merges = .{
.results = .{},
.br_list = .{},
@ -1183,8 +1223,11 @@ fn analyzeCall(
.code = module_fn.zir,
.inst_map = try sema.gpa.alloc(*ir.Inst, module_fn.zir.instructions.len),
.owner_decl = sema.owner_decl,
.owner_func = sema.owner_func,
.func = module_fn,
.param_inst_list = casted_args,
.branch_quota = sema.branch_quota,
.branch_count = sema.branch_count,
};
defer sema.gpa.free(inline_sema.inst_map);
@ -1210,7 +1253,12 @@ fn analyzeCall(
// the block_inst above.
_ = try inline_sema.root(&child_block);
break :res try inline_sema.analyzeBlockBody(block, &child_block, merges);
const result = try inline_sema.analyzeBlockBody(block, &child_block, merges);
sema.branch_quota = inline_sema.branch_quota;
sema.branch_count = inline_sema.branch_count;
break :res result;
} else res: {
try sema.requireRuntimeBlock(block, call_src);
break :res try block.addCall(call_src, ret_type, func, casted_args);
@ -3169,9 +3217,8 @@ fn safetyPanic(sema: *Sema, block: *Scope.Block, src: LazySrcLoc, panic_id: Pani
}
fn emitBackwardBranch(sema: *Sema, block: *Scope.Block, src: LazySrcLoc) !void {
const shared = block.inlining.?.shared;
shared.branch_count += 1;
if (shared.branch_count > sema.branch_quota) {
sema.branch_count += 1;
if (sema.branch_count > sema.branch_quota) {
// TODO show the "called from here" stack
return sema.mod.fail(&block.base, src, "evaluation exceeded {d} backwards branches", .{sema.branch_quota});
}

View File

@ -1055,6 +1055,7 @@ fn blockExprStmts(
.bit_or,
.block,
.block_comptime,
.loop,
.bool_br_and,
.bool_br_or,
.bool_not,
@ -1156,12 +1157,13 @@ fn blockExprStmts(
.ret_tok,
.ret_coerce,
.@"unreachable",
.loop,
.elided,
.store,
.store_to_block_ptr,
.store_to_inferred_ptr,
.resolve_inferred_alloc,
.repeat,
.repeat_inline,
=> break :b true,
}
} else switch (maybe_unused_result) {
@ -2145,20 +2147,16 @@ fn whileExpr(
node: ast.Node.Index,
while_full: ast.full.While,
) InnerError!zir.Inst.Ref {
if (true) @panic("TODO update for zir-memory-layout");
if (while_full.label_token) |label_token| {
try checkLabelRedefinition(mod, scope, label_token);
}
if (while_full.inline_token) |inline_token| {
return mod.failTok(scope, inline_token, "TODO inline while", .{});
}
const parent_gz = scope.getGenZir();
const loop_block = try parent_gz.addBlock(.loop, node);
try parent_gz.instructions.append(mod.gpa, loop_block);
var loop_scope: Scope.GenZir = .{
.parent = scope,
.decl = scope.ownerDecl().?,
.arena = scope.arena(),
.zir_code = parent_gz.zir_code,
.force_comptime = parent_gz.force_comptime,
.instructions = .{},
};
@ -2167,21 +2165,12 @@ fn whileExpr(
var continue_scope: Scope.GenZir = .{
.parent = &loop_scope.base,
.decl = loop_scope.decl,
.arena = loop_scope.arena,
.zir_code = parent_gz.zir_code,
.force_comptime = loop_scope.force_comptime,
.instructions = .{},
};
defer continue_scope.instructions.deinit(mod.gpa);
const tree = gz.tree();
const main_tokens = tree.nodes.items(.main_token);
const while_src = token_starts[while_full.ast.while_token];
const void_type = try addZIRInstConst(mod, scope, while_src, .{
.ty = Type.initTag(.type),
.val = Value.initTag(.void_type),
});
const cond = c: {
// TODO https://github.com/ziglang/zig/issues/7929
if (while_full.error_token) |error_token| {
@ -2189,59 +2178,41 @@ fn whileExpr(
} else if (while_full.payload_token) |payload_token| {
return mod.failTok(scope, payload_token, "TODO implement while optional", .{});
} else {
const bool_type = try addZIRInstConst(mod, &continue_scope.base, while_src, .{
.ty = Type.initTag(.type),
.val = Value.initTag(.bool_type),
});
break :c try expr(mod, &continue_scope.base, .{ .ty = bool_type }, while_full.ast.cond_expr);
const bool_type_rl: ResultLoc = .{ .ty = @enumToInt(zir.Const.bool_type) };
break :c try expr(mod, &continue_scope.base, bool_type_rl, while_full.ast.cond_expr);
}
};
const condbr = try addZIRInstSpecial(mod, &continue_scope.base, while_src, zir.Inst.CondBr, .{
.condition = cond,
.then_body = undefined, // populated below
.else_body = undefined, // populated below
}, .{});
const cond_block = try addZIRInstBlock(mod, &loop_scope.base, while_src, .block, .{
.instructions = try loop_scope.arena.dupe(zir.Inst.Ref, continue_scope.instructions.items),
});
const condbr = try continue_scope.addCondBr(node);
const cond_block = try loop_scope.addBlock(.block, node);
try loop_scope.instructions.append(mod.gpa, cond_block);
try continue_scope.setBlockBody(cond_block);
// TODO avoid emitting the continue expr when there
// are no jumps to it. This happens when the last statement of a while body is noreturn
// and there are no `continue` statements.
// The "repeat" at the end of a loop body is implied.
if (while_full.ast.cont_expr != 0) {
_ = try expr(mod, &loop_scope.base, .{ .ty = void_type }, while_full.ast.cont_expr);
const void_type_rl: ResultLoc = .{ .ty = @enumToInt(zir.Const.void_type) };
_ = try expr(mod, &loop_scope.base, void_type_rl, while_full.ast.cont_expr);
}
const loop = try scope.arena().create(zir.Inst.Loop);
loop.* = .{
.base = .{
.tag = .loop,
.src = while_src,
},
.positionals = .{
.body = .{
.instructions = try scope.arena().dupe(zir.Inst.Ref, loop_scope.instructions.items),
},
},
.kw_args = .{},
};
const while_block = try addZIRInstBlock(mod, scope, while_src, .block, .{
.instructions = try scope.arena().dupe(zir.Inst.Ref, &[1]zir.Inst.Ref{&loop.base}),
});
loop_scope.break_block = while_block;
const is_inline = while_full.inline_token != null;
const repeat_tag: zir.Inst.Tag = if (is_inline) .repeat_inline else .repeat;
_ = try loop_scope.addNode(repeat_tag, node);
try loop_scope.setBlockBody(loop_block);
loop_scope.break_block = loop_block;
loop_scope.continue_block = cond_block;
if (while_full.label_token) |label_token| {
loop_scope.label = @as(?Scope.GenZir.Label, Scope.GenZir.Label{
.token = label_token,
.block_inst = while_block,
.block_inst = loop_block,
});
}
const then_src = token_starts[tree.lastToken(while_full.ast.then_expr)];
var then_scope: Scope.GenZir = .{
.parent = &continue_scope.base,
.decl = continue_scope.decl,
.arena = continue_scope.arena,
.zir_code = parent_gz.zir_code,
.force_comptime = continue_scope.force_comptime,
.instructions = .{},
};
@ -2254,29 +2225,31 @@ fn whileExpr(
var else_scope: Scope.GenZir = .{
.parent = &continue_scope.base,
.decl = continue_scope.decl,
.arena = continue_scope.arena,
.zir_code = parent_gz.zir_code,
.force_comptime = continue_scope.force_comptime,
.instructions = .{},
};
defer else_scope.instructions.deinit(mod.gpa);
const else_node = while_full.ast.else_expr;
const else_info: struct { src: usize, result: ?*zir.Inst } = if (else_node != 0) blk: {
const else_info: struct {
src: ast.Node.Index,
result: zir.Inst.Ref,
} = if (else_node != 0) blk: {
loop_scope.break_count += 1;
const sub_scope = &else_scope.base;
break :blk .{
.src = token_starts[tree.lastToken(else_node)],
.src = else_node,
.result = try expr(mod, sub_scope, loop_scope.break_result_loc, else_node),
};
} else .{
.src = token_starts[tree.lastToken(while_full.ast.then_expr)],
.result = null,
.src = while_full.ast.then_expr,
.result = 0,
};
if (loop_scope.label) |some| {
if (!some.used) {
return mod.fail(scope, token_starts[some.token], "unused while loop label", .{});
return mod.failTok(scope, some.token, "unused while loop label", .{});
}
}
return finishThenElseBlock(
@ -2289,11 +2262,11 @@ fn whileExpr(
&else_scope,
condbr,
cond,
then_src,
while_full.ast.then_expr,
else_info.src,
then_result,
else_info.result,
while_block,
loop_block,
cond_block,
);
}

View File

@ -80,22 +80,24 @@ pub const Inst = struct {
condbr,
constant,
dbg_stmt,
// ?T => bool
/// ?T => bool
is_null,
// ?T => bool (inverted logic)
/// ?T => bool (inverted logic)
is_non_null,
// *?T => bool
/// *?T => bool
is_null_ptr,
// *?T => bool (inverted logic)
/// *?T => bool (inverted logic)
is_non_null_ptr,
// E!T => bool
/// E!T => bool
is_err,
// *E!T => bool
/// *E!T => bool
is_err_ptr,
bool_and,
bool_or,
/// Read a value from a pointer.
load,
/// A labeled block of code that loops forever. At the end of the body it is implied
/// to repeat; no explicit "repeat" instruction terminates loop bodies.
loop,
ptrtoint,
ref,
@ -112,9 +114,9 @@ pub const Inst = struct {
not,
floatcast,
intcast,
// ?T => T
/// ?T => T
optional_payload,
// *?T => *T
/// *?T => *T
optional_payload_ptr,
wrap_optional,
/// E!T -> T

View File

@ -649,11 +649,19 @@ pub const Inst = struct {
/// Return a boolean true if dereferenced pointer is an error
/// Uses the `un_tok` field.
is_err_ptr,
/// A labeled block of code that loops forever. At the end of the body it is implied
/// to repeat; no explicit "repeat" instruction terminates loop bodies.
/// A labeled block of code that loops forever. At the end of the body will have either
/// a `repeat` instruction or a `repeat_inline` instruction.
/// Uses the `pl_node` field. The AST node is either a for loop or while loop.
/// This ZIR instruction is needed because TZIR does not (yet?) match ZIR, and Sema
/// needs to emit more than 1 TZIR block for this instruction.
/// The payload is `Block`.
loop,
/// Sends runtime control flow back to the beginning of the current block.
/// Uses the `node` field.
repeat,
/// Sends comptime control flow back to the beginning of the current block.
/// Uses the `node` field.
repeat_inline,
/// Merge two error sets into one, `E1 || E2`.
merge_error_sets,
/// Ambiguously remainder division or modulus. If the computation would possibly have
@ -736,6 +744,7 @@ pub const Inst = struct {
/// Uses the `pl_node` field. AST node is the slice syntax. Payload is `SliceSentinel`.
slice_sentinel,
/// Write a value to a pointer. For loading, see `deref`.
/// Uses the `bin` union field.
store,
/// Same as `store` but the type of the value being stored will be used to infer
/// the block type. The LHS is the pointer to store to.
@ -902,6 +911,7 @@ pub const Inst = struct {
.bit_or,
.block,
.block_comptime,
.loop,
.bool_br_and,
.bool_br_or,
.bool_not,
@ -1012,7 +1022,8 @@ pub const Inst = struct {
.ret_tok,
.ret_coerce,
.@"unreachable",
.loop,
.repeat,
.repeat_inline,
=> true,
};
}
@ -1355,12 +1366,13 @@ const Writer = struct {
.bit_and,
.bit_or,
.as,
.@"break",
.coerce_result_ptr,
.elem_ptr,
.elem_val,
.intcast,
.merge_error_sets,
.store,
.store_to_block_ptr,
=> try self.writeBin(stream, inst),
.alloc,
@ -1425,6 +1437,7 @@ const Writer = struct {
.elided => try stream.writeAll(")"),
.break_void_node => try self.writeBreakVoidNode(stream, inst),
.int_type => try self.writeIntType(stream, inst),
.@"break" => try self.writeBreak(stream, inst),
.@"asm",
.asm_volatile,
@ -1436,7 +1449,6 @@ const Writer = struct {
.field_ptr_named,
.field_val_named,
.floatcast,
.loop,
.slice_start,
.slice_end,
.slice_sentinel,
@ -1473,6 +1485,7 @@ const Writer = struct {
.block,
.block_comptime,
.loop,
=> try self.writePlNodeBlock(stream, inst),
.condbr => try self.writePlNodeCondBr(stream, inst),
@ -1483,6 +1496,8 @@ const Writer = struct {
.dbg_stmt_node,
.ret_ptr,
.ret_type,
.repeat,
.repeat_inline,
=> try self.writeNode(stream, inst),
.decl_ref,
@ -1506,8 +1521,6 @@ const Writer = struct {
.bitcast_result_ptr,
.error_union_type,
.error_set,
.store,
.store_to_block_ptr,
.store_to_inferred_ptr,
=> try stream.writeAll("TODO)"),
}
@ -1772,6 +1785,15 @@ const Writer = struct {
try self.writeSrc(stream, int_type.src());
}
fn writeBreak(self: *Writer, stream: anytype, inst: Inst.Index) !void {
const inst_data = self.code.instructions.items(.data)[inst].@"break";
try self.writeInstIndex(stream, inst_data.block_inst);
try stream.writeAll(", ");
try self.writeInstRef(stream, inst_data.operand);
try stream.writeAll(")");
}
fn writeUnreachable(self: *Writer, stream: anytype, inst: Inst.Index) !void {
const inst_data = self.code.instructions.items(.data)[inst].@"unreachable";
const safety_str = if (inst_data.safety) "safe" else "unsafe";