authorchloe caruso <git@paperclover.net> 2024-09-23 00:48:19-07:00
committerchloe caruso <git@paperclover.net> 2024-09-23 00:48:19-07:00
log230648717c1f197ece3aec476cda99ad3fa158a6
treec93d55e6833ac38ba179c7e23e53687b8145e683
parent5b35e0a263fe2a35d0e494709fd2f716c565e591

feat: basic version of assert (comptime only)


10 files changed, 220 insertions(+), 25 deletions(-)

CHANGELOG.md+3
...@@ -3,8 +3,11 @@...@@ -3,8 +3,11 @@
3- chore: rename the language to "ix"3- chore: rename the language to "ix"
4- feat: language manual gets a pleasing redesign4- feat: language manual gets a pleasing redesign
5- feat: allow any expression as a function body5- feat: allow any expression as a function body
6- feat: add `assert` keyword
6- fix: coerce `break` operands and implicit breaks to result type7- fix: coerce `break` operands and implicit breaks to result type
7- fix: coerce results when `if` expression branches merge8- fix: coerce results when `if` expression branches merge
9- fix: ensure if branch results to a runtime value when the condition is not compile-time known
10- fix(js): stop printing redundant parenthesis on ternary expressions
811
9## 0.1.612## 0.1.6
1013
build.zig+1-1
...@@ -121,7 +121,7 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup {...@@ -121,7 +121,7 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup {
121121
122 // Main CLI122 // Main CLI
123 const cli = b.addExecutable(.{123 const cli = b.addExecutable(.{
124 .name = "compiler",124 .name = "ix",
125 .root_source_file = b.path("src/main.zig"),125 .root_source_file = b.path("src/main.zig"),
126 .target = options.target,126 .target = options.target,
127 .optimize = options.optimize,127 .optimize = options.optimize,
src/core/Comp.zig+18-1
...@@ -203,7 +203,12 @@ fn runAnalysisJob(comp: *Comp, unit: InternPool.AnalysisUnit, first_blocked_sema...@@ -203,7 +203,12 @@ fn runAnalysisJob(comp: *Comp, unit: InternPool.AnalysisUnit, first_blocked_sema
203 => |e| return e,203 => |e| return e,
204204
205 error.AnalysisFail => |e| {205 error.AnalysisFail => |e| {
206 const report = try sema.report.finalize();206 const report = sema.report.finalize() catch {
207 comp.ongoing_analysis.removeByPtr(gop.key_ptr);
208 _ = sema.deinit(.free_all); // TODO: arena reuse
209 comp.gpa.destroy(sema);
210 return e;
211 };
207 defer report.deinit(comp.gpa);212 defer report.deinit(comp.gpa);
208213
209 report.dump(.{ .comp = .{ .comp = comp, .file = sema.file } }, comp.out) catch {};214 report.dump(.{ .comp = .{ .comp = comp, .file = sema.file } }, comp.out) catch {};
...@@ -216,6 +221,18 @@ fn runAnalysisJob(comp: *Comp, unit: InternPool.AnalysisUnit, first_blocked_sema...@@ -216,6 +221,18 @@ fn runAnalysisJob(comp: *Comp, unit: InternPool.AnalysisUnit, first_blocked_sema
216 },221 },
217 };222 };
218223
224 errdefer {
225 comp.ongoing_analysis.removeByPtr(gop.key_ptr);
226 _ = sema.deinit(.free_all); // TODO: arena reuse
227 comp.gpa.destroy(sema);
228 }
229
230 const report = try sema.report.finalize();
231 defer report.deinit(comp.gpa); // TODO: store warnings
232 if (report.warnings.len > 0) {
233 report.dump(.{ .comp = .{ .comp = comp, .file = sema.file } }, comp.out) catch {};
234 }
235
219 switch (unit.unwrap()) {236 switch (unit.unwrap()) {
220 .decl => |index| {237 .decl => |index| {
221 comp.intern_pool.declPtr(index).analysis = .complete;238 comp.intern_pool.declPtr(index).analysis = .complete;
src/core/Hir.Dump.zig+6-1
...@@ -219,13 +219,18 @@ const State = struct {...@@ -219,13 +219,18 @@ const State = struct {
219 .compile_error,219 .compile_error,
220 .type_of,220 .type_of,
221 .array_type,221 .array_type,
222 .assert,
223 => |comptime_tag| {222 => |comptime_tag| {
224 const data = hir.instData(comptime_tag, inst);223 const data = hir.instData(comptime_tag, inst);
225 try writer.writeAll("(");224 try writer.writeAll("(");
226 try s.dumpRefMarker(writer, data.operand);225 try s.dumpRefMarker(writer, data.operand);
227 try writer.writeAll(")");226 try writer.writeAll(")");
228 },227 },
228 .assert => {
229 const data = hir.instData(.assert, inst);
230 try writer.writeAll("(");
231 try s.dumpRef(writer, data.operand);
232 try writer.writeAll(")");
233 },
229 .call => {234 .call => {
230 const call: Hir.Call = hir.instData(.call, inst).extra_index.get(hir);235 const call: Hir.Call = hir.instData(.call, inst).extra_index.get(hir);
231 try writer.writeAll("(");236 try writer.writeAll("(");
src/core/Hir.zig+1-1
...@@ -6,7 +6,7 @@...@@ -6,7 +6,7 @@
6//! expressions as flat instruction sequences.6//! expressions as flat instruction sequences.
7//!7//!
8//! Hir for a given file has no dependencies on it's imports, meaning it can be8//! Hir for a given file has no dependencies on it's imports, meaning it can be
9//! computed in parallel and cached when not changed.9//! computed in parallel and then cached when not changed.
10//!10//!
11//! A helper program exists to visualize Hir: './build transform -- ./file.ix'11//! A helper program exists to visualize Hir: './build transform -- ./file.ix'
12//! Alternatively, `ix build` has the `--dump-ir` option.12//! Alternatively, `ix build` has the `--dump-ir` option.
src/core/InternPool.zig+21
...@@ -410,6 +410,27 @@ pub const Value = enum(u32) {...@@ -410,6 +410,27 @@ pub const Value = enum(u32) {
410 else => null,410 else => null,
411 };411 };
412 }412 }
413
414 pub fn displayName(value: Value, ip: *const InternPool) std.fmt.Formatter(formatDisplayName) {
415 return .{ .data = .{ .value = value, .ip = ip } };
416 }
417
418 fn formatDisplayName(
419 ctx: struct { value: Value, ip: *const InternPool },
420 comptime _: []const u8,
421 _: std.fmt.FormatOptions,
422 writer: anytype,
423 ) !void {
424 const value = ctx.value;
425 const ip = ctx.ip;
426 const key = value.toKey(ip);
427 switch (key) {
428 inline .comptime_float, .int32, .float => |n| {
429 try writer.print("{d}", .{n});
430 },
431 else => |tag| std.debug.panic("TODO: print value .{s}", .{@tagName(tag)}),
432 }
433 }
413};434};
414435
415/// Type is subset of Value which represents the values that have typeOf() == Value.type_type436/// Type is subset of Value which represents the values that have typeOf() == Value.type_type
src/core/Report.zig+61-1
...@@ -56,6 +56,52 @@ pub const Err = union(enum) {...@@ -56,6 +56,52 @@ pub const Err = union(enum) {
56 try ctx.msgNode(try data.src.get(ctx), "value must be known at compile-time", .{});56 try ctx.msgNode(try data.src.get(ctx), "value must be known at compile-time", .{});
57 }57 }
58 },58 },
59 user_assertion_basic: struct {
60 src: LazyNode,
61
62 fn build(data: @This(), ctx: *MsgContext) !void {
63 try ctx.msgNode(try data.src.get(ctx), "assertion failure", .{});
64 }
65 },
66 user_assertion_bin: struct {
67 src: LazyNode,
68 left: Value,
69 right: Value,
70
71 fn build(data: @This(), ctx: *MsgContext) !void {
72 const node = try data.src.get(ctx);
73 try ctx.msgNode(node, "assertion failure: {} {s} {}", .{
74 fmtAssertionOperand(data.left, ctx.ip()),
75 switch (ctx.ast.nodeTag(node)) {
76 .eq => "==",
77 .ne => "!=",
78 .lt => "<",
79 .le => "<=",
80 .gt => ">",
81 .ge => ">=",
82 else => unreachable,
83 },
84 fmtAssertionOperand(data.right, ctx.ip()),
85 });
86 }
87
88 fn fmtAssertionOperand(v: Value, ip: *const InternPool) std.fmt.Formatter(fmtAssertionOperandImpl) {
89 return .{ .data = .{ .value = v, .ip = ip } };
90 }
91
92 fn fmtAssertionOperandImpl(
93 data: struct { value: Value, ip: *const InternPool },
94 comptime _: []const u8,
95 _: std.fmt.FormatOptions,
96 w: anytype,
97 ) !void {
98 if (data.value.toType(data.ip)) |t| {
99 try w.print("any {}", .{t.displayName(data.ip)});
100 } else {
101 try w.print("{}", .{data.value.displayName(data.ip)});
102 }
103 }
104 },
59 cannot_be_comptime: struct {105 cannot_be_comptime: struct {
60 src: LazyNode,106 src: LazyNode,
61107
...@@ -538,7 +584,8 @@ pub const Warn = union(enum) {...@@ -538,7 +584,8 @@ pub const Warn = union(enum) {
538584
539 fn build(data: @This(), ctx: *MsgContext) !void {585 fn build(data: @This(), ctx: *MsgContext) !void {
540 const node = try data.src.get(ctx);586 const node = try data.src.get(ctx);
541 try ctx.msgNode(node, "constant integer cannot be represented as a runtime value, implicitly casting to \"float\"", .{});587 try ctx.msgNode(node, "constant numeric values at runtime needs a result type, implicitly casting to \"float\"", .{});
588 // TODO: trace to the result type and point at it
542 try ctx.msgAlone("add \": float\" or \": int\" after the variable name to set the type", .{});589 try ctx.msgAlone("add \": float\" or \": int\" after the variable name to set the type", .{});
543 }590 }
544 },591 },
...@@ -1206,6 +1253,19 @@ pub const LazyNode = struct {...@@ -1206,6 +1253,19 @@ pub const LazyNode = struct {
1206 .field => {1253 .field => {
1207 return base_node;1254 return base_node;
1208 },1255 },
1256 .@"if" => {
1257 assert(node_tag == .@"if");
1258 switch (lazy.operand) {
1259 .operand => return data.@"if".cond,
1260 .lhs => return ctx.ast.extraData(data.@"if".extra, Ast.If).yes,
1261 .rhs => return ctx.ast.extraData(data.@"if".extra, Ast.If).no.unwrap().?,
1262 else => {},
1263 }
1264 },
1265 .assert => {
1266 assert(node_tag == .assert);
1267 return data.un.expr;
1268 },
1209 else => {},1269 else => {},
1210 }1270 }
12111271
src/core/Sema.zig+75-9
...@@ -490,19 +490,23 @@ const InstructionKind = enum {...@@ -490,19 +490,23 @@ const InstructionKind = enum {
490 pub inline fn fromTag(tag: Hir.Tag) InstructionKind {490 pub inline fn fromTag(tag: Hir.Tag) InstructionKind {
491 return switch (tag) {491 return switch (tag) {
492 // these push new frames, and frame end will assign its value492 // these push new frames, and frame end will assign its value
493 .func,
494 .block,
495 .@"comptime",493 .@"comptime",
496 .@"if",494 .@"if",
495 .assert,
496 .block,
497 .func,
497 => .control_flow,498 => .control_flow,
499
498 // break is used to pop a frame, which has the500 // break is used to pop a frame, which has the
499 // possibility to end analysis501 // possibility to end analysis
500 .@"break",502 .@"break",
501 => .pop_control_flow,503 => .pop_control_flow,
504
502 // these are statements which do not evaluate to anything (void)505 // these are statements which do not evaluate to anything (void)
503 .discard,506 .discard,
504 .ensure_void,507 .ensure_void,
505 => .void,508 => .void,
509
506 // all other instructions are single-visit, returning a value510 // all other instructions are single-visit, returning a value
507 else => .result,511 else => .result,
508 };512 };
...@@ -521,14 +525,14 @@ const PopControlFlowState = enum { stop, @"continue" };...@@ -521,14 +525,14 @@ const PopControlFlowState = enum { stop, @"continue" };
521525
522fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag).Result() {526fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag).Result() {
523 switch (tag) {527 switch (tag) {
524 .block => unreachable, // frame must already be pushed528 .block,
529 => unreachable, // frame must already be pushed
530
525 .declaration,531 .declaration,
526 .variable_declaration,532 .variable_declaration,
527 .test_declaration,533 .test_declaration,
528 => unreachable, // declarations are not analzed by Sema534 => unreachable, // declarations are not analzed by Sema
529535
530 // TODO: pre-evaluate `.param` nodes, since they are always at the start of a function.
531 // this way we can avoid repeating the non-trivial call into InternPool
532 .param => {536 .param => {
533 const param: Hir.DataNode = s.frame.currentData(.param);537 const param: Hir.DataNode = s.frame.currentData(.param);
534 assert(s.frame.mode == .run_time); // never emitted in a comptime block538 assert(s.frame.mode == .run_time); // never emitted in a comptime block
...@@ -909,9 +913,24 @@ fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag)...@@ -909,9 +913,24 @@ fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag)
909 });913 });
910 },914 },
911915
912 // .assert => {916 .assert => {
913 // return Value.void.toAir();917 const operand = s.frame.currentData(.assert).operand;
914 // },918 switch (operand.decode()) {
919 .value => |val| {
920 const coerced = try s.coerceValue(s.lazyNode(.operand), val, .bool);
921 if (coerced == .bool_false) {
922 return s.fail(.user_assertion_basic, .{ .src = s.lazyNode(.operand) });
923 }
924 s.frame.advance();
925 return;
926 },
927 .index => |index| {
928 const mode = s.frame.mode;
929 try s.pushFrame();
930 s.frame = Frame.initFromBlock(s, index, mode, .none);
931 },
932 }
933 },
915934
916 .@"comptime" => {935 .@"comptime" => {
917 const index = s.frame.currentIndex();936 const index = s.frame.currentIndex();
...@@ -1031,6 +1050,54 @@ fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag)...@@ -1031,6 +1050,54 @@ fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag)
1031 s.inst_map.add(s.frame.currentIndex(), operand);1050 s.inst_map.add(s.frame.currentIndex(), operand);
1032 s.frame.advance();1051 s.frame.advance();
1033 },1052 },
1053 .assert => {
1054 const coerced = try s.coerceRef(s.lazyNode(.operand), operand, .bool);
1055 switch (coerced.decode()) {
1056 .value => |val| switch (val) {
1057 .bool_true => {}, // pass
1058 .bool_false => {
1059 // Scan for the pattern:
1060 // %n = assert(%n-3 = block [
1061 // ...
1062 // %n-4 = eq(...)
1063 // %n-2 = coercion(%bool_type, ...)
1064 // %n-1 = break(%x, %n-2)
1065 // ])
1066 const cmp_index: Hir.Inst.Index = @enumFromInt(@intFromEnum(s.frame.currentIndex()) - 4);
1067 switch (s.hir.instTag(cmp_index)) {
1068 .eq,
1069 .ne,
1070 .lt,
1071 .le,
1072 .gt,
1073 .ge,
1074 => {
1075 const cmp_inst = s.hir.instDataUnsafe(Hir.PlNode(Hir.Bin), cmp_index);
1076 const cmp_pl: Hir.Bin = cmp_inst.extra_index.get(&s.hir);
1077 return s.fail(.user_assertion_bin, .{
1078 .src = s.lazyNode(.operand),
1079 .left = switch (s.getRef(cmp_pl.lhs, .no_mark).decode()) {
1080 .value => |v| v,
1081 .index => @panic("TODO"),
1082 },
1083 .right = switch (s.getRef(cmp_pl.rhs, .no_mark).decode()) {
1084 .value => |v| v,
1085 .index => @panic("TODO"),
1086 },
1087 });
1088 },
1089 else => {},
1090 }
1091 return s.fail(.user_assertion_basic, .{ .src = s.lazyNode(.operand) });
1092 },
1093 else => unreachable,
1094 },
1095 .index => {
1096 @panic("TODO");
1097 },
1098 }
1099 s.frame.advance();
1100 },
1034 .@"if" => {1101 .@"if" => {
1035 const if_data: Hir.If = s.frame.currentPayload(.@"if");1102 const if_data: Hir.If = s.frame.currentPayload(.@"if");
10361103
...@@ -1842,7 +1909,6 @@ fn ensureValueRuntime(s: *Sema, src: LazyNode, value: Value) Error!Value {...@@ -1842,7 +1909,6 @@ fn ensureValueRuntime(s: *Sema, src: LazyNode, value: Value) Error!Value {
1842 .numeric_literal => {1909 .numeric_literal => {
1843 const float = value.toFloat(s.ip) orelse unreachable;1910 const float = value.toFloat(s.ip) orelse unreachable;
18441911
1845 std.debug.print("wtf:{d}\n", .{float});
1846 if (core.inInt32Range(float)) {1912 if (core.inInt32Range(float)) {
1847 try s.report.warn(.numeric_int_var_ambiguity, .{1913 try s.report.warn(.numeric_int_var_ambiguity, .{
1848 .src = src,1914 .src = src,
src/core/Transform.zig+29-9
...@@ -153,17 +153,13 @@ const ResultType = union(enum) {...@@ -153,17 +153,13 @@ const ResultType = union(enum) {
153153
154const BlockContext = struct {154const BlockContext = struct {
155 decl_node_offset: i32,155 decl_node_offset: i32,
156 return_target: ?*ReturnTarget = null,
157 break_target: ?*BreakTarget = null,156 break_target: ?*BreakTarget = null,
158157 return_target: ?*const ReturnTarget = null,
159 const ReturnTarget = struct {
160 block_index: Inst.Index,
161 result_type: ResultType,
162 };
163158
164 /// This struct is always stored on the stack; the idea is to pass mutable159 /// This struct is always stored on the stack; the idea is to pass mutable
165 /// pointers to it. When `break :brk` is encountered, it will generate the160 /// pointers to it. When `break :brk` is encountered, it will generate the
166 /// block instruction. This way, redundant blocks are never created.161 /// block instruction and save it in `block_index`. This way, the caller
162 /// can avoid having a Hir block instruction if it wont be used.
167 const BreakTarget = struct {163 const BreakTarget = struct {
168 label: ?[]const u8,164 label: ?[]const u8,
169 /// This allows `continue` as well as implicit break targeting.165 /// This allows `continue` as well as implicit break targeting.
...@@ -182,6 +178,14 @@ const BlockContext = struct {...@@ -182,6 +178,14 @@ const BlockContext = struct {
182 }178 }
183 };179 };
184180
181 /// Same strategy as `BreakTarget`, but it is not mutated. A function's
182 /// block is always generated first so that it's index is the first of it's
183 /// contents.
184 const ReturnTarget = struct {
185 block_index: Inst.Index,
186 result_type: ResultType,
187 };
188
185 fn init(root_node: NodeIndex) BlockContext {189 fn init(root_node: NodeIndex) BlockContext {
186 return .{190 return .{
187 .decl_node_offset = @intCast(root_node),191 .decl_node_offset = @intCast(root_node),
...@@ -198,7 +202,7 @@ const BlockContext = struct {...@@ -198,7 +202,7 @@ const BlockContext = struct {
198 };202 };
199 }203 }
200204
201 fn withReturn(self: BlockContext, return_target: ?*ReturnTarget) BlockContext {205 fn withReturn(self: BlockContext, return_target: ?*const ReturnTarget) BlockContext {
202 return .{206 return .{
203 .decl_node_offset = self.decl_node_offset,207 .decl_node_offset = self.decl_node_offset,
204 .return_target = return_target,208 .return_target = return_target,
...@@ -1113,7 +1117,6 @@ fn transformExpression(...@@ -1113,7 +1117,6 @@ fn transformExpression(
1113 .bool_not,1117 .bool_not,
1114 .bit_not,1118 .bit_not,
1115 .array_type,1119 .array_type,
1116 .assert,
1117 => |tag| {1120 => |tag| {
1118 const ast_un = t.ast.nodeData(node).un;1121 const ast_un = t.ast.nodeData(node).un;
1119 const teg_tag = @field(Hir.Tag, @tagName(tag));1122 const teg_tag = @field(Hir.Tag, @tagName(tag));
...@@ -1154,6 +1157,23 @@ fn transformExpression(...@@ -1154,6 +1157,23 @@ fn transformExpression(
1154 return operand;1157 return operand;
1155 },1158 },
11561159
1160 .assert => {
1161 const ast_un = t.ast.nodeData(node).un;
1162 const operand = try t.transformToBlock(ctx, ast_un.expr, .untracked_result, .{ .ref = .bool_type }, .run_time);
1163 // Move the source index to the `comptime` node
1164 if (operand.index()) |index| {
1165 const slice = t.insts.slice();
1166 const data: *EncodedData = &slice.items(.data)[@intFromEnum(index)];
1167 var existing = data.decode(.block, slice.items(.tag)[@intFromEnum(index)]);
1168 existing.src_node = ctx.nodeOffset(node);
1169 data.* = .encode(.block, existing);
1170 }
1171 return try t.addRef(.assert, .{
1172 .src_node = ctx.nodeOffset(node),
1173 .operand = operand,
1174 });
1175 },
1176
1157 .label => {1177 .label => {
1158 const expr = t.ast.nodeData(node).label.expr;1178 const expr = t.ast.nodeData(node).label.expr;
1159 const label = t.ast.tokenSlice(t.ast.nodeToken(node));1179 const label = t.ast.tokenSlice(t.ast.nodeToken(node));
src/core/js/Link.zig+5-2
...@@ -480,13 +480,16 @@ fn printExprLevel(...@@ -480,13 +480,16 @@ fn printExprLevel(
480 .ternary => {480 .ternary => {
481 const ternary: Tree.Ternary = tree.nodeData(.ternary, node);481 const ternary: Tree.Ternary = tree.nodeData(.ternary, node);
482 const data = ternary.data.get(tree);482 const data = ternary.data.get(tree);
483 try writer.writeAll("(");483 const wrap = Level.conditional.needsParens(level);
484 if (wrap)
485 try writer.writeAll("(");
484 try link.printExpr(tree, ternary.cond, writer);486 try link.printExpr(tree, ternary.cond, writer);
485 try writer.writeAll(" ? ");487 try writer.writeAll(" ? ");
486 try link.printExpr(tree, data.yes, writer);488 try link.printExpr(tree, data.yes, writer);
487 try writer.writeAll(" : ");489 try writer.writeAll(" : ");
488 try link.printExpr(tree, data.no, writer);490 try link.printExpr(tree, data.no, writer);
489 try writer.writeAll(")");491 if (wrap)
492 try writer.writeAll(")");
490 },493 },
491494
492 .call => {495 .call => {