From a516acc316e7dea059e152177f4908611ff50f46 Mon Sep 17 00:00:00 2001 From: fubark Date: Tue, 3 Oct 2023 09:07:38 -0400 Subject: [PATCH] Object initializer type checks and inference. --- src/builtins/bindings.zig | 14 ++-- src/chunk.zig | 19 ++--- src/codegen.zig | 44 +++++------- src/module.zig | 48 +++++++++++-- src/parser.zig | 15 ++-- src/sema.zig | 147 ++++++++++++++++++++++++++------------ src/vm.c | 2 +- src/vm.h | 2 - src/vm.zig | 2 +- test/behavior_test.zig | 107 ++++++++++++++++++++++----- test/ffi_test.cy | 2 +- test/import_test.cy | 12 ++-- test/test_mods/a.cy | 2 +- test/typealias_test.cy | 4 +- 14 files changed, 287 insertions(+), 133 deletions(-) diff --git a/src/builtins/bindings.zig b/src/builtins/bindings.zig index fd2db62a7..ce42c7832 100644 --- a/src/builtins/bindings.zig +++ b/src/builtins/bindings.zig @@ -2026,11 +2026,11 @@ pub const ModuleBuilder = struct { } pub fn setVar(self: *const ModuleBuilder, name: []const u8, typeSymId: sema.SymbolId, val: Value) !void { - try self.mod().setTypedVar(self.compiler, name, typeSymId, val); + try self.getMod().setTypedVar(self.compiler, name, typeSymId, val); } pub fn setFunc(self: *const ModuleBuilder, name: []const u8, params: []const sema.SymbolId, ret: sema.SymbolId, ptr: cy.ZHostFuncFn) !void { - try self.mod().setNativeTypedFunc(self.compiler, name, params, ret, ptr); + try self.getMod().setNativeTypedFunc(self.compiler, name, params, ret, ptr); } pub fn ensureMethodGroup(self: *const ModuleBuilder, name: []const u8) !vmc.MethodGroupId { @@ -2062,7 +2062,7 @@ pub const ModuleBuilder = struct { } } - pub fn mod(self: *const ModuleBuilder) *cy.Module { + pub fn getMod(self: *const ModuleBuilder) *cy.Module { return self.compiler.sema.getModulePtr(self.modId); } @@ -2070,7 +2070,7 @@ pub const ModuleBuilder = struct { const nameId = try cy.sema.ensureNameSym(self.compiler, name); const key = sema.ResolvedSymKey{ .resolvedSymKey = .{ - .parentSymId = self.mod().resolvedRootSymId, + .parentSymId = self.getMod().resolvedRootSymId, .nameId = nameId, }, }; @@ -2079,10 +2079,16 @@ pub const ModuleBuilder = struct { const sym = self.compiler.sema.getSymbol(res.sTypeId); const typeId = sym.inner.object.typeId; + const mod = self.compiler.sema.getModulePtr(modId); + const modFields = try self.compiler.alloc.alloc(cy.module.FieldInfo, fields.len); + for (fields, 0..) |field, i| { const id = try self.vm.ensureFieldSym(field); try self.vm.addFieldSym(typeId, id, @intCast(i), bt.Any); + const fieldNameId = try cy.sema.ensureNameSym(self.compiler, field); + try cy.module.setField(mod, self.compiler.alloc, fieldNameId, @intCast(i), bt.Any); } + mod.fields = modFields; self.vm.types.buf[typeId].data.numFields = @intCast(fields.len); return typeId; } diff --git a/src/chunk.zig b/src/chunk.zig index 78d56f6ed..77dd38daa 100644 --- a/src/chunk.zig +++ b/src/chunk.zig @@ -30,8 +30,8 @@ pub const Chunk = struct { parser: cy.Parser, parserAstRootId: cy.NodeId, - /// Generic linked list buffer. - dataNodes: std.ArrayListUnmanaged(DataNode), + /// Generic stack data that is untouched by block pre/post tasks. + stackData: std.ArrayListUnmanaged(GenericItem), /// Used for temp string building. tempBufU8: std.ArrayListUnmanaged(u8), @@ -183,7 +183,7 @@ pub const Chunk = struct { .curSemaInitingSym = @bitCast(@as(u32, cy.NullId)), .semaVarDeclDeps = .{}, .bufU32 = .{}, - .dataNodes = .{}, + .stackData = .{}, .tempBufU8 = .{}, .srcOwned = false, .modId = cy.NullId, @@ -237,7 +237,7 @@ pub const Chunk = struct { self.bufU32.deinit(self.alloc); self.semaVarDeclDeps.deinit(self.alloc); - self.dataNodes.deinit(self.alloc); + self.stackData.deinit(self.alloc); self.blockJumpStack.deinit(self.alloc); self.subBlockJumpStack.deinit(self.alloc); @@ -887,17 +887,12 @@ pub const Chunk = struct { } }; -const DataNode = extern struct { - inner: extern union { - funcSym: extern struct { - symId: u32, - }, - }, - next: u32, +const GenericItem = extern union { + nodeId: cy.NodeId, }; test "chunk internals." { - try t.eq(@sizeOf(DataNode), 8); + try t.eq(@sizeOf(GenericItem), 4); } const GenBlock = struct { diff --git a/src/codegen.zig b/src/codegen.zig index 93b618f90..299dcf05e 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -253,25 +253,23 @@ fn objectInit(self: *Chunk, nodeId: cy.NodeId, req: RegisterCstr) !GenValue { if (node.head.objectInit.sema_symId != cy.NullId) { const rSym = self.compiler.sema.getSymbol(node.head.objectInit.sema_symId); if (rSym.symT == .object) { - const typeId = rSym.getObjectTypeId(self.compiler.vm).?; const initializer = self.nodes[node.head.objectInit.initializer]; // TODO: Would it be faster/efficient to copy the fields into contiguous registers // and copy all at once to heap or pass locals into the operands and iterate each and copy to heap? // The current implementation is the former. - // TODO: Have sema sort the fields so eval can handle default values easier. - // Push props onto stack. + const mod = self.compiler.sema.getModule(rSym.inner.object.modId); - const numFields = self.compiler.vm.types.buf[typeId].data.numFields; - // Repurpose stack for sorting fields. - const sortedFieldsStart = self.assignedVarStack.items.len; - try self.assignedVarStack.resize(self.alloc, self.assignedVarStack.items.len + numFields); - defer self.assignedVarStack.items.len = sortedFieldsStart; + // TODO: This is one case where having an IR would be helpful + // to avoid initializing this buffer again. + const fieldsDataStart = self.stackData.items.len; + try self.stackData.resize(self.alloc, self.stackData.items.len + mod.fields.len); + defer self.stackData.items.len = fieldsDataStart; // Initially set to NullId so leftovers are defaulted to `none`. - const initFields = self.assignedVarStack.items[sortedFieldsStart..]; - @memset(initFields, cy.NullId); + const fieldsData = self.stackData.items[fieldsDataStart..]; + @memset(fieldsData, .{ .nodeId = cy.NullId }); const dst = try self.rega.selectFromNonLocalVar(req, true); const tempStart = self.rega.getNextTemp(); @@ -279,39 +277,31 @@ fn objectInit(self: *Chunk, nodeId: cy.NodeId, req: RegisterCstr) !GenValue { var i: u32 = 0; var entryId = initializer.head.child_head; - // First iteration to sort the initializer fields. + while (entryId != cy.NullId) : (i += 1) { const entry = self.nodes[entryId]; - const prop = self.nodes[entry.head.mapEntry.left]; - const fieldName = self.getNodeTokenString(prop); - const fieldIdx = self.compiler.vm.getStructFieldIdx(typeId, fieldName) orelse { - const vmType = self.compiler.vm.types.buf[typeId]; - const objectName = vmType.namePtr[0..vmType.nameLen]; - return self.reportErrorAt("Missing field `{}` in `{}`.", &.{v(fieldName), v(objectName)}, entry.head.mapEntry.left); - }; - initFields[fieldIdx] = entryId; + fieldsData[entry.head.mapEntry.semaFieldIdx] = .{ .nodeId = entryId }; entryId = entry.next; } - i = 0; - while (i < numFields) : (i += 1) { - entryId = self.assignedVarStack.items[sortedFieldsStart + i]; - if (entryId == cy.NullId) { + for (fieldsData) |item| { + if (item.nodeId == cy.NullId) { // Push none. const local = try self.rega.consumeNextTemp(); try self.buf.pushOp1(.none, local); } else { - const entry = self.nodes[entryId]; + const entry = self.nodes[item.nodeId]; _ = try expression(self, entry.head.mapEntry.right, RegisterCstr.tempMustRetain); } } - if (self.compiler.vm.types.buf[typeId].data.numFields <= 4) { + const typeId = rSym.getObjectTypeId(self.compiler.vm).?; + if (mod.fields.len <= 4) { try self.pushOptionalDebugSym(nodeId); - try self.buf.pushOpSlice(.objectSmall, &[_]u8{ @intCast(typeId), tempStart, @intCast(numFields), dst }); + try self.buf.pushOpSlice(.objectSmall, &[_]u8{ @intCast(typeId), tempStart, @intCast(mod.fields.len), dst }); } else { try self.pushDebugSym(nodeId); - try self.buf.pushOpSlice(.object, &[_]u8{ @intCast(typeId), tempStart, @intCast(numFields), dst }); + try self.buf.pushOpSlice(.object, &[_]u8{ @intCast(typeId), tempStart, @intCast(mod.fields.len), dst }); } const type_ = node.head.objectInit.sema_symId; return GenValue.initTempValue(dst, type_, true); diff --git a/src/module.zig b/src/module.zig index cb1924444..b866df755 100644 --- a/src/module.zig +++ b/src/module.zig @@ -40,6 +40,9 @@ pub const Module = struct { /// If this is a submodule, it is a relative name. absSpec: []const u8, + /// For object module. + fields: []const FieldInfo = &.{}, + pub fn setNativeTypedFunc(self: *Module, c: *cy.VMcompiler, name: []const u8, sig: []const sema.SymbolId, retSymId: sema.SymbolId, func: cy.ZHostFuncFn) !void { return self.setNativeTypedFuncExt(c, name, false, sig, retSymId, func); @@ -297,6 +300,7 @@ pub const Module = struct { } } self.syms.deinit(alloc); + alloc.free(self.fields); alloc.free(self.absSpec); } @@ -331,6 +335,8 @@ const ModuleSymType = enum { enumMember, userObject, typeAlias, + + field, }; const ModuleSym = struct { @@ -394,6 +400,10 @@ const ModuleSym = struct { userObject: struct { declId: cy.NodeId, }, + field: struct { + idx: u32, + typeId: types.TypeId, + }, }, }; @@ -402,14 +412,37 @@ const ModuleFuncNode = struct { funcSigId: sema.FuncSigId, }; +pub const FieldInfo = packed struct { + nameId: u31, + required: bool, +}; + +pub fn getSym(mod: *const Module, nameId: sema.NameSymId) ?ModuleSym { + const key = ModuleSymKey.initModuleSymKey(nameId, null); + return mod.syms.get(key); +} + +pub fn symNameExists(mod: *const Module, nameId: sema.NameSymId) bool { + const key = ModuleSymKey.initModuleSymKey(nameId, null); + return mod.syms.contains(key); +} + +pub fn setField(mod: *Module, alloc: std.mem.Allocator, nameId: sema.NameSymId, idx: u32, typeId: types.TypeId) !void { + const key = ModuleSymKey.initModuleSymKey(nameId, null); + try mod.syms.put(alloc, key, .{ + .symT = .field, + .inner = .{ + .field = .{ + .idx = idx, + .typeId = typeId, + }, + }, + }); +} + pub fn declareTypeObject(c: *cy.VMcompiler, modId: ModuleId, name: []const u8, chunkId: cy.ChunkId, declId: cy.NodeId) !ModuleId { const nameId = try sema.ensureNameSym(c, name); - const key = ModuleSymKey{ - .moduleSymKey = .{ - .nameId = nameId, - .funcSigId = cy.NullId, - }, - }; + const key = ModuleSymKey.initModuleSymKey(nameId, null); if (c.sema.getModule(modId).syms.contains(key)) { return error.DuplicateSymName; } @@ -591,6 +624,9 @@ pub fn findDistinctModuleSym(chunk: *cy.Chunk, modId: ModuleId, nameId: sema.Nam const name = sema.getName(chunk.compiler, nameId); return chunk.reportError("Symbol `{}` is ambiguous. There are multiple functions with the same name.", &.{v(name)}); }, + .field => { + return false; + }, } } return false; diff --git a/src/parser.zig b/src/parser.zig index 91dfcace0..022e8a8fc 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -752,12 +752,13 @@ pub const Parser = struct { return id; } - fn pushObjectDecl(self: *Parser, start: TokenId, name: NodeId, modifierHead: NodeId, fieldsHead: NodeId, funcsHead: NodeId) !NodeId { + fn pushObjectDecl(self: *Parser, start: TokenId, name: NodeId, modifierHead: NodeId, fieldsHead: NodeId, numFields: u32, funcsHead: NodeId) !NodeId { const body = try self.pushNode(.objectDeclBody, start); self.nodes.items[body].head = .{ .objectDeclBody = .{ .fieldsHead = fieldsHead, .funcsHead = funcsHead, + .numFields = numFields, }, }; @@ -801,21 +802,23 @@ pub const Parser = struct { defer self.cur_indent = prevIndent; var firstField = (try self.parseObjectField()) orelse NullId; + var numFields: u32 = 1; if (firstField != NullId) { var lastField = firstField; while (true) { const start2 = self.next_pos; const indent = (try self.consumeIndentBeforeStmt()) orelse { - return self.pushObjectDecl(start, name, modifierHead, firstField, NullId); + return self.pushObjectDecl(start, name, modifierHead, firstField, numFields, NullId); }; if (indent == reqIndent) { const id = (try self.parseObjectField()) orelse break; + numFields += 1; self.nodes.items[lastField].next = id; lastField = id; } else if (try isRecedingIndent(self, prevIndent, reqIndent, indent)) { self.next_pos = start2; - return self.pushObjectDecl(start, name, modifierHead, firstField, NullId); + return self.pushObjectDecl(start, name, modifierHead, firstField, numFields, NullId); } else { return self.reportParseError("Unexpected indentation.", &.{}); } @@ -846,7 +849,7 @@ pub const Parser = struct { return self.reportParseError("Unexpected indentation.", &.{}); } } - return self.pushObjectDecl(start, name, modifierHead, firstField, firstFunc); + return self.pushObjectDecl(start, name, modifierHead, firstField, numFields, firstFunc); } else { return self.reportParseError("Expected function.", &.{}); } @@ -3540,6 +3543,9 @@ pub const Node = struct { mapEntry: struct { left: NodeId, right: NodeId, + + // Used for object initializers to map an entry to an object field. + semaFieldIdx: u32 = cy.NullId, }, caseBlock: struct { firstCond: NodeId, @@ -3636,6 +3642,7 @@ pub const Node = struct { objectDeclBody: struct { fieldsHead: NodeId, funcsHead: NodeId, + numFields: u32, }, varSpec: struct { name: NodeId, diff --git a/src/sema.zig b/src/sema.zig index 334d76559..11d4497d2 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -505,7 +505,7 @@ pub fn semaStmt(c: *cy.Chunk, nodeId: cy.NodeId) !void { if (!types.isTypeSymCompat(c.compiler, rightT, res.exprT)) { const fieldTypeName = getSymName(c.compiler, res.exprT); const rightTypeName = getSymName(c.compiler, rightT); - return c.reportError("Assigning to `{}` member with incompatible type `{}`.", &.{v(fieldTypeName), v(rightTypeName)}); + return c.reportError("Assigning to `{}` field with incompatible type `{}`.", &.{v(fieldTypeName), v(rightTypeName)}); } } } else { @@ -938,6 +938,8 @@ pub fn declareObjectMembers(c: *cy.Chunk, nodeId: cy.NodeId) !void { const body = c.nodes[node.head.objectDecl.body]; if (node.node_t == .objectDecl) { + const mod = c.compiler.sema.getModulePtr(objModId); + const fields = try c.alloc.alloc(cy.module.FieldInfo, body.head.objectDeclBody.numFields); var fieldId = body.head.objectDeclBody.fieldsHead; while (fieldId != cy.NullId) : (i += 1) { const field = c.nodes[fieldId]; @@ -949,19 +951,21 @@ pub fn declareObjectMembers(c: *cy.Chunk, nodeId: cy.NodeId) !void { if (field.head.objectField.typeSpecHead != cy.NullId) { fieldType = try getOrResolveTypeSymFromSpecNode(c, field.head.objectField.typeSpecHead); } else { - fieldType = bt.Any; + fieldType = bt.Dynamic; + } + + if (!cy.module.symNameExists(mod, fieldNameId)) { + try cy.module.setField(mod, c.alloc, fieldNameId, i, fieldType); + fields[i] = .{ .nameId = @intCast(fieldNameId), .required = fieldType == bt.Dynamic }; + } else { + return reportDuplicateModSym(c, mod, fieldNameId, nodeId); } try c.compiler.vm.addFieldSym(rtTypeId, fieldSymId, @intCast(i), fieldType); - try c.compiler.sema.objectMembers.put(c.alloc, .{ - .objectMemberKey = .{ - .objSymId = objSymId, - .memberNameId = fieldNameId, - }, - }, {}); fieldId = field.next; } + mod.fields = fields; c.compiler.vm.types.buf[rtTypeId].data.numFields = i; } @@ -973,6 +977,18 @@ pub fn declareObjectMembers(c: *cy.Chunk, nodeId: cy.NodeId) !void { } } +fn reportDuplicateModSym(c: *cy.Chunk, mod: *cy.Module, nameId: NameSymId, nodeId: cy.NodeId) !void { + const key = ModuleSymKey.initModuleSymKey(nameId, nodeId); + const sym = mod.syms.get(key).?; + const name = getName(c.compiler, nameId); + const modName = getSymName(c.compiler, mod.resolvedRootSymId); + switch (sym.symT) { + else => { + return c.reportErrorAt("The symbol `{}` already exists in `{}`.", &.{v(name), v(modName)}, nodeId); + } + } +} + fn objectDecl(c: *cy.Chunk, nodeId: cy.NodeId) !void { const node = c.nodes[nodeId]; // const nameN = c.nodes[node.head.objectDecl.name]; @@ -1497,15 +1513,59 @@ fn semaExprInner(c: *cy.Chunk, nodeId: cy.NodeId, preferType: TypeId) anyerror!T if (csymId.isPresent()) { if (!csymId.isFuncSymId) { c.nodes[nodeId].head.objectInit.sema_symId = csymId.id; - const initializer = c.nodes[node.head.objectInit.initializer]; - var i: u32 = 0; - var entry_id = initializer.head.child_head; - while (entry_id != cy.NullId) : (i += 1) { - var entry = c.nodes[entry_id]; - _ = try semaExpr(c, entry.head.mapEntry.right); - entry_id = entry.next; + + const objSym = c.compiler.sema.getSymbol(csymId.id); + if (objSym.symT == .object) { + const mod = c.compiler.sema.getModulePtr(objSym.inner.object.modId); + + // Set up a temp buffer to map initializer entries to type fields. + const fieldsDataStart = c.stackData.items.len; + try c.stackData.resize(c.alloc, c.stackData.items.len + mod.fields.len); + defer c.stackData.items.len = fieldsDataStart; + + // Initially set to NullId so missed mappings are known from a linear scan. + const fieldsData = c.stackData.items[fieldsDataStart..]; + @memset(fieldsData, .{ .nodeId = cy.NullId }); + + const initializer = c.nodes[node.head.objectInit.initializer]; + + var i: u32 = 0; + var entryId = initializer.head.child_head; + while (entryId != cy.NullId) : (i += 1) { + var entry = c.nodes[entryId]; + + const field = c.nodes[entry.head.mapEntry.left]; + const fieldName = c.getNodeTokenString(field); + const fieldNameId = try ensureNameSym(c.compiler, fieldName); + + if (cy.module.getSym(mod, fieldNameId)) |sym| { + if (sym.symT != .field) { + const objectName = getSymName(c.compiler, csymId.id); + return c.reportErrorAt("`{}` is not a field in `{}`.", &.{v(fieldName), v(objectName)}, entry.head.mapEntry.left); + } + _ = try semaExprCstr(c, entry.head.mapEntry.right, sym.inner.field.typeId, true); + c.nodes[entryId].head.mapEntry.semaFieldIdx = sym.inner.field.idx; + fieldsData[sym.inner.field.idx] = .{ .nodeId = entryId }; + entryId = entry.next; + } else { + const objectName = getSymName(c.compiler, csymId.id); + return c.reportErrorAt("Field `{}` does not exist in `{}`.", &.{v(fieldName), v(objectName)}, entry.head.mapEntry.left); + } + } + + // Check that all required type fields were set. + for (fieldsData, 0..) |item, fIdx| { + if (item.nodeId == cy.NullId) { + if (mod.fields[fIdx].required) { + continue; + } + const name = getName(c.compiler, mod.fields[fIdx].nameId); + return c.reportErrorAt("Expected required field `{}` in initializer.", &.{v(name)}, nodeId); + } + } + + return csymId.id; } - return csymId.id; } } @@ -1898,8 +1958,8 @@ fn callExpr(c: *cy.Chunk, nodeId: cy.NodeId) !TypeId { const nameId = try ensureNameSym(c.compiler, name); const sym = c.compiler.sema.getSymbol(crLeftSym.id); - if (sym.symT == .module) { - if (getFirstFuncSigInModule(c, sym.inner.module.id, nameId)) |funcSigId| { + if (sym.getModuleId()) |modId| { + if (getFirstFuncSigInModule(c, modId, nameId)) |funcSigId| { const funcSig = c.compiler.sema.getFuncSig(funcSigId); const preferredParamTypes = funcSig.params(); @@ -2392,17 +2452,15 @@ fn getOrLookupVar(self: *cy.Chunk, name: []const u8, staticLookup: bool) !VarLoo // Look for object member if inside method. if (sblock.isMethodBlock) { const nameId = try ensureNameSym(self.compiler, name); - const key = ObjectMemberKey{ - .objectMemberKey = .{ - .objSymId = self.curObjectSymId, - .memberNameId = nameId, - }, - }; - if (self.compiler.sema.objectMembers.contains(key)) { - const id = try pushObjectMemberAlias(self, name); - return VarLookupResult{ - .local = id, - }; + const objSym = self.compiler.sema.getSymbol(self.curObjectSymId); + const mod = self.compiler.sema.getModulePtr(objSym.inner.object.modId); + if (cy.module.getSym(mod, nameId)) |sym| { + if (sym.symT == .field) { + const id = try pushObjectMemberAlias(self, name); + return VarLookupResult{ + .local = id, + }; + } } } @@ -2499,18 +2557,16 @@ fn lookupParentLocal(c: *cy.Chunk, name: []const u8) !?LookupParentLocalResult { // Look for object member if inside method. if (prev.isMethodBlock) { const nameId = try ensureNameSym(c.compiler, name); - const key = ObjectMemberKey{ - .objectMemberKey = .{ - .objSymId = c.curObjectSymId, - .memberNameId = nameId, - }, - }; - if (c.compiler.sema.objectMembers.contains(key)) { - return .{ - .varId = prev.nameToVar.get("self").?, - .blockDepth = c.semaBlockDepth(), - .isObjectMember = true, - }; + const objSym = c.compiler.sema.getSymbol(c.curObjectSymId); + const mod = c.compiler.sema.getModulePtr(objSym.inner.object.modId); + if (cy.module.getSym(mod, nameId)) |sym| { + if (sym.symT == .field) { + return .{ + .varId = prev.nameToVar.get("self").?, + .blockDepth = c.semaBlockDepth(), + .isObjectMember = true, + }; + } } } } @@ -3556,6 +3612,9 @@ fn resolveSymFromModule(chunk: *cy.Chunk, modId: cy.ModuleId, nameId: NameSymId, .userObject => { return chunk.reportError("Unsupported module sym: userObject", &.{}); }, + .field => { + return chunk.reportError("Unsupported module sym: field", &.{}); + }, } } return null; @@ -4504,8 +4563,6 @@ pub const Model = struct { /// Owned absolute specifier path to module. moduleMap: std.StringHashMapUnmanaged(cy.ModuleId), - objectMembers: std.HashMapUnmanaged(ObjectMemberKey, void, cy.hash.KeyU64Context, 80), - pub fn init(alloc: std.mem.Allocator, compiler: *cy.VMcompiler) Model { return .{ .alloc = alloc, @@ -4521,7 +4578,6 @@ pub const Model = struct { .resolvedUntypedFuncSigs = .{}, .modules = .{}, .moduleMap = .{}, - .objectMembers = .{}, }; } @@ -4531,13 +4587,11 @@ pub const Model = struct { self.resolvedSymMap.clearRetainingCapacity(); self.resolvedFuncSyms.clearRetainingCapacity(); self.resolvedFuncSymMap.clearRetainingCapacity(); - self.objectMembers.clearRetainingCapacity(); } else { self.resolvedSyms.deinit(alloc); self.resolvedSymMap.deinit(alloc); self.resolvedFuncSyms.deinit(alloc); self.resolvedFuncSymMap.deinit(alloc); - self.objectMembers.deinit(alloc); } for (self.modules.items) |*mod| { @@ -4757,5 +4811,4 @@ test "sema internals." { try t.eq(@offsetOf(Model, "resolvedUntypedFuncSigs"), @offsetOf(vmc.SemaModel, "resolvedUntypedFuncSigs")); try t.eq(@offsetOf(Model, "modules"), @offsetOf(vmc.SemaModel, "modules")); try t.eq(@offsetOf(Model, "moduleMap"), @offsetOf(vmc.SemaModel, "moduleMap")); - try t.eq(@offsetOf(Model, "objectMembers"), @offsetOf(vmc.SemaModel, "objectMembers")); } diff --git a/src/vm.c b/src/vm.c index a605b7f12..0fe927692 100644 --- a/src/vm.c +++ b/src/vm.c @@ -511,7 +511,7 @@ static void panicIncompatibleFieldType(VM* vm, SemaTypeId fieldSemaTypeId, Value TypeId rightTypeId = getTypeId(rightv); SemaTypeId rightSemaTypeId = ((Type*)vm->types.buf)[rightTypeId].semaTypeId; Str rightTypeName = getSemaSymName(vm, rightSemaTypeId); - zPanicFmt(vm, "Assigning to `{}` member with incompatible type `{}`.", (FmtValue[]){ + zPanicFmt(vm, "Assigning to `{}` field with incompatible type `{}`.", (FmtValue[]){ FMT_STR(fieldTypeName), FMT_STR(rightTypeName) }, 2); release(vm, rightv); diff --git a/src/vm.h b/src/vm.h index b97240818..822769c1d 100644 --- a/src/vm.h +++ b/src/vm.h @@ -658,8 +658,6 @@ typedef struct SemaModel { ZList modules; ZHashMap moduleMap; - - ZHashMap objectMembers; } SemaModel; typedef struct Compiler { diff --git a/src/vm.zig b/src/vm.zig index 72f0f52f4..2716455b0 100644 --- a/src/vm.zig +++ b/src/vm.zig @@ -3792,7 +3792,7 @@ fn panicIncompatibleFieldType(vm: *cy.VM, fieldSemaTypeId: types.TypeId, rightv: const rightSemaTypeId = vm.types.buf[rightTypeId].rTypeSymId; const rightTypeName = sema.getSymName(&vm.compiler, rightSemaTypeId); return vm.panicFmt( - \\Assigning to `{}` member with incompatible type `{}`. + \\Assigning to `{}` field with incompatible type `{}`. , &.{ v(fieldTypeName), v(rightTypeName), }, diff --git a/test/behavior_test.zig b/test/behavior_test.zig index ecad100e4..0472a62e1 100644 --- a/test/behavior_test.zig +++ b/test/behavior_test.zig @@ -1287,11 +1287,6 @@ test "Objects." { \\ x \\ y ); - try evalPass(.{}, - \\type Vec2 object: - \\ x float - \\ y float - ); // Initialize with undeclared field. try eval(.{ .silent = true }, @@ -1327,16 +1322,73 @@ test "Objects." { }}.func); } -test "Object fields." { - // Compile time type check. +test "Typed object fields." { + // Field declaration ends the file without parser error. + try evalPass(.{}, + \\type Vec2 object: + \\ x float + \\ y float + ); + + // Initialize field with exact type match. + try evalPass(.{}, + \\import test + \\type S object: + \\ x float + \\var s = S{ x: 1.23 } + \\test.eq(s.x, 1.23) + ); + + // Inferred initializer to field type. + try evalPass(.{}, + \\import test + \\type S object: + \\ x float + \\var s = S{ x: 123 } + \\test.eq(s.x, 123.0) + ); + + // Missing required field. try eval(.{ .silent = true }, \\type S object: \\ a float \\var o = S{} + , struct { fn func(run: *VMrunner, res: EvalResult) !void { + try run.expectErrorReport(res, error.CompileError, + \\CompileError: Expected required field `a` in initializer. + \\ + \\main:3:9: + \\var o = S{} + \\ ^ + \\ + ); + }}.func); + + // No such field. + try eval(.{ .silent = true }, + \\type S object: + \\ a float + \\var o = S{ b: 123 } + , struct { fn func(run: *VMrunner, res: EvalResult) !void { + try run.expectErrorReport(res, error.CompileError, + \\CompileError: Field `b` does not exist in `S`. + \\ + \\main:3:12: + \\var o = S{ b: 123 } + \\ ^ + \\ + ); + }}.func); + + // Set field with incompatible type. Static rhs. + try eval(.{ .silent = true }, + \\type S object: + \\ a float + \\var o = S{ a: 123 } \\o.a = [] , struct { fn func(run: *VMrunner, res: EvalResult) !void { try run.expectErrorReport(res, error.CompileError, - \\CompileError: Assigning to `float` member with incompatible type `List`. + \\CompileError: Assigning to `float` field with incompatible type `List`. \\ \\main:4:7: \\o.a = [] @@ -1345,16 +1397,16 @@ test "Object fields." { ); }}.func); - // Runtime type check when right is dynamic. + // Set field with incompatiable type. Dynamic rhs. try eval(.{ .silent = false }, \\type S object: \\ a float \\func foo(): return [] - \\var o = S{} + \\var o = S{ a: 123 } \\o.a = foo() , struct { fn func(run: *VMrunner, res: EvalResult) !void { try run.expectErrorReport(res, error.Panic, - \\panic: Assigning to `float` member with incompatible type `List`. + \\panic: Assigning to `float` field with incompatible type `List`. \\ \\main:5:1 main: \\o.a = foo() @@ -1363,16 +1415,16 @@ test "Object fields." { ); }}.func); - // Runtime type check when receiver is `any`. + // Set field with incompatible type. Dynamic lhs. try eval(.{ .silent = false }, \\import t 'test' \\type S object: \\ a float - \\var o = t.erase(S{}) + \\var o = t.erase(S{ a: 123 }) \\o.a = [] , struct { fn func(run: *VMrunner, res: EvalResult) !void { try run.expectErrorReport(res, error.Panic, - \\panic: Assigning to `float` member with incompatible type `List`. + \\panic: Assigning to `float` field with incompatible type `List`. \\ \\main:5:1 main: \\o.a = [] @@ -2523,6 +2575,23 @@ test "Typed static functions." { \\ ); }}.func); + + // Infer type from param. + try evalPass(.{}, + \\import test + \\func foo(a float): + \\ return a + \\test.eq(foo(2), 2.0) + ); + + // Infer type from object func param. + try evalPass(.{}, + \\import test + \\type S object: + \\ func foo(a float): + \\ return a + \\test.eq(S.foo(2), 2.0) + ); } test "Static functions." { @@ -2858,11 +2927,11 @@ test "ARC cycles." { _ = try evalPass(.{}, \\import t 'test' \\type T object: - \\ a any - \\ b any - \\ c any - \\ d any - \\ e any + \\ a + \\ b + \\ c + \\ d + \\ e \\func foo(): \\ var a = T{} \\ var b = T{} diff --git a/test/ffi_test.cy b/test/ffi_test.cy index 59560e55a..e68e84099 100644 --- a/test/ffi_test.cy +++ b/test/ffi_test.cy @@ -23,7 +23,7 @@ t.eq(lib, error.MissingSymbol) type MyObject object: a float b int - c string + c pointer d boolean lib = os.bindLib(libPath, [ diff --git a/test/import_test.cy b/test/import_test.cy index 4d5286631..9ed92ee81 100644 --- a/test/import_test.cy +++ b/test/import_test.cy @@ -44,12 +44,12 @@ type Vec2 object: var v1 = Vec2{ x: 1, y: 2 } var v2 = a.Vec2{ x: 3, y: 4 } t.eq(typeof(v1) != typeof(v2), true) -t.eq(v1.x, 1) -t.eq(v1.y, 2) -t.eq(v2.x, 3) -t.eq(v2.y, 4) +t.eq(v1.x, 1.0) +t.eq(v1.y, 2.0) +t.eq(v2.x, 3.0) +t.eq(v2.y, 4.0) -- Calling object func from another module. var v = a.Vec2.new(3, 4) -t.eq(v.x, 3) -t.eq(v.y, 4) \ No newline at end of file +t.eq(v.x, 3.0) +t.eq(v.y, 4.0) \ No newline at end of file diff --git a/test/test_mods/a.cy b/test/test_mods/a.cy index d46af433f..f1f8c30ef 100644 --- a/test/test_mods/a.cy +++ b/test/test_mods/a.cy @@ -51,7 +51,7 @@ type Vec2 object: x float y float - func new(x, y): + func new(x float, y float): return Vec2{ x: x, y: y } type Vec2Alias Vec2 diff --git a/test/typealias_test.cy b/test/typealias_test.cy index 70663e235..207831638 100644 --- a/test/typealias_test.cy +++ b/test/typealias_test.cy @@ -11,8 +11,8 @@ type Parent object: v Vec2 var v = Vec2{ x: 1, y: 2 } -t.eq(v.x, 1) -t.eq(v.y, 2) +t.eq(v.x, 1.0) +t.eq(v.y, 2.0) func foo(v Vec2): pass