10 files changed, 220 insertions(+), 25 deletions(-)
CHANGELOG.md+3| ... | ... | @@ -3,8 +3,11 @@ |
| 3 | 3 | - chore: rename the language to "ix" |
| 4 | 4 | - feat: language manual gets a pleasing redesign |
| 5 | 5 | - feat: allow any expression as a function body |
| 6 | - feat: add `assert` keyword | |
| 6 | 7 | - fix: coerce `break` operands and implicit breaks to result type |
| 7 | 8 | - 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 | |
| 8 | 11 | |
| 9 | 12 | ## 0.1.6 |
| 10 | 13 |
build.zig+1-1| ... | ... | @@ -121,7 +121,7 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup { |
| 121 | 121 | |
| 122 | 122 | // Main CLI |
| 123 | 123 | const cli = b.addExecutable(.{ |
| 124 | .name = "compiler", | |
| 124 | .name = "ix", | |
| 125 | 125 | .root_source_file = b.path("src/main.zig"), |
| 126 | 126 | .target = options.target, |
| 127 | 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 | 203 | => |e| return e, |
| 204 | 204 | |
| 205 | 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 | 212 | defer report.deinit(comp.gpa); |
| 208 | 213 | |
| 209 | 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 | 221 | }, |
| 217 | 222 | }; |
| 218 | 223 | |
| 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 | 236 | switch (unit.unwrap()) { |
| 220 | 237 | .decl => |index| { |
| 221 | 238 | comp.intern_pool.declPtr(index).analysis = .complete; |
src/core/Hir.Dump.zig+6-1| ... | ... | @@ -219,13 +219,18 @@ const State = struct { |
| 219 | 219 | .compile_error, |
| 220 | 220 | .type_of, |
| 221 | 221 | .array_type, |
| 222 | .assert, | |
| 223 | 222 | => |comptime_tag| { |
| 224 | 223 | const data = hir.instData(comptime_tag, inst); |
| 225 | 224 | try writer.writeAll("("); |
| 226 | 225 | try s.dumpRefMarker(writer, data.operand); |
| 227 | 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 | 234 | .call => { |
| 230 | 235 | const call: Hir.Call = hir.instData(.call, inst).extra_index.get(hir); |
| 231 | 236 | try writer.writeAll("("); |
src/core/Hir.zig+1-1| ... | ... | @@ -6,7 +6,7 @@ |
| 6 | 6 | //! expressions as flat instruction sequences. |
| 7 | 7 | //! |
| 8 | 8 | //! 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 | 11 | //! A helper program exists to visualize Hir: './build transform -- ./file.ix' |
| 12 | 12 | //! Alternatively, `ix build` has the `--dump-ir` option. |
src/core/InternPool.zig+21| ... | ... | @@ -410,6 +410,27 @@ pub const Value = enum(u32) { |
| 410 | 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 | }; |
| 414 | 435 | |
| 415 | 436 | /// 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 | 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 | 105 | cannot_be_comptime: struct { |
| 60 | 106 | src: LazyNode, |
| 61 | 107 | |
| ... | ... | @@ -538,7 +584,8 @@ pub const Warn = union(enum) { |
| 538 | 584 | |
| 539 | 585 | fn build(data: @This(), ctx: *MsgContext) !void { |
| 540 | 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 | 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 | 1253 | .field => { |
| 1207 | 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 | 1269 | else => {}, |
| 1210 | 1270 | } |
| 1211 | 1271 |
src/core/Sema.zig+75-9| ... | ... | @@ -490,19 +490,23 @@ const InstructionKind = enum { |
| 490 | 490 | pub inline fn fromTag(tag: Hir.Tag) InstructionKind { |
| 491 | 491 | return switch (tag) { |
| 492 | 492 | // these push new frames, and frame end will assign its value |
| 493 | .func, | |
| 494 | .block, | |
| 495 | 493 | .@"comptime", |
| 496 | 494 | .@"if", |
| 495 | .assert, | |
| 496 | .block, | |
| 497 | .func, | |
| 497 | 498 | => .control_flow, |
| 499 | ||
| 498 | 500 | // break is used to pop a frame, which has the |
| 499 | 501 | // possibility to end analysis |
| 500 | 502 | .@"break", |
| 501 | 503 | => .pop_control_flow, |
| 504 | ||
| 502 | 505 | // these are statements which do not evaluate to anything (void) |
| 503 | 506 | .discard, |
| 504 | 507 | .ensure_void, |
| 505 | 508 | => .void, |
| 509 | ||
| 506 | 510 | // all other instructions are single-visit, returning a value |
| 507 | 511 | else => .result, |
| 508 | 512 | }; |
| ... | ... | @@ -521,14 +525,14 @@ const PopControlFlowState = enum { stop, @"continue" }; |
| 521 | 525 | |
| 522 | 526 | fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag).Result() { |
| 523 | 527 | switch (tag) { |
| 524 | .block => unreachable, // frame must already be pushed | |
| 528 | .block, | |
| 529 | => unreachable, // frame must already be pushed | |
| 530 | ||
| 525 | 531 | .declaration, |
| 526 | 532 | .variable_declaration, |
| 527 | 533 | .test_declaration, |
| 528 | 534 | => unreachable, // declarations are not analzed by Sema |
| 529 | 535 | |
| 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 | 536 | .param => { |
| 533 | 537 | const param: Hir.DataNode = s.frame.currentData(.param); |
| 534 | 538 | 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 | 913 | }); |
| 910 | 914 | }, |
| 911 | 915 | |
| 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 | }, | |
| 915 | 934 | |
| 916 | 935 | .@"comptime" => { |
| 917 | 936 | const index = s.frame.currentIndex(); |
| ... | ... | @@ -1031,6 +1050,54 @@ fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag) |
| 1031 | 1050 | s.inst_map.add(s.frame.currentIndex(), operand); |
| 1032 | 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 | 1101 | .@"if" => { |
| 1035 | 1102 | const if_data: Hir.If = s.frame.currentPayload(.@"if"); |
| 1036 | 1103 | |
| ... | ... | @@ -1842,7 +1909,6 @@ fn ensureValueRuntime(s: *Sema, src: LazyNode, value: Value) Error!Value { |
| 1842 | 1909 | .numeric_literal => { |
| 1843 | 1910 | const float = value.toFloat(s.ip) orelse unreachable; |
| 1844 | 1911 | |
| 1845 | std.debug.print("wtf:{d}\n", .{float}); | |
| 1846 | 1912 | if (core.inInt32Range(float)) { |
| 1847 | 1913 | try s.report.warn(.numeric_int_var_ambiguity, .{ |
| 1848 | 1914 | .src = src, |
src/core/Transform.zig+29-9| ... | ... | @@ -153,17 +153,13 @@ const ResultType = union(enum) { |
| 153 | 153 | |
| 154 | 154 | const BlockContext = struct { |
| 155 | 155 | decl_node_offset: i32, |
| 156 | return_target: ?*ReturnTarget = null, | |
| 157 | 156 | 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, | |
| 163 | 158 | |
| 164 | 159 | /// This struct is always stored on the stack; the idea is to pass mutable |
| 165 | 160 | /// 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 | 163 | const BreakTarget = struct { |
| 168 | 164 | label: ?[]const u8, |
| 169 | 165 | /// This allows `continue` as well as implicit break targeting. |
| ... | ... | @@ -182,6 +178,14 @@ const BlockContext = struct { |
| 182 | 178 | } |
| 183 | 179 | }; |
| 184 | 180 | |
| 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 | 189 | fn init(root_node: NodeIndex) BlockContext { |
| 186 | 190 | return .{ |
| 187 | 191 | .decl_node_offset = @intCast(root_node), |
| ... | ... | @@ -198,7 +202,7 @@ const BlockContext = struct { |
| 198 | 202 | }; |
| 199 | 203 | } |
| 200 | 204 | |
| 201 | fn withReturn(self: BlockContext, return_target: ?*ReturnTarget) BlockContext { | |
| 205 | fn withReturn(self: BlockContext, return_target: ?*const ReturnTarget) BlockContext { | |
| 202 | 206 | return .{ |
| 203 | 207 | .decl_node_offset = self.decl_node_offset, |
| 204 | 208 | .return_target = return_target, |
| ... | ... | @@ -1113,7 +1117,6 @@ fn transformExpression( |
| 1113 | 1117 | .bool_not, |
| 1114 | 1118 | .bit_not, |
| 1115 | 1119 | .array_type, |
| 1116 | .assert, | |
| 1117 | 1120 | => |tag| { |
| 1118 | 1121 | const ast_un = t.ast.nodeData(node).un; |
| 1119 | 1122 | const teg_tag = @field(Hir.Tag, @tagName(tag)); |
| ... | ... | @@ -1154,6 +1157,23 @@ fn transformExpression( |
| 1154 | 1157 | return operand; |
| 1155 | 1158 | }, |
| 1156 | 1159 | |
| 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 | 1177 | .label => { |
| 1158 | 1178 | const expr = t.ast.nodeData(node).label.expr; |
| 1159 | 1179 | const label = t.ast.tokenSlice(t.ast.nodeToken(node)); |
src/core/js/Link.zig+5-2| ... | ... | @@ -480,13 +480,16 @@ fn printExprLevel( |
| 480 | 480 | .ternary => { |
| 481 | 481 | const ternary: Tree.Ternary = tree.nodeData(.ternary, node); |
| 482 | 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 | 486 | try link.printExpr(tree, ternary.cond, writer); |
| 485 | 487 | try writer.writeAll(" ? "); |
| 486 | 488 | try link.printExpr(tree, data.yes, writer); |
| 487 | 489 | try writer.writeAll(" : "); |
| 488 | 490 | try link.printExpr(tree, data.no, writer); |
| 489 | try writer.writeAll(")"); | |
| 491 | if (wrap) | |
| 492 | try writer.writeAll(")"); | |
| 490 | 493 | }, |
| 491 | 494 | |
| 492 | 495 | .call => { |