diff --git a/build.zig b/build.zig
index 482d2dfc5..4a49652b7 100644
--- a/build.zig
+++ b/build.zig
@@ -462,6 +462,7 @@ pub fn buildCVM(b: *std.Build, opts: Options) !*std.build.Step.Compile {
.target = opts.target,
.optimize = opts.optimize,
});
+ lib.linkLibC();
var cflags = std.ArrayList([]const u8).init(b.allocator);
if (opts.optimize == .Debug) {
diff --git a/docs/docs.md b/docs/docs.md
index 5b1797823..a7a21b035 100644
--- a/docs/docs.md
+++ b/docs/docs.md
@@ -797,7 +797,7 @@ print int(currency) -- '123' or some arbitrary id.
* [Type functions.](#type-functions)
* [Type variables.](#type-variables)
* [Structs.](#structs)
- * [Declare struct type.](#declare-struct-type)
+ * [Declare struct.](#declare-struct)
* [Copy structs.](#copy-structs)
* [Enums.](#enums)
* [Choices.](#choices)
@@ -807,6 +807,13 @@ print int(currency) -- '123' or some arbitrary id.
* [Optionals.](#optionals)
+ * [Wrap value.](#wrap-value)
+ * [Wrap `none`.](#wrap-none)
+ * [Unwrap or panic.](#unwrap-or-panic)
+ * [Unwrap or default.](#unwrap-or-default)
+ * [Optional chaining.](#optional-chaining)
+ * [`if` unwrap.](#if-unwrap)
+ * [`while` unwrap.](#while-unwrap)
* [Type aliases.](#type-aliases)
* [Traits.](#traits)
* [Union types.](#union-types)
@@ -971,7 +978,7 @@ Struct types can contain field and method members just like object types, but th
Unlike objects, structs do not have a reference count. They can be safely referenced using borrow semantics. Unsafe pointers can also reference structs.
-### Declare struct type.
+### Declare struct.
Struct types are created using the `type struct` declaration:
```cy
type Vec2 struct:
@@ -1076,7 +1083,80 @@ print s.!line -- Prints '20'
```
## Optionals.
-> _Planned Feature_
+The generic `Option` type is a choice type that either holds a `none` value or contains `some` value. The option template is defined as:
+```cy
+template(T type)
+type Option enum:
+ case none
+ case some #T
+```
+A type prefixed with `?` is a more idiomatic way to create an option type. The following String optional types are equivalent:
+```cy
+Option#String
+?String
+```
+
+### Wrap value.
+Use `?` as a type prefix to turn it into an `Option` type. A value is automatically wrapped into the inferred optional's `some` case:
+```cy
+var a ?String = 'abc'
+print a -- Prints 'Option(abc)'
+```
+
+### Wrap `none`.
+`none` is automatically initialized to the inferred optional's `none` case:
+```cy
+var a ?String = none
+print a -- Prints 'Option.none'
+```
+
+### Unwrap or panic.
+The `.?` access operator is used to unwrap an optional. If the expression evaluates to the `none` case, the runtime panics:
+```cy
+var opt ?String = 'abc'
+var v = opt.?
+print v -- Prints 'abc'
+```
+
+### Unwrap or default.
+The `?else` control flow operator either returns the unwrapped value or a default value when the optional is `none`: *Planned Feature*
+```cy
+var opt ?String = none
+var v = opt ?else 'empty'
+print v -- Prints 'empty'
+```
+
+`else` can also start a new statement block: *Planned Feature*
+```cy
+var v = opt else:
+ break 'empty'
+
+var v = opt else:
+ throw error.Missing
+```
+
+### Optional chaining.
+Given the last member's type `T` in a chain of `?.` access operators, the chain's execution will either return `Option(T).none` on the first encounter of `none` or returns the last member as an `Option(T).some`: *Planned Feature*
+```cy
+print root?.a?.b?.c?.last
+```
+
+### `if` unwrap.
+The `if` statement can be amended to unwrap an optional value using the capture `->` operator: *Planned Feature*
+```cy
+var opt ?String = 'abc'
+if opt -> v:
+ print v -- Prints 'abc'
+```
+
+### `while` unwrap.
+The `while` statement can be amended to unwrap an optional value using the capture `->` operator.
+The loop exits when `none` is encountered: *Planned Feature*
+```cy
+var iter = dir.walk()
+while iter.next() -> entry:
+ print entry.name
+```
## Type aliases.
A type alias is declared from a single line `type` statement. This creates a new type symbol for an existing data type.
diff --git a/src/ast.zig b/src/ast.zig
index 8923fd469..1e3f83398 100644
--- a/src/ast.zig
+++ b/src/ast.zig
@@ -36,6 +36,7 @@ pub const NodeType = enum(u8) {
enumDecl,
enumMember,
errorSymLit,
+ expandOpt,
exprStmt,
falseLit,
forIterHeader,
@@ -93,6 +94,8 @@ pub const NodeType = enum(u8) {
typeAliasDecl,
typeTemplate,
unary_expr,
+ unwrap,
+ unwrapDef,
varSpec,
whileCondStmt,
whileInfStmt,
@@ -131,6 +134,9 @@ const NodeHead = packed struct {
/// At most 16 bytes in release mode.
const NodeData = union {
uninit: void,
+ expandOpt: struct {
+ param: NodeId,
+ },
exprStmt: struct {
child: NodeId,
isLastRootStmt: bool = false,
@@ -238,6 +244,13 @@ const NodeData = union {
left: NodeId,
right: NodeId,
},
+ unwrap: struct {
+ opt: NodeId,
+ },
+ unwrapDef: struct {
+ opt: NodeId,
+ default: NodeId,
+ },
callExpr: packed struct {
callee: u24,
numArgs: u8,
diff --git a/src/bc_gen.zig b/src/bc_gen.zig
index b5724c5f4..af9f60e93 100644
--- a/src/bc_gen.zig
+++ b/src/bc_gen.zig
@@ -406,6 +406,7 @@ fn genExpr(c: *Chunk, idx: usize, cstr: Cstr) anyerror!GenValue {
.truev => genTrue(c, cstr, nodeId),
.tryExpr => genTryExpr(c, idx, cstr, nodeId),
.typeSym => genTypeSym(c, idx, cstr, nodeId),
+ .unwrapChoice => genUnwrapChoice(c, idx, cstr, nodeId),
.varSym => genVarSym(c, idx, cstr, nodeId),
.blockExpr => genBlockExpr(c, idx, cstr, nodeId),
else => {
@@ -1032,6 +1033,21 @@ fn genSymbol(c: *Chunk, idx: usize, cstr: Cstr, nodeId: cy.NodeId) !GenValue {
return finishNoErrNoDepInst(c, inst, false);
}
+fn genUnwrapChoice(c: *Chunk, loc: usize, cstr: Cstr, nodeId: cy.NodeId) !GenValue {
+ const data = c.ir.getExprData(loc, .unwrapChoice);
+ const retain = c.sema.isRcCandidateType(data.payload_t);
+ const inst = try c.rega.selectForDstInst(cstr, retain, nodeId);
+
+ const choice = try genExpr(c, data.choice, Cstr.simple);
+ try pushUnwindValue(c, choice);
+
+ try c.pushFCode(.unwrapChoice, &.{ choice.reg, data.tag, data.fieldIdx, inst.dst }, nodeId);
+
+ try popTempAndUnwind(c, choice);
+ try releaseTempValue(c, choice, nodeId);
+ return finishDstInst(c, inst, retain);
+}
+
fn genTrue(c: *Chunk, cstr: Cstr, nodeId: cy.NodeId) !GenValue {
const inst = try c.rega.selectForNoErrNoDepInst(cstr, false, nodeId);
if (inst.requiresPreRelease) {
diff --git a/src/builtins/builtins.cy b/src/builtins/builtins.cy
index ecf77f0de..7491640f9 100644
--- a/src/builtins/builtins.cy
+++ b/src/builtins/builtins.cy
@@ -381,4 +381,9 @@ type Fiber:
#host
type metatype:
- #host func id() int
\ No newline at end of file
+ #host func id() int
+
+template(T type)
+type Option enum:
+ case none
+ case some #T
\ No newline at end of file
diff --git a/src/builtins/builtins.zig b/src/builtins/builtins.zig
index a9a651dd9..9ba8289f2 100644
--- a/src/builtins/builtins.zig
+++ b/src/builtins/builtins.zig
@@ -594,6 +594,9 @@ fn genDeclEntry(vm: *cy.VM, ast: cy.ast.AstView, decl: cy.parser.StaticDecl, sta
const header = ast.node(typeDecl.data.objectDecl.header);
name = ast.nodeStringById(header.data.objectHeader.name);
},
+ .enumDecl => {
+ name = ast.nodeStringById(typeDecl.data.enumDecl.name);
+ },
else => {
return error.Unsupported;
},
@@ -743,9 +746,13 @@ fn genDocComment(vm: *cy.VM, ast: cy.ast.AstView, decl: cy.parser.StaticDecl, st
fn parseCyberGenResult(vm: *cy.VM, parser: *const cy.Parser) !Value {
const root = try vm.allocEmptyMap();
+ errdefer vm.release(root);
+
const map = root.asHeapObject().map.map();
const decls = try vm.allocEmptyList();
+ errdefer vm.release(decls);
+
const declsList = decls.asHeapObject().list.getList();
var state = ParseCyberState{
diff --git a/src/bytecode.zig b/src/bytecode.zig
index 738f86b99..411549d9b 100644
--- a/src/bytecode.zig
+++ b/src/bytecode.zig
@@ -995,6 +995,7 @@ pub fn getInstLenAt(pc: [*]const Inst) u8 {
const numEntries = pc[2].val;
return 4 + numEntries * 2;
},
+ .unwrapChoice,
.cast,
.castAbstract,
.pushTry,
@@ -1187,6 +1188,7 @@ pub const OpCode = enum(u8) {
ref = vmc.CodeRef,
refCopyObj = vmc.CodeRefCopyObj,
setRef = vmc.CodeSetRef,
+ unwrapChoice = vmc.CodeUnwrapChoice,
setFieldDyn = vmc.CodeSetFieldDyn,
setFieldDynIC = vmc.CodeSetFieldDynIC,
@@ -1273,7 +1275,7 @@ pub const OpCode = enum(u8) {
};
test "bytecode internals." {
- try t.eq(std.enums.values(OpCode).len, 118);
+ try t.eq(std.enums.values(OpCode).len, 119);
try t.eq(@sizeOf(Inst), 1);
if (cy.is32Bit) {
try t.eq(@sizeOf(DebugMarker), 16);
diff --git a/src/cte.zig b/src/cte.zig
index 6bc3124e2..5e128544d 100644
--- a/src/cte.zig
+++ b/src/cte.zig
@@ -6,21 +6,7 @@ const v = cy.fmt.v;
const cte = @This();
-pub fn callTemplate(c: *cy.Chunk, nodeId: cy.NodeId) !*cy.Sym {
- const node = c.ast.node(nodeId);
- const callee = node.data.callTemplate.callee;
- const numArgs = node.data.callTemplate.numArgs;
- _ = numArgs;
-
- const calleeRes = try c.semaExprSkipSym(node.data.callTemplate.callee);
- if (calleeRes.resType != .sym) {
- return c.reportErrorAt("Expected template symbol.", &.{}, node.data.callTemplate.callee);
- }
- const sym = calleeRes.data.sym;
- if (sym.type != .typeTemplate) {
- return c.reportErrorAt("Expected template symbol.", &.{}, node.data.callTemplate.callee);
- }
-
+pub fn callTemplate2(c: *cy.Chunk, template: *cy.sym.TypeTemplate, argHead: cy.NodeId, nodeId: cy.NodeId) !*cy.Sym {
// Accumulate compile-time args.
const typeStart = c.typeStack.items.len;
const valueStart = c.valueStack.items.len;
@@ -34,7 +20,7 @@ pub fn callTemplate(c: *cy.Chunk, nodeId: cy.NodeId) !*cy.Sym {
}
c.valueStack.items.len = valueStart;
}
- var arg: cy.NodeId = node.data.callTemplate.argHead;
+ var arg: cy.NodeId = argHead;
while (arg != cy.NullNode) {
const res = try nodeToCtValue(c, arg);
try c.typeStack.append(c.alloc, res.type);
@@ -45,20 +31,18 @@ pub fn callTemplate(c: *cy.Chunk, nodeId: cy.NodeId) !*cy.Sym {
const args = c.valueStack.items[valueStart..];
// Check against template signature.
- const typeTemplate = sym.cast(.typeTemplate);
-
- if (!cy.types.isTypeFuncSigCompat(c.compiler, @ptrCast(argTypes), bt.Type, typeTemplate.sigId)) {
- const expSig = try c.sema.allocFuncSigStr(typeTemplate.sigId);
+ if (!cy.types.isTypeFuncSigCompat(c.compiler, @ptrCast(argTypes), bt.Type, template.sigId)) {
+ const expSig = try c.sema.allocFuncSigStr(template.sigId);
defer c.alloc.free(expSig);
return c.reportErrorAt(
\\Expected template signature `{}{}`.
- , &.{v(typeTemplate.head.name()), v(expSig)}, callee);
+ , &.{v(template.head.name()), v(expSig)}, nodeId);
}
// Ensure variant type.
- const res = try typeTemplate.variantCache.getOrPut(c.alloc, args);
+ const res = try template.variantCache.getOrPut(c.alloc, args);
if (!res.found_existing) {
- const patchNodes = try execTemplateCtNodes(c, typeTemplate, args);
+ const patchNodes = try execTemplateCtNodes(c, template, args);
// Dupe args and retain
const params = try c.alloc.dupe(cy.Value, args);
@@ -67,25 +51,38 @@ pub fn callTemplate(c: *cy.Chunk, nodeId: cy.NodeId) !*cy.Sym {
}
// Generate variant type.
- const id = typeTemplate.variants.items.len;
- try typeTemplate.variants.append(c.alloc, .{
+ const id = template.variants.items.len;
+ try template.variants.append(c.alloc, .{
.patchNodes = patchNodes,
.params = params,
.sym = undefined,
});
- const newSym = try sema.declareTemplateVariant(c, typeTemplate, @intCast(id));
- typeTemplate.variants.items[id].sym = newSym;
+ const newSym = try sema.declareTemplateVariant(c, template, @intCast(id));
+ template.variants.items[id].sym = newSym;
res.key_ptr.* = params;
res.value_ptr.* = @intCast(id);
}
const variantId = res.value_ptr.*;
- const variantSym = typeTemplate.variants.items[variantId].sym;
+ const variantSym = template.variants.items[variantId].sym;
return variantSym;
}
+pub fn callTemplate(c: *cy.Chunk, nodeId: cy.NodeId) !*cy.Sym {
+ const node = c.ast.node(nodeId);
+ const calleeRes = try c.semaExprSkipSym(node.data.callTemplate.callee);
+ if (calleeRes.resType != .sym) {
+ return c.reportErrorAt("Expected template symbol.", &.{}, node.data.callTemplate.callee);
+ }
+ const sym = calleeRes.data.sym;
+ if (sym.type != .typeTemplate) {
+ return c.reportErrorAt("Expected template symbol.", &.{}, node.data.callTemplate.callee);
+ }
+ return cte.callTemplate2(c, sym.cast(.typeTemplate), node.data.callTemplate.argHead, nodeId);
+}
+
/// Visit each top level ctNode, perform template param substitution or CTE,
/// and generate new nodes. Return the root of each resulting node.
fn execTemplateCtNodes(c: *cy.Chunk, template: *cy.sym.TypeTemplate, params: []const cy.Value) ![]const cy.NodeId {
diff --git a/src/ir.zig b/src/ir.zig
index 9337b3952..f9b4b3490 100644
--- a/src/ir.zig
+++ b/src/ir.zig
@@ -132,6 +132,14 @@ pub const ExprCode = enum(u8) {
blockExpr,
mainEnd,
elseBlock,
+ unwrapChoice,
+};
+
+pub const UnwrapChoice = struct {
+ choice: Loc,
+ payload_t: cy.TypeId,
+ tag: u8,
+ fieldIdx: u8,
};
pub const Coresume = struct {
@@ -613,6 +621,7 @@ pub fn ExprData(comptime code: ExprCode) type {
.symbol => Symbol,
.blockExpr => BlockExpr,
.coresume => Coresume,
+ .unwrapChoice => UnwrapChoice,
else => void,
};
}
diff --git a/src/module.zig b/src/module.zig
index 1b458c078..adddef30a 100644
--- a/src/module.zig
+++ b/src/module.zig
@@ -298,6 +298,11 @@ pub const ChunkExt = struct {
.kind = if (isChoiceType) .choice else .@"enum",
.data = undefined,
};
+ if (isChoiceType) {
+ c.compiler.sema.types.items[typeId].data = .{ .choice = .{
+ .isOptional = false,
+ }};
+ }
return sym;
}
@@ -417,6 +422,12 @@ pub const ChunkExt = struct {
.kind = if (isChoiceType) .choice else .@"enum",
.data = undefined,
};
+
+ if (isChoiceType) {
+ c.compiler.sema.types.items[typeId].data = .{ .choice = .{
+ .isOptional = parent == c.sema.optionSym,
+ }};
+ }
return sym;
}
diff --git a/src/parser.zig b/src/parser.zig
index 7ff9af4c4..c4ec9d430 100644
--- a/src/parser.zig
+++ b/src/parser.zig
@@ -782,6 +782,16 @@ pub const Parser = struct {
return self.reportError("Unnamed type is not allowed in this context.", &.{});
}
},
+ .question => {
+ const start = self.next_pos;
+ self.advance();
+ const param = try self.parseTermExpr();
+ const id = try self.pushNode(.expandOpt, start);
+ self.ast.setNodeData(id, .{ .expandOpt = .{
+ .param = param,
+ }});
+ return id;
+ },
.pound,
.type_k,
.none_k,
@@ -2400,9 +2410,7 @@ pub const Parser = struct {
}
fn parseCondExpr(self: *Parser, cond: NodeId, start: u32) !NodeId {
- // Assume `?`.
- self.advance();
-
+ // Assume `?` is consumed.
const res = try self.pushNode(.condExpr, start);
const body = (try self.parseExpr(.{})) orelse {
@@ -2833,6 +2841,14 @@ pub const Parser = struct {
}});
left_id = expr_id;
},
+ .dot_question => {
+ self.advance();
+ const expr = try self.pushNode(.unwrap, start);
+ self.ast.setNodeData(expr, .{ .unwrap = .{
+ .opt = left_id,
+ }});
+ left_id = expr;
+ },
.pound => {
self.advance();
const call = try self.pushNode(.callTemplate, start);
@@ -3123,7 +3139,21 @@ pub const Parser = struct {
left_id = try self.parseBinExpr(left_id, .or_op);
},
.question => {
- left_id = try self.parseCondExpr(left_id, start);
+ self.advance();
+ if (self.peek().tag() == .else_k) {
+ self.advance();
+ const default = (try self.parseExpr(.{})) orelse {
+ return self.reportError("Expected default expression.", &.{});
+ };
+ const expr = try self.pushNode(.unwrapDef, start);
+ self.ast.setNodeData(expr, .{ .unwrapDef = .{
+ .opt = left_id,
+ .default = default,
+ }});
+ left_id = expr;
+ } else {
+ left_id = try self.parseCondExpr(left_id, start);
+ }
},
.right_bracket,
.right_paren,
diff --git a/src/sema.zig b/src/sema.zig
index 0547a514f..19b2f36c2 100644
--- a/src/sema.zig
+++ b/src/sema.zig
@@ -1901,6 +1901,10 @@ fn resolveTypeExpr(c: *cy.Chunk, exprId: cy.NodeId) !TypeExprResult {
const sym = try cte.callTemplate(c, exprId);
return TypeExprResult{ .sym = sym, .type = sym.getStaticType() };
},
+ .expandOpt => {
+ const sym = try cte.callTemplate2(c, c.sema.optionSym, expr.data.expandOpt.param, exprId);
+ return TypeExprResult{ .sym = sym, .type = sym.getStaticType() };
+ },
else => {
return c.reportErrorAt("Unsupported type expr: `{}`", &.{v(expr.type())}, exprId);
}
@@ -2792,10 +2796,14 @@ fn reportIncompatibleFuncSig(c: *cy.Chunk, name: []const u8, funcSigId: FuncSigI
}
fn checkTypeCstr(c: *cy.Chunk, ctype: CompactType, cstrTypeId: TypeId, nodeId: cy.NodeId) !void {
+ return checkTypeCstr2(c, ctype, cstrTypeId, cstrTypeId, nodeId);
+}
+
+fn checkTypeCstr2(c: *cy.Chunk, ctype: CompactType, cstrTypeId: TypeId, reportCstrTypeId: TypeId, nodeId: cy.NodeId) !void {
// Dynamic is allowed.
if (!ctype.dynamic) {
if (!types.isTypeSymCompat(c.compiler, ctype.id, cstrTypeId)) {
- const cstrName = try c.sema.allocTypeName(cstrTypeId);
+ const cstrName = try c.sema.allocTypeName(reportCstrTypeId);
defer c.alloc.free(cstrName);
const typeName = try c.sema.allocTypeName(ctype.id);
defer c.alloc.free(typeName);
@@ -3159,7 +3167,7 @@ pub const ChunkExt = struct {
pub fn semaZeroInit(c: *cy.Chunk, typeId: cy.TypeId, nodeId: cy.NodeId) !ExprResult {
switch (typeId) {
bt.Any,
- bt.Dynamic => return c.semaNone(nodeId),
+ bt.Dynamic => return c.semaNone(null, nodeId),
bt.Boolean => return c.semaFalse(nodeId),
bt.Integer => return c.semaInt(0, nodeId),
bt.Float => return c.semaFloat(0, nodeId),
@@ -3501,7 +3509,28 @@ pub const ChunkExt = struct {
pub fn semaExpr2(c: *cy.Chunk, expr: Expr) !ExprResult {
const res = try c.semaExprNoCheck(expr);
if (expr.hasTypeCstr) {
- try checkTypeCstr(c, res.type, expr.preferType, expr.nodeId);
+ const type_e = c.sema.types.items[expr.preferType];
+ if (type_e.kind == .choice and type_e.data.choice.isOptional) {
+ // Already the same optional type.
+ if (res.type.id == expr.preferType) {
+ return res;
+ }
+ // Check if type is compatible with Optional's some payload.
+ const someMember = type_e.sym.cast(.enum_t).getMemberByIdx(1);
+ try checkTypeCstr2(c, res.type, someMember.payloadType, expr.preferType, expr.nodeId);
+
+ // Generate IR to wrap value into optional.
+ var b: ObjectBuilder = .{ .c = c };
+ try b.begin(expr.preferType, 2, expr.nodeId);
+ const tag = try c.semaInt(1, expr.nodeId);
+ b.pushArg(tag);
+ b.pushArg(res);
+ const irIdx = b.end();
+
+ return ExprResult.initStatic(irIdx, expr.preferType);
+ } else {
+ try checkTypeCstr(c, res.type, expr.preferType, expr.nodeId);
+ }
}
return res;
}
@@ -3518,7 +3547,13 @@ pub const ChunkExt = struct {
const node = c.ast.node(nodeId);
c.curNodeId = nodeId;
switch (node.type()) {
- .noneLit => return c.semaNone(nodeId),
+ .noneLit => {
+ if (expr.hasTypeCstr) {
+ return c.semaNone(expr.preferType, nodeId);
+ } else {
+ return c.semaNone(null, nodeId);
+ }
+ },
.errorSymLit => {
const sym = c.ast.node(node.data.errorSymLit.symbol);
const name = c.ast.nodeString(sym);
@@ -3627,7 +3662,7 @@ pub const ChunkExt = struct {
if (node.data.condExpr.elseExpr != cy.NullNode) {
elseBody = try c.semaExpr(node.data.condExpr.elseExpr, .{});
} else {
- elseBody = try c.semaNone(nodeId);
+ elseBody = try c.semaNone(bt.Any, nodeId);
}
c.ir.setExprData(irIdx, .condExpr, .{
.condLoc = cond.irIdx,
@@ -3671,9 +3706,32 @@ pub const ChunkExt = struct {
const ctype = CompactType.initStatic(sym.getStaticType().?);
return ExprResult.initCustom(cy.NullId, .sym, ctype, .{ .sym = sym });
},
+ .expandOpt => {
+ const sym = try cte.callTemplate2(c, c.sema.optionSym, node.data.expandOpt.param, nodeId);
+ const ctype = CompactType.initStatic(sym.getStaticType().?);
+ return ExprResult.initCustom(cy.NullId, .sym, ctype, .{ .sym = sym });
+ },
.accessExpr => {
return try c.semaAccessExpr(expr, true);
},
+ .unwrap => {
+ const opt = try c.semaExpr(node.data.unwrap.opt, .{});
+ const type_e = c.sema.types.items[opt.type.id];
+ if (type_e.kind != .choice or !type_e.data.choice.isOptional) {
+ const name = try c.sema.allocTypeName(opt.type.id);
+ defer c.alloc.free(name);
+ return c.reportErrorAt("Unwrap operator expects an optional type, found: `{}`", &.{v(name)}, nodeId);
+ }
+
+ const someMember = type_e.sym.cast(.enum_t).getMemberByIdx(1);
+ const loc = try c.ir.pushExpr(c.alloc, .unwrapChoice, nodeId, .{
+ .choice = opt.irIdx,
+ .tag = 1,
+ .payload_t = someMember.payloadType,
+ .fieldIdx = 1,
+ });
+ return ExprResult.initStatic(loc, someMember.payloadType);
+ },
.indexExpr => {
const preIdx = try c.ir.pushEmptyExpr(c.alloc, .pre, nodeId);
@@ -3715,13 +3773,13 @@ pub const ChunkExt = struct {
if (range.data.range.start != cy.NullNode) {
left = try c.semaExprPrefer(range.data.range.start, preferT);
} else {
- left = try c.semaNone(nodeId);
+ left = try c.semaNone(null, nodeId);
}
var right: ExprResult = undefined;
if (range.data.range.end != cy.NullNode) {
right = try c.semaExprPrefer(range.data.range.end, preferT);
} else {
- right = try c.semaNone(nodeId);
+ right = try c.semaNone(null, nodeId);
}
if (recv.type.id == bt.List) {
@@ -4121,7 +4179,22 @@ pub const ChunkExt = struct {
return ExprResult.initStatic(irIdx, bt.Boolean);
}
- pub fn semaNone(c: *cy.Chunk, nodeId: cy.NodeId) !ExprResult {
+ pub fn semaNone(c: *cy.Chunk, preferTypeOpt: ?cy.TypeId, nodeId: cy.NodeId) !ExprResult {
+ if (preferTypeOpt) |preferType| {
+ const type_e = c.sema.types.items[preferType];
+ if (type_e.kind == .choice and type_e.data.choice.isOptional) {
+ // Generate IR to wrap value into optional.
+ var b: ObjectBuilder = .{ .c = c };
+ try b.begin(preferType, 2, nodeId);
+ const tag = try c.semaInt(0, nodeId);
+ b.pushArg(tag);
+ const payload = try c.semaInt(0, nodeId);
+ b.pushArg(payload);
+ const loc = b.end();
+
+ return ExprResult.initStatic(loc, preferType);
+ }
+ }
const irIdx = try c.ir.pushExpr(c.alloc, .none, nodeId, {});
return ExprResult.initStatic(irIdx, bt.None);
}
@@ -4753,10 +4826,13 @@ pub const Sema = struct {
funcSigs: std.ArrayListUnmanaged(FuncSig),
funcSigMap: std.HashMapUnmanaged(FuncSigKey, FuncSigId, FuncSigKeyContext, 80),
+ optionSym: *cy.sym.TypeTemplate,
+
pub fn init(alloc: std.mem.Allocator, compiler: *cy.VMcompiler) Sema {
return .{
.alloc = alloc,
.compiler = compiler,
+ .optionSym = undefined,
.funcSigs = .{},
.funcSigMap = .{},
.types = .{},
diff --git a/src/sym.zig b/src/sym.zig
index 847acd65d..31d032bbd 100644
--- a/src/sym.zig
+++ b/src/sym.zig
@@ -552,6 +552,12 @@ pub const EnumType = extern struct {
return @ptrCast(&self.mod);
}
+ pub fn getMemberByIdx(self: *EnumType, idx: u32) *EnumMember {
+ const mod = self.head.getMod().?;
+ const symId = self.members[idx];
+ return mod.syms.items[symId].cast(.enumMember);
+ }
+
pub fn getMember(self: *EnumType, name: []const u8) ?*EnumMember {
const mod = self.head.getMod().?;
if (mod.getSym(name)) |res| {
@@ -644,7 +650,7 @@ test "sym internals" {
if (builtin.mode == .ReleaseFast) {
if (cy.is32Bit) {
try t.eq(@sizeOf(Sym), 16);
- try t.eq(@sizeOf(Func), 28);
+ try t.eq(@sizeOf(Func), 32);
} else {
try t.eq(@sizeOf(Sym), 24);
try t.eq(@sizeOf(Func), 40);
diff --git a/src/tokenizer.zig b/src/tokenizer.zig
index 4f9e7c8fa..85af1734e 100644
--- a/src/tokenizer.zig
+++ b/src/tokenizer.zig
@@ -59,6 +59,7 @@ pub const TokenType = enum(u8) {
coyield_k,
dec,
dot,
+ dot_question,
dot_dot,
else_k,
enum_k,
@@ -314,6 +315,9 @@ pub const Tokenizer = struct {
if (peek(t) == '.') {
advance(t);
try t.pushToken(.dot_dot, start);
+ } else if (peek(t) == '?') {
+ advance(t);
+ try t.pushToken(.dot_question, start);
} else {
try t.pushToken(.dot, start);
}
@@ -1012,6 +1016,6 @@ test "tokenizer internals." {
try tt.eq(@alignOf(Token), 4);
try tt.eq(@sizeOf(TokenizeState), 4);
- try tt.eq(std.enums.values(TokenType).len, 67);
+ try tt.eq(std.enums.values(TokenType).len, 68);
try tt.eq(keywords.kvs.len, 33);
}
\ No newline at end of file
diff --git a/src/types.zig b/src/types.zig
index 2b1d21a13..a472ab729 100644
--- a/src/types.zig
+++ b/src/types.zig
@@ -41,6 +41,9 @@ pub const Type = extern struct {
@"struct": extern struct {
numFields: u16,
},
+ choice: extern struct {
+ isOptional: bool,
+ },
},
};
@@ -157,13 +160,21 @@ pub const SemaExt = struct {
}
pub fn allocTypeName(s: *cy.Sema, id: TypeId) ![]const u8 {
- const typ = s.types.items[id];
- switch (typ.kind) {
- // .value => {
- // return try std.fmt.allocPrint(s.alloc, "+{s}", .{typ.sym.name()});
- // },
+ const type_e = s.types.items[id];
+ switch (type_e.kind) {
+ .choice => {
+ if (type_e.data.choice.isOptional) {
+ const template = type_e.sym.parent.?.cast(.typeTemplate);
+ const variant = template.variants.items[type_e.sym.cast(.enum_t).variantId];
+ const param = variant.params[0].asHeapObject();
+ const name = s.getTypeBaseName(param.type.type);
+ return try std.fmt.allocPrint(s.alloc, "?{s}", .{name});
+ } else {
+ return try s.alloc.dupe(u8, type_e.sym.name());
+ }
+ },
else => {
- return try s.alloc.dupe(u8, typ.sym.name());
+ return try s.alloc.dupe(u8, type_e.sym.name());
}
}
}
diff --git a/src/vm.c b/src/vm.c
index e54243002..a5de1689a 100644
--- a/src/vm.c
+++ b/src/vm.c
@@ -502,6 +502,12 @@ static inline void panicFieldMissing(VM* vm) {
panicStaticMsg(vm, "Field not found in value.");
}
+static inline void panicUnexpectedChoice(VM* vm, i48 tag, i48 exp) {
+ zPanicFmt(vm, "Expected active choice tag `{}`, found `{}`.", (FmtValue[]){
+ FMT_U32((u32)exp), FMT_U32((u32)tag),
+ }, 2);
+}
+
static void panicIncompatibleType(VM* vm, TypeId actType, TypeId expType) {
Str actTypeName = zGetTypeName(vm, actType);
Str expTypeName = zGetTypeName(vm, expType);
@@ -722,6 +728,7 @@ ResultCode execBytecode(VM* vm) {
JENTRY(Ref),
JENTRY(RefCopyObj),
JENTRY(SetRef),
+ JENTRY(UnwrapChoice),
JENTRY(SetFieldDyn),
JENTRY(SetFieldDynIC),
JENTRY(SetField),
@@ -1619,6 +1626,20 @@ ResultCode execBytecode(VM* vm) {
pc += 3;
NEXT();
}
+ CASE(UnwrapChoice): {
+ HeapObject* obj = VALUE_AS_HEAPOBJECT(stack[pc[1]]);
+ Value* fields = objectGetValuesPtr(&obj->object);
+ u8 tag = pc[2];
+ if (VALUE_AS_INTEGER(fields[0]) == (_BitInt(48))pc[2]) {
+ retain(vm, fields[pc[3]]);
+ stack[pc[4]] = fields[pc[3]];
+ pc += 5;
+ NEXT();
+ } else {
+ panicUnexpectedChoice(vm, VALUE_AS_INTEGER(fields[0]), (_BitInt(48))pc[2]);
+ RETURN(RES_CODE_PANIC);
+ }
+ }
CASE(SetFieldDyn): {
Value recv = stack[pc[1]];
if (VALUE_IS_POINTER(recv)) {
diff --git a/src/vm.h b/src/vm.h
index 7ed5c1563..eab21e82e 100644
--- a/src/vm.h
+++ b/src/vm.h
@@ -10,6 +10,7 @@ typedef uint32_t u32;
typedef int32_t i32;
typedef uint64_t u64;
typedef int64_t i64;
+typedef _BitInt(48) i48;
#define BITCAST(type, x) (((union {typeof(x) src; type dst;})(x)).dst)
#define LIKELY(x) __builtin_expect(!!(x), 1)
@@ -299,6 +300,7 @@ typedef enum {
CodeRef,
CodeRefCopyObj,
CodeSetRef,
+ CodeUnwrapChoice,
/// Set field with runtime type check.
CodeSetFieldDyn,
diff --git a/src/vm_compiler.zig b/src/vm_compiler.zig
index 1cc8807a4..44c203b59 100644
--- a/src/vm_compiler.zig
+++ b/src/vm_compiler.zig
@@ -652,6 +652,10 @@ fn declareImportsAndTypes(self: *VMcompiler, mainChunk: *cy.Chunk) !void {
break;
}
}
+
+ // Extract special syms. Assumes chunks[1] is the builtins chunk.
+ const builtins = self.chunks.items[1].sym.getMod();
+ self.sema.optionSym = builtins.getSym("Option").?.cast(.typeTemplate);
}
fn loadPredefinedTypes(self: *VMcompiler, parent: *cy.Sym) !void {
diff --git a/test/behavior_test.zig b/test/behavior_test.zig
index 3baade95d..30d5dbb37 100644
--- a/test/behavior_test.zig
+++ b/test/behavior_test.zig
@@ -193,7 +193,9 @@ if (!aot) {
run.case("types/object_zero_init.cy");
run.case("types/object_zero_init_error.cy");
run.case("types/objects.cy");
- // run.case("types/optionals.cy");
+ run.case("types/optionals_incompat_value_error.cy");
+ run.case("types/optionals_unwrap_panic.cy");
+ run.case("types/optionals.cy");
run.case("types/struct_zero_init_error.cy");
run.case("types/structs.cy");
run.case("types/template_choices.cy");
@@ -263,7 +265,6 @@ if (!aot) {
run.case("builtins/maps.cy");
run.case("builtins/must.cy");
run.case("builtins/must_panic.cy");
- run.case("builtins/optionals.cy");
run.case("builtins/op_precedence.cy");
run.case("builtins/panic_panic.cy");
run.case("builtins/raw_string_single_quote_error.cy");
diff --git a/test/builtins/optionals.cy b/test/builtins/optionals.cy
deleted file mode 100644
index 013520ef3..000000000
--- a/test/builtins/optionals.cy
+++ /dev/null
@@ -1,6 +0,0 @@
-import test
-
-var foo = none
-test.eq(foo, none)
-
---cytest: pass
\ No newline at end of file
diff --git a/test/types/optionals.cy b/test/types/optionals.cy
new file mode 100644
index 000000000..1d2351479
--- /dev/null
+++ b/test/types/optionals.cy
@@ -0,0 +1,32 @@
+import test
+
+-- Explicit type.
+var a Option#int = 123
+-- test.eq(a == none, false)
+-- test.eq(a != none, true)
+test.eq(a.?, 123)
+
+-- Shorthand.
+var b ?int = 123
+test.eq(b.?, 123)
+
+-- Wrap object.
+type Foo:
+ var a int
+var c ?Foo = [Foo a: 123]
+test.eq(c.?.a, 123)
+
+-- Wrapping none implicitly.
+b = none
+-- test.eq(b == none, true)
+-- test.eq(b != none, false)
+
+-- -- Unwrap or default.
+-- b = none
+-- test.eq(b ?else 123, 123)
+
+-- Dynamic none.
+my foo = none
+test.eq(foo, none)
+
+--cytest: pass
\ No newline at end of file
diff --git a/test/types/optionals_incompat_value_error.cy b/test/types/optionals_incompat_value_error.cy
new file mode 100644
index 000000000..f779ceac9
--- /dev/null
+++ b/test/types/optionals_incompat_value_error.cy
@@ -0,0 +1,9 @@
+var a ?int = 'abc'
+
+--cytest: error
+--CompileError: Expected type `?int`, got `String`.
+--
+--main:1:15:
+--var a ?int = 'abc'
+-- ^
+--
\ No newline at end of file
diff --git a/test/types/optionals_unwrap_panic.cy b/test/types/optionals_unwrap_panic.cy
new file mode 100644
index 000000000..4c817bd97
--- /dev/null
+++ b/test/types/optionals_unwrap_panic.cy
@@ -0,0 +1,10 @@
+var a ?int = none
+var b = a.?
+
+--cytest: error
+--panic: Expected active choice tag `1`, found `0`.
+--
+--main:2:9 main:
+--var b = a.?
+-- ^
+--
\ No newline at end of file
|