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 @@
33- chore: rename the language to "ix"
44- feat: language manual gets a pleasing redesign
55- feat: allow any expression as a function body
6- feat: add `assert` keyword
67- fix: coerce `break` operands and implicit breaks to result type
78- 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
912## 0.1.6
1013
build.zig+1-1
......@@ -121,7 +121,7 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup {
121121
122122 // Main CLI
123123 const cli = b.addExecutable(.{
124 .name = "compiler",
124 .name = "ix",
125125 .root_source_file = b.path("src/main.zig"),
126126 .target = options.target,
127127 .optimize = options.optimize,
src/core/Comp.zig+18-1
......@@ -203,7 +203,12 @@ fn runAnalysisJob(comp: *Comp, unit: InternPool.AnalysisUnit, first_blocked_sema
203203 => |e| return e,
204204
205205 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 };
207212 defer report.deinit(comp.gpa);
208213
209214 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
216221 },
217222 };
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
219236 switch (unit.unwrap()) {
220237 .decl => |index| {
221238 comp.intern_pool.declPtr(index).analysis = .complete;
src/core/Hir.Dump.zig+6-1
......@@ -219,13 +219,18 @@ const State = struct {
219219 .compile_error,
220220 .type_of,
221221 .array_type,
222 .assert,
223222 => |comptime_tag| {
224223 const data = hir.instData(comptime_tag, inst);
225224 try writer.writeAll("(");
226225 try s.dumpRefMarker(writer, data.operand);
227226 try writer.writeAll(")");
228227 },
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 },
229234 .call => {
230235 const call: Hir.Call = hir.instData(.call, inst).extra_index.get(hir);
231236 try writer.writeAll("(");
src/core/Hir.zig+1-1
......@@ -6,7 +6,7 @@
66//! expressions as flat instruction sequences.
77//!
88//! 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.
1010//!
1111//! A helper program exists to visualize Hir: './build transform -- ./file.ix'
1212//! Alternatively, `ix build` has the `--dump-ir` option.
src/core/InternPool.zig+21
......@@ -410,6 +410,27 @@ pub const Value = enum(u32) {
410410 else => null,
411411 };
412412 }
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 }
413434};
414435
415436/// 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) {
5656 try ctx.msgNode(try data.src.get(ctx), "value must be known at compile-time", .{});
5757 }
5858 },
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 },
59105 cannot_be_comptime: struct {
60106 src: LazyNode,
61107
......@@ -538,7 +584,8 @@ pub const Warn = union(enum) {
538584
539585 fn build(data: @This(), ctx: *MsgContext) !void {
540586 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
542589 try ctx.msgAlone("add \": float\" or \": int\" after the variable name to set the type", .{});
543590 }
544591 },
......@@ -1206,6 +1253,19 @@ pub const LazyNode = struct {
12061253 .field => {
12071254 return base_node;
12081255 },
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 },
12091269 else => {},
12101270 }
12111271
src/core/Sema.zig+75-9
......@@ -490,19 +490,23 @@ const InstructionKind = enum {
490490 pub inline fn fromTag(tag: Hir.Tag) InstructionKind {
491491 return switch (tag) {
492492 // these push new frames, and frame end will assign its value
493 .func,
494 .block,
495493 .@"comptime",
496494 .@"if",
495 .assert,
496 .block,
497 .func,
497498 => .control_flow,
499
498500 // break is used to pop a frame, which has the
499501 // possibility to end analysis
500502 .@"break",
501503 => .pop_control_flow,
504
502505 // these are statements which do not evaluate to anything (void)
503506 .discard,
504507 .ensure_void,
505508 => .void,
509
506510 // all other instructions are single-visit, returning a value
507511 else => .result,
508512 };
......@@ -521,14 +525,14 @@ const PopControlFlowState = enum { stop, @"continue" };
521525
522526fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag).Result() {
523527 switch (tag) {
524 .block => unreachable, // frame must already be pushed
528 .block,
529 => unreachable, // frame must already be pushed
530
525531 .declaration,
526532 .variable_declaration,
527533 .test_declaration,
528534 => 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
532536 .param => {
533537 const param: Hir.DataNode = s.frame.currentData(.param);
534538 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)
909913 });
910914 },
911915
912 // .assert => {
913 // return Value.void.toAir();
914 // },
916 .assert => {
917 const operand = s.frame.currentData(.assert).operand;
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
916935 .@"comptime" => {
917936 const index = s.frame.currentIndex();
......@@ -1031,6 +1050,54 @@ fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag)
10311050 s.inst_map.add(s.frame.currentIndex(), operand);
10321051 s.frame.advance();
10331052 },
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 },
10341101 .@"if" => {
10351102 const if_data: Hir.If = s.frame.currentPayload(.@"if");
10361103
......@@ -1842,7 +1909,6 @@ fn ensureValueRuntime(s: *Sema, src: LazyNode, value: Value) Error!Value {
18421909 .numeric_literal => {
18431910 const float = value.toFloat(s.ip) orelse unreachable;
18441911
1845 std.debug.print("wtf:{d}\n", .{float});
18461912 if (core.inInt32Range(float)) {
18471913 try s.report.warn(.numeric_int_var_ambiguity, .{
18481914 .src = src,
src/core/Transform.zig+29-9
......@@ -153,17 +153,13 @@ const ResultType = union(enum) {
153153
154154const BlockContext = struct {
155155 decl_node_offset: i32,
156 return_target: ?*ReturnTarget = null,
157156 break_target: ?*BreakTarget = null,
158
159 const ReturnTarget = struct {
160 block_index: Inst.Index,
161 result_type: ResultType,
162 };
157 return_target: ?*const ReturnTarget = null,
163158
164159 /// This struct is always stored on the stack; the idea is to pass mutable
165160 /// 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.
167163 const BreakTarget = struct {
168164 label: ?[]const u8,
169165 /// This allows `continue` as well as implicit break targeting.
......@@ -182,6 +178,14 @@ const BlockContext = struct {
182178 }
183179 };
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
185189 fn init(root_node: NodeIndex) BlockContext {
186190 return .{
187191 .decl_node_offset = @intCast(root_node),
......@@ -198,7 +202,7 @@ const BlockContext = struct {
198202 };
199203 }
200204
201 fn withReturn(self: BlockContext, return_target: ?*ReturnTarget) BlockContext {
205 fn withReturn(self: BlockContext, return_target: ?*const ReturnTarget) BlockContext {
202206 return .{
203207 .decl_node_offset = self.decl_node_offset,
204208 .return_target = return_target,
......@@ -1113,7 +1117,6 @@ fn transformExpression(
11131117 .bool_not,
11141118 .bit_not,
11151119 .array_type,
1116 .assert,
11171120 => |tag| {
11181121 const ast_un = t.ast.nodeData(node).un;
11191122 const teg_tag = @field(Hir.Tag, @tagName(tag));
......@@ -1154,6 +1157,23 @@ fn transformExpression(
11541157 return operand;
11551158 },
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
11571177 .label => {
11581178 const expr = t.ast.nodeData(node).label.expr;
11591179 const label = t.ast.tokenSlice(t.ast.nodeToken(node));
src/core/js/Link.zig+5-2
......@@ -480,13 +480,16 @@ fn printExprLevel(
480480 .ternary => {
481481 const ternary: Tree.Ternary = tree.nodeData(.ternary, node);
482482 const data = ternary.data.get(tree);
483 try writer.writeAll("(");
483 const wrap = Level.conditional.needsParens(level);
484 if (wrap)
485 try writer.writeAll("(");
484486 try link.printExpr(tree, ternary.cond, writer);
485487 try writer.writeAll(" ? ");
486488 try link.printExpr(tree, data.yes, writer);
487489 try writer.writeAll(" : ");
488490 try link.printExpr(tree, data.no, writer);
489 try writer.writeAll(")");
491 if (wrap)
492 try writer.writeAll(")");
490493 },
491494
492495 .call => {