From 55f919b5c0cd47d3b4282b6d85bc57f5f93f0357 Mon Sep 17 00:00:00 2001 From: chloe caruso Date: Wed, 29 May 2024 00:59:55 -0700 Subject: [PATCH] some more type-checking errors --- CHANGELOG.md | 4 ++ src/core/InternPool.zig | 20 +++++++ src/core/JsLink.zig | 53 ++++++++++++++++-- src/core/Report.zig | 66 ++++++++++++++++++++-- src/core/Sema.zig | 120 +++++++++++++++++++++++++++++++--------- src/core/Teg.Dump.zig | 1 + src/core/Teg.zig | 14 +++-- src/core/Transform.zig | 101 ++++++++++++++++++++++++++------- 8 files changed, 319 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d22d8243f42922400d7aea953d7e99af67086007..c99c6bedae0e9582d991fcd98f6676831f60331a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,13 @@ ## 0.1.3 - feat(sema): coersion: numeric_literal -> float +- feat(sema): prevent passing non-runtime compatible types like `type` to opaque functions - feat(transform): lhs of assignments now validated - feat(transform): mark useless discard +- feat: implement the `comptime` keyword (many comptime ops are not implemented) - fix(sema): properly coerce assignments +- fix(transform): empty block generated in top level code tripped sema assertion +- fix(link): emit parenthesis to preserve operator precedence ## 0.1.2 diff --git a/src/core/InternPool.zig b/src/core/InternPool.zig index 7863bf6debc6688cc2765fa2bd99bfef73f3294d..2271e911eb5a9adfb21d779c7a2923bd2b4e916a 100644 --- a/src/core/InternPool.zig +++ b/src/core/InternPool.zig @@ -336,6 +336,26 @@ pub const Type = enum(u32) { return ip.indexToKey(ty.value()).type_function; } + pub fn hasRuntimeRepresentation(ty: Type, ip: *const InternPool) bool { + return switch (ty) { + .bool, + .int32, + .float, + .string, + .void, + .@"opaque", + => true, + .numeric_literal, + .type, + .noreturn, + => false, + else => switch (ty.tag(ip)) { + .simple_type => unreachable, // all simple types are already printed + .type_function => true, + }, + }; + } + pub fn displayName(ty: Type, ip: *const InternPool) std.fmt.Formatter(formatDisplayName) { return .{ .data = .{ .type = ty, .ip = ip } }; } diff --git a/src/core/JsLink.zig b/src/core/JsLink.zig index 99a557bcab8a283b579d45d9208b7cc340ad24c0..16cac90e68d9a145a0e7ba51dd2aa3ffc5af467e 100644 --- a/src/core/JsLink.zig +++ b/src/core/JsLink.zig @@ -69,7 +69,7 @@ pub fn linkJsCode( try link.decls.putNoClobber(comp.gpa, decl_index, @enumFromInt(gop.index)); } - try writer.print("// Language Compiler v0.1.2. Alpha quality software.\n", .{}); + try writer.print("// Language Compiler v0.1.3. Alpha quality software.\n", .{}); // 2. Print every function decl for (decls[1..]) |decl_index| { @@ -197,7 +197,22 @@ fn printBlock(link: *JsLink, tree: *const JsTree, list: JsTree.IndexList, writer } } -fn printExpr(link: *JsLink, tree: *const JsTree, node: Node.Index, writer: anytype) @TypeOf(writer).Error!void { +fn printExpr( + link: *JsLink, + tree: *const JsTree, + node: Node.Index, + writer: anytype, +) @TypeOf(writer).Error!void { + return link.printExprLevel(tree, node, .comma, writer); +} + +fn printExprLevel( + link: *JsLink, + tree: *const JsTree, + node: Node.Index, + level: Level, + writer: anytype, +) @TypeOf(writer).Error!void { var t = tracer.trace(@src()); defer t.end(); @@ -314,11 +329,16 @@ fn printExpr(link: *JsLink, tree: *const JsTree, node: Node.Index, writer: anyty => |tag| { const bin: JsTree.Bin = tree.nodeData(tag, node); const op: Operator = @field(Operator, @tagName(tag)); - try link.printExpr(tree, bin.lhs, writer); + const inner_level = comptime op.level(); + if (inner_level.needsParens(level)) + try writer.writeAll("("); + try link.printExprLevel(tree, bin.lhs, inner_level, writer); try writer.writeAll(" "); try writer.writeAll(comptime op.symbol()); try writer.writeAll(" "); - try link.printExpr(tree, bin.rhs, writer); + try link.printExprLevel(tree, bin.rhs, inner_level, writer); + if (inner_level.needsParens(level)) + try writer.writeAll(")"); }, inline .negate, @@ -411,6 +431,10 @@ const Level = enum { new, access_and_call, member, + + pub fn needsParens(inner_level: Level, outer_level: Level) bool { + return @intFromEnum(outer_level) > @intFromEnum(inner_level); + } }; const Operator = enum { @@ -431,6 +455,27 @@ const Operator = enum { pre_inc, pre_dec, + pub fn level(op: Operator) Level { + return switch (op) { + .assign, + .assign_add, + .assign_sub, + .assign_mul, + .assign_div, + => .assignment, + .add, .sub => .additive, + .mul, .div => .multiplicative, + .eq => .equality, + .lt, .gt => .relational, + + .pre_inc, + .pre_dec, + .bool_not, + .negate, + => .prefix, + }; + } + pub fn symbol(op: Operator) []const u8 { return switch (op) { .assign => "=", diff --git a/src/core/Report.zig b/src/core/Report.zig index 814a0f3510f5d342912441a6b39b4a66df3cd735..5cd324aed044c0210fb1d9f87798ce9cb875960f 100644 --- a/src/core/Report.zig +++ b/src/core/Report.zig @@ -44,6 +44,16 @@ pub const Err = union(enum) { ast: @import("Parse.zig").AstError, + user_compile_error: struct { + src: LazyNode, + msg: String, + + fn build(data: @This(), ctx: *MsgContext) !void { + try ctx.msgNode(try data.src.get(ctx), "{s}", .{ + data.msg.slice(ctx.report), + }); + } + }, unknown_identifier: struct { node: NodeIndex, @@ -64,6 +74,17 @@ pub const Err = union(enum) { ); } }, + builtin_mismatch_args: struct { + node: NodeIndex, + args: u32, + + fn build(data: @This(), ctx: *MsgContext) !void { + try ctx.msgNode(data.node, "built-in function expected {d} arguments, recieved {d}", .{ + data.args, + ctx.ast.extraData(ctx.ast.nodeData(data.node).call.extra, Ast.Call).args.len, + }); + } + }, duplicate_function: struct { node: NodeIndex, prev: NodeIndex, @@ -169,6 +190,28 @@ pub const Err = union(enum) { }); } }, + runtime_incompatible_type: struct { + src: LazyNode, + type: Type, + + fn build(data: @This(), ctx: *MsgContext) !void { + const node = try data.src.get(ctx); + try ctx.msgNode(node, "type \"{}\" cannot be represented as a runtime value", .{ + data.type.displayName(&ctx.comp.intern_pool), + }); + } + }, + param_comptime_only: struct { + src: LazyNode, + type: Type, + + fn build(data: @This(), ctx: *MsgContext) !void { + const node = try data.src.get(ctx); + try ctx.msgNode(node, "parameter of comptime type \"{s}\" must be marked \"comptime\"", .{ + data.type.displayName(&ctx.comp.intern_pool), + }); + } + }, assign_builtin: struct { node: NodeIndex, @@ -279,10 +322,10 @@ pub const Warn = union(enum) { } }, numeric_int_var_ambiguity: struct { - node: LazyNode, + src: LazyNode, fn build(data: @This(), ctx: *MsgContext) !void { - const node = try data.node.get(ctx); + const node = try data.src.get(ctx); try ctx.msgNode(node, "constant integer in a \"var\" has an unknown type, assuming \"float\"", .{}); try ctx.msgAlone("add \": float\" or \": int\" after the variable name to set the type", .{}); } @@ -356,7 +399,7 @@ pub const String = struct { start: u32, len: u32, - pub fn slice(str: String, report: Report) []const u8 { + pub fn slice(str: String, report: *const Report) []const u8 { return report.string_bytes[str.start..][0..str.len]; } }; @@ -392,7 +435,7 @@ pub fn dumpToWriter(report: Report, comp: *Comp, writer: anytype, tty: std.io.tt } pub fn formatItem( - report: Report, + report: *const Report, item: Item, item_type: Item.Type, comp: *Comp, @@ -414,6 +457,7 @@ pub fn formatItem( .comp = comp, .file = file, .ast = &ast_data, + .report = report, .utf8_source = (try file.getSource(comp.gpa)).code, .components = ArrayListUnmanaged(Component).initCapacity(alloc, 16) catch unreachable, // stack-fallback allocator has enough space. @@ -450,7 +494,11 @@ pub fn formatItem( try tty.setColor(writer, .dim); try writer.writeAll(": "); try tty.setColor(writer, .reset); + if (i == 1) + try tty.setColor(writer, .bold); try writer.writeAll(component.message_utf8); + if (i == 1) + try tty.setColor(writer, .reset); try writer.writeAll("\n"); const utf8_source = ctx.utf8_source; @@ -685,6 +733,7 @@ pub const MsgContext = struct { file: *File, comp: *Comp, ast: *const Ast, + report: *const Report, utf8_source: [:0]const u8, /// This arena is is a stack fallback with 16kb, then @@ -799,6 +848,14 @@ pub const LazyNode = struct { else => unreachable, } }, + .declaration => { + assert(node_tag == .fn_decl); + const fn_decl = ctx.ast.extraData(data.fn_decl.extra, Ast.FnDecl); + return switch (lazy.operand) { + .return_type => fn_decl.return_type.unwrap().?, + else => fn_decl.params.slice(ctx.ast)[lazy.operand.getArgId()], + }; + }, else => {}, } @@ -823,6 +880,7 @@ pub const Operand = enum(u32) { lhs = 4, /// Right hand side of a binary instruction rhs = 5, + _, /// One of: /// - An argument of a .call node diff --git a/src/core/Sema.zig b/src/core/Sema.zig index cae7f9f6fd9a8663acf6d0f2c55f06954ec7e15f..894d536dd11852edb184c236bc954aed3d90c58c 100644 --- a/src/core/Sema.zig +++ b/src/core/Sema.zig @@ -144,10 +144,15 @@ pub fn analyzeDeclaration(s: *Sema) Error!Value { defer s.gpa.free(s.params); for (fn_decl.params.slice(&s.code), 0..) |param, i| { - s.params[i] = try s.resolveType( - s.inst_map.start.op(Report.Operand.argument(@intCast(i))), - param, - ); + const src = s.inst_map.start.op(Report.Operand.argument(@intCast(i))); + const ty = try s.resolveType(src, param); + if (!ty.hasRuntimeRepresentation(s.ip)) { + return s.fail(.param_comptime_only, .{ + .src = s.lazyNode(src), + .type = ty, + }); + } + s.params[i] = ty; } const return_type = try s.resolveType( @@ -398,9 +403,11 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { scratch[i] = coerced; } } else { - // just copy the args in without checking for (call.args.slice(&s.code), 0..) |arg, i| { - const ref = try s.resolveRefRuntime(arg); + const ref = try s.ensureValueRuntime( + in.op(Report.Operand.argument(@intCast(i))), + try s.resolveRefRuntime(arg), + ); scratch[i] = ref; } } @@ -475,23 +482,15 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { .memory => { const data = s.code.instData(.memory, in); - var initial_value = try s.resolveRefRuntime(data.operand); + const initial_value = try s.resolveRefRuntime(data.operand); - switch (initial_value.decode()) { - .value => |v| switch (v.toKey(s.ip)) { - .comptime_float => |val| { - try s.report.warn(.numeric_int_var_ambiguity, .{ - .node = s.nodeLoc(in, .operand), - }); - initial_value = (try s.makeValue(.{ .float = val })).toAir(); - }, - else => {}, - }, - else => {}, - } + const runtime_known = try s.ensureValueRuntime( + in.op(.operand), + initial_value, + ); return try s.addInstRef(.memory, .{ - .init = initial_value, + .init = runtime_known, .ty = s.typeOfRef(initial_value), }); }, @@ -518,6 +517,29 @@ fn analyzeCodeInner(s: *Sema, in: Teg.Inst.Index) Error!Air.Inst.Ref { const operand = try s.resolveRefRuntime(as.operand); return try s.coerceRef(in.op(.rhs), operand, coerce_to.toType(s.ip).?); }, + .@"comptime" => { + const node: Teg.Inst.RefNode = s.code.instData(.@"comptime", in); + const value = try s.resolveRefComptimeAny(node.operand); + return value.toAir(); + }, + .type_of => { + const node: Teg.Inst.RefNode = s.code.instData(.type_of, in); + const value = try s.resolveRefComptimeAny(node.operand); + return value.typeOf(s.ip).value().toAir(); + }, + .compile_error => { + const node: Teg.Inst.RefNode = s.code.instData(.compile_error, in); + const value = try s.resolveRefComptimeType( + in.op(Report.Operand.argument(0)), + node.operand, + .string, + ); + const str = value.asString(s.ip).toSlice(s.ip); + return s.fail(.user_compile_error, .{ + .src = s.lazyNode(in.op(.main_node)), + .msg = try s.report.addString(str), + }); + }, else => |tag| std.debug.panic("TODO: analyze of .{s}", .{@tagName(tag)}), } } @@ -662,6 +684,7 @@ fn coerceValue(s: *Sema, src: InstOp, value: Value, to: Type) !Value { } switch (to) { .@"opaque" => { + // }, .bool => { @@ -699,23 +722,68 @@ fn coerceValue(s: *Sema, src: InstOp, value: Value, to: Type) !Value { else => {}, }, .string => { - // + // TODO: }, .noreturn => { - // + // nothing coerces to `noreturn` + }, + .numeric_literal => { + if (value.toFloat(s.ip)) |float| { + return try s.makeValue(.{ + .comptime_float = float, + }); + } }, .type => { if (value == .void) return .void; + + // non-types do not coerce to types }, - .numeric_literal => {}, - .void => { - if (value == .void) return .void; - }, + .void => if (value == .void) return .void, else => {}, } return s.coersionError(src, from, to); } +fn ensureValueRuntime(s: *Sema, src: InstOp, ref: Air.Inst.Ref) !Air.Inst.Ref { + switch (ref.decode()) { + .value => |v| { + const ty = v.typeOf(s.ip); + switch (ty) { + .numeric_literal => { + try s.report.warn(.numeric_int_var_ambiguity, .{ + .src = s.lazyNode(src), + }); + return (try s.makeValue(.{ + .float = v.toFloat(s.ip) orelse unreachable, + })).toAir(); + }, + .type, + .noreturn, + => return s.fail(.runtime_incompatible_type, .{ + .src = s.lazyNode(src), + .type = ty, + }), + .@"opaque" => @panic("TODO: represent comptime-known opaques better"), + // Allowed at runtime + .bool, + .float, + .int32, + .string, + .void, + => {}, + _ => switch (ty.tag(s.ip)) { + .simple_type => unreachable, // handled above + // Allowed at runtime + .type_function => {}, + }, + } + }, + else => {}, + } + return ref; +} + fn resolveDecl(s: *Sema, decl_index: Decl.Index) Error!Value { const decl = s.ip.declPtr(decl_index); switch (decl.analysis) { diff --git a/src/core/Teg.Dump.zig b/src/core/Teg.Dump.zig index 52581428b2ca53e01ae97a850a4b60f5e879426a..d7700f140a74099b8b1c7106790b706b271057f6 100644 --- a/src/core/Teg.Dump.zig +++ b/src/core/Teg.Dump.zig @@ -171,6 +171,7 @@ const State = struct { .import, .fetch, .compile_error, + .type_of, => |comptime_tag| { const data = teg.instData(comptime_tag, inst); try writer.writeAll("("); diff --git a/src/core/Teg.zig b/src/core/Teg.zig index a5356ee82896c1bfc041b59f81bf2017202b54c5..9ca445aced09f084856915b78f2c5263a0050d6b 100644 --- a/src/core/Teg.zig +++ b/src/core/Teg.zig @@ -78,6 +78,8 @@ pub const Inst = struct { fetch, /// Implements the `@compileError` builtin compile_error, + /// Implements the `@typeOf` builtin + type_of, pub fn Data(comptime tag: Tag) type { return switch (tag) { @@ -93,7 +95,6 @@ pub const Inst = struct { .load_decl => StrNode, .param => DataNode, .store => PlNode(Store), - .@"comptime" => RefNode, // Single instruction operand .memory, @@ -107,6 +108,8 @@ pub const Inst = struct { .import, .fetch, .compile_error, + .@"comptime", + .type_of, => RefNode, // Two instruction operands @@ -403,11 +406,10 @@ pub fn instPayload(teg: *const Teg, comptime tag: Inst.Tag, index: Inst.Index) t } pub fn instNodeOffset(teg: *const Teg, index: Inst.Index) i32 { - switch (teg.instTag(index)) { - inline else => |tag| { - return teg.instData(tag, index).src_node; - }, - } + return switch (teg.instTag(index)) { + .declaration => 0, + inline else => |tag| teg.instData(tag, index).src_node, + }; } pub fn blockInstructions(teg: *const Teg, index: Inst.Index) []const Inst.Index { diff --git a/src/core/Transform.zig b/src/core/Transform.zig index bf9fe0453dc63ae75b90ab151e9572b684582c03..54cf3e692682cb6e541d889e64d09471f41d99a0 100644 --- a/src/core/Transform.zig +++ b/src/core/Transform.zig @@ -276,12 +276,7 @@ fn transformModule(t: *Transform, module_node: NodeIndex) Allocator.Error!Inst.I }; try t.extra.list.appendSlice(t.gpa, @ptrCast(temp)); - const block = try t.addRootInst(.{ - .tag = .block, - .data = undefined, - }); - - { + const top_level_code_ref: Inst.Ref = ref: { const scratch_top = t.scratch.items.len; defer t.scratch.shrinkRetainingCapacity(scratch_top); @@ -289,11 +284,23 @@ fn transformModule(t: *Transform, module_node: NodeIndex) Allocator.Error!Inst.I try t.transformModuleStatement(decl, statement); } + // if there are no new instructions, we dont have to create a block context + if (scratch_top == t.scratch.items.len) { + break :ref .void; + } + + const block = try t.addRootInst(.{ + .tag = .block, + .data = undefined, + }); + t.getInstData(block).* = EncodedData.encode(.block, .{ .src_node = decl.nodeOffset(module_node), .extra_index = try t.extra.append(t.gpa, IndexList, try t.scratchToIndexList(scratch_top)), }); - } + + break :ref block.toRef(); + }; const decl_extra, var trail = try t.extra.appendTrailing(t.gpa, Inst.Decl, .{ .kind = .function, @@ -305,7 +312,7 @@ fn transformModule(t: *Transform, module_node: NodeIndex) Allocator.Error!Inst.I trail.writeStruct(Inst.DeclFn, .{ .params = .{ .start = 0, .len = 0 }, .return_type = Inst.Ref.void, - .body = block.toRef(), + .body = top_level_code_ref, }); t.getInstData(top_level_fn).* = EncodedData.encode(.declaration, .{ @@ -480,11 +487,12 @@ fn transformExpressionToBlock( // if there are no new instructions, we dont have to create a block context if (scratch_top == t.scratch.items.len) { - const result = expr orelse unreachable; // null means we added break or return + const result = expr orelse unreachable; // null means we *added* break or return // TODO: evaluate if `if` blocks would be fine in sema with instruction refs // with this extra if check here, we filter out non-blocks. - if (result.toIndex() == null) { + if (result.toValue() != null) { + assert(block == null); return result; } } @@ -519,6 +527,7 @@ fn transformExpressionToBlock( } const content = try t.scratchToIndexList(scratch_top); + assert(content.len > 0); t.getInstData(ret_block).* = EncodedData.encode(.block, .{ .src_node = decl.nodeOffset(node), @@ -554,25 +563,29 @@ fn transformParamDecl(t: *Transform, decl: BlockContext, param: NodeIndex) Alloc return value; } -const Builtin = enum { +pub const Builtin = enum { js, import, fetch, compile_error, + type_of, const map = std.ComptimeStringMap(Builtin, .{ - .{ "js", .js }, - .{ "import", .import }, - .{ "fetch", .fetch }, + // sort-lines: start .{ "compileError", .compile_error }, + .{ "fetch", .fetch }, + .{ "import", .import }, + .{ "js", .js }, + .{ "typeOf", .type_of }, + // sort-lines: end }); - fn builtin_js(t: *Transform, decl: BlockContext, args: []const NodeIndex) !?Inst.Ref { + fn builtin_js(t: *Transform, decl: BlockContext, node: NodeIndex, args: []const NodeIndex) !?Inst.Ref { const trace = tracer.trace(@src()); defer trace.end(); if (args.len != 1) { - // try t.report.err(.invalid_builtin_args, .{ .id = "js" }); + try t.report.err(.builtin_mismatch_args, .{ .node = node, .args = 1 }); return .void; } @@ -586,6 +599,48 @@ const Builtin = enum { }), }); } + + fn builtin_compileError(t: *Transform, decl: BlockContext, node: NodeIndex, args: []const NodeIndex) !?Inst.Ref { + const trace = tracer.trace(@src()); + defer trace.end(); + + if (args.len != 1) { + try t.report.err(.builtin_mismatch_args, .{ .node = node, .args = 1 }); + return .void; + } + + const source = try t.transformExpression(decl, args[0]) orelse + return null; + + return try t.addInstRef(.{ + .tag = .compile_error, + .data = EncodedData.encode(.js, .{ + .src_node = decl.nodeOffset(node), + .operand = source, + }), + }); + } + + fn builtin_typeOf(t: *Transform, decl: BlockContext, node: NodeIndex, args: []const NodeIndex) !?Inst.Ref { + const trace = tracer.trace(@src()); + defer trace.end(); + + if (args.len != 1) { + try t.report.err(.builtin_mismatch_args, .{ .node = node, .args = 1 }); + return .void; + } + + const source = try t.transformExpression(decl, args[0]) orelse + return null; + + return try t.addInstRef(.{ + .tag = .type_of, + .data = EncodedData.encode(.js, .{ + .src_node = decl.nodeOffset(node), + .operand = source, + }), + }); + } }; /// This is the primary transformation function: converting an AST expression into instructions. @@ -874,10 +929,11 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc const args = base.slice(&t.ast); return switch (builtin) { - .js => Builtin.builtin_js(t, decl, args), - // .import => Builtin.import(t, decl, args), - // .fetch => Builtin.fetch(t, decl, args), - // .compile_error => Builtin.compile_error(t, decl, args), + .js => Builtin.builtin_js(t, decl, node, args), + // .import => Builtin.import(t, decl, node, args), + // .fetch => Builtin.fetch(t, decl, node, args), + .compile_error => Builtin.builtin_compileError(t, decl, node, args), + .type_of => Builtin.builtin_typeOf(t, decl, node, args), else => @panic("TODO"), }; }, @@ -1071,6 +1127,11 @@ fn transformExpression(t: *Transform, decl: BlockContext, node: NodeIndex) Alloc return null; // control flow ends }, + .group => { + const group_node = t.ast.nodeData(node).un; + return try t.transformExpression(decl, group_node.expr); + }, + else => |tag| std.debug.panic("TODO: expr {s}", .{@tagName(tag)}), } } -- 2.54.0