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");