From fd9bc5492894391c36ce4ee485e103ceb0255f76 Mon Sep 17 00:00:00 2001 From: chloe caruso Date: Sun, 27 Oct 2024 01:50:09 -0700 Subject: [PATCH] feat: safety check code (#121) array out of bounds should raise an unrecoverable error. this will also happen for addition out of bounds, runtime assertions, and other things. safety checks are generated Air instructions using existing constructs like `if`, with a new instruction tag: `panic` which is intended to raise an unrecoverable error. using air lets these checks lower to any backend without additional work, as well as take advantage of existing and future optimizations. this also adds `bool_and` and `bool_or` in the Transform pass, but these generate `if` instructions to re-use logic for short circuiting. the js backend can't actually make an unrecoverable error. since try catch blocks are banned, this is fine. in a node.js target, NodeGen could make this use `process.exit`. for wasm and native, this can lower to a log combined with a trap instruction. --- CHANGELOG.md | 3 + build.zig | 3 +- build.zig.zon | 2 +- src/core/Air.Dump.zig | 3 + src/core/Air.zig | 16 ++-- src/core/Ast.zig | 12 +-- src/core/Hir.Dump.zig | 2 +- src/core/Hir.zig | 7 +- src/core/Parse.zig | 27 +++--- src/core/Sema.zig | 191 +++++++++++++++++++++++++++++-------- src/core/Tokenizer.zig | 2 + src/core/Transform.zig | 116 ++++++++++++++++------ src/core/js/Link.zig | 34 ++++++- src/core/js/NodeGen.zig | 127 ++++++++++++++++++++++-- src/core/js/Tree.zig | 18 ++++ src/deps/quickjs/build.zig | 4 +- test/behavior/array | 4 +- test/compiler_tests.zig | 2 +- 18 files changed, 458 insertions(+), 115 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cef67cca065aa40e8ff5ed3caecaa8f70b5cec40..8b2cdddfab73437c4e9666c37c0f23b07d6ffd14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,14 @@ - chore: rename the language to "ix" - feat: language manual gets a pleasing redesign +- feat: add boolean `and` and `or` operator keywords - feat: allow any expression as a function body - feat: add `assert` keyword +- feat: add safety checks for array out of bounds - 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: inferred array type uses item type for items instead of the container type. - fix(js): stop printing redundant parenthesis on ternary expressions ## 0.1.6 diff --git a/build.zig b/build.zig index 257a59d66d88b2d01d1ad288c07282f3f7dc5ed6..4b86b16e047ec9411a45e199aeabf8d0ebe055bc 100644 --- a/build.zig +++ b/build.zig @@ -594,7 +594,8 @@ fn crossTarget(cpu_arch: std.Target.Cpu.Arch, os_tag: std.Target.Os.Tag) Build.R }; return .{ .query = query, - .result = std.zig.system.resolveTargetQuery(query) catch unreachable, + .result = std.zig.system.resolveTargetQuery(query) catch + @panic("unable to resolve target query"), }; } diff --git a/build.zig.zon b/build.zig.zon index 538cf310e795f86cb98eef8c489f183a6d7315ff..aa369c31acbf845428cae143b9e24886e0ab9ac9 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,7 +2,7 @@ .name = "", .version = "0.0.0", - .minimum_zig_version = "0.14.0-dev.1511+54b668f8a", + .minimum_zig_version = "0.14.0-dev.2034+56996a280", .dependencies = .{ // Used to determine unicode properties to tokenize identifiers diff --git a/src/core/Air.Dump.zig b/src/core/Air.Dump.zig index d95fe6e89ff03e21ffc1188369aefe606239aed4..dd4ed41e246d1f3575913faf5b36ab02a76b79bb 100644 --- a/src/core/Air.Dump.zig +++ b/src/core/Air.Dump.zig @@ -87,6 +87,8 @@ pub const State = struct { .lt, .gt, .eq, + .le, + .ge, .field_array, .field_opaque, => |comptime_tag| { @@ -100,6 +102,7 @@ pub const State = struct { inline .negate, .bool_not, .array_len, + .panic, => |comptime_tag| { const bin: Air.Un = air.instData(comptime_tag, inst); try w.writeAll("("); diff --git a/src/core/Air.zig b/src/core/Air.zig index 50c3119f6789dbecdff0f15b66e333fef6ee981a..058465edbf0d61453464213ac96e36f671168bd7 100644 --- a/src/core/Air.zig +++ b/src/core/Air.zig @@ -34,20 +34,19 @@ pub const Inst = struct { array_len, - // bin add, sub, mul, div, - eq, - lt, + le, gt, + ge, - // un negate, bool_not, + panic, pub fn Data(comptime tag: Tag) type { return switch (tag) { @@ -71,10 +70,13 @@ pub const Inst = struct { .lt, .gt, .eq, + .le, + .ge, => Bin, .negate, .bool_not, .array_len, + .panic, => Un, }; } @@ -85,6 +87,8 @@ pub const Inst = struct { kind: Kind, data: u31, + pub const @"void" = Value.void.toAir(); + pub const Kind = enum(u1) { value, index }; pub const Decoded = union(enum) { @@ -345,8 +349,8 @@ pub fn typeOfIndex(air: *const Air, index: Inst.Index, ip: *const InternPool) In .js_new => .@"opaque", // These operations always produce known types - .lt, .gt, .eq, .bool_not => .bool, - .@"break" => .never, + .lt, .gt, .le, .ge, .eq, .bool_not => .bool, + .@"break", .panic => .never, .field_opaque => .@"opaque", .array_len => .int32, diff --git a/src/core/Ast.zig b/src/core/Ast.zig index 5fbfba34a7793845b15fbc35223e064d6455b46d..5404ccb3ac2e3601867cf50b925fcc3a12cd4b1d 100644 --- a/src/core/Ast.zig +++ b/src/core/Ast.zig @@ -144,9 +144,9 @@ pub const Node = struct { /// `lhs ** rhs`. main_token is the operator. pow, /// `lhs && rhs`. main_token is the operator. - @"and", + bool_and, /// `lhs || rhs`. main_token is the operator. - @"or", + bool_or, /// `lhs & rhs`. main_token is the operator. bit_and, /// `lhs | rhs`. main_token is the operator. @@ -247,8 +247,8 @@ pub const Node = struct { .div, .rem, .pow, - .@"and", - .@"or", + .bool_and, + .bool_or, .bit_and, .bit_or, .bit_xor, @@ -280,11 +280,11 @@ pub const Node = struct { pub const data_tags = std.enums.directEnumArray(Tag, Data.FieldEnum, 0, .{ // sort-lines:start - .@"and" = .bin, + .bool_and = .bin, .@"break" = .@"break", .@"comptime" = .un, .@"if" = .@"if", - .@"or" = .bin, + .bool_or = .bin, .@"return" = .@"return", .add = .bin, .add_assign = .bin, diff --git a/src/core/Hir.Dump.zig b/src/core/Hir.Dump.zig index 37340b43f28d7dee5bc1e760d479fb24b9a4f82f..e4ab47d1c5c69cad842aee41d752f4c543df03a1 100644 --- a/src/core/Hir.Dump.zig +++ b/src/core/Hir.Dump.zig @@ -307,7 +307,7 @@ const State = struct { if (data.type_hint.unwrap()) |type_hint| { try writer.writeAll("("); - try s.dumpRef(writer, type_hint); + try s.dumpRefMarker(writer, type_hint); try writer.writeAll(", "); } diff --git a/src/core/Hir.zig b/src/core/Hir.zig index bbccba1dfd9e34ed4008d8664cb9b69354c3fb52..7040ab59dd7664ea7866e42d23c98973973b2810 100644 --- a/src/core/Hir.zig +++ b/src/core/Hir.zig @@ -60,9 +60,12 @@ pub const Tag = enum(u8) { /// Asserts the operand is `void`. Used for undiscarded expressions. ensure_void, - /// Branch takes a boolean condition and takes either a yes or no path. + // TODO: Rename to `.branch` + /// Given a boolean condition, pick one of two branches. This is used to + /// implement `if` statements as well as boolean `and` / `or` operators. @"if", - /// Given a block instruction + /// Given a block instruction, evaluate to a Value without generating + /// any runtime instructions. @"comptime", /// A function expression, with or without captures diff --git a/src/core/Parse.zig b/src/core/Parse.zig index 84421173d3a04f341bd60349920b23db46e44279..487c8195bd1986dfe7dcae8f93c750cbe55567b5 100644 --- a/src/core/Parse.zig +++ b/src/core/Parse.zig @@ -111,8 +111,8 @@ const Operator = enum(u8) { div = @intFromEnum(Token.Tag.slash), rem = @intFromEnum(Token.Tag.percent), pow = @intFromEnum(Token.Tag.star_star), - @"and" = @intFromEnum(Token.Tag.and_and), - @"or" = @intFromEnum(Token.Tag.pipe_pipe), + bool_and = @intFromEnum(Token.Tag.kw_and), + bool_or = @intFromEnum(Token.Tag.kw_or), bit_and = @intFromEnum(Token.Tag.@"and"), bit_or = @intFromEnum(Token.Tag.pipe), bit_xor = @intFromEnum(Token.Tag.caret), @@ -209,8 +209,8 @@ const Operator = enum(u8) { .shl, .shr => .bitwise_shift, .lt, .le, .gt, .ge => .relational, .eq, .ne => .equality, - .@"and" => .logical_and, - .@"or" => .logical_or, + .bool_and => .logical_and, + .bool_or => .logical_or, .bit_and => .bit_and, .bit_xor => .bit_xor, .bit_or => .bit_or, @@ -1493,14 +1493,15 @@ const Comp = core.Comp; test "fuzz test parser for crashes" { try core.unicode_props.init(); + try std.testing.fuzz(struct { + fn run(input_bytes: []const u8) !void { + const sentinel = try std.testing.allocator.dupeZ(u8, input_bytes); + defer std.testing.allocator.free(sentinel); - const input_bytes = std.testing.fuzzInput(.{}); - - const sentinel = try std.testing.allocator.dupeZ(u8, input_bytes); - defer std.testing.allocator.free(sentinel); - - var err: AstError.WithTrace = undefined; - var ast = parse(std.testing.allocator, sentinel, &err) catch - return; // ignore parser errors, only fail on crashes - ast.deinit(std.testing.allocator); + var err: AstError.WithTrace = undefined; + var ast = parse(std.testing.allocator, sentinel, &err) catch + return; // ignore parser errors, only fail on crashes + ast.deinit(std.testing.allocator); + } + }.run, .{}); } diff --git a/src/core/Sema.zig b/src/core/Sema.zig index 21dbaad637bbc48ff2334d958393577a15bf4bf9..e4ae44be6e1ce2193fbf37a49f0d6552465a6ec6 100644 --- a/src/core/Sema.zig +++ b/src/core/Sema.zig @@ -13,6 +13,8 @@ arena_state: std.heap.ArenaAllocator, /// When this is .decl, we are producing a `Value` owner: AnalysisUnit, +generate_safety_checks: bool = true, + /// The namespace index that is used for declaration lookups ns_index: Namespace.Index, /// Non-owning copy @@ -358,8 +360,9 @@ fn markInst(s: *Sema, inst: Air.Inst.Index) void { flags.uses = flags.uses.increment(); } -// TODO: name when awaake pls -const DoMark = enum { +const LivenessMark = enum { + /// The value retrieved will not be used as the operand for another + /// instruction. no_mark, /// A Ref must be marked if it's value is used as the operand for another /// Air instruction. This informs the Js backend if an expression must @@ -367,7 +370,7 @@ const DoMark = enum { marked, }; -fn getRef(s: *Sema, teg_ref: Hir.Inst.Ref, mark: DoMark) Air.Inst.Ref { +fn getRef(s: *Sema, teg_ref: Hir.Inst.Ref, mark: LivenessMark) Air.Inst.Ref { switch (teg_ref.decode()) { .value => |value| return value.toAir(), .index => |index| { @@ -400,7 +403,7 @@ fn getCoercedValue(s: *Sema, src: LazyNode, teg_ref: Hir.Inst.Ref, ty: Type) !Va return try s.coerceValue(src, any, ty); } -fn getCoercedRef(s: *Sema, src: LazyNode, teg_ref: Hir.Inst.Ref, ty: Type, mark: DoMark) !Air.Inst.Ref { +fn getCoercedRef(s: *Sema, src: LazyNode, teg_ref: Hir.Inst.Ref, ty: Type, mark: LivenessMark) !Air.Inst.Ref { const any = s.getRef(teg_ref, mark); return try s.coerceRef(src, any, ty); } @@ -619,6 +622,8 @@ fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag) }); } + try s.ensureAtRuntime(s.lazyNode(.operand)); + return s.addRef(.negate, .{ .operand = operand }); }, .bool_not => { @@ -799,10 +804,13 @@ fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag) const scratch = try s.scratch.reserve(s.arena(), Air.Inst.Ref, node.list.len); defer scratch.deinit(); - var item_type: ?Type = if (node.type_hint.unwrap()) |ty| - try s.getType(s.lazyNode(.operand), ty) - else - null; + var item_type: ?Type = if (node.type_hint.unwrap()) |ty| inferred_type: { + const parent_type = try s.getType(s.lazyNode(.operand), ty); + const array_data: InternPool.Key.ArrayType = parent_type.toArrayType(s.ip) orelse + // Allow the coercion to fail later. + break :inferred_type null; + break :inferred_type array_data.item_type; + } else null; var is_constant = true; @@ -1103,13 +1111,12 @@ fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag) switch (data.conditional) { .comptime_condition => { - assert(operand.kind == .value); // must be comptime s.inst_map.add(s.frame.currentIndex(), operand); s.frame.advance(); return .@"continue"; }, .runtime_yes => { - try s.emitRuntimeBlock(if_data.yes.decode().index, operand, mark); + try s.emitRuntimeBranchForIf(if_data.yes.decode().index, operand, mark); mark.deinit(); switch (if_data.no.decode()) { @@ -1127,7 +1134,7 @@ fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag) } }, .runtime_no => { - try s.emitRuntimeBlock(if_data.no.decode().index, operand, mark); + try s.emitRuntimeBranchForIf(if_data.no.decode().index, operand, mark); mark.deinit(); }, } @@ -1140,6 +1147,12 @@ fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag) if (yes.value()) |val| yes = (try s.ensureValueRuntime(s.lazyNode(.lhs), val)).toAir(); if (no.value()) |val| no = (try s.ensureValueRuntime(s.lazyNode(.rhs), val)).toAir(); + if (yes.value() == no.value()) { + s.inst_map.add(s.frame.currentIndex(), yes); + s.frame.advance(); + return .@"continue"; + } + const union_type = s.peerTypeResolution(&.{ s.typeOfRef(yes), s.typeOfRef(no) }); const yes_coerced = try s.coerceBranch(s.lazyNode(.lhs), yes, union_type); @@ -1166,11 +1179,15 @@ fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag) const return_type = switch (data.func) { .argument => |arg_num| ret: { + s.inst_map.add( + func_data.params.slice(&s.hir)[arg_num].unwrap().?.decode().index, + operand, + ); + for ( func_data.params.slice(&s.hir)[arg_num + 1 ..], - reserve.items(.params)[arg_num + 1 ..], arg_num + 1.., - ) |param, *dst, i| { + ) |param, i| { const known = param.unwrap() orelse @panic("TODO: inferred param sets"); switch (known.decode()) { .value => |val| { @@ -1181,7 +1198,6 @@ fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag) .type = ty, }); } - dst.* = ty; }, .index => |block| { const mode = s.frame.mode; @@ -1225,6 +1241,13 @@ fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag) }, }; + for (func_data.params.slice(&s.hir), reserve.items(.params)) |param, *dst| { + dst.* = (switch (param.unwrap().?.decode()) { + .value => |value| value, + .index => |index| s.inst_map.get(index).decode().value, + }).toTypeUnsafe(s.ip); + } + try s.postVisitFunctionPrototype(reserve, return_type); return .@"continue"; }, @@ -1319,6 +1342,12 @@ fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag) const yes_coerced = try s.coerceValue(s.lazyNode(.lhs), yn[0], union_type); const no_coerced = try s.coerceValue(s.lazyNode(.rhs), yn[1], union_type); + if (yes_coerced == no_coerced) { + s.inst_map.add(s.frame.currentIndex(), yes_coerced.toAir()); + s.frame.advance(); + return; + } + s.inst_map.add(s.frame.currentIndex(), try s.addRef(.@"if", .{ .cond = condition, .data = try s.func.?.extra.append(s.gpa, Air.If.Data, .{ @@ -1573,39 +1602,54 @@ fn stepInner(s: *Sema, comptime tag: Hir.Tag) Error!InstructionKind.fromTag(tag) } } -fn emitRuntimeBlock( +fn emitRuntimeBranchForIf( s: *Sema, from: Hir.Inst.Index, - break_operand: Air.Inst.Ref, + result_value: Air.Inst.Ref, mark: core.U32Stack.Mark, ) !void { s.inst_map.add( from, - if (mark.len() == 0) brk: { - assert(break_operand.kind == .value); - break :brk break_operand; - } else brk: { - assert(s.frame.mode == .run_time); - const func = &s.func.?; - const idx = func.extra.list.items.len; - const block_instructions = mark.slice(u32); - try func.extra.list.ensureUnusedCapacity(s.gpa, block_instructions.len); - const block = try s.addIndexUntracked(.block, .{ - .start = @intCast(idx), - .len = @intCast(block_instructions.len + 1), - }); - const brk_instruction = try s.addIndexUntracked(.@"break", .{ - .block = block, - .value = break_operand, - }); - func.extra.list.appendSliceAssumeCapacity(block_instructions); - func.extra.list.appendAssumeCapacity(@intFromEnum(brk_instruction)); - break :brk block.toRef(); - }, + if (mark.len() == 0 and result_value.kind == .value) brk: { + break :brk result_value; + } else (try s.createRuntimeBlock(mark, result_value)).toRef(), ); mark.deinit(); } +fn createRuntimeBlock(s: *Sema, mark: core.U32Stack.Mark, result: ?Air.Inst.Ref) !Air.Inst.Index { + assert(mark.len() > 0); + assert(s.frame.mode == .run_time); + + const func = &s.func.?; + const idx = func.extra.list.items.len; + + if (core.runtime_safety) { + const ty = s.tempAir().typeOfIndex(mark.slice(Air.Inst.Index)[0], s.ip); + if (result != null) { + assert(ty != .never); // control flow already ended + } else { + assert(ty == .never); // does not stop control flow + } + } + + const block_instructions = mark.slice(u32); + try func.extra.list.ensureUnusedCapacity(s.gpa, block_instructions.len); + const block = try s.addIndexUntracked(.block, .{ + .start = @intCast(idx), + .len = @intCast(block_instructions.len + @intFromBool(result != null)), + }); + func.extra.list.appendSliceAssumeCapacity(block_instructions); + if (result) |value| { + const brk_instruction = try s.addIndexUntracked(.@"break", .{ + .block = block, + .value = value, + }); + func.extra.list.appendAssumeCapacity(@intFromEnum(brk_instruction)); + } + return block; +} + const BinaryOp = enum { add, sub, @@ -1889,7 +1933,8 @@ fn coerceBranch(s: *Sema, src: LazyNode, ref: Air.Inst.Ref, to: Type) !Air.Inst. return switch (ref.decode()) { .index => |block| { if (s.typeOfIndex(block) != to) return ref; - @panic("TODO"); + // @panic("TODO"); + return ref; }, .value => |value| (try s.coerceValue(src, value, to)).toAir(), }; @@ -2225,6 +2270,38 @@ fn fieldAccessArray(s: *Sema, lhs: Air.Inst.Ref, lhs_type: Type, rhs_teg: Hir.In try s.ensureAtRuntime(s.lazyNode(.main_node)); + if (s.generate_safety_checks) { + const is_above_bounds = try s.addRefMark(.ge, .{ + .lhs = index, + .rhs = try s.addRefMark(.array_len, .{ + .operand = lhs, + }), + }); + const is_oob = switch (index.decode()) { + .value => |value| is_oob: { + const int = value.toInt32(s.ip) orelse unreachable; + if (int < 0) @panic("TODO: always out of bounds"); + break :is_oob is_above_bounds; + }, + .index => is_oob: { + const is_below_zero = brk: { + const mark = s.scratch.mark(); + defer mark.deinit(); + + const is_below_zero = try s.addRefMark(.lt, .{ + .lhs = index, + .rhs = try s.makeValueAir(.{ .int32 = 0 }), + }); + + break :brk try s.createRuntimeBlock(mark, is_below_zero); + }; + break :is_oob try s.emitOrFromRefAndBlock(is_above_bounds, is_below_zero); + }, + }; + + try s.emitSafetyCheck(is_oob, "array out of bounds"); + } + return s.addRef(.field_array, .{ .lhs = lhs, .rhs = index, @@ -2233,6 +2310,17 @@ fn fieldAccessArray(s: *Sema, lhs: Air.Inst.Ref, lhs_type: Type, rhs_teg: Hir.In } } +fn emitOrFromRefAndBlock(s: *Sema, lhs: Air.Inst.Ref, short_circuiting_rhs: Air.Inst.Index) !Air.Inst.Ref { + return try s.addRefMark(.@"if", .{ + .cond = lhs, + .data = try s.func.?.extra.append(s.gpa, Air.If.Data, .{ + .yes = Value.bool_true.toAir(), + .no = short_circuiting_rhs.toRef(), + .union_ty = .bool, + }), + }); +} + fn fieldAccessTuple( s: *Sema, lhs: Air.Inst.Ref, @@ -2297,6 +2385,27 @@ fn postVisitFunctionPrototype(s: *Sema, reserve: FunctionReserve, return_type: ? try s.comp.jobs.writeItem(.{ .analysis = .{ .au = .wrap(.{ .func = func_value }) } }); } +fn emitSafetyCheck(s: *Sema, should_panic: Air.Inst.Ref, panic_message: []const u8) !void { + const yes = brk: { + const mark = s.scratch.mark(); + defer mark.deinit(); + + _ = try s.addRefMark(.panic, .{ + .operand = try s.makeValueAir(.{ .string = panic_message }), + }); + + break :brk try s.createRuntimeBlock(mark, null); + }; + _ = try s.addRefMark(.@"if", .{ + .cond = should_panic, + .data = try s.func.?.extra.append(s.gpa, Air.If.Data, .{ + .yes = yes.toRef(), + .no = .void, + .union_ty = .void, + }), + }); +} + // fn tempAir(s: *const Sema) Air { @@ -2342,6 +2451,12 @@ fn addRef(s: *Sema, comptime tag: Air.Inst.Tag, data: tag.Data()) !Air.Inst.Ref return (try s.addIndex(tag, data)).toRef(); } +fn addRefMark(s: *Sema, comptime tag: Air.Inst.Tag, data: tag.Data()) !Air.Inst.Ref { + const inst = try s.addIndex(tag, data); + s.markInst(inst); + return inst.toRef(); +} + fn typeOfIndex(s: *Sema, i: Air.Inst.Index) Type { return s.tempAir().typeOfIndex(i, s.ip); } diff --git a/src/core/Tokenizer.zig b/src/core/Tokenizer.zig index f14b847ebe7fa4f7f511f737fc50f4468295aed4..fc1bd7e1b7754f3ed7d9674b1cb40e4e2b5b193a 100644 --- a/src/core/Tokenizer.zig +++ b/src/core/Tokenizer.zig @@ -55,6 +55,7 @@ pub const Token = struct { module_doc_comment, // sort-lines: start + kw_and, kw_assert, kw_break, kw_comptime, @@ -70,6 +71,7 @@ pub const Token = struct { kw_let, kw_match, kw_module, + kw_or, kw_pub, kw_pure, kw_return, diff --git a/src/core/Transform.zig b/src/core/Transform.zig index 81a7060b43296d36083d609a5b57283ccaf61805..ea0dc52d7b42ec0a9ae441248e214c6d4ea5ca33 100644 --- a/src/core/Transform.zig +++ b/src/core/Transform.zig @@ -363,19 +363,24 @@ const Symbol = struct { } }; -const built_in_ids = std.StaticStringMap(Inst.Ref).initComptime(.{ - .{ "void", .void }, - .{ "true", .bool_true }, - .{ "false", .bool_false }, - .{ "int", .int32_type }, - .{ "float", .float_type }, - .{ "string", .string_type }, - .{ "opaque", .opaque_type }, - // .{ "bigint", .bigint_type }, - .{ "bool", .bool_type }, - .{ "never", .never_type }, - .{ "type", .type_type }, - // .{ "std", .standard_library }, +const BuiltIn = struct { + type: Inst.Ref, + value: Inst.Ref, +}; + +const built_in_ids = std.StaticStringMap(BuiltIn).initComptime(.{ + .{ "void", .{ .value = .void, .type = .void } }, + .{ "true", .{ .value = .bool_true, .type = .bool_type } }, + .{ "false", .{ .value = .bool_false, .type = .bool_type } }, + .{ "int", .{ .value = .int32_type, .type = .type_type } }, + .{ "float", .{ .value = .float_type, .type = .type_type } }, + .{ "string", .{ .value = .string_type, .type = .type_type } }, + .{ "opaque", .{ .value = .opaque_type, .type = .type_type } }, + // .{ "bigint", .{ .value = .bigint_type, .type = .type_type } }, + .{ "bool", .{ .value = .bool_type, .type = .type_type } }, + .{ "never", .{ .value = .never_type, .type = .type_type } }, + .{ "type", .{ .value = .type_type, .type = .type_type } }, + // .{ "std", .{ .value = .standard_library, .type = ... } }, }); fn transformModule(t: *Transform, module_node: NodeIndex) Allocator.Error!Inst.Index { @@ -940,8 +945,13 @@ fn transformExpression( return .void; } - if (built_in_ids.get(id)) |ref| { - return ref; + if (built_in_ids.get(id)) |built_in| { + // Elide the coercion when the type is known to be the type of the built-in + if (result.* == .ref and result.ref == built_in.type) { + result.* = .used; + } + + return built_in.value; } const lookup = t.lookupId(id) orelse { @@ -1048,8 +1058,6 @@ fn transformExpression( .div, .rem, .pow, - .@"and", - .@"or", .bit_and, .bit_or, .bit_xor, @@ -1262,7 +1270,7 @@ fn transformExpression( // .fetch => Builtin.fetch(t, decl, node, args), .compile_error => Builtin.builtin_compileError(t, ctx, node, args), .type_of => Builtin.builtin_typeOf(t, ctx, node, args), - else => @panic("TODO"), + else => |tag| std.debug.panic("TODO builtin {s}", .{@tagName(tag)}), }; }, @@ -1447,17 +1455,7 @@ fn transformExpression( else .void; - const coerced_operand = switch (break_result) { - .none, .used => operand, - else => try t.addRef(.coercion, .{ - .src_node = ctx.nodeOffset(node), - .extra_index = try t.extra.append(t.gpa, Hir.Coercion, .{ - .type = try break_result.consume(t) orelse - unreachable, // `.none` already handled - .operand = operand, - }), - }), - }; + const coerced_operand = try t.maybeCoerce(ctx.nodeOffset(node), operand, &break_result); _ = try t.addIndex(.@"break", .{ .src_node = ctx.nodeOffset(node), @@ -1697,6 +1695,52 @@ fn transformExpression( }); }, + .bool_and => { + const bin = t.ast.nodeData(node).bin; + + var lhs_result: ResultType = .{ .ref = .bool_type }; + const lhs = try t.maybeCoerce( + ctx.nodeOffset(node), + try t.transformExpression(ctx, &lhs_result, bin.lhs) orelse + return null, + &lhs_result, + ); + + const rhs = try t.transformToBlock(ctx, bin.rhs, .untracked_result, .{ .ref = .bool_type }, .run_time); + + return try t.addRef(.@"if", .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Hir.If, .{ + .cond = lhs, + .yes = rhs, + .no = .bool_false, + }), + }); + }, + + .bool_or => { + const bin = t.ast.nodeData(node).bin; + + var lhs_result: ResultType = .{ .ref = .bool_type }; + const lhs = try t.maybeCoerce( + ctx.nodeOffset(node), + try t.transformExpression(ctx, &lhs_result, bin.lhs) orelse + return null, + &lhs_result, + ); + + const rhs = try t.transformToBlock(ctx, bin.rhs, .untracked_result, .{ .ref = .bool_type }, .run_time); + + return try t.addRef(.@"if", .{ + .src_node = ctx.nodeOffset(node), + .extra_index = try t.extra.append(t.gpa, Hir.If, .{ + .cond = lhs, + .yes = .bool_true, + .no = rhs, + }), + }); + }, + else => |tag| std.debug.panic("TODO: expr {s}", .{@tagName(tag)}), } } @@ -2198,6 +2242,20 @@ fn getInstData(t: *Transform, index: Inst.Index) *EncodedData { return &t.insts.items(.data)[@intFromEnum(index)]; } +fn maybeCoerce(t: *Transform, node_offset: i32, operand: Inst.Ref, result: *ResultType) !Inst.Ref { + return switch (result.*) { + .none, .used => operand, + else => try t.addRef(.coercion, .{ + .src_node = node_offset, + .extra_index = try t.extra.append(t.gpa, Hir.Coercion, .{ + .type = try result.consume(t) orelse + unreachable, // `.none` already handled + .operand = operand, + }), + }), + }; +} + fn scratchToRefList(t: *Transform, scratch_top: core.U32Stack.Mark) !RefList { const items = scratch_top.slice(u32); const extra_top = t.extra.list.items.len; diff --git a/src/core/js/Link.zig b/src/core/js/Link.zig index 948f5261aaf2fe154de5cabf26c3639f1eaf4edc..7da6da8e6db045a873a40af8b03781a3686a4001 100644 --- a/src/core/js/Link.zig +++ b/src/core/js/Link.zig @@ -388,6 +388,12 @@ fn printExprLevel( } }, + .stmt_throw => { + const stmt: Tree.StmtThrow = tree.nodeData(.stmt_throw, node); + try writer.writeAll("throw "); + try link.printExpr(tree, stmt.operand, writer); + }, + .stmt_declare_local => { const stmt: Tree.StmtDeclareLocal = tree.nodeData(.stmt_declare_local, node); try writer.writeAll("let "); @@ -435,10 +441,14 @@ fn printExprLevel( .eq, .lt, .gt, + .ge, + .le, .assign_add, .assign_sub, .assign_mul, .assign_div, + .@"and", + .@"or", => |tag| { const bin: Tree.Bin = tree.nodeData(tag, node); const op: Operator = @field(Operator, @tagName(tag)); @@ -466,6 +476,14 @@ fn printExprLevel( try writer.writeAll(")"); }, + .comma_chain => { + const bin: Tree.IndexList = tree.nodeData(.comma_chain, node); + for (bin.slice(tree), 0..) |expr, i| { + if (i != 0) try writer.writeAll(", "); + try link.printExprLevel(tree, expr, .assignment, writer); + } + }, + inline .negate, .bool_not, .pre_dec, @@ -483,7 +501,7 @@ fn printExprLevel( const wrap = Level.conditional.needsParens(level); if (wrap) try writer.writeAll("("); - try link.printExpr(tree, ternary.cond, writer); + try link.printExprLevel(tree, ternary.cond, .logical_or, writer); try writer.writeAll(" ? "); try link.printExpr(tree, data.yes, writer); try writer.writeAll(" : "); @@ -788,10 +806,14 @@ const Operator = enum { eq, lt, gt, + ge, + le, negate, bool_not, pre_inc, pre_dec, + @"and", + @"or", pub fn level(op: Operator) Level { return switch (op) { @@ -804,8 +826,9 @@ const Operator = enum { .add, .sub => .additive, .mul, .div => .multiplicative, .eq => .equality, - .lt, .gt => .relational, - + .lt, .gt, .ge, .le => .relational, + .@"and" => .logical_and, + .@"or" => .logical_or, .pre_inc, .pre_dec, .bool_not, @@ -828,6 +851,11 @@ const Operator = enum { .eq => "===", .lt => "<", .gt => ">", + .le => "<=", + .ge => ">=", + + .@"and" => "&&", + .@"or" => "||", .pre_inc => "++", .pre_dec => "++", diff --git a/src/core/js/NodeGen.zig b/src/core/js/NodeGen.zig index 72a3411dab72b76f9ddc6d5398e4cbefe36dff0d..8a0086bf990ab2648c53af8cfe40beebedb6ac35 100644 --- a/src/core/js/NodeGen.zig +++ b/src/core/js/NodeGen.zig @@ -157,6 +157,8 @@ fn codeGenInst(c: *NodeGen, inst: Air.Inst.Index) Error!void { inline .gt, .lt, .eq, + .ge, + .le, .add, .sub, .mul, @@ -250,6 +252,28 @@ fn codeGenInst(c: *NodeGen, inst: Air.Inst.Index) Error!void { try c.markExpression(inst, node); }, + .panic => { + const data: Air.Un = c.air.instData(.panic, inst); + const string = try c.fetchExpression(data.operand); + + const error_constructor_string = (try c.ip.getOrPutString(c.gpa, "Error", .maybe_embedded_nulls)).withLen("Error".len); + try c.extern_symbols.put(c.gpa, error_constructor_string, {}); + + try c.extra.list.ensureUnusedCapacity(c.gpa, 1); + const start = c.extra.list.items.len; + c.extra.list.appendAssumeCapacity(@intFromEnum(string)); + + try c.addStmt(try c.add(.stmt_throw, .{ + .operand = try c.add(.js_new, .{ + .base = try c.add(.global, .{ .name = error_constructor_string }), + .args = try c.extra.append(c.gpa, Tree.IndexList, .{ + .start = @intCast(start), + .len = 1, + }), + }), + })); + }, + .field_opaque => { const data: Air.Bin = c.air.instData(.field_opaque, inst); if (data.rhs.value()) |field_value| { @@ -336,8 +360,34 @@ fn codeGenInst(c: *NodeGen, inst: Air.Inst.Index) Error!void { if (c.canLowerToExpression(extra.yes) and c.canLowerToExpression(extra.no)) { + if (extra.no.value()) |no_value| { + if (no_value == .bool_false) { + const yes = try c.lowerToExpression(extra.yes); + + const conditional = try c.add(.@"and", .{ + .lhs = cond, + .rhs = yes, + }); + try c.markExpression(inst, conditional); + return; + } + } + + if (extra.yes.value()) |yes_value| { + if (yes_value == .bool_true) { + const no = try c.lowerToExpression(extra.no); + + const conditional = try c.add(.@"or", .{ + .lhs = cond, + .rhs = no, + }); + try c.markExpression(inst, conditional); + return; + } + } + const yes = try c.lowerToExpression(extra.yes); - const no = try c.lowerToExpression(extra.yes); + const no = try c.lowerToExpression(extra.no); const ternary = try c.add(.ternary, .{ .cond = cond, @@ -559,7 +609,7 @@ fn fetchInstExpression(c: *NodeGen, index: Air.Inst.Index) !Node.Index { switch (flags.uses) { .none => { // incorrectly marked instruction as unused - std.debug.panic("called fetchInstExpression(#{d}), but no expression was generated", .{ + std.debug.panic("called fetchInstExpression(#{d}), but it was marked as unused", .{ @intFromEnum(index), }); }, @@ -722,7 +772,6 @@ fn codeGenBlock(c: *NodeGen, inst: Air.Inst.Index) !void { const next_label = c.next_label; try c.scratch.appendSlice(c.arena, &.{ - // @intFromEnum(if (temporary) |temp| temp.toOptional() else .none), @intFromEnum(Node.Index.Optional.none), next_label, }); @@ -749,6 +798,46 @@ fn codeGenBlock(c: *NodeGen, inst: Air.Inst.Index) !void { })); } +fn codeGenBlockAsExpression(c: *NodeGen, inst: Air.Inst.Index) !Node.Index { + const t = tracer.trace(@src()); + defer t.end(); + + const scratch_top = c.scratch.items.len; + errdefer c.scratch.shrinkRetainingCapacity(scratch_top); + + try c.scratch.appendSlice(c.arena, &.{ + @intFromEnum(Node.Index.Optional.none), + 0, + }); + + c.air_node_map[@intFromEnum(inst)] = .{ + .block = .{ .index = @intCast(scratch_top) }, + }; + const block: Air.IndexList = c.air.instData(.block, inst); + const instructions = block.slice(&c.air); + for (instructions[0 .. instructions.len - 1]) |item| { + try c.codeGenInst(item); + } + + const last_break: Air.Break = c.air.instData(.@"break", instructions[instructions.len - 1]); + const last_break_operand = try c.fetchExpression(last_break.value); + + try c.scratch.append(c.arena, @intFromEnum(last_break_operand)); + + const items = c.scratch.items[scratch_top + 2 ..]; + const top = c.extra.list.items.len; + defer c.scratch.shrinkRetainingCapacity(scratch_top); + assert(items.len > 0); + if (items.len == 1) { + return @enumFromInt(items[0]); + } + try c.extra.list.appendSlice(c.gpa, items); + return try c.add(.comma_chain, .{ + .start = @intCast(top), + .len = @intCast(items.len), + }); +} + // Ideally, we convert everything to an expression. This allows for much // more compact code. When that isnt possible (`return`, `while`, etc), // we detect that here and ensure to lower `if` to actual if statements, @@ -774,6 +863,8 @@ fn canLowerToExpression(c: *const NodeGen, inst: Air.Inst.Ref) bool { .eq, .lt, .gt, + .ge, + .le, .negate, .bool_not, .store, @@ -787,15 +878,28 @@ fn canLowerToExpression(c: *const NodeGen, inst: Air.Inst.Ref) bool { // Later, this could be made true if we can hoist temporaries up. .memory => false, - // break/return must be a js statement - .@"break", - => false, + // these must be lowered to js statements + .@"break", .panic => false, + // A block can become a comma expression if all expressions + // are lowerable. The break at the end just becomes the result. .block => { - // TODO: blocks that are - // - all statements except last are lowerable - // - the last statement is lowerable OR a break of this block - return false; + const block: Air.IndexList = c.air.instData(.block, index); + const items = block.slice(&c.air); + for (items[0 .. items.len - 1]) |item| { + if (c.air.instFlags(item).uses == .multi) + return false; + if (!c.canLowerToExpression(item.toRef())) + return false; + } + const last_item = items[items.len - 1]; + if (c.air.instTag(last_item) != .@"break") { + return c.canLowerToExpression(last_item.toRef()); + } + const brk = c.air.instData(.@"break", last_item); + // The last break will only be lowerable if it + // breaks out of this specific block + return brk.block == index; }, .@"if" => { @@ -818,6 +922,9 @@ fn lowerToExpression(c: *NodeGen, ref: Air.Inst.Ref) !Node.Index { return switch (ref.decode()) { .value => |v| try c.valueToExpression(v), .index => |index| { + if (c.air.instTag(index) == .block) { + return try c.codeGenBlockAsExpression(index); + } try c.codeGenInst(index); return c.fetchExpression(ref); }, diff --git a/src/core/js/Tree.zig b/src/core/js/Tree.zig index ab19b9086076a6df76fc0fd64f9d892575bd954a..11e6e1f7cb9ce7b8d5080037d3bdce6143d49516 100644 --- a/src/core/js/Tree.zig +++ b/src/core/js/Tree.zig @@ -39,6 +39,10 @@ pub const Tag = enum { eq, lt, gt, + ge, + le, + @"and", + @"or", // Exprs call, js_new, @@ -47,6 +51,7 @@ pub const Tag = enum { object_literal, array, index, + comma_chain, // Prefix ops negate, bool_not, @@ -63,6 +68,8 @@ pub const Tag = enum { stmt_return, /// `...: { ... }` stmt_block, + /// `throw x` + stmt_throw, pub fn Data(comptime tag: Tag) type { return switch (tag) { @@ -83,7 +90,11 @@ pub const Tag = enum { .eq, .lt, .gt, + .ge, + .le, .index, + .@"and", + .@"or", => Bin, .ternary => Ternary, .negate, @@ -95,9 +106,11 @@ pub const Tag = enum { .array => IndexList, .call, .js_new => Call, .field => Field, + .comma_chain => IndexList, .stmt_if => StmtIf, .stmt_break => StmtBreak, .stmt_return => StmtReturn, + .stmt_throw => StmtThrow, .stmt_declare_local => StmtDeclareLocal, .stmt_block => StmtBlock, }; @@ -261,6 +274,11 @@ pub const StmtReturn = struct { unused: Unused = .zero, }; +pub const StmtThrow = struct { + operand: Node.Index, + unused: Unused = .zero, +}; + pub const StmtBlock = struct { label: u32, content: ExtraIndex(IndexList), diff --git a/src/deps/quickjs/build.zig b/src/deps/quickjs/build.zig index 93577ac69b9dc99c667c22ab5d443b416ba739d2..243c4620453ffd31663d9a4b3837c5270063eb63 100644 --- a/src/deps/quickjs/build.zig +++ b/src/deps/quickjs/build.zig @@ -136,7 +136,7 @@ pub fn build(b: *Build) !void { .optimize = optimize, .link_libc = true, }); - if (pthread.pthreads) |pt| translate.addIncludeDir(pt.include.getPath(b)); + if (pthread.pthreads) |pt| translate.addIncludePath(pt.include); const module = translate.addModule("quickjs"); module.linkLibrary(libquickjs); @@ -147,7 +147,7 @@ pub fn build(b: *Build) !void { .link_libc = true, }); const module2 = translate_libc.addModule("quickjs-libc"); - if (pthread.pthreads) |pt| translate_libc.addIncludeDir(pt.include.getPath(b)); + if (pthread.pthreads) |pt| translate_libc.addIncludePath(pt.include); module2.linkLibrary(libquickjs); module2.linkLibrary(libquickjs_libc); } diff --git a/test/behavior/array b/test/behavior/array index 0058e44528c91ac04b09e0750f94674f4dc73441..96cf1f26bb98bfb67fbb89e647a21a7023183a7d 100644 --- a/test/behavior/array +++ b/test/behavior/array @@ -1,8 +1,7 @@ let assertEqual = @extern("assertEqual"); fn main { - let array = [i(9), i(3), i(8), i(3), i(-1)]; - + let array = a([9, 3, 8, 3, -1]); assertEqual(array.len, i(5)); assertEqual(array[0], i(9)); assertEqual(array[1], i(3)); @@ -12,3 +11,4 @@ fn main { } fn i(a: int) { a } +fn a(a: []int) { a } diff --git a/test/compiler_tests.zig b/test/compiler_tests.zig index 5231041adac86fca5ef0380ae640adc968374f6a..5b535211eba9c871880b8bc5982d46013ab790e2 100644 --- a/test/compiler_tests.zig +++ b/test/compiler_tests.zig @@ -506,7 +506,7 @@ pub fn panic(msg: []const u8, trace: ?*const std.builtin.StackTrace, first_trace if (current_case) |case| { std.debug.print("Crash during case: {s}\n", .{case.display}); } - std.debug.panicImpl(trace, first_trace_addr, msg); + std.debug.defaultPanic(msg, trace, first_trace_addr); } const std = @import("std"); -- 2.54.0