authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2022-04-02 17:59:07-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2022-04-02 18:30:44-07:00
log4618c41fa6ca70f06c7e65762d2f38d57b00818c
tree0561268dd3acf7a00e6e7a5ab8f1bb99c08b3999
parent83bb98e13b19d417c9a8de819b98a1566718b993

Sema: mechanism for converting comptime breaks to runtime

closes #11369

2 files changed, 312 insertions(+), 15 deletions(-)

src/Sema.zig+207-15
...@@ -68,6 +68,11 @@ preallocated_new_func: ?*Module.Fn = null,...@@ -68,6 +68,11 @@ preallocated_new_func: ?*Module.Fn = null,
68/// TODO: after upgrading to use InternPool change the key here to be an68/// TODO: after upgrading to use InternPool change the key here to be an
69/// InternPool value index.69/// InternPool value index.
70types_to_resolve: std.ArrayListUnmanaged(Air.Inst.Ref) = .{},70types_to_resolve: std.ArrayListUnmanaged(Air.Inst.Ref) = .{},
71/// These are lazily created runtime blocks from inline_block instructions.
72/// They are created when an inline_break passes through a runtime condition, because
73/// Sema must convert comptime control flow to runtime control flow, which means
74/// breaking from a block.
75post_hoc_blocks: std.AutoHashMapUnmanaged(Air.Inst.Index, *LabeledBlock) = .{},
7176
72const std = @import("std");77const std = @import("std");
73const mem = std.mem;78const mem = std.mem;
...@@ -525,6 +530,18 @@ pub const Block = struct {...@@ -525,6 +530,18 @@ pub const Block = struct {
525 };530 };
526};531};
527532
533const LabeledBlock = struct {
534 block: Block,
535 label: Block.Label,
536
537 fn destroy(lb: *LabeledBlock, gpa: Allocator) void {
538 lb.block.instructions.deinit(gpa);
539 lb.label.merges.results.deinit(gpa);
540 lb.label.merges.br_list.deinit(gpa);
541 gpa.destroy(lb);
542 }
543};
544
528pub fn deinit(sema: *Sema) void {545pub fn deinit(sema: *Sema) void {
529 const gpa = sema.gpa;546 const gpa = sema.gpa;
530 sema.air_instructions.deinit(gpa);547 sema.air_instructions.deinit(gpa);
...@@ -533,6 +550,14 @@ pub fn deinit(sema: *Sema) void {...@@ -533,6 +550,14 @@ pub fn deinit(sema: *Sema) void {
533 sema.inst_map.deinit(gpa);550 sema.inst_map.deinit(gpa);
534 sema.decl_val_table.deinit(gpa);551 sema.decl_val_table.deinit(gpa);
535 sema.types_to_resolve.deinit(gpa);552 sema.types_to_resolve.deinit(gpa);
553 {
554 var it = sema.post_hoc_blocks.iterator();
555 while (it.next()) |entry| {
556 const labeled_block = entry.value_ptr.*;
557 labeled_block.destroy(gpa);
558 }
559 sema.post_hoc_blocks.deinit(gpa);
560 }
536 sema.* = undefined;561 sema.* = undefined;
537}562}
538563
...@@ -573,8 +598,8 @@ pub fn analyzeBody(...@@ -573,8 +598,8 @@ pub fn analyzeBody(
573598
574const BreakData = struct {599const BreakData = struct {
575 block_inst: Zir.Inst.Index,600 block_inst: Zir.Inst.Index,
576 operand: Air.Inst.Ref,601 operand: Zir.Inst.Ref,
577 inst: Air.Inst.Index,602 inst: Zir.Inst.Index,
578};603};
579604
580pub fn analyzeBodyBreak(605pub fn analyzeBodyBreak(
...@@ -1192,20 +1217,67 @@ fn analyzeBodyInner(...@@ -1192,20 +1217,67 @@ fn analyzeBodyInner(
1192 },1217 },
1193 .block_inline => blk: {1218 .block_inline => blk: {
1194 // Directly analyze the block body without introducing a new block.1219 // Directly analyze the block body without introducing a new block.
1220 // However, in the case of a corresponding break_inline which reaches
1221 // through a runtime conditional branch, we must retroactively emit
1222 // a block, so we remember the block index here just in case.
1223 const block_index = block.instructions.items.len;
1195 const inst_data = datas[inst].pl_node;1224 const inst_data = datas[inst].pl_node;
1196 const extra = sema.code.extraData(Zir.Inst.Block, inst_data.payload_index);1225 const extra = sema.code.extraData(Zir.Inst.Block, inst_data.payload_index);
1197 const inline_body = sema.code.extra[extra.end..][0..extra.data.body_len];1226 const inline_body = sema.code.extra[extra.end..][0..extra.data.body_len];
1227 const gpa = sema.gpa;
1198 // If this block contains a function prototype, we need to reset the1228 // If this block contains a function prototype, we need to reset the
1199 // current list of parameters and restore it later.1229 // current list of parameters and restore it later.
1200 // Note: this probably needs to be resolved in a more general manner.1230 // Note: this probably needs to be resolved in a more general manner.
1201 const prev_params = block.params;1231 const prev_params = block.params;
1202 block.params = .{};1232 block.params = .{};
1203 defer {1233 defer {
1204 block.params.deinit(sema.gpa);1234 block.params.deinit(gpa);
1205 block.params = prev_params;1235 block.params = prev_params;
1206 }1236 }
1207 const break_data = (try sema.analyzeBodyBreak(block, inline_body)) orelse1237 const opt_break_data = try sema.analyzeBodyBreak(block, inline_body);
1208 break always_noreturn;1238 // A runtime conditional branch that needs a post-hoc block to be
1239 // emitted communicates this by mapping the block index into the inst map.
1240 if (map.get(inst)) |new_block_ref| ph: {
1241 // Comptime control flow populates the map, so we don't actually know
1242 // if this is a post-hoc runtime block until we check the
1243 // post_hoc_block map.
1244 const new_block_inst = Air.refToIndex(new_block_ref) orelse break :ph;
1245 const labeled_block = sema.post_hoc_blocks.get(new_block_inst) orelse
1246 break :ph;
1247
1248 // In this case we need to move all the instructions starting at
1249 // block_index from the current block into this new one.
1250
1251 if (opt_break_data) |break_data| {
1252 // This is a comptime break which we now change to a runtime break
1253 // since it crosses a runtime branch.
1254 // It may pass through our currently being analyzed block_inline or it
1255 // may point directly to it. In the latter case, this modifies the
1256 // block that we are about to look up in the post_hoc_blocks map below.
1257 try sema.addRuntimeBreak(block, break_data);
1258 } else {
1259 // Here the comptime control flow ends with noreturn; however
1260 // we have runtime control flow continuing after this block.
1261 // This branch is therefore handled by the `i += 1; continue;`
1262 // logic below.
1263 }
1264
1265 try labeled_block.block.instructions.appendSlice(gpa, block.instructions.items[block_index..]);
1266 block.instructions.items.len = block_index;
1267
1268 const block_result = try sema.analyzeBlockBody(block, inst_data.src(), &labeled_block.block, &labeled_block.label.merges);
1269 {
1270 // Destroy the ad-hoc block entry so that it does not interfere with
1271 // the next iteration of comptime control flow, if any.
1272 labeled_block.destroy(gpa);
1273 assert(sema.post_hoc_blocks.remove(new_block_inst));
1274 }
1275 try map.put(gpa, inst, block_result);
1276 i += 1;
1277 continue;
1278 }
1279
1280 const break_data = opt_break_data orelse break always_noreturn;
1209 if (inst == break_data.block_inst) {1281 if (inst == break_data.block_inst) {
1210 break :blk sema.resolveInst(break_data.operand);1282 break :blk sema.resolveInst(break_data.operand);
1211 } else {1283 } else {
...@@ -3996,13 +4068,12 @@ fn zirBlock(sema: *Sema, parent_block: *Block, inst: Zir.Inst.Index) CompileErro...@@ -3996,13 +4068,12 @@ fn zirBlock(sema: *Sema, parent_block: *Block, inst: Zir.Inst.Index) CompileErro
3996 .inlining = parent_block.inlining,4068 .inlining = parent_block.inlining,
3997 .is_comptime = parent_block.is_comptime,4069 .is_comptime = parent_block.is_comptime,
3998 };4070 };
3999 const merges = &child_block.label.?.merges;
40004071
4001 defer child_block.instructions.deinit(gpa);4072 defer child_block.instructions.deinit(gpa);
4002 defer merges.results.deinit(gpa);4073 defer label.merges.results.deinit(gpa);
4003 defer merges.br_list.deinit(gpa);4074 defer label.merges.br_list.deinit(gpa);
40044075
4005 return sema.resolveBlockBody(parent_block, src, &child_block, body, inst, merges);4076 return sema.resolveBlockBody(parent_block, src, &child_block, body, inst, &label.merges);
4006}4077}
40074078
4008fn resolveBlockBody(4079fn resolveBlockBody(
...@@ -7955,7 +8026,18 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError...@@ -7955,7 +8026,18 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
7955 const item = sema.resolveInst(item_ref);8026 const item = sema.resolveInst(item_ref);
7956 // `item` is already guaranteed to be constant known.8027 // `item` is already guaranteed to be constant known.
79578028
7958 try sema.analyzeBody(&case_block, body);8029 _ = sema.analyzeBodyInner(&case_block, body) catch |err| switch (err) {
8030 error.ComptimeBreak => {
8031 const zir_datas = sema.code.instructions.items(.data);
8032 const break_data = zir_datas[sema.comptime_break_inst].@"break";
8033 try sema.addRuntimeBreak(&case_block, .{
8034 .block_inst = break_data.block_inst,
8035 .operand = break_data.operand,
8036 .inst = sema.comptime_break_inst,
8037 });
8038 },
8039 else => |e| return e,
8040 };
79598041
7960 try wip_captures.finalize();8042 try wip_captures.finalize();
79618043
...@@ -7998,7 +8080,18 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError...@@ -7998,7 +8080,18 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
79988080
7999 const body = sema.code.extra[extra_index..][0..body_len];8081 const body = sema.code.extra[extra_index..][0..body_len];
8000 extra_index += body_len;8082 extra_index += body_len;
8001 try sema.analyzeBody(&case_block, body);8083 _ = sema.analyzeBodyInner(&case_block, body) catch |err| switch (err) {
8084 error.ComptimeBreak => {
8085 const zir_datas = sema.code.instructions.items(.data);
8086 const break_data = zir_datas[sema.comptime_break_inst].@"break";
8087 try sema.addRuntimeBreak(&case_block, .{
8088 .block_inst = break_data.block_inst,
8089 .operand = break_data.operand,
8090 .inst = sema.comptime_break_inst,
8091 });
8092 },
8093 else => |e| return e,
8094 };
80028095
8003 try cases_extra.ensureUnusedCapacity(gpa, 2 + items.len +8096 try cases_extra.ensureUnusedCapacity(gpa, 2 + items.len +
8004 case_block.instructions.items.len);8097 case_block.instructions.items.len);
...@@ -8073,7 +8166,18 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError...@@ -8073,7 +8166,18 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
80738166
8074 const body = sema.code.extra[extra_index..][0..body_len];8167 const body = sema.code.extra[extra_index..][0..body_len];
8075 extra_index += body_len;8168 extra_index += body_len;
8076 try sema.analyzeBody(&case_block, body);8169 _ = sema.analyzeBodyInner(&case_block, body) catch |err| switch (err) {
8170 error.ComptimeBreak => {
8171 const zir_datas = sema.code.instructions.items(.data);
8172 const break_data = zir_datas[sema.comptime_break_inst].@"break";
8173 try sema.addRuntimeBreak(&case_block, .{
8174 .block_inst = break_data.block_inst,
8175 .operand = break_data.operand,
8176 .inst = sema.comptime_break_inst,
8177 });
8178 },
8179 else => |e| return e,
8180 };
80778181
8078 try wip_captures.finalize();8182 try wip_captures.finalize();
80798183
...@@ -8110,7 +8214,18 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError...@@ -8110,7 +8214,18 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
8110 case_block.wip_capture_scope = wip_captures.scope;8214 case_block.wip_capture_scope = wip_captures.scope;
81118215
8112 if (special.body.len != 0) {8216 if (special.body.len != 0) {
8113 try sema.analyzeBody(&case_block, special.body);8217 _ = sema.analyzeBodyInner(&case_block, special.body) catch |err| switch (err) {
8218 error.ComptimeBreak => {
8219 const zir_datas = sema.code.instructions.items(.data);
8220 const break_data = zir_datas[sema.comptime_break_inst].@"break";
8221 try sema.addRuntimeBreak(&case_block, .{
8222 .block_inst = break_data.block_inst,
8223 .operand = break_data.operand,
8224 .inst = sema.comptime_break_inst,
8225 });
8226 },
8227 else => |e| return e,
8228 };
8114 } else {8229 } else {
8115 // We still need a terminator in this block, but we have proven8230 // We still need a terminator in this block, but we have proven
8116 // that it is unreachable.8231 // that it is unreachable.
...@@ -11979,11 +12094,33 @@ fn zirCondbr(...@@ -11979,11 +12094,33 @@ fn zirCondbr(
11979 sub_block.runtime_index += 1;12094 sub_block.runtime_index += 1;
11980 defer sub_block.instructions.deinit(gpa);12095 defer sub_block.instructions.deinit(gpa);
1198112096
11982 try sema.analyzeBody(&sub_block, then_body);12097 _ = sema.analyzeBodyInner(&sub_block, then_body) catch |err| switch (err) {
12098 error.ComptimeBreak => {
12099 const zir_datas = sema.code.instructions.items(.data);
12100 const break_data = zir_datas[sema.comptime_break_inst].@"break";
12101 try sema.addRuntimeBreak(&sub_block, .{
12102 .block_inst = break_data.block_inst,
12103 .operand = break_data.operand,
12104 .inst = sema.comptime_break_inst,
12105 });
12106 },
12107 else => |e| return e,
12108 };
11983 const true_instructions = sub_block.instructions.toOwnedSlice(gpa);12109 const true_instructions = sub_block.instructions.toOwnedSlice(gpa);
11984 defer gpa.free(true_instructions);12110 defer gpa.free(true_instructions);
1198512111
11986 try sema.analyzeBody(&sub_block, else_body);12112 _ = sema.analyzeBodyInner(&sub_block, else_body) catch |err| switch (err) {
12113 error.ComptimeBreak => {
12114 const zir_datas = sema.code.instructions.items(.data);
12115 const break_data = zir_datas[sema.comptime_break_inst].@"break";
12116 try sema.addRuntimeBreak(&sub_block, .{
12117 .block_inst = break_data.block_inst,
12118 .operand = break_data.operand,
12119 .inst = sema.comptime_break_inst,
12120 });
12121 },
12122 else => |e| return e,
12123 };
11987 try sema.air_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.CondBr).Struct.fields.len +12124 try sema.air_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.CondBr).Struct.fields.len +
11988 true_instructions.len + sub_block.instructions.items.len);12125 true_instructions.len + sub_block.instructions.items.len);
11989 _ = try parent_block.addInst(.{12126 _ = try parent_block.addInst(.{
...@@ -12001,6 +12138,61 @@ fn zirCondbr(...@@ -12001,6 +12138,61 @@ fn zirCondbr(
12001 return always_noreturn;12138 return always_noreturn;
12002}12139}
1200312140
12141// A `break` statement is inside a runtime condition, but trying to
12142// break from an inline loop. In such case we must convert it to
12143// a runtime break.
12144fn addRuntimeBreak(sema: *Sema, child_block: *Block, break_data: BreakData) !void {
12145 const gop = try sema.inst_map.getOrPut(sema.gpa, break_data.block_inst);
12146 const labeled_block = if (!gop.found_existing) blk: {
12147 try sema.post_hoc_blocks.ensureUnusedCapacity(sema.gpa, 1);
12148
12149 const new_block_inst = @intCast(Air.Inst.Index, sema.air_instructions.len);
12150 gop.value_ptr.* = Air.indexToRef(new_block_inst);
12151 try sema.air_instructions.append(sema.gpa, .{
12152 .tag = .block,
12153 .data = undefined,
12154 });
12155 const labeled_block = try sema.gpa.create(LabeledBlock);
12156 labeled_block.* = .{
12157 .label = .{
12158 .zir_block = break_data.block_inst,
12159 .merges = .{
12160 .results = .{},
12161 .br_list = .{},
12162 .block_inst = new_block_inst,
12163 },
12164 },
12165 .block = .{
12166 .parent = child_block,
12167 .sema = sema,
12168 .src_decl = child_block.src_decl,
12169 .namespace = child_block.namespace,
12170 .wip_capture_scope = child_block.wip_capture_scope,
12171 .instructions = .{},
12172 .label = &labeled_block.label,
12173 .inlining = child_block.inlining,
12174 .is_comptime = child_block.is_comptime,
12175 },
12176 };
12177 sema.post_hoc_blocks.putAssumeCapacityNoClobber(new_block_inst, labeled_block);
12178 break :blk labeled_block;
12179 } else blk: {
12180 const new_block_inst = Air.refToIndex(gop.value_ptr.*).?;
12181 const labeled_block = sema.post_hoc_blocks.get(new_block_inst).?;
12182 break :blk labeled_block;
12183 };
12184
12185 const operand = sema.resolveInst(break_data.operand);
12186 const br_ref = try child_block.addBr(labeled_block.label.merges.block_inst, operand);
12187 try labeled_block.label.merges.results.append(sema.gpa, operand);
12188 try labeled_block.label.merges.br_list.append(sema.gpa, Air.refToIndex(br_ref).?);
12189 labeled_block.block.runtime_index += 1;
12190 if (labeled_block.block.runtime_cond == null and labeled_block.block.runtime_loop == null) {
12191 labeled_block.block.runtime_cond = child_block.runtime_cond orelse child_block.runtime_loop;
12192 labeled_block.block.runtime_loop = child_block.runtime_loop;
12193 }
12194}
12195
12004fn zirUnreachable(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Zir.Inst.Index {12196fn zirUnreachable(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Zir.Inst.Index {
12005 const tracy = trace(@src());12197 const tracy = trace(@src());
12006 defer tracy.end();12198 defer tracy.end();
test/behavior/eval.zig+105
...@@ -893,3 +893,108 @@ test "closure capture type of runtime-known parameter" {...@@ -893,3 +893,108 @@ test "closure capture type of runtime-known parameter" {
893 var c: i32 = 1234;893 var c: i32 = 1234;
894 try S.b(c);894 try S.b(c);
895}895}
896
897test "comptime break passing through runtime condition converted to runtime break" {
898 if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
899 if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
900 if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
901
902 const S = struct {
903 fn doTheTest() !void {
904 var runtime: u8 = 'b';
905 inline for ([3]u8{ 'a', 'b', 'c' }) |byte| {
906 bar();
907 if (byte == runtime) {
908 foo(byte);
909 break;
910 }
911 }
912 try expect(ok);
913 try expect(count == 2);
914 }
915 var ok = false;
916 var count: usize = 0;
917
918 fn foo(byte: u8) void {
919 ok = byte == 'b';
920 }
921
922 fn bar() void {
923 count += 1;
924 }
925 };
926
927 try S.doTheTest();
928}
929
930test "comptime break to outer loop passing through runtime condition converted to runtime break" {
931 if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
932 if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
933 if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
934
935 const S = struct {
936 fn doTheTest() !void {
937 var runtime: u8 = 'b';
938 outer: inline for ([3]u8{ 'A', 'B', 'C' }) |outer_byte| {
939 inline for ([3]u8{ 'a', 'b', 'c' }) |byte| {
940 bar(outer_byte);
941 if (byte == runtime) {
942 foo(byte);
943 break :outer;
944 }
945 }
946 }
947 try expect(ok);
948 try expect(count == 2);
949 }
950 var ok = false;
951 var count: usize = 0;
952
953 fn foo(byte: u8) void {
954 ok = byte == 'b';
955 }
956
957 fn bar(byte: u8) void {
958 _ = byte;
959 count += 1;
960 }
961 };
962
963 try S.doTheTest();
964}
965
966test "comptime break operand passing through runtime condition converted to runtime break" {
967 const S = struct {
968 fn doTheTest(runtime: u8) !void {
969 const result = inline for ([3]u8{ 'a', 'b', 'c' }) |byte| {
970 if (byte == runtime) {
971 break runtime;
972 }
973 } else 'z';
974 try expect(result == 'b');
975 }
976 };
977
978 try S.doTheTest('b');
979 comptime try S.doTheTest('b');
980}
981
982test "comptime break operand passing through runtime switch converted to runtime break" {
983 if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
984 if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
985
986 const S = struct {
987 fn doTheTest(runtime: u8) !void {
988 const result = inline for ([3]u8{ 'a', 'b', 'c' }) |byte| {
989 switch (runtime) {
990 byte => break runtime,
991 else => {},
992 }
993 } else 'z';
994 try expect(result == 'b');
995 }
996 };
997
998 try S.doTheTest('b');
999 comptime try S.doTheTest('b');
1000}