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