From 230648717c1f197ece3aec476cda99ad3fa158a6 Mon Sep 17 00:00:00 2001 From: chloe caruso Date: Mon, 23 Sep 2024 00:48:19 -0700 Subject: [PATCH] feat: basic version of assert (comptime only) --- CHANGELOG.md | 3 ++ build.zig | 2 +- src/core/Comp.zig | 19 +++++++++- src/core/Hir.Dump.zig | 7 +++- src/core/Hir.zig | 2 +- src/core/InternPool.zig | 21 +++++++++++ src/core/Report.zig | 62 +++++++++++++++++++++++++++++- src/core/Sema.zig | 84 ++++++++++++++++++++++++++++++++++++----- src/core/Transform.zig | 38 ++++++++++++++----- src/core/js/Link.zig | 7 +++- 10 files changed, 220 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2c692c61352a13bc8ab50470aaca7c3e6e4d562..cef67cca065aa40e8ff5ed3caecaa8f70b5cec40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,11 @@ - chore: rename the language to "ix" - feat: language manual gets a pleasing redesign - feat: allow any expression as a function body +- feat: add `assert` keyword - fix: coerce `break` operands and implicit breaks to result type - fix: coerce results when `if` expression branches merge +- fix: ensure if branch results to a runtime value when the condition is not compile-time known +- fix(js): stop printing redundant parenthesis on ternary expressions ## 0.1.6 diff --git a/build.zig b/build.zig index fd1ddde0fd0851b9a7bfc20743532ba72102d5d0..257a59d66d88b2d01d1ad288c07282f3f7dc5ed6 100644 --- a/build.zig +++ b/build.zig @@ -121,7 +121,7 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup { // Main CLI const cli = b.addExecutable(.{ - .name = "compiler", + .name = "ix", .root_source_file = b.path("src/main.zig"), .target = options.target, .optimize = options.optimize, diff --git a/src/core/Comp.zig b/src/core/Comp.zig index 667560a920c7971bbd8db7b51ecdf90e4ce6041b..d3b7c1331f7ce555fcaa6d699ecccb50a6e376a0 100644 --- a/src/core/Comp.zig +++ b/src/core/Comp.zig @@ -203,7 +203,12 @@ fn runAnalysisJob(comp: *Comp, unit: InternPool.AnalysisUnit, first_blocked_sema => |e| return e, error.AnalysisFail => |e| { - const report = try sema.report.finalize(); + const report = sema.report.finalize() catch { + comp.ongoing_analysis.removeByPtr(gop.key_ptr); + _ = sema.deinit(.free_all); // TODO: arena reuse + comp.gpa.destroy(sema); + return e; + }; defer report.deinit(comp.gpa); 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 }, }; + errdefer { + comp.ongoing_analysis.removeByPtr(gop.key_ptr); + _ = sema.deinit(.free_all); // TODO: arena reuse + comp.gpa.destroy(sema); + } + + const report = try sema.report.finalize(); + defer report.deinit(comp.gpa); // TODO: store warnings + if (report.warnings.len > 0) { + report.dump(.{ .comp = .{ .comp = comp, .file = sema.file } }, comp.out) catch {}; + } + switch (unit.unwrap()) { .decl => |index| { comp.intern_pool.declPtr(index).analysis = .complete; diff --git a/src/core/Hir.Dump.zig b/src/core/Hir.Dump.zig index 53ee3b62b2379aa76b05b38af5132b0e6af6ebe7..37340b43f28d7dee5bc1e760d479fb24b9a4f82f 100644 --- a/src/core/Hir.Dump.zig +++ b/src/core/Hir.Dump.zig @@ -219,13 +219,18 @@ const State = struct { .compile_error, .type_of, .array_type, - .assert, => |comptime_tag| { const data = hir.instData(comptime_tag, inst); try writer.writeAll("("); try s.dumpRefMarker(writer, data.operand); try writer.writeAll(")"); }, + .assert => { + const data = hir.instData(.assert, inst); + try writer.writeAll("("); + try s.dumpRef(writer, data.operand); + try writer.writeAll(")"); + }, .call => { const call: Hir.Call = hir.instData(.call, inst).extra_index.get(hir); try writer.writeAll("("); diff --git a/src/core/Hir.zig b/src/core/Hir.zig index 9225070af8bc6b57955d667d7f732c9cdcc68d27..bbccba1dfd9e34ed4008d8664cb9b69354c3fb52 100644 --- a/src/core/Hir.zig +++ b/src/core/Hir.zig @@ -6,7 +6,7 @@ //! expressions as flat instruction sequences. //! //! Hir for a given file has no dependencies on it's imports, meaning it can be -//! computed in parallel and cached when not changed. +//! computed in parallel and then cached when not changed. //! //! A helper program exists to visualize Hir: './build transform -- ./file.ix' //! Alternatively, `ix build` has the `--dump-ir` option. diff --git a/src/core/InternPool.zig b/src/core/InternPool.zig index 5671940652839126f561c7ebdd219e84f02d8073..87b13fd222a812f94e3bce7b056f727045c0970d 100644 --- a/src/core/InternPool.zig +++ b/src/core/InternPool.zig @@ -410,6 +410,27 @@ pub const Value = enum(u32) { else => null, }; } + + pub fn displayName(value: Value, ip: *const InternPool) std.fmt.Formatter(formatDisplayName) { + return .{ .data = .{ .value = value, .ip = ip } }; + } + + fn formatDisplayName( + ctx: struct { value: Value, ip: *const InternPool }, + comptime _: []const u8, + _: std.fmt.FormatOptions, + writer: anytype, + ) !void { + const value = ctx.value; + const ip = ctx.ip; + const key = value.toKey(ip); + switch (key) { + inline .comptime_float, .int32, .float => |n| { + try writer.print("{d}", .{n}); + }, + else => |tag| std.debug.panic("TODO: print value .{s}", .{@tagName(tag)}), + } + } }; /// Type is subset of Value which represents the values that have typeOf() == Value.type_type diff --git a/src/core/Report.zig b/src/core/Report.zig index a24ea2b3b5924335cd6d76c444130094ad3957a6..c2140a8f559ae83e59fd513d8f058c1654fcd10b 100644 --- a/src/core/Report.zig +++ b/src/core/Report.zig @@ -56,6 +56,52 @@ pub const Err = union(enum) { try ctx.msgNode(try data.src.get(ctx), "value must be known at compile-time", .{}); } }, + user_assertion_basic: struct { + src: LazyNode, + + fn build(data: @This(), ctx: *MsgContext) !void { + try ctx.msgNode(try data.src.get(ctx), "assertion failure", .{}); + } + }, + user_assertion_bin: struct { + src: LazyNode, + left: Value, + right: Value, + + fn build(data: @This(), ctx: *MsgContext) !void { + const node = try data.src.get(ctx); + try ctx.msgNode(node, "assertion failure: {} {s} {}", .{ + fmtAssertionOperand(data.left, ctx.ip()), + switch (ctx.ast.nodeTag(node)) { + .eq => "==", + .ne => "!=", + .lt => "<", + .le => "<=", + .gt => ">", + .ge => ">=", + else => unreachable, + }, + fmtAssertionOperand(data.right, ctx.ip()), + }); + } + + fn fmtAssertionOperand(v: Value, ip: *const InternPool) std.fmt.Formatter(fmtAssertionOperandImpl) { + return .{ .data = .{ .value = v, .ip = ip } }; + } + + fn fmtAssertionOperandImpl( + data: struct { value: Value, ip: *const InternPool }, + comptime _: []const u8, + _: std.fmt.FormatOptions, + w: anytype, + ) !void { + if (data.value.toType(data.ip)) |t| { + try w.print("any {}", .{t.displayName(data.ip)}); + } else { + try w.print("{}", .{data.value.displayName(data.ip)}); + } + } + }, cannot_be_comptime: struct { src: LazyNode, @@ -538,7 +584,8 @@ pub const Warn = union(enum) { fn build(data: @This(), ctx: *MsgContext) !void { const node = try data.src.get(ctx); - try ctx.msgNode(node, "constant integer cannot be represented as a runtime value, implicitly casting to \"float\"", .{}); + try ctx.msgNode(node, "constant numeric values at runtime needs a result type, implicitly casting to \"float\"", .{}); + // TODO: trace to the result type and point at it try ctx.msgAlone("add \": float\" or \": int\" after the variable name to set the type", .{}); } }, @@ -1206,6 +1253,19 @@ pub const LazyNode = struct { .field => { return base_node; }, + .@"if" => { + assert(node_tag == .@"if"); + switch (lazy.operand) { + .operand => return data.@"if".cond, + .lhs => return ctx.ast.extraData(data.@"if".extra, Ast.If).yes, + .rhs => return ctx.ast.extraData(data.@"if".extra, Ast.If).no.unwrap().?, + else => {}, + } + }, + .assert => { + assert(node_tag == .assert); + return data.un.expr; + }, else => {}, } diff --git a/src/core/Sema.zig b/src/core/Sema.zig index b18358192c1a7dbd78d449a28c139f16143f8c01..21dbaad637bbc48ff2334d958393577a15bf4bf9 100644 --- a/src/core/Sema.zig +++ b/src/core/Sema.zig @@ -490,19 +490,23 @@ const InstructionKind = enum { pub inline fn fromTag(tag: Hir.Tag) InstructionKind { return switch (tag) { // these push new frames, and frame end will assign its value - .func, - .block, .@"comptime", .@"if", + .assert, + .block, + .func, => .control_flow, + // break is used to pop a frame, which has the // possibility to end analysis .@"break", => .pop_control_flow, + // these are statements which do not evaluate to anything (void) .discard, .ensure_void, => .void, + // all other instructions are single-visit, returning a value else => .result, }; @@ -521,14 +525,14 @@ const PopControlFlowState = enum { stop, @"continue" }; fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag).Result() { switch (tag) { - .block => unreachable, // frame must already be pushed + .block, + => unreachable, // frame must already be pushed + .declaration, .variable_declaration, .test_declaration, => unreachable, // declarations are not analzed by Sema - // TODO: pre-evaluate `.param` nodes, since they are always at the start of a function. - // this way we can avoid repeating the non-trivial call into InternPool .param => { const param: Hir.DataNode = s.frame.currentData(.param); 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) }); }, - // .assert => { - // return Value.void.toAir(); - // }, + .assert => { + const operand = s.frame.currentData(.assert).operand; + switch (operand.decode()) { + .value => |val| { + const coerced = try s.coerceValue(s.lazyNode(.operand), val, .bool); + if (coerced == .bool_false) { + return s.fail(.user_assertion_basic, .{ .src = s.lazyNode(.operand) }); + } + s.frame.advance(); + return; + }, + .index => |index| { + const mode = s.frame.mode; + try s.pushFrame(); + s.frame = Frame.initFromBlock(s, index, mode, .none); + }, + } + }, .@"comptime" => { const index = s.frame.currentIndex(); @@ -1031,6 +1050,54 @@ fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag) s.inst_map.add(s.frame.currentIndex(), operand); s.frame.advance(); }, + .assert => { + const coerced = try s.coerceRef(s.lazyNode(.operand), operand, .bool); + switch (coerced.decode()) { + .value => |val| switch (val) { + .bool_true => {}, // pass + .bool_false => { + // Scan for the pattern: + // %n = assert(%n-3 = block [ + // ... + // %n-4 = eq(...) + // %n-2 = coercion(%bool_type, ...) + // %n-1 = break(%x, %n-2) + // ]) + const cmp_index: Hir.Inst.Index = @enumFromInt(@intFromEnum(s.frame.currentIndex()) - 4); + switch (s.hir.instTag(cmp_index)) { + .eq, + .ne, + .lt, + .le, + .gt, + .ge, + => { + const cmp_inst = s.hir.instDataUnsafe(Hir.PlNode(Hir.Bin), cmp_index); + const cmp_pl: Hir.Bin = cmp_inst.extra_index.get(&s.hir); + return s.fail(.user_assertion_bin, .{ + .src = s.lazyNode(.operand), + .left = switch (s.getRef(cmp_pl.lhs, .no_mark).decode()) { + .value => |v| v, + .index => @panic("TODO"), + }, + .right = switch (s.getRef(cmp_pl.rhs, .no_mark).decode()) { + .value => |v| v, + .index => @panic("TODO"), + }, + }); + }, + else => {}, + } + return s.fail(.user_assertion_basic, .{ .src = s.lazyNode(.operand) }); + }, + else => unreachable, + }, + .index => { + @panic("TODO"); + }, + } + s.frame.advance(); + }, .@"if" => { const if_data: Hir.If = s.frame.currentPayload(.@"if"); @@ -1842,7 +1909,6 @@ fn ensureValueRuntime(s: *Sema, src: LazyNode, value: Value) Error!Value { .numeric_literal => { const float = value.toFloat(s.ip) orelse unreachable; - std.debug.print("wtf:{d}\n", .{float}); if (core.inInt32Range(float)) { try s.report.warn(.numeric_int_var_ambiguity, .{ .src = src, diff --git a/src/core/Transform.zig b/src/core/Transform.zig index a2c2d8b1f9fe7b70ef9f07c5ffec1593ad00791c..81a7060b43296d36083d609a5b57283ccaf61805 100644 --- a/src/core/Transform.zig +++ b/src/core/Transform.zig @@ -153,17 +153,13 @@ const ResultType = union(enum) { const BlockContext = struct { decl_node_offset: i32, - return_target: ?*ReturnTarget = null, break_target: ?*BreakTarget = null, - - const ReturnTarget = struct { - block_index: Inst.Index, - result_type: ResultType, - }; + return_target: ?*const ReturnTarget = null, /// This struct is always stored on the stack; the idea is to pass mutable /// pointers to it. When `break :brk` is encountered, it will generate the - /// block instruction. This way, redundant blocks are never created. + /// block instruction and save it in `block_index`. This way, the caller + /// can avoid having a Hir block instruction if it wont be used. const BreakTarget = struct { label: ?[]const u8, /// This allows `continue` as well as implicit break targeting. @@ -182,6 +178,14 @@ const BlockContext = struct { } }; + /// Same strategy as `BreakTarget`, but it is not mutated. A function's + /// block is always generated first so that it's index is the first of it's + /// contents. + const ReturnTarget = struct { + block_index: Inst.Index, + result_type: ResultType, + }; + fn init(root_node: NodeIndex) BlockContext { return .{ .decl_node_offset = @intCast(root_node), @@ -198,7 +202,7 @@ const BlockContext = struct { }; } - fn withReturn(self: BlockContext, return_target: ?*ReturnTarget) BlockContext { + fn withReturn(self: BlockContext, return_target: ?*const ReturnTarget) BlockContext { return .{ .decl_node_offset = self.decl_node_offset, .return_target = return_target, @@ -1113,7 +1117,6 @@ fn transformExpression( .bool_not, .bit_not, .array_type, - .assert, => |tag| { const ast_un = t.ast.nodeData(node).un; const teg_tag = @field(Hir.Tag, @tagName(tag)); @@ -1154,6 +1157,23 @@ fn transformExpression( return operand; }, + .assert => { + const ast_un = t.ast.nodeData(node).un; + const operand = try t.transformToBlock(ctx, ast_un.expr, .untracked_result, .{ .ref = .bool_type }, .run_time); + // Move the source index to the `comptime` node + if (operand.index()) |index| { + const slice = t.insts.slice(); + const data: *EncodedData = &slice.items(.data)[@intFromEnum(index)]; + var existing = data.decode(.block, slice.items(.tag)[@intFromEnum(index)]); + existing.src_node = ctx.nodeOffset(node); + data.* = .encode(.block, existing); + } + return try t.addRef(.assert, .{ + .src_node = ctx.nodeOffset(node), + .operand = operand, + }); + }, + .label => { const expr = t.ast.nodeData(node).label.expr; const label = t.ast.tokenSlice(t.ast.nodeToken(node)); diff --git a/src/core/js/Link.zig b/src/core/js/Link.zig index fb436e07f7413b812bdb416542ebfb4944faa6b9..948f5261aaf2fe154de5cabf26c3639f1eaf4edc 100644 --- a/src/core/js/Link.zig +++ b/src/core/js/Link.zig @@ -480,13 +480,16 @@ fn printExprLevel( .ternary => { const ternary: Tree.Ternary = tree.nodeData(.ternary, node); const data = ternary.data.get(tree); - try writer.writeAll("("); + const wrap = Level.conditional.needsParens(level); + if (wrap) + try writer.writeAll("("); try link.printExpr(tree, ternary.cond, writer); try writer.writeAll(" ? "); try link.printExpr(tree, data.yes, writer); try writer.writeAll(" : "); try link.printExpr(tree, data.no, writer); - try writer.writeAll(")"); + if (wrap) + try writer.writeAll(")"); }, .call => { -- 2.54.0