Skip to content

Commit

Permalink
Zero initialization for typed object fields.
Browse files Browse the repository at this point in the history
  • Loading branch information
fubark committed Oct 3, 2023
1 parent a516acc commit 2dae051
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 23 deletions.
38 changes: 34 additions & 4 deletions docs/hugo/content/docs/toc/type-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ The following builtin types are available in every module: `boolean`, `float`, `

### Typed variables.
A typed local variable can be declared by attaching a type specifier after its name. The value assigned to the variable must satisfy the type constraint or a compile error is issued.
> _Incomplete: Only function parameter and object member type specifiers have meaning to the VM at the moment. Variable type specifiers have no meaning and will be discarded._
> _Incomplete: Only function parameter and object field type specifiers have meaning to the VM at the moment. Variable type specifiers have no meaning and will be discarded._
```cy
var a float = 123

Expand All @@ -128,7 +128,7 @@ var foo Foo = none -- CompileError. Type `Foo` is not declared.
```

### `auto` declarations.
The `auto` declaration infers the type of the assigned value and initializes the variable with the same type.
The `auto` declaration creates a new variable with the type inferred from the initializer.
> _Planned Feature_
```cy
-- Initialized as an `int` variable.
Expand All @@ -145,21 +145,51 @@ auto a = getValue()
```

### Object types.
A `type object` declaration creates a new object type. Member types are declared with a type specifier after their name.
A `type object` declaration creates a new object type. Field types are optional and declared with a type specifier after their name.
```cy
type Student object: -- Creates a new type named `Student`
name string
age int
gpa float
```

Circular type references are allowed.
Circular type dependencies are allowed.
```cy
type Node object:
val any
next Node -- Valid type specifier.
```

Instantiating a new object does not require typed fields to be initialized. Missing field values will default to their [zero value](#zero-values):
```cy
var s = Student{}
print s.name -- Prints ""
print s.age -- Prints "0"
print s.gpa -- Prints "0.0"
```

Invoking the zero initializer is not allowed for circular dependencies:
```cy
var n = Node{} -- CompileError. Can not zero initialize `next`
-- because of circular dependency.
```

### Zero values.
The following shows the zero values of builtin or created types.
|Type|Zero value|
|--|--|
|`boolean`|`false`|
|`int`|`0`|
|`float`|`0.0`|
|`string`|`''`|
|`rawstring`|`''`|
|`List`|`[]`|
|`Map`|`{}`|
|`type S object`|`S{}`|
|`@host type S object`|`S.$zero()`|
|`dynamic`|`none`|
|`any`|`none`|

### Type aliases.
A type alias is declared from a single line `type` statement. This creates a new type symbol for an existing data type.
```cy
Expand Down
16 changes: 16 additions & 0 deletions src/chunk.zig
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ pub const Chunk = struct {
/// Using modules.
usingModules: std.ArrayListUnmanaged(cy.ModuleId),

/// Object type dependency graph.
/// This is only needed for default initializers so it is created on demand per chunk.
typeDeps: std.ArrayListUnmanaged(TypeDepNode),
typeDepsMap: std.AutoHashMapUnmanaged(cy.ModuleId, u32),

///
/// Codegen pass
///
Expand Down Expand Up @@ -206,6 +211,8 @@ pub const Chunk = struct {
.exprDoneStack = undefined,
.exprStack = undefined,
.llvmFuncs = undefined,
.typeDeps = .{},
.typeDepsMap = .{},
};
if (cy.hasJIT) {
new.tempTypeRefs = .{};
Expand Down Expand Up @@ -264,6 +271,9 @@ pub const Chunk = struct {
self.alloc.free(self.llvmFuncs);
}

self.typeDeps.deinit(self.alloc);
self.typeDepsMap.deinit(self.alloc);

self.semaFuncDecls.deinit(self.alloc);
self.localSyms.deinit(self.alloc);
self.usingModules.deinit(self.alloc);
Expand Down Expand Up @@ -982,4 +992,10 @@ const ExprState = struct {
pub const LLVM_Func = struct {
typeRef: llvm.TypeRef,
funcRef: llvm.ValueRef,
};

pub const TypeDepNode = struct {
visited: bool,
hasCircularDep: bool,
hasUnsupported: bool,
};
65 changes: 59 additions & 6 deletions src/codegen.zig
Original file line number Diff line number Diff line change
Expand Up @@ -284,11 +284,15 @@ fn objectInit(self: *Chunk, nodeId: cy.NodeId, req: RegisterCstr) !GenValue {
entryId = entry.next;
}

for (fieldsData) |item| {
for (fieldsData, 0..) |item, fidx| {
if (item.nodeId == cy.NullId) {
// Push none.
const local = try self.rega.consumeNextTemp();
try self.buf.pushOp1(.none, local);
if (mod.fields[fidx].typeId == bt.Dynamic) {
const local = try self.rega.consumeNextTemp();
try self.buf.pushOp1(.none, local);
} else {
const local = try self.rega.consumeNextTemp();
try genZeroInit(self, mod.fields[fidx].typeId, local, nodeId);
}
} else {
const entry = self.nodes[item.nodeId];
_ = try expression(self, entry.head.mapEntry.right, RegisterCstr.tempMustRetain);
Expand All @@ -311,6 +315,55 @@ fn objectInit(self: *Chunk, nodeId: cy.NodeId, req: RegisterCstr) !GenValue {
return self.reportErrorAt("Expected object type: `{}`", &.{v(oname)}, nodeId);
}

fn genZeroInit(c: *cy.Chunk, typeId: types.TypeId, dst: RegisterId, debugNodeId: cy.NodeId) !void {
switch (typeId) {
bt.Any,
bt.Dynamic => try c.buf.pushOp1(.none, dst),
bt.Boolean => try c.buf.pushOp1(.false, dst),
bt.Integer => _ = try constInt(c, 0, dst),
bt.Float => _ = try constFloat(c, 0, dst),
bt.List => {
try c.pushOptionalDebugSym(debugNodeId);
try c.buf.pushOp3(.list, dst, 0, dst);
},
bt.Map => {
try c.buf.pushOp1(.mapEmpty, dst);
},
// bt.Rawstring => {
// },
bt.String => {
_ = try string(c, "", dst);
},
else => {
const sym = c.compiler.sema.getSymbol(typeId);
if (sym.symT == .object) {
// Unwind stack registers.
const tempStart = c.rega.getNextTemp();
defer c.rega.setNextTemp(tempStart);

const modId = sym.inner.object.modId;
const mod = c.compiler.sema.getModule(modId);

for (mod.fields) |field| {
const local = try c.rega.consumeNextTemp();
try genZeroInit(c, field.typeId, local, debugNodeId);
}

const rtTypeId = sym.getObjectTypeId(c.compiler.vm).?;
if (mod.fields.len <= 4) {
try c.pushOptionalDebugSym(debugNodeId);
try c.buf.pushOpSlice(.objectSmall, &[_]u8{ @intCast(rtTypeId), tempStart, @intCast(mod.fields.len), dst });
} else {
try c.pushDebugSym(debugNodeId);
try c.buf.pushOpSlice(.object, &[_]u8{ @intCast(rtTypeId), tempStart, @intCast(mod.fields.len), dst });
}
} else {
return error.Unsupported;
}
},
}
}

fn mapInit(self: *Chunk, nodeId: cy.NodeId, req: RegisterCstr) !GenValue {
const node = self.nodes[nodeId];
const operandStart = self.operandStack.items.len;
Expand Down Expand Up @@ -1010,7 +1063,7 @@ fn accessExpr(self: *Chunk, nodeId: cy.NodeId, req: RegisterCstr) !GenValue {
}
}
} else {
return field(self, nodeId, req);
return genField(self, nodeId, req);
}
}

Expand All @@ -1030,7 +1083,7 @@ fn releaseIfRetainedTemp(c: *cy.Chunk, val: GenValue) !void {
return releaseIfRetainedTempAt(c, val, c.curNodeId);
}

fn field(self: *cy.Chunk, nodeId: cy.NodeId, req: RegisterCstr) !GenValue {
fn genField(self: *cy.Chunk, nodeId: cy.NodeId, req: RegisterCstr) !GenValue {
const node = self.nodes[nodeId];
const leftId = node.head.accessExpr.left;
const rightId = node.head.accessExpr.right;
Expand Down
4 changes: 2 additions & 2 deletions src/module.zig
Original file line number Diff line number Diff line change
Expand Up @@ -413,8 +413,8 @@ const ModuleFuncNode = struct {
};

pub const FieldInfo = packed struct {
nameId: u31,
required: bool,
nameId: sema.NameSymId,
typeId: types.TypeId,
};

pub fn getSym(mod: *const Module, nameId: sema.NameSymId) ?ModuleSym {
Expand Down
13 changes: 6 additions & 7 deletions src/sema.zig
Original file line number Diff line number Diff line change
Expand Up @@ -956,7 +956,10 @@ pub fn declareObjectMembers(c: *cy.Chunk, nodeId: cy.NodeId) !void {

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 };
fields[i] = .{
.nameId = fieldNameId,
.typeId = fieldType,
};
} else {
return reportDuplicateModSym(c, mod, fieldNameId, nodeId);
}
Expand Down Expand Up @@ -1553,14 +1556,10 @@ fn semaExprInner(c: *cy.Chunk, nodeId: cy.NodeId, preferType: TypeId) anyerror!T
}
}

// Check that all required type fields were set.
// Check that unset fields can be zero initialized.
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);
try types.checkForZeroInit(c, mod.fields[fIdx].typeId, nodeId);
}
}

Expand Down
101 changes: 101 additions & 0 deletions src/types.zig
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,105 @@ pub fn isRcCandidateType(c: *cy.VMcompiler, id: TypeId) bool {

pub fn getTypeName(c: *cy.VMcompiler, id: TypeId) []const u8 {
return sema.getSymName(c, id);
}

pub fn checkForZeroInit(c: *cy.Chunk, typeId: TypeId, nodeId: cy.NodeId) !void {
var res = hasZeroInit(c, typeId);
if (res == .missingEntry) {
const sym = c.compiler.sema.getSymbol(typeId);
const modId = sym.inner.object.modId;
res = try visitTypeHasZeroInit(c, modId);
}
switch (res) {
.hasZeroInit => return,
.missingEntry => cy.unexpected(),
.unsupported => {
const name = getTypeName(c.compiler, typeId);
return c.reportErrorAt("Unsupported zero initializer for `{}`.", &.{v(name)}, nodeId);
},
.circularDep => {
const name = getTypeName(c.compiler, typeId);
return c.reportErrorAt("Can not zero initialize `{}` because of circular dependency.", &.{v(name)}, nodeId);
}
}
}

const ZeroInitResult = enum {
hasZeroInit,
missingEntry,
unsupported,
circularDep,
};

fn hasZeroInit(c: *cy.Chunk, typeId: TypeId) ZeroInitResult {
switch (typeId) {
bt.Dynamic,
bt.Any,
bt.Boolean,
bt.Integer,
bt.Float,
bt.List,
bt.Map,
// bt.Rawstring,
bt.String => return .hasZeroInit,
else => {
const sym = c.compiler.sema.getSymbol(typeId);
if (sym.symT == .object) {
const modId = sym.inner.object.modId;
if (c.typeDepsMap.get(modId)) |entryId| {
if (!c.typeDeps.items[entryId].visited) {
// Still being visited, which indicates a circular reference.
return .circularDep;
}
if (c.typeDeps.items[entryId].hasCircularDep) {
return .circularDep;
}
if (c.typeDeps.items[entryId].hasUnsupported) {
return .unsupported;
}
return .hasZeroInit;
} else {
return .missingEntry;
}
}
return .unsupported;
},
}
}

fn visitTypeHasZeroInit(c: *cy.Chunk, modId: cy.ModuleId) !ZeroInitResult {
const entryId = c.typeDeps.items.len;
try c.typeDeps.append(c.alloc, .{ .visited = false, .hasCircularDep = false, .hasUnsupported = false });
try c.typeDepsMap.put(c.alloc, modId, @intCast(entryId));

const mod = c.compiler.sema.getModule(modId);
var finalRes = ZeroInitResult.hasZeroInit;
for (mod.fields) |field| {
var res = hasZeroInit(c, field.typeId);
if (res == .missingEntry) {
res = try visitTypeHasZeroInit(c, modId);
}
switch (res) {
.hasZeroInit => continue,
.missingEntry => cy.unexpected(),
.unsupported => {
if (finalRes == .hasZeroInit) {
finalRes = .unsupported;
}
},
.circularDep => {
if (finalRes == .hasZeroInit) {
finalRes = .circularDep;
}
},
}
}

if (finalRes == .circularDep) {
c.typeDeps.items[entryId].hasCircularDep = true;
} else if (finalRes == .unsupported) {
c.typeDeps.items[entryId].hasUnsupported = true;
}
c.typeDeps.items[entryId].visited = true;
return finalRes;
}
Loading

0 comments on commit 2dae051

Please sign in to comment.