diff --git a/CHANGELOG.md b/CHANGELOG.md index 5588dc20..2f15018f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - `rg.contains` - `map.hasKey` - `toml` std lib +- `match` statement/expression for value matching with type-specific condition semantics (https://github.com/buzz-language/buzz/issues/80) ## Changed @@ -20,7 +21,7 @@ - `int` are now `i48` instead of `i32` (https://github.com/buzz-language/buzz/issues/306). If you're wondering why, it's because all buzz values live in a NaN boxed f64 and the maximum bits available for an integer in there is 48. However, C ABI does not understand `i48` so we're still stuck with `i32` in FFI for now. - `main` signature can omit `args` argument - Maximum number of enum cases is now 16 777 215 instead of 255 -- `pattern.match` returns now a list of `obj{ start: int, end: int, capture: str }` and `matchAll` a list of those lists +- `pattern.matchAgainst` returns now a list of `obj{ start: int, end: int, capture: str }` and `matchAllAgainst` a list of those lists - Selective import erases the imported namespace: `import print from "std"; ... print("hello world");` - Common part of imported namespace gets erased: il imported file as namesapce `a\b\c` and importing script has namespace `a\b`, only `c\` remains - Enum name can be omitted if it can be inferred (`final list: [Locale] = [ .fr, .it, .en ]`) (https://github.com/buzz-language/buzz/issues/360) diff --git a/src/Ast.zig b/src/Ast.zig index f2a895cd..3868acb3 100644 --- a/src/Ast.zig +++ b/src/Ast.zig @@ -85,6 +85,14 @@ pub const Slice = struct { }, .Block => try node_queue.appendSlice(allocator, comp.Block), .BlockExpression => try node_queue.appendSlice(allocator, comp.BlockExpression), + .Match => { + try node_queue.append(allocator, comp.Match.value); + + for (comp.Match.branches) |branch| { + try node_queue.appendSlice(allocator, branch.conditions); + try node_queue.append(allocator, branch.expression); + } + }, .Call => { // Avoid loop between Call and Dot nodes if (tags[comp.Call.callee] != .Dot or @@ -424,6 +432,31 @@ pub const Slice = struct { return true; } }, + .Match => { + const components = ast.nodes.items(.components)[node].Match; + const type_defs = ast.nodes.items(.type_def); + const value_type_def = type_defs[components.value].?; + + if (components.is_statement) { + self.result = false; + return true; + } + + for (components.branches) |branch| { + for (branch.conditions) |condition| { + const condition_type_def = type_defs[condition].?; + + if ((!condition_type_def.optional and condition_type_def.def_type == .Pattern and + value_type_def.def_type == .String and !value_type_def.optional) or + (!condition_type_def.optional and condition_type_def.def_type == .String and + value_type_def.def_type == .Pattern and !value_type_def.optional)) + { + self.result = false; + return true; + } + } + } + }, .List => { const components = ast.nodes.items(.components)[node].List; const node_types = ast.nodes.items(.tag); @@ -801,6 +834,43 @@ pub const Slice = struct { } } + fn matchConditionValue( + self: Self.Slice, + gc: *GC, + value_type_def: *obj.ObjTypeDef, + match_value: Value, + condition: Node.Index, + ) !bool { + const condition_type_def = self.nodes.items(.type_def)[condition].?; + const condition_value = try self.toValue(condition, gc); + + if (!condition_type_def.optional and condition_type_def.def_type == .Range and + (value_type_def.def_type == .Integer or value_type_def.def_type == .Double) and + !value_type_def.optional) + { + const range = obj.ObjRange.cast(condition_value.obj()).?; + const number = if (match_value.isInteger()) + @as(v.Double, @floatFromInt(match_value.integer())) + else + match_value.double(); + const low: v.Double = @floatFromInt(range.low); + const high: v.Double = @floatFromInt(range.high); + + return (high >= low and number >= low and number < high) or + (low >= high and number >= high and number < low); + } else if (!condition_type_def.optional and condition_type_def.def_type == .Type and + (value_type_def.optional or value_type_def.def_type != .Type)) + { + return condition_value.is(match_value); + } else if (!value_type_def.optional and value_type_def.def_type == .Type and + (condition_type_def.optional or condition_type_def.def_type != .Type)) + { + return match_value.is(condition_value); + } else { + return match_value.eql(condition_value); + } + } + pub fn typeCheckAndToValue( self: Self.Slice, node: Node.Index, @@ -913,6 +983,29 @@ pub const Slice = struct { else try self.toValue(if_components.else_branch.?, gc); }, + .Match => match: { + const match_components = components[node].Match; + const value_type_def = self.nodes.items(.type_def)[match_components.value].?; + const match_value = try self.toValue(match_components.value, gc); + + for (match_components.branches) |branch| { + for (branch.conditions) |condition| { + if (try self.matchConditionValue( + gc, + value_type_def, + match_value, + condition, + )) { + break :match try self.toValue(branch.expression, gc); + } + } + } + + break :match if (match_components.else_branch) |else_branch| + try self.toValue(else_branch, gc) + else + Value.Void; + }, .Range => range: { const rg_components = components[node].Range; @@ -1180,6 +1273,7 @@ pub const Node = struct { ListType, Map, MapType, + Match, Namespace, NamedVariable, Null, @@ -1257,6 +1351,7 @@ pub const Node = struct { ListType: Node.Index, Map: Map, MapType: MapType, + Match: Match, Namespace: []const TokenIndex, NamedVariable: NamedVariable, Null: void, @@ -1450,7 +1545,7 @@ pub const AnonymousEnumCase = struct { pub const Binary = struct { left: Node.Index, right: Node.Index, - operator: Token.Type, + operator: Token.Tag, }; pub const BreakContinue = struct { @@ -1621,6 +1716,18 @@ pub const If = struct { is_statement: bool, }; +pub const Match = struct { + is_statement: bool, + value: Node.Index, + branches: []const Branch, + else_branch: ?Node.Index, + + pub const Branch = struct { + conditions: []const Node.Index, + expression: Node.Index, + }; +}; + pub const Import = struct { imported_symbols: []const TokenIndex, prefix: ?[]const TokenIndex, @@ -1753,7 +1860,7 @@ pub const Try = struct { }; pub const Unary = struct { - operator: Token.Type, + operator: Token.Tag, expression: Node.Index, }; diff --git a/src/Codegen.zig b/src/Codegen.zig index b412f620..d147027d 100644 --- a/src/Codegen.zig +++ b/src/Codegen.zig @@ -116,6 +116,7 @@ const generators = [@typeInfo(Ast.Node.Tag).@"enum".fields.len]?NodeGen{ null, // ListType, generateMap, // Map, null, // MapType, + generateMatch, null, // Namespace, generateNamedVariable, // NamedVariable, generateNull, // Null, @@ -1560,7 +1561,7 @@ fn generateFor(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj. self.reporter.reportPlaceholder(self.ast, condition_type_def.resolved_type.?.Placeholder); } - if (condition_type_def.def_type != .Bool) { + if (condition_type_def.def_type != .Boolean) { self.reporter.reportErrorAt( .for_condition_type, self.ast.tokens.get(locations[components.condition]), @@ -2098,6 +2099,284 @@ fn generateIf(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.O return null; } +fn matchSimpleEquality(self: *Self, condition: Ast.Node.Index, breaks: ?*Breaks) Error!void { + const location = self.ast.nodes.items(.location)[condition]; + + try self.OP_COPY(location); + _ = try self.generateNode(condition, breaks); + try self.OP_EQUAL(location); + try self.OP_NOT(location); +} + +fn matchNumberInRange(self: *Self, condition: Ast.Node.Index, breaks: ?*Breaks) Error!void { + std.debug.assert(self.ast.nodes.items(.tag)[condition] == .Range); + const location = self.ast.nodes.items(.location)[condition]; + + try self.OP_COPY(location); + try self.OP_COPY(location); + _ = try self.generateNode(condition, breaks); // The range + try self.OP_SWAP(location, 0, 1); + + // Invoke rg.contains + try self.emitCodeArg( + location, + .OP_RANGE_INVOKE, + @intCast(obj.ObjRange.members_name.get("contains").?), + ); + try self.emitTwo( + location, + 2, // receiver and arg + 0, // no catch value + ); + + try self.OP_NOT(location); +} + +fn matchStringMatchesPattern(self: *Self, condition: Ast.Node.Index, breaks: ?*Breaks) Error!void { + std.debug.assert(self.ast.nodes.items(.tag)[condition] == .Pattern); + const location = self.ast.nodes.items(.location)[condition]; + + try self.OP_COPY(location); // The string + try self.OP_COPY(location); // Preserved call slot + _ = try self.generateNode(condition, breaks); // The pattern + try self.OP_SWAP(location, 0, 1); + + // Invoke pat.matchAgainst + try self.emitCodeArg( + location, + .OP_PATTERN_INVOKE, + @intCast(obj.ObjPattern.members_name.get("matchAgainst").?), + ); + try self.emitTwo( + location, + 2, // receiver and arg + 0, // no catch value + ); + + // Result must be != null + try self.OP_NULL(location); + try self.OP_EQUAL(location); +} + +fn matchPatternMatchesString(self: *Self, condition: Ast.Node.Index, breaks: ?*Breaks) Error!void { + std.debug.assert(self.ast.nodes.items(.tag)[condition] == .String); + const location = self.ast.nodes.items(.location)[condition]; + + try self.OP_COPY(location); // The pattern + try self.OP_COPY(location); // Preserved call slot + _ = try self.generateNode(condition, breaks); // The string + + // Invoke pat.matchAgainst + try self.emitCodeArg( + location, + .OP_PATTERN_INVOKE, + @intCast(obj.ObjPattern.members_name.get("matchAgainst").?), + ); + try self.emitTwo( + location, + 2, // receiver and arg + 0, // no catch value + ); + + // Result must be != null + try self.OP_NULL(location); + try self.OP_EQUAL(location); +} + +fn matchValueIsType(self: *Self, condition: Ast.Node.Index, breaks: ?*Breaks) Error!void { + const location = self.ast.nodes.items(.location)[condition]; + + try self.OP_COPY(location); // The value + _ = try self.generateNode(condition, breaks); // The type + + try self.OP_IS(location); + try self.OP_NOT(location); +} + +fn matchTypeIsValue(self: *Self, condition: Ast.Node.Index, breaks: ?*Breaks) Error!void { + const location = self.ast.nodes.items(.location)[condition]; + + try self.OP_COPY(location); // The type + _ = try self.generateNode(condition, breaks); // The condition + try self.OP_SWAP(location, 0, 1); + + try self.OP_IS(location); + try self.OP_NOT(location); +} + +fn generateMatch(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { + const type_defs = self.ast.nodes.items(.type_def); + const locations = self.ast.nodes.items(.location); + const lexemes = self.ast.tokens.items(.lexeme); + const components = self.ast.nodes.items(.components)[node].Match; + const value_type_def = type_defs[components.value].?; + const tags = self.ast.nodes.items(.tag); + + _ = try self.generateNode(components.value, breaks); + + var out_jumps = try std.ArrayList(usize).initCapacity(self.gc.allocator, components.branches.len); + defer out_jumps.deinit(self.gc.allocator); + + // Keep tracks of covered enum cases + var enum_cases = if (components.else_branch == null and + !value_type_def.optional and + value_type_def.def_type == .EnumInstance) + cases: { + var cases = std.StringArrayHashMapUnmanaged(usize).empty; + for (value_type_def.resolved_type.?.EnumInstance.of.resolved_type.?.Enum.cases, 0..) |case, idx| { + try cases.put(self.gc.allocator, case, idx); + } + break :cases cases; + } else null; + defer if (enum_cases) |*cases| + cases.deinit(self.gc.allocator); + + // Keep track of boolean cases + var bool_exhaustive = if (!value_type_def.optional and value_type_def.def_type == .Boolean) + std.AutoArrayHashMapUnmanaged(bool, void).empty + else + null; + defer if (bool_exhaustive) |*boolean| + boolean.deinit(self.gc.allocator); + + for (components.branches, 0..) |branch, bidx| { + var jumps = try std.ArrayList(usize).initCapacity(self.gc.allocator, branch.conditions.len); + defer jumps.deinit(self.gc.allocator); + var branch_out_jump: usize = undefined; + + for (branch.conditions, 0..) |condition, idx| { + const condition_location = locations[condition]; + const condition_type_def = type_defs[condition].?; + + // Depending on the value and condition type we might wanna do more than simple `==` + if (!condition_type_def.optional and condition_type_def.def_type == .Range and + (value_type_def.def_type == .Integer or value_type_def.def_type == .Double) and + !value_type_def.optional) + { + try self.matchNumberInRange(condition, breaks); + } else if (!condition_type_def.optional and condition_type_def.def_type == .Pattern and + value_type_def.def_type == .String and !value_type_def.optional) + { + try self.matchStringMatchesPattern(condition, breaks); + } else if (!condition_type_def.optional and condition_type_def.def_type == .String and + value_type_def.def_type == .Pattern and !value_type_def.optional) + { + try self.matchPatternMatchesString(condition, breaks); + } else if (!condition_type_def.optional and condition_type_def.def_type == .Type and + (value_type_def.optional or value_type_def.def_type != .Type)) + { + try self.matchValueIsType(condition, breaks); + } else if (!value_type_def.optional and value_type_def.def_type == .Type and + (condition_type_def.optional or condition_type_def.def_type != .Type)) + { + try self.matchTypeIsValue(condition, breaks); + } else { + try self.matchSimpleEquality(condition, breaks); + } + + // Exhaustiveness + if (enum_cases) |*cases| { + const condition_components = self.ast.nodes.items(.components)[condition]; + + if (tags[condition] == .Dot and condition_components.Dot.member_kind == .EnumCase) { + var it = cases.iterator(); + while (it.next()) |entry| { + if (entry.value_ptr.* == condition_components.Dot.value_or_call_or_enum.EnumCase) { + _ = cases.orderedRemove(entry.key_ptr.*); + break; + } + } + } else if (tags[condition] == .AnonymousEnumCase) { + _ = cases.orderedRemove(lexemes[condition_components.AnonymousEnumCase.case_name]); + } + } else if (bool_exhaustive) |*boolean| { + if (tags[condition] == .Boolean) { + try boolean.put( + self.gc.allocator, + self.ast.nodes.items(.components)[condition].Boolean, + {}, + ); + } + } + + // Jump to branch expression if equal + try jumps.append(self.gc.allocator, try self.OP_JUMP_IF_FALSE(condition_location)); + + try self.OP_POP(condition_location); // Pop comparison + + // Last condition of the branch and still no match, jump over resolution of the branch + if (idx == branch.conditions.len - 1) { + branch_out_jump = try self.OP_JUMP(condition_location); + } + } + + for (jumps.items) |jump| { + self.patchJump(jump); + } + try self.OP_POP(branch.expression); // Pop comparison + try self.OP_POP(branch.expression); // Pop value + _ = try self.generateNode(branch.expression, breaks); + + // Jump out of the match statement/expression + try out_jumps.append( + self.gc.allocator, + try self.OP_JUMP(locations[branch.expression]), + ); + + // Patch of jump when no condition matched + self.patchJump(branch_out_jump); + + // If last branch, no else branch, and still no match, pop the value + if (bidx == components.branches.len - 1 and components.else_branch == null) { + try self.OP_POP(locations[components.value]); + } + } + + if (components.else_branch) |eb| { + try self.OP_POP(locations[components.value]); + _ = try self.generateNode(eb, breaks); + } else if (enum_cases != null and enum_cases.?.count() > 0) { + const keys = enum_cases.?.keys(); + + var missing_cases = std.ArrayList(u8).empty; + defer missing_cases.deinit(self.gc.allocator); + + for (keys, 0..) |key, idx| { + try missing_cases.appendSlice(self.gc.allocator, key); + + if (idx < keys.len - 1) { + try missing_cases.appendSlice(self.gc.allocator, ", "); + } + } + + self.reporter.reportErrorFmt( + .unexhaustive_match, + self.ast.tokens.get(locations[node]), + self.ast.tokens.get(self.ast.nodes.items(.end_location)[node]), + "Non-exhaustive `match` over enum value, missing enum cases are: {s}", + .{ + missing_cases.items, + }, + ); + } else if (bool_exhaustive == null or bool_exhaustive.?.count() < 2) { + self.reporter.reportErrorAt( + .unexhaustive_match, + self.ast.tokens.get(locations[node]), + self.ast.tokens.get(self.ast.nodes.items(.end_location)[node]), + "Non-exhaustive `match`, `else` branch required", + ); + } + + for (out_jumps.items) |out_jump| { + self.patchJump(out_jump); + } + + try self.patchOptJumps(node); + try self.endScope(node); + + return null; +} + fn generateImport(self: *Self, node: Ast.Node.Index, breaks: ?*Breaks) Error!?*obj.ObjFunction { const components = self.ast.nodes.items(.components)[node].Import; const location = self.ast.nodes.items(.location)[node]; diff --git a/src/FFI.zig b/src/FFI.zig index 8e1a0865..73cbcf97 100644 --- a/src/FFI.zig +++ b/src/FFI.zig @@ -39,7 +39,7 @@ const basic_types = std.StaticStringMap(o.ObjTypeDef).initComptime( .{ "u64", o.ObjTypeDef{ .def_type = .UserData } }, .{ "usize", o.ObjTypeDef{ .def_type = .UserData } }, - .{ "bool", o.ObjTypeDef{ .def_type = .Bool } }, + .{ "bool", o.ObjTypeDef{ .def_type = .Boolean } }, .{ "void", o.ObjTypeDef{ .def_type = .Void } }, .{ "anyopaque", o.ObjTypeDef{ .def_type = .Void } }, diff --git a/src/Jit.zig b/src/Jit.zig index 325f4b18..e8972d1b 100644 --- a/src/Jit.zig +++ b/src/Jit.zig @@ -844,7 +844,7 @@ fn generateNode(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { var value = if (constant != null) m.MIR_new_uint_op(self.ctx, constant.?.val) - else switch (tag) { + else switch (tag) { // FIXME: should be an array like we do in CodeGen and TypeChecker .Boolean => m.MIR_new_uint_op( self.ctx, Value.fromBoolean(components[node].Boolean).val, @@ -894,6 +894,7 @@ fn generateNode(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { .Dot => try self.generateDot(node), .Subscript => try self.generateSubscript(node), .Map => try self.generateMap(node), + .Match => try self.generateMatch(node), .Is => try self.generateIs(node), .As => try self.generateAs(node), .Try => try self.generateTry(node), @@ -2005,7 +2006,7 @@ fn buildReturn(self: *Self, value: m.MIR_op_t) !void { // Unwrap buzz value to its raw mir Value fn unwrap(self: *Self, def_type: o.ObjTypeDef.Type, value: m.MIR_op_t, dest: m.MIR_op_t) !void { return switch (def_type) { - .Bool => self.buildValueToBoolean(value, dest), + .Boolean => self.buildValueToBoolean(value, dest), .Integer => self.buildValueToInteger(value, dest), .Double => try self.buildValueToDouble(value, dest), .Void => self.MOV(dest, value), @@ -2036,7 +2037,7 @@ fn unwrap(self: *Self, def_type: o.ObjTypeDef.Type, value: m.MIR_op_t, dest: m.M // Wrap mir value to buzz Value fn wrap(self: *Self, def_type: o.ObjTypeDef.Type, value: m.MIR_op_t, dest: m.MIR_op_t) !void { return switch (def_type) { - .Bool => self.buildValueFromBoolean(value, dest), + .Boolean => self.buildValueFromBoolean(value, dest), .Integer => self.buildValueFromInteger(value, dest), .Double => try self.buildValueFromDouble(value, dest), .Void => self.MOV(dest, m.MIR_new_uint_op(self.ctx, Value.Void.val)), @@ -2839,7 +2840,7 @@ fn generateIf(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { ); try self.unwrap( - .Bool, + .Boolean, condition, condition, ); @@ -2880,7 +2881,7 @@ fn generateIf(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { self.append(out_label); } else if (constant_condition == null) { try self.unwrap( - .Bool, + .Boolean, condition_value.?, condition, ); @@ -2965,6 +2966,182 @@ fn generateIf(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { m.MIR_new_reg_op(self.ctx, resolved.?); } +fn generateMatchCondition( + self: *Self, + match_value: m.MIR_op_t, + value_type_def: *o.ObjTypeDef, + condition: Ast.Node.Index, +) Error!m.MIR_op_t { + const type_defs = self.state.?.ast.nodes.items(.type_def); + const condition_type_def = type_defs[condition].?; + const condition_value = (try self.generateNode(condition)).?; + const result_value = m.MIR_new_reg_op( + self.ctx, + try self.REG("match_condition_value", m.MIR_T_I64), + ); + + if (!condition_type_def.optional and condition_type_def.def_type == .Range and + (value_type_def.def_type == .Integer or value_type_def.def_type == .Double) and + !value_type_def.optional) + { + try self.buildExternApiCall( + .bz_rangeContains, + result_value, + &.{ + condition_value, + match_value, + }, + ); + } else if (!condition_type_def.optional and condition_type_def.def_type == .Pattern and + value_type_def.def_type == .String and !value_type_def.optional) + { + try self.buildPush(condition_value); + try self.buildExternApiCall( + .bz_patternMatches, + result_value, + &.{ + m.MIR_new_reg_op(self.ctx, self.state.?.vm_reg.?), + condition_value, + match_value, + }, + ); + try self.buildPop(null); + } else if (!condition_type_def.optional and condition_type_def.def_type == .String and + value_type_def.def_type == .Pattern and !value_type_def.optional) + { + try self.buildPush(condition_value); + try self.buildExternApiCall( + .bz_patternMatches, + result_value, + &.{ + m.MIR_new_reg_op(self.ctx, self.state.?.vm_reg.?), + match_value, + condition_value, + }, + ); + try self.buildPop(null); + } else if (!condition_type_def.optional and condition_type_def.def_type == .Type and + (value_type_def.optional or value_type_def.def_type != .Type)) + { + try self.buildExternApiCall( + .bz_valueIs, + result_value, + &.{ + match_value, + condition_value, + }, + ); + } else if (!value_type_def.optional and value_type_def.def_type == .Type and + (condition_type_def.optional or condition_type_def.def_type != .Type)) + { + try self.buildExternApiCall( + .bz_valueIs, + result_value, + &.{ + condition_value, + match_value, + }, + ); + } else { + try self.buildExternApiCall( + .bz_valueEqual, + result_value, + &.{ + match_value, + condition_value, + }, + ); + } + + const result = m.MIR_new_reg_op( + self.ctx, + try self.REG("match_condition", m.MIR_T_I64), + ); + try self.unwrap(.Boolean, result_value, result); + + return result; +} + +fn generateMatch(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { + const type_defs = self.state.?.ast.nodes.items(.type_def); + const components = self.state.?.ast.nodes.items(.components)[node].Match; + const value_type_def = type_defs[components.value].?; + const out_label = m.MIR_new_label(self.ctx); + const match_value = m.MIR_new_reg_op( + self.ctx, + try self.REG("match_value", m.MIR_T_I64), + ); + const resolved = if (!components.is_statement) + m.MIR_new_reg_op( + self.ctx, + try self.REG("match_resolved", m.MIR_T_I64), + ) + else + null; + + self.MOV( + match_value, + (try self.generateNode(components.value)).?, + ); + try self.buildPush(match_value); + + for (components.branches) |branch| { + const branch_label = m.MIR_new_label(self.ctx); + const next_branch_label = m.MIR_new_label(self.ctx); + + for (branch.conditions) |condition| { + const matches = try self.generateMatchCondition( + match_value, + value_type_def, + condition, + ); + + self.BEQ( + m.MIR_new_label_op(self.ctx, branch_label), + matches, + m.MIR_new_uint_op(self.ctx, 1), + ); + } + + self.JMP(next_branch_label); + self.append(branch_label); + + try self.buildPop(null); + if (components.is_statement) { + _ = try self.generateNode(branch.expression); + } else { + self.MOV( + resolved.?, + (try self.generateNode(branch.expression)).?, + ); + } + self.JMP(out_label); + + self.append(next_branch_label); + } + + try self.buildPop(null); + if (components.else_branch) |else_branch| { + if (components.is_statement) { + _ = try self.generateNode(else_branch); + } else { + self.MOV( + resolved.?, + (try self.generateNode(else_branch)).?, + ); + } + } else if (!components.is_statement) { + self.MOV( + resolved.?, + m.MIR_new_uint_op(self.ctx, Value.Void.val), + ); + } + + self.append(out_label); + + return resolved; +} + fn generateTypeExpression(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { const type_expression = self.state.?.ast.nodes.items(.components)[node].TypeExpression; return m.MIR_new_uint_op( @@ -2994,7 +3171,7 @@ fn generateTypeOfExpression(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t fn buildBinary( self: *Self, - operator: Token.Type, + operator: Token.Tag, def_type: o.ObjTypeDef.Type, left_value: m.MIR_op_t, right_value: m.MIR_op_t, @@ -3368,7 +3545,7 @@ fn generateComparison(self: *Self, components: Ast.Binary) Error!?m.MIR_op_t { }, ); - try self.unwrap(.Bool, res, res); + try self.unwrap(.Boolean, res, res); const true_label = m.MIR_new_label(self.ctx); const out_label = m.MIR_new_label(self.ctx); @@ -3423,7 +3600,7 @@ fn generateComparison(self: *Self, components: Ast.Binary) Error!?m.MIR_op_t { else => unreachable, } - try self.wrap(.Bool, res, res); + try self.wrap(.Boolean, res, res); } else { switch (components.operator) { .Greater => self.GT(res, left, right), @@ -3433,7 +3610,7 @@ fn generateComparison(self: *Self, components: Ast.Binary) Error!?m.MIR_op_t { else => unreachable, } - try self.wrap(.Bool, res, res); + try self.wrap(.Boolean, res, res); } }, else => {}, @@ -4743,7 +4920,7 @@ fn generateTry(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { ); try self.unwrap( - .Bool, + .Boolean, m.MIR_new_reg_op(self.ctx, matches), m.MIR_new_reg_op(self.ctx, matches), ); @@ -5035,7 +5212,7 @@ fn generateUnary(self: *Self, node: Ast.Node.Index) Error!?m.MIR_op_t { try self.wrap(.Integer, result, result); }, .Bang => { - try self.unwrap(.Bool, left, result); + try self.unwrap(.Boolean, left, result); const true_label = m.MIR_new_label(self.ctx); const out_label = m.MIR_new_label(self.ctx); diff --git a/src/Parser.zig b/src/Parser.zig index d6ec1ab8..f83ff32f 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -501,7 +501,7 @@ else "/usr/local/lib/lib?.!", }; -const rules = [_]ParseRule{ +const rules = [@typeInfo(Token.Tag).@"enum".fields.len]ParseRule{ .{ .prefix = list, .infix = subscript, .precedence = .Call }, // LeftBracket .{}, // RightBracket .{ .prefix = grouping, .infix = call, .precedence = .Call }, // LeftParen @@ -613,6 +613,7 @@ const rules = [_]ParseRule{ .{}, // BnotEqual .{}, // AmpersandEqual .{}, // PercentEqual + .{ .prefix = matchExpression }, // Match }; pub fn reportErrorAtNode(self: *Self, error_type: Reporter.Error, node: Ast.Node.Index, comptime fmt: []const u8, args: anytype) void { @@ -697,7 +698,7 @@ fn advancePastEof(self: *Self) !void { } } -pub fn consume(self: *Self, tag: Token.Type, comptime message: []const u8) !void { +pub fn consume(self: *Self, tag: Token.Tag, comptime message: []const u8) !void { if (self.ast.tokens.items(.tag)[self.current_token.?] == tag) { try self.advance(); return; @@ -754,12 +755,12 @@ pub fn consume(self: *Self, tag: Token.Type, comptime message: []const u8) !void } // Check next token -fn check(self: *Self, tag: Token.Type) bool { +fn check(self: *Self, tag: Token.Tag) bool { return self.ast.tokens.items(.tag)[self.current_token.?] == tag; } // Check `n` tokens ahead -fn checkAhead(self: *Self, tag: Token.Type, n: usize) !bool { +fn checkAhead(self: *Self, tag: Token.Tag, n: usize) !bool { // Parse tokens if we didn't already look that far ahead while (n + 1 > self.ast.tokens.len - self.current_token.? - 1) { while (true) { @@ -793,7 +794,7 @@ fn checkAhead(self: *Self, tag: Token.Type, n: usize) !bool { return self.ast.tokens.items(.tag)[self.current_token.? + n + 1] == tag; } -fn match(self: *Self, tag: Token.Type) !bool { +fn match(self: *Self, tag: Token.Tag) !bool { if (!self.check(tag)) { return false; } @@ -1344,7 +1345,7 @@ fn closeScope(self: *Self, upto_depth: usize) ![]Ast.Close { return try closing.toOwnedSlice(self.gc.allocator); } -inline fn getRule(token: Token.Type) ParseRule { +inline fn getRule(token: Token.Tag) ParseRule { return rules[@intFromEnum(token)]; } @@ -1498,14 +1499,14 @@ fn simpleType(self: *Self, def_type: obj.ObjTypeDef.Type) Error!Ast.Node.Index { ); } -fn simpleTypeFromToken(token: Token.Type) ?obj.ObjTypeDef.Type { +fn simpleTypeFromToken(token: Token.Tag) ?obj.ObjTypeDef.Type { return switch (token) { .Pat => .Pattern, .Ud => .UserData, .Str => .String, .Int => .Integer, .Double => .Double, - .Bool => .Bool, + .Bool => .Boolean, .Range => .Range, .Type => .Type, .Any => .Any, @@ -1552,7 +1553,7 @@ fn declaration(self: *Self, docblock: ?Ast.TokenIndex) Error!?Ast.Node.Index { // FIXME: shouldn't we call parseTypeDef?? // Simple types - for ([_]Token.Type{ .Pat, .Ud, .Str, .Int, .Double, .Bool, .Range, .Type, .Any }) |token| { + for ([_]Token.Tag{ .Pat, .Ud, .Str, .Int, .Double, .Bool, .Range, .Type, .Any }) |token| { if (try self.match(token)) { break :variable try self.varDeclaration( identifier, @@ -1668,6 +1669,9 @@ fn statement(self: *Self, docblock: ?Ast.TokenIndex, hanging: bool, loop_scope: if (try self.match(.If)) { std.debug.assert(!hanging); return try self.ifStatement(loop_scope); + } else if (try self.match(.Match)) { + std.debug.assert(!hanging); + return try self.matchStatement(); } else if (try self.match(.For)) { std.debug.assert(!hanging); return try self.forStatement(); @@ -2946,7 +2950,7 @@ fn parseTypeDef( .type_def = try self.gc.type_registry.getTypeDef( .{ .optional = optional, - .def_type = .Bool, + .def_type = .Boolean, }, ), .components = .{ @@ -5918,6 +5922,143 @@ fn inlineIf(self: *Self, _: bool) Error!Ast.Node.Index { return try self.@"if"(false, null); } +fn matchStatementOrExpression(self: *Self, is_statement: bool) Error!Ast.Node.Index { + const start_location = self.current_token.? - 1; + + try self.consume(.LeftParen, "Expected `(` after `match`"); + const value = try self.expression(false); + try self.consume(.RightParen, "Expected `)` after `match`"); + + try self.consume(.LeftBrace, "Expected `{`"); + + var branches = std.ArrayList(Ast.Match.Branch).empty; + while (!self.check(.Eof) and !self.check(.RightBrace) and !self.check(.Else)) { + var conditions = std.ArrayList(Ast.Node.Index).empty; + + while (!self.check(.Eof)) { + try conditions.append(self.gc.allocator, try self.expression(false)); + + if (!try self.match(.Comma) or self.check(.Arrow)) { + break; + } + } + + try self.consume(.Arrow, "Expected `->`"); + + try branches.append( + self.gc.allocator, + .{ + .conditions = try conditions.toOwnedSlice(self.gc.allocator), + .expression = try self.expression(false), + }, + ); + + if (!try self.match(.Comma) or self.check(.RightBrace)) { + break; + } + } + + const else_branch = if (try self.match(.Else)) else_branch: { + try self.consume(.Arrow, "Expected `->` after `else`"); + break :else_branch try self.expression(false); + } else null; + + if (else_branch != null) { + _ = try self.match(.Comma); + } + + try self.consume(.RightBrace, "Expected `}`"); + + if (branches.items.len == 0 and else_branch == null and self.reporter.last_error != .unclosed) { + self.reporter.report( + .syntax, + self.ast.tokens.get(start_location), + self.ast.tokens.get(self.current_token.? - 1), + if (is_statement) + "`match` statement requires at least one branch" + else + "`match` expression requires at least one branch", + ); + } + + return try self.ast.appendNode( + .{ + .tag = .Match, + .location = start_location, + .end_location = self.current_token.? - 1, + .type_def = type_def: { + const type_defs = self.ast.nodes.items(.type_def); + + var type_def: ?*obj.ObjTypeDef = null; + var is_optional = false; + for (branches.items) |branch| { + const branch_type_def = type_defs[branch.expression]; + + if (branch_type_def) |btd| { + is_optional = is_optional or btd.optional or btd.def_type == .Void; + + if (btd.def_type == .Void) { + if (type_def == null) { + type_def = btd; + } + } else if (type_def) |previous| { + if (previous.def_type == .Void) { + type_def = btd; + } else if (!previous.eql(btd)) { + type_def = self.gc.type_registry.any_type; + break; + } + } else { + type_def = btd; + } + } + } + + if (else_branch) |eb| { + if (type_defs[eb]) |btd| { + is_optional = is_optional or btd.def_type == .Void or btd.optional; + + if (btd.def_type == .Void) { + if (type_def == null) { + type_def = btd; + } + } else if (type_def) |previous| { + if (previous.def_type == .Void) { + type_def = btd; + } else if (!previous.eql(btd)) { + type_def = self.gc.type_registry.any_type; + } + } else { + type_def = btd; + } + } + } + + break :type_def if (type_def != null and is_optional and !type_def.?.optional and type_def.?.def_type != .Void) + try type_def.?.cloneOptional(&self.gc.type_registry) + else + type_def; + }, + .components = .{ + .Match = .{ + .is_statement = is_statement, + .value = value, + .branches = try branches.toOwnedSlice(self.gc.allocator), + .else_branch = else_branch, + }, + }, + }, + ); +} + +fn matchStatement(self: *Self) Error!Ast.Node.Index { + return self.matchStatementOrExpression(true); +} + +fn matchExpression(self: *Self, _: bool) Error!Ast.Node.Index { + return self.matchStatementOrExpression(false); +} + fn isAs(self: *Self, left: Ast.Node.Index, is_expr: bool) Error!Ast.Node.Index { const start_location = self.ast.nodes.items(.location)[left]; const constant = try self.parseTypeDef(null, true); @@ -6494,7 +6635,7 @@ fn function( if (function_type.canHaveErrorSet() and (try self.match(.BangGreater))) { var error_typedefs = std.ArrayList(*obj.ObjTypeDef).empty; - const end_token: Token.Type = if (function_type.canOmitBody()) .Semicolon else .LeftBrace; + const end_token: Token.Tag = if (function_type.canOmitBody()) .Semicolon else .LeftBrace; while (!self.check(end_token) and !self.check(.DoubleArrow) and !self.check(.Eof)) { const error_type_node = try self.parseTypeDef( diff --git a/src/Reporter.zig b/src/Reporter.zig index d23643a2..c9fc2ce7 100644 --- a/src/Reporter.zig +++ b/src/Reporter.zig @@ -137,6 +137,8 @@ pub const Error = enum(u8) { unassigned_final_local = 102, source_not_utf8 = 103, default_value_type = 104, + unexhaustive_match = 105, + match_condition_type = 106, }; // Inspired by https://github.com/zesterer/ariadne diff --git a/src/Scanner.zig b/src/Scanner.zig index f3cfe572..bfed7587 100644 --- a/src/Scanner.zig +++ b/src/Scanner.zig @@ -591,7 +591,7 @@ fn match(self: *Self, expected: u8) bool { return true; } -fn makeToken(self: *Self, tag: Token.Type, literal: Token.Literal) Token { +fn makeToken(self: *Self, tag: Token.Tag, literal: Token.Literal) Token { self.token_index += 1; return Token{ .tag = tag, @@ -726,6 +726,7 @@ pub fn highlight(self: *Self, out: *std.Io.Writer, true_color: bool) void { .BnotEqual, .PercentEqual, .AmpersandEqual, + .Match, => if (true_color) Color.keyword else Color.magenta, // Punctuation .LeftBracket, diff --git a/src/Token.zig b/src/Token.zig index 7722c7cd..3f0b83ec 100644 --- a/src/Token.zig +++ b/src/Token.zig @@ -22,7 +22,7 @@ pub const NoLiteral = Literal{ .None = {}, }; -tag: Type, +tag: Tag, /// When true, token inserted by the parser utility_token: bool = false, source: []const u8, @@ -91,7 +91,7 @@ pub fn getLines(self: Self, allocator: mem.Allocator, before: usize, after: usiz } // WARNING: don't reorder without reordering `rules` in parser.zig -pub const Type = enum { +pub const Tag = enum { LeftBracket, // [ RightBracket, // ] LeftParen, // ( @@ -205,6 +205,7 @@ pub const Type = enum { BnotEqual, // ~= AmpersandEqual, // &= PercentEqual, // %= + Match, // match pub fn isAssignShortcut(self: @This()) bool { return switch (self) { @@ -226,36 +227,37 @@ pub const Type = enum { }; // FIXME if case had the same name as the actual token we could simply use @tagName -pub const keywords = std.StaticStringMap(Type).initComptime( +pub const keywords = std.StaticStringMap(Tag).initComptime( .{ + .{ "Function", .Function }, .{ "and", .And }, .{ "any", .Any }, .{ "as", .As }, .{ "bool", .Bool }, .{ "break", .Break }, .{ "catch", .Catch }, - .{ "final", .Final }, .{ "continue", .Continue }, .{ "do", .Do }, + .{ "double", .Double }, .{ "else", .Else }, .{ "enum", .Enum }, .{ "export", .Export }, .{ "extern", .Extern }, .{ "false", .False }, .{ "fib", .Fib }, - .{ "double", .Double }, + .{ "final", .Final }, .{ "for", .For }, .{ "foreach", .ForEach }, .{ "from", .From }, .{ "fun", .Fun }, - .{ "Function", .Function }, .{ "if", .If }, .{ "import", .Import }, .{ "in", .In }, .{ "int", .Int }, .{ "is", .Is }, - .{ "namespace", .Namespace }, + .{ "match", .Match }, .{ "mut", .Mut }, + .{ "namespace", .Namespace }, .{ "null", .Null }, .{ "obj", .Obj }, .{ "object", .Object }, @@ -263,10 +265,10 @@ pub const keywords = std.StaticStringMap(Type).initComptime( .{ "out", .Out }, .{ "pat", .Pat }, .{ "protocol", .Protocol }, - .{ "rg", .Range }, .{ "resolve", .Resolve }, .{ "resume", .Resume }, .{ "return", .Return }, + .{ "rg", .Range }, .{ "static", .Static }, .{ "str", .Str }, .{ "test", .Test }, diff --git a/src/TypeChecker.zig b/src/TypeChecker.zig index 4adfa4c3..f56e26d7 100644 --- a/src/TypeChecker.zig +++ b/src/TypeChecker.zig @@ -51,6 +51,7 @@ const checkers = [@typeInfo(Ast.Node.Tag).@"enum".fields.len]?NodeCheck{ null, // ListType checkMap, null, // MapType + checkMatch, null, // Namespace checkNamedVariable, null, // Null @@ -411,7 +412,7 @@ fn checkBinary(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Index, .And, .Or, => { - if (left_type.def_type != .Bool) { + if (left_type.def_type != .Boolean) { reporter.reportErrorAt( .logical_operand_type, ast.tokens.get(locations[node_components.Binary.left]), @@ -1073,7 +1074,7 @@ fn checkDoUntil(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Index const condition_type_def = type_defs[components.condition] orelse gc.type_registry.any_type; - if (condition_type_def.def_type != .Bool) { + if (condition_type_def.def_type != .Boolean) { reporter.reportErrorAt( .do_condition_type, ast.tokens.get(locations[components.condition]), @@ -1156,7 +1157,7 @@ fn checkFor(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Index, no const components = node_components[node].For; const condition_type_def = type_defs[components.condition] orelse gc.type_registry.any_type; - if (condition_type_def.def_type != .Bool) { + if (condition_type_def.def_type != .Boolean) { reporter.reportErrorAt( .for_condition_type, ast.tokens.get(locations[components.condition]), @@ -1596,7 +1597,7 @@ fn checkIf(ast: Ast.Slice, reporter: *Reporter, _: *GC, _: ?Ast.Node.Index, node } if (components.casted_type == null and components.unwrapped_identifier == null) { - if (type_defs[components.condition].?.def_type != .Bool) { + if (type_defs[components.condition].?.def_type != .Boolean) { reporter.reportErrorAt( .if_condition_type, ast.tokens.get(locations[components.condition]), @@ -1620,6 +1621,102 @@ fn checkIf(ast: Ast.Slice, reporter: *Reporter, _: *GC, _: ?Ast.Node.Index, node return had_error; } +fn checkMatch(ast: Ast.Slice, reporter: *Reporter, _: *GC, _: ?Ast.Node.Index, node: Ast.Node.Index) error{OutOfMemory}!bool { + const type_defs = ast.nodes.items(.type_def); + const locations = ast.nodes.items(.location); + const end_locations = ast.nodes.items(.end_location); + const components = ast.nodes.items(.components); + const node_components = components[node].Match; + const value_type_def = type_defs[node_components.value].?; + + var had_error = false; + if (!value_type_def.optional) { + switch (value_type_def.def_type) { + // match on string accepts str, pat and type as condition types + .String => for (node_components.branches) |branch| { + for (branch.conditions) |condition| { + const condition_type_def = type_defs[condition]; + if (condition_type_def == null or condition_type_def.?.optional or + (condition_type_def.?.def_type != .String and + condition_type_def.?.def_type != .Pattern and + condition_type_def.?.def_type != .Type)) + { + reporter.reportErrorAt( + .match_condition_type, + ast.tokens.get(locations[condition]), + ast.tokens.get(end_locations[condition]), + "`match` condition must be of type `str`, `pat` or `type`", + ); + had_error = true; + } + } + }, + // match on pattern accepts str, pat and type as condition types + .Pattern => for (node_components.branches) |branch| { + for (branch.conditions) |condition| { + const condition_type_def = type_defs[condition]; + if (condition_type_def == null or condition_type_def.?.optional or + (condition_type_def.?.def_type != .String and + condition_type_def.?.def_type != .Pattern and + condition_type_def.?.def_type != .Type)) + { + reporter.reportErrorAt( + .match_condition_type, + ast.tokens.get(locations[condition]), + ast.tokens.get(end_locations[condition]), + "`match` condition must be of type `str`, `pat` or `type`", + ); + had_error = true; + } + } + }, + // match on number accepts int, double, rg or type + .Integer, .Double => for (node_components.branches) |branch| { + for (branch.conditions) |condition| { + const condition_type_def = type_defs[condition]; + if (condition_type_def == null or condition_type_def.?.optional or + (condition_type_def.?.def_type != .Integer and + condition_type_def.?.def_type != .Range and + condition_type_def.?.def_type != .Double and + condition_type_def.?.def_type != .Type)) + { + reporter.reportErrorAt( + .match_condition_type, + ast.tokens.get(locations[condition]), + ast.tokens.get(end_locations[condition]), + "`match` condition must be of type `int`, `double`, `rg` or `type`", + ); + had_error = true; + } + } + }, + // Anything goes: any matches against anything and type will result in equality against another type and `is` or anything else + .Type, .Any => {}, + else => for (node_components.branches) |branch| { + for (branch.conditions) |condition| { + const condition_type_def = type_defs[condition]; + if (condition_type_def == null or + (!value_type_def.eql(condition_type_def.?) and condition_type_def.?.def_type != .Type)) + { + reporter.reportTypeCheck( + .match_condition_type, + ast.tokens.get(locations[node_components.value]), + ast.tokens.get(end_locations[node_components.value]), + value_type_def, + ast.tokens.get(locations[condition]), + ast.tokens.get(end_locations[condition]), + condition_type_def.?, + "Bad `match` condition type", + ); + } + } + }, + } + } + + return had_error; +} + fn checkList(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Index, node: Ast.Node.Index) error{OutOfMemory}!bool { const locations = ast.nodes.items(.location); const end_locations = ast.nodes.items(.end_location); @@ -2435,7 +2532,7 @@ fn checkUnary(ast: Ast.Slice, reporter: *Reporter, gc: *GC, _: ?Ast.Node.Index, ); had_error = true; }, - .Bang => if (expression_type_def.def_type != .Bool) { + .Bang => if (expression_type_def.def_type != .Boolean) { reporter.reportErrorFmt( .bitwise_operand_type, ast.tokens.get(expression_location), @@ -2532,7 +2629,7 @@ fn checkWhile(ast: Ast.Slice, reporter: *Reporter, _: *GC, _: ?Ast.Node.Index, n const end_locations = ast.nodes.items(.end_location); const condition_type_def = type_defs[components.condition].?; - if (condition_type_def.def_type != .Bool) { + if (condition_type_def.def_type != .Boolean) { reporter.reportErrorAt( .while_condition_type, ast.tokens.get(locations[components.condition]), diff --git a/src/TypeRegistry.zig b/src/TypeRegistry.zig index 12c5eae9..d6daca35 100644 --- a/src/TypeRegistry.zig +++ b/src/TypeRegistry.zig @@ -50,7 +50,7 @@ pub fn init(gc: *GC) !TypeRegistry { self.str_type = try self.getTypeDef(.{ .def_type = .String }); self.int_type = try self.getTypeDef(.{ .def_type = .Integer }); self.double_type = try self.getTypeDef(.{ .def_type = .Double }); - self.bool_type = try self.getTypeDef(.{ .def_type = .Bool }); + self.bool_type = try self.getTypeDef(.{ .def_type = .Boolean }); self.any_type = try self.getTypeDef( .{ .def_type = .Any, @@ -164,7 +164,7 @@ fn hashHelper(hasher: *std.hash.Wyhash, type_def: *const o.ObjTypeDef) void { // in that case we wan't to use the pointer (real this time) as hash value .Placeholder => std.hash.autoHash(hasher, type_def), - .Bool, + .Boolean, .Double, .Integer, .Pattern, diff --git a/src/builtin/pattern.zig b/src/builtin/pattern.zig index 4793505e..d9a086ff 100644 --- a/src/builtin/pattern.zig +++ b/src/builtin/pattern.zig @@ -129,7 +129,7 @@ fn matchType(vm: *VM) !*o.ObjTypeDef { ); } -fn rawMatch(self: *o.ObjPattern, vm: *VM, subject: *o.ObjString, offset: *usize) !?*o.ObjList { +pub fn rawMatch(self: *o.ObjPattern, vm: *VM, subject: *o.ObjString, offset: *usize) !?*o.ObjList { if (subject.string.len == 0) { return null; } @@ -349,7 +349,7 @@ fn rawReplaceAll(self: *o.ObjPattern, vm: *VM, subject: *o.ObjString, replacemen return current; } -pub fn match(ctx: *o.NativeCtx) callconv(.c) c_int { +pub fn matchAgainst(ctx: *o.NativeCtx) callconv(.c) c_int { const self = o.ObjPattern.cast(ctx.vm.peek(1).obj()).?; const subject = o.ObjString.cast(ctx.vm.peek(0).obj()).?; @@ -431,7 +431,7 @@ pub fn replace(ctx: *o.NativeCtx) callconv(.c) c_int { return 1; } -pub fn matchAll(ctx: *o.NativeCtx) callconv(.c) c_int { +pub fn matchAllAgainst(ctx: *o.NativeCtx) callconv(.c) c_int { const self = o.ObjPattern.cast(ctx.vm.peek(1).obj()).?; const subject = o.ObjString.cast(ctx.vm.peek(0).obj()).?; diff --git a/src/builtin/range.zig b/src/builtin/range.zig index c8d4f36b..716147c1 100644 --- a/src/builtin/range.zig +++ b/src/builtin/range.zig @@ -172,12 +172,17 @@ pub fn low(ctx: *obj.NativeCtx) callconv(.c) c_int { pub fn contains(ctx: *obj.NativeCtx) callconv(.c) c_int { const range = ctx.vm.peek(1).obj().access(obj.ObjRange, .Range, ctx.vm.gc).?; - const value = ctx.vm.peek(0).integer(); + const value = if (ctx.vm.peek(0).isInteger()) + @as(v.Double, @floatFromInt(ctx.vm.peek(0).integer())) + else + ctx.vm.peek(0).double(); + const low_value: v.Double = @floatFromInt(range.low); + const high_value: v.Double = @floatFromInt(range.high); ctx.vm.push( v.Value.fromBoolean( - (range.high >= range.low and value >= range.low and value < range.high) or - (range.low >= range.high and value >= range.high and value < range.low), + (high_value >= low_value and value >= low_value and value < high_value) or + (low_value >= high_value and value >= high_value and value < low_value), ), ); diff --git a/src/buzz_api.zig b/src/buzz_api.zig index 1fcccb01..428853b0 100644 --- a/src/buzz_api.zig +++ b/src/buzz_api.zig @@ -1026,6 +1026,51 @@ export fn bz_valueEqual(self: v.Value, other: v.Value) callconv(.c) v.Value { return v.Value.fromBoolean(self.eql(other)); } +export fn bz_rangeContains(range_value: v.Value, value: v.Value) callconv(.c) v.Value { + const range = o.ObjRange.cast(range_value.obj()).?; + const number = if (value.isInteger()) + @as(v.Double, @floatFromInt(value.integer())) + else + value.double(); + const low: v.Double = @floatFromInt(range.low); + const high: v.Double = @floatFromInt(range.high); + + return v.Value.fromBoolean( + (high >= low and number >= low and number < high) or + (low >= high and number >= high and number < low), + ); +} + +export fn bz_patternMatches(vm: *VM, pattern_value: v.Value, subject_value: v.Value) callconv(.c) v.Value { + if (comptime is_wasm) { + return bz_patternMatchesWasm(vm, pattern_value, subject_value); + } else { + return bz_patternMatchesNative(vm, pattern_value, subject_value); + } +} + +fn bz_patternMatchesWasm(_: *VM, _: v.Value, _: v.Value) v.Value { + return v.Value.False; +} + +fn bz_patternMatchesNative(vm: *VM, pattern_value: v.Value, subject_value: v.Value) v.Value { + const pattern = o.ObjPattern.cast(pattern_value.obj()).?; + const subject = o.ObjString.cast(subject_value.obj()).?; + + var offset: usize = 0; + return v.Value.fromBoolean( + (buzz_builtin.pattern.rawMatch( + pattern, + vm, + subject, + &offset, + ) catch { + vm.panic("Out of memory"); + unreachable; + }) != null, + ); +} + export fn bz_valueTypeOf(self: v.Value, vm: *VM) callconv(.c) v.Value { return (self.typeOf(vm.gc) catch { vm.panic("Out of memory"); diff --git a/src/jit_extern_api.zig b/src/jit_extern_api.zig index 1f35200f..fadb68c2 100644 --- a/src/jit_extern_api.zig +++ b/src/jit_extern_api.zig @@ -23,6 +23,8 @@ pub const ExternApi = enum { bz_listGet, bz_listSet, bz_valueEqual, + bz_rangeContains, + bz_patternMatches, bz_listConcat, bz_newMap, bz_mapSet, @@ -297,7 +299,9 @@ pub const ExternApi = enum { }, }, ), - .bz_valueEqual => m.MIR_new_proto_arr( + .bz_valueEqual, + .bz_rangeContains, + => m.MIR_new_proto_arr( ctx, self.pname(), 1, @@ -316,6 +320,30 @@ pub const ExternApi = enum { }, }, ), + .bz_patternMatches => m.MIR_new_proto_arr( + ctx, + self.pname(), + 1, + &.{m.MIR_T_U64}, + 3, + &.{ + .{ + .type = m.MIR_T_P, + .name = "vm", + .size = undefined, + }, + .{ + .type = m.MIR_T_U64, + .name = "pattern", + .size = undefined, + }, + .{ + .type = m.MIR_T_U64, + .name = "subject", + .size = undefined, + }, + }, + ), .bz_listConcat, .bz_mapConcat, .bz_stringConcat, @@ -1130,6 +1158,8 @@ pub const ExternApi = enum { .bz_mapNext => @as(*anyopaque, @ptrFromInt(@intFromPtr(&api.Value.bz_mapNext))), .bz_mapConcat => @as(*anyopaque, @ptrFromInt(@intFromPtr(&api.Value.bz_mapConcat))), .bz_valueEqual => @as(*anyopaque, @ptrFromInt(@intFromPtr(&api.Value.bz_valueEqual))), + .bz_rangeContains => @as(*anyopaque, @ptrFromInt(@intFromPtr(&api.Value.bz_rangeContains))), + .bz_patternMatches => @as(*anyopaque, @ptrFromInt(@intFromPtr(&api.Value.bz_patternMatches))), .bz_valueIs => @as(*anyopaque, @ptrFromInt(@intFromPtr(&api.Value.bz_valueIs))), .bz_closure => @as(*anyopaque, @ptrFromInt(@intFromPtr(&api.VM.bz_closure))), .bz_context => @as(*anyopaque, @ptrFromInt(@intFromPtr(&api.VM.bz_context))), diff --git a/src/lib/buzz_api.zig b/src/lib/buzz_api.zig index 29e465e4..6dd950a9 100644 --- a/src/lib/buzz_api.zig +++ b/src/lib/buzz_api.zig @@ -142,6 +142,8 @@ pub const Value = extern struct { pub extern fn bz_valueIsForeignContainer(value: Value) callconv(.c) bool; pub extern fn bz_valueDump(value: Value, vm: *VM) callconv(.c) void; pub extern fn bz_valueEqual(self: Value, other: Value) callconv(.c) Value; + pub extern fn bz_rangeContains(range: Value, value: Value) callconv(.c) Value; + pub extern fn bz_patternMatches(vm: *VM, pattern: Value, subject: Value) callconv(.c) Value; pub extern fn bz_valueIs(self: Value, type_def: Value) callconv(.c) Value; pub extern fn bz_valueTypeOf(self: Value, vm: *VM) callconv(.c) Value; pub extern fn bz_getUserDataPtr(userdata: Value) callconv(.c) u64; diff --git a/src/lib/serialize.buzz b/src/lib/serialize.buzz index 4b81e1df..322174f1 100644 --- a/src/lib/serialize.buzz +++ b/src/lib/serialize.buzz @@ -146,7 +146,7 @@ object JsonParser { return this.source[this.offset]; } - mut fun match(expected: str) > bool { + mut fun matchNext(expected: str) > bool { if (this.offset > this.source.len() or this.source[this.offset] != expected) { return false; } @@ -157,7 +157,7 @@ object JsonParser { } mut fun consume(expected: str) > void !> JsonParseError { - if (!this.match(expected)) { + if (!this.matchNext(expected)) { throw JsonParseError{ message = "Could not parse JSON: expected `{expected}` got `{this.peek()}` at offset {this.offset}" }; } } @@ -211,7 +211,7 @@ object JsonParser { while (true) { this.skipWhitespaces(); - if (this.match("]")) { + if (this.matchNext("]")) { break; } @@ -219,7 +219,7 @@ object JsonParser { this.skipWhitespaces(); - if (this.match("]")) { + if (this.matchNext("]")) { break; } @@ -235,7 +235,7 @@ object JsonParser { while (true) { this.skipWhitespaces(); - if (this.match("}")) { + if (this.matchNext("}")) { break; } @@ -252,7 +252,7 @@ object JsonParser { this.skipWhitespaces(); - if (this.match("}")) { + if (this.matchNext("}")) { break; } @@ -297,15 +297,15 @@ object JsonParser { while (char != null and char != "\"") { if (char == "\\") { - if (this.match("\"")) { + if (this.matchNext("\"")) { string.write("\""); - } else if (this.match("\\")) { + } else if (this.matchNext("\\")) { string.write("\\"); - } else if (this.match("n")) { + } else if (this.matchNext("n")) { string.write("\n"); - } else if (this.match("t")) { + } else if (this.matchNext("t")) { string.write("\t"); - } else if (this.match("r")) { + } else if (this.matchNext("r")) { string.write("\r"); } } else { diff --git a/src/lib/toml.buzz b/src/lib/toml.buzz index c2096ae2..1706f5ff 100644 --- a/src/lib/toml.buzz +++ b/src/lib/toml.buzz @@ -329,7 +329,7 @@ object Scanner { return char; } - fun match(expected: int) > bool { + fun matchChar(expected: int) > bool { if (this.isEOF()) { return false; } @@ -510,7 +510,7 @@ object Scanner { return this.makeToken(.Newline); } else if (char == '\r') { - if (!this.match('\n')) { + if (!this.matchChar('\n')) { return this.makeError("Expected LF after CR"); } @@ -1419,8 +1419,8 @@ object Parser { parser.advance(); parser.skipLineIgnorable(); - while (!parser.match(.Eof)) { - if (parser.match(.LeftBracket)) { + while (!parser.matchToken(.Eof)) { + if (parser.matchToken(.LeftBracket)) { final firstLeft = parser.scanner.tokens[parser.currentToken! - 1]; // pop previous section if (parser.section -> section) { @@ -1475,15 +1475,15 @@ object Parser { } mut fun skipLineIgnorable() !> Error { - while (this.match(.Newline)) {} + while (this.matchToken(.Newline)) {} } mut fun skipNewlines() !> Error { - while (this.match(.Newline)) {} + while (this.matchToken(.Newline)) {} } mut fun consumeLineEnd() !> Error { - if (this.match(.Eof)) { + if (this.matchToken(.Eof)) { return; } @@ -1799,7 +1799,7 @@ object Parser { fun isKeyStartTag(tag: TokenTag) => tag == .LeftBracket or tag == .BareKey or tag == .LiteralString or tag == .BasicString or tag == .Boolean or tag == .Integer or tag == .Float or tag == .LocalDate; - mut fun match(tag: TokenTag) > bool !> Error { + mut fun matchToken(tag: TokenTag) > bool !> Error { if (!this.check(tag)) { return false; } @@ -1859,7 +1859,7 @@ object Parser { mut fun parseKey() > [str] !> Error { final result = mut []; - while (!this.match(.Eof)) { + while (!this.matchToken(.Eof)) { this.consumeAnyOf( [ .BareKey, @@ -1888,7 +1888,7 @@ object Parser { } - if (!this.match(.Dot)) { + if (!this.matchToken(.Dot)) { break; } } @@ -1897,7 +1897,7 @@ object Parser { } mut fun parseValue() > bool !> Error { - if (this.match(.Boolean)) { + if (this.matchToken(.Boolean)) { final token = this.scanner.tokens[this.currentToken! - 1]; final scalar = scalarFromToken(token); @@ -1910,7 +1910,7 @@ object Parser { return this.setValue(scalar); } - if (this.match(.LeftBrace)) { + if (this.matchToken(.LeftBrace)) { return this.parseInlineTable(); } @@ -1936,7 +1936,7 @@ object Parser { return this.setValue(scalar); } - if (this.match(.Integer)) { + if (this.matchToken(.Integer)) { final token = this.scanner.tokens[this.currentToken! - 1]; final value = scalarFromToken(token); @@ -1949,7 +1949,7 @@ object Parser { return this.setValue(value); } - if (this.match(.Float)) { + if (this.matchToken(.Float)) { final token = this.scanner.tokens[this.currentToken! - 1]; final value = scalarFromToken(token); @@ -1962,7 +1962,7 @@ object Parser { return this.setValue(value); } - if (this.match(.LeftBracket)) { + if (this.matchToken(.LeftBracket)) { return this.parseArray(); } @@ -1976,11 +1976,11 @@ object Parser { } while (true) { - if (this.match(.RightBrace)) { + if (this.matchToken(.RightBrace)) { break; } - if (this.match(.Eof)) { + if (this.matchToken(.Eof)) { this.report("Unfinished inline table"); throw Error.Invalid; @@ -1990,7 +1990,7 @@ object Parser { return false; } - if (!this.match(.Comma)) { + if (!this.matchToken(.Comma)) { this.consume(.RightBrace, message: "Expected `}`"); break; } @@ -2015,11 +2015,11 @@ object Parser { var i = 0; while (true) { - if (this.match(.RightBracket)) { + if (this.matchToken(.RightBracket)) { break; } - if (this.match(.Eof)) { + if (this.matchToken(.Eof)) { this.report("Unfinished array"); throw Error.Invalid; @@ -2036,7 +2036,7 @@ object Parser { this.skipNewlines(); - if (!this.match(.Comma)) { + if (!this.matchToken(.Comma)) { this.skipNewlines(); this.consume(.RightBracket, message: "Expected `]`"); break; diff --git a/src/obj.zig b/src/obj.zig index 297f7a98..aa9188e8 100644 --- a/src/obj.zig +++ b/src/obj.zig @@ -870,8 +870,8 @@ pub const ObjPattern = struct { pub const members = if (!is_wasm) [_]NativeFn{ - buzz_builtin.pattern.match, - buzz_builtin.pattern.matchAll, + buzz_builtin.pattern.matchAgainst, + buzz_builtin.pattern.matchAllAgainst, buzz_builtin.pattern.replace, buzz_builtin.pattern.replaceAll, } @@ -883,8 +883,8 @@ pub const ObjPattern = struct { const members_typedef = if (!is_wasm) [_][]const u8{ - "extern fun match(subject: str) > [obj{ capture: str, start: int, end: int }]?", - "extern fun matchAll(subject: str) > [[obj{ capture: str, start: int, end: int }]]?", + "extern fun matchAgainst(subject: str) > [obj{ capture: str, start: int, end: int }]?", + "extern fun matchAllAgainst(subject: str) > [[obj{ capture: str, start: int, end: int }]]?", "extern fun replace(subject: str, with: str) > str", "extern fun replaceAll(subject: str, with: str) > str", } @@ -897,8 +897,8 @@ pub const ObjPattern = struct { pub const members_name = std.StaticStringMap(usize).initComptime( if (!is_wasm) .{ - .{ "match", 0 }, - .{ "matchAll", 1 }, + .{ "matchAgainst", 0 }, + .{ "matchAllAgainst", 1 }, .{ "replace", 2 }, .{ "replaceAll", 3 }, } @@ -4331,7 +4331,7 @@ pub const ObjTypeDef = struct { // WARN: order is important pub const Type = enum(u8) { Any, - Bool, + Boolean, Double, Integer, Pattern, @@ -4365,7 +4365,7 @@ pub const ObjTypeDef = struct { // Always keep types with void value first. pub const TypeUnion = union(Type) { Any: bool, // true if mutable - Bool: void, + Boolean: void, Double: void, Integer: void, Pattern: void, @@ -4465,7 +4465,7 @@ pub const ObjTypeDef = struct { } const result = switch (self.def_type) { - .Bool, + .Boolean, .Integer, .Double, .String, @@ -4990,7 +4990,7 @@ pub const ObjTypeDef = struct { }, ), .UserData => try writer.writeAll("ud"), - .Bool => try writer.writeAll("bool"), + .Boolean => try writer.writeAll("bool"), .Integer => try writer.writeAll("int"), .Double => try writer.writeAll("double"), .String => try writer.writeAll("str"), @@ -5427,7 +5427,7 @@ pub const ObjTypeDef = struct { } return switch (expected) { - .Bool, + .Boolean, .Integer, .Double, .String, @@ -5560,7 +5560,7 @@ pub const ObjTypeDef = struct { pub fn isConstant(self: *Self) bool { return switch (self.def_type) { - .Bool, + .Boolean, .Double, .Integer, .Pattern, diff --git a/src/renderer.zig b/src/renderer.zig index 97921e7f..782fd0cc 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -7,7 +7,7 @@ const Token = @import("Token.zig"); pub const Renderer = struct { const Self = @This(); - const equals: []const Token.Type = &.{ + const equals: []const Token.Tag = &.{ .Equal, .PlusEqual, .MinusEqual, @@ -116,7 +116,7 @@ pub const Renderer = struct { return Self.renderers[@intFromEnum(self.ast.nodes.items(.tag)[node])](self, node, space); } - const renderers = [_]RenderNode{ + const renderers = [@typeInfo(Ast.Node.Tag).@"enum".fields.len]RenderNode{ renderAnonymousObjectType, renderAnonymousEnumCase, renderAs, @@ -153,6 +153,7 @@ pub const Renderer = struct { renderListType, renderMap, renderMapType, + renderMatch, renderNamespace, renderNamedVariable, renderNull, @@ -194,7 +195,7 @@ pub const Renderer = struct { try self.renderExpectedToken(components.case_name, .Identifier, space); } - fn renderExpectedTokenSequence(self: *Self, start_token: Ast.TokenIndex, comptime expected: []const Token.Type, space: Space) Error!void { + fn renderExpectedTokenSequence(self: *Self, start_token: Ast.TokenIndex, comptime expected: []const Token.Tag, space: Space) Error!void { for (expected, 0..) |tag, offset| { try self.renderExpectedToken( start_token + @as(Ast.TokenIndex, @intCast(offset)), @@ -233,8 +234,8 @@ pub const Renderer = struct { std.debug.print("\n", .{}); } - fn renderOneOfExpectedToken(self: *Self, token: Ast.TokenIndex, comptime expected: []const Token.Type, space: Space) Error!void { - if (std.mem.indexOf(Token.Type, expected, &.{self.ast.tokens.items(.tag)[token]}) == null) { + fn renderOneOfExpectedToken(self: *Self, token: Ast.TokenIndex, comptime expected: []const Token.Tag, space: Space) Error!void { + if (std.mem.indexOf(Token.Tag, expected, &.{self.ast.tokens.items(.tag)[token]}) == null) { std.debug.print( "\nGot {s} at {} `{s}`\n", .{ @@ -247,11 +248,11 @@ pub const Renderer = struct { self.dumpTokens(token - 1, token + 5); } - assert(std.mem.indexOf(Token.Type, expected, &.{self.ast.tokens.items(.tag)[token]}) != null); + assert(std.mem.indexOf(Token.Tag, expected, &.{self.ast.tokens.items(.tag)[token]}) != null); return self.renderToken(token, space); } - fn renderExpectedToken(self: *Self, token: Ast.TokenIndex, expected: Token.Type, space: Space) Error!void { + fn renderExpectedToken(self: *Self, token: Ast.TokenIndex, expected: Token.Tag, space: Space) Error!void { if (builtin.mode == .Debug and self.ast.tokens.items(.tag)[token] != expected) { std.debug.print( "\nExpected {s} got {s} at {} `{s}`\n", @@ -2594,6 +2595,124 @@ pub const Renderer = struct { } } + fn renderMatch(self: *Self, node: Ast.Node.Index, space: Space) Error!void { + const locations = self.ast.nodes.items(.location); + const end_locations = self.ast.nodes.items(.end_location); + const components = self.ast.nodes.items(.components)[node].Match; + const tags = self.ast.tokens.items(.tag); + + // match + try self.renderExpectedToken( + locations[node], + .Match, + .Space, + ); + + try self.ais.pushIndent(self.allocator, .normal); + + // ( + try self.renderExpectedToken( + locations[node] + 1, + .LeftParen, + .None, + ); + + // value + try self.renderNode( + components.value, + .None, + ); + + // ) + try self.renderExpectedToken( + end_locations[components.value] + 1, + .RightParen, + .Space, + ); + + // { + try self.renderExpectedToken( + locations[node], + .LeftBrace, + .Newline, + ); + + for (components.branches, 0..) |branch, bidx| { + const last_token = end_locations[branch.conditions[branch.conditions.len - 1]] + 1; + const conditions_ends_with_comma = tags[last_token] == .Comma; + + // expr, expr, ... (if ends with a comma we get one per line) + for (branch.conditions, 0..) |condition, idx| { + try self.renderNode( + condition, + if (conditions_ends_with_comma) .Newline else .Space, + ); + + if (idx == branch.conditions.len - 1) { + if (conditions_ends_with_comma) { + try self.renderExpectedToken( + end_locations[condition] + 1, + .Comma, + .Newline, + ); + } + } else { + try self.renderExpectedToken( + end_locations[condition] + 1, + .Comma, + .Space, + ); + } + } + + // -> + try self.renderExpectedToken( + if (conditions_ends_with_comma) last_token + 1 else last_token, + .Arrow, + .Space, + ); + + // expr + try self.renderNode(branch.expression, .None); + + // , + const last_branch_token = end_locations[branch.expression] + 1; + if (bidx < components.branches.len - 1 or tags[last_branch_token] == .Comma) { + try self.renderExpectedToken( + last_branch_token, + .Comma, + .Newline, + ); + } + } + + if (components.else_branch) |eb| { + try self.renderExpectedToken( + locations[eb] - 2, + .Else, + .Space, + ); + try self.renderExpectedToken( + locations[eb] - 1, + .Arrow, + .Space, + ); + try self.renderNode(eb, .None); + } + + self.ais.popIndent(); + + // } + try self.renderExpectedToken( + if (components.else_branch) |eb| + end_locations[eb] + 1 + else + end_locations[components.branches[components.branches.len - 1].expression], + .RightBrace, + space, + ); + } + fn renderImport(self: *Self, node: Ast.Node.Index, space: Space) Error!void { const locations = self.ast.nodes.items(.location); const components = self.ast.nodes.items(.components)[node].Import; diff --git a/src/repl.zig b/src/repl.zig index a11488fd..d8b0c8ec 100644 --- a/src/repl.zig +++ b/src/repl.zig @@ -236,15 +236,18 @@ pub fn repl(process: std.process.Init, allocator: std.mem.Allocator) !void { // We might have declared new globals, types, etc. and encounter an error // FIXME: why can't I deinit those? // parser.globals.deinit(); - runner.parser.globals = previous_parser_globals; - runner.parser.globals_lookup = previous_globals_lookup; + runner.parser.globals = try previous_parser_globals.clone(allocator); + runner.parser.globals_lookup = try previous_globals_lookup.cloneContext( + allocator, + QualifiedNameContext{ .ast = &runner.parser.ast }, + ); // vm.globals.deinit(); - runner.vm.globals = previous_globals; + runner.vm.globals = try previous_globals.clone(allocator); runner.vm.globals_count = previous_global_top; // gc.type_registry.registry.deinit(); - runner.gc.type_registry.registry = previous_type_registry; + runner.gc.type_registry.registry = try previous_type_registry.clone(allocator); // If syntax error was unclosed block, keep previous input if (runner.parser.reporter.last_error == .unclosed) { diff --git a/src/value.zig b/src/value.zig index a57f8609..72ecc8ea 100644 --- a/src/value.zig +++ b/src/value.zig @@ -258,7 +258,7 @@ pub const Value = extern struct { } return switch (value.getTag()) { - TagBoolean => type_def.def_type == .Bool, + TagBoolean => type_def.def_type == .Boolean, // TODO: this one is ambiguous at runtime, is it the `null` constant? or an optional local with a null value? TagNull => type_def.def_type == .Void or type_def.optional, TagVoid => type_def.def_type == .Void, @@ -280,7 +280,7 @@ pub const Value = extern struct { } return switch (value.getTag()) { - TagBoolean => type_def.def_type == .Bool, + TagBoolean => type_def.def_type == .Boolean, // TODO: this one is ambiguous at runtime, is it the `null` constant? or an optional local with a null value? TagNull => type_def.def_type == .Void or type_def.optional, TagVoid => type_def.def_type == .Void, diff --git a/src/vm.zig b/src/vm.zig index 521d5967..6f4cf109 100644 --- a/src/vm.zig +++ b/src/vm.zig @@ -701,7 +701,7 @@ pub const VM = struct { const OpFn = *const fn (*Self, *CallFrame, u32, Chunk.OpCode, u24) void; // WARNING: same order as Chunk.OpCode enum - const op_table = [_]OpFn{ + const op_table = [@typeInfo(Chunk.OpCode).@"enum".fields.len]OpFn{ OP_CONSTANT, OP_NULL, OP_VOID, diff --git a/tests/behavior/match.buzz b/tests/behavior/match.buzz new file mode 100644 index 00000000..d7309fa3 --- /dev/null +++ b/tests/behavior/match.buzz @@ -0,0 +1,171 @@ +import "std"; + +object MatchBox { + value: int = 1, +} + +enum MatchEnum { + one, + two, +} + +fun statementMatch(value: int) > void { + match (value) { + 1, 2 -> std\assert(value < 3, message: "selected numbered branch"), + else -> std\assert(value >= 3, message: "selected else branch"), + } +} + +test "match expression and statement" { + final value = 2; + + std\assert( + match (value) { + 1 -> "one", + 2 -> "two", + else -> "other", + } == "two", + message: "match expression returns selected branch", + ); + + statementMatch(1); + statementMatch(3); +} + +test "match string and pattern" { + final string_value = "hello joe"; + final pattern_value = $"hello [a-z]+"; + + std\assert( + match (string_value) { + "hello ann" -> "string equality", + $"hello [a-z]+" -> "pattern match", + else -> "none", + } == "pattern match", + message: "string value can match pattern condition", + ); + + std\assert( + match (pattern_value) { + $"hello [a-z]+" -> "pattern equality", + else -> "none", + } == "pattern equality", + message: "pattern value can match pattern condition by equality", + ); + + std\assert( + match (pattern_value) { + $"bye [a-z]+" -> "pattern equality", + "hello joe" -> "string match", + else -> "none", + } == "string match", + message: "pattern value can match string condition", + ); +} + +test "match numbers and ranges" { + final int_value = 7; + final double_value = 3.14; + + std\assert( + match (int_value) { + 0..5 -> "low", + 5..10 -> "middle", + else -> "high", + } == "middle", + message: "integer value can match range condition", + ); + + std\assert( + match (double_value) { + 0..3 -> "low", + 3..4 -> "range", + else -> "none", + } == "range", + message: "double value can match range condition", + ); + + std\assert( + match (double_value) { + 1 -> "int", + 3.14 -> "double", + else -> "none", + } == "double", + message: "double value can match double condition", + ); +} + +test "match types" { + final string_type = ; + final int_type = ; + final box = MatchBox{}; + + std\assert( + match (string_type) { + -> "type equality", + "hello" -> "condition is matched type", + else -> "none", + } == "condition is matched type", + message: "type value can match condition using is", + ); + + std\assert( + match (int_type) { + -> "type equality", + "hello" -> "condition is matched type", + else -> "none", + } == "type equality", + message: "type value can match another type by equality", + ); + + std\assert( + match (box) { + -> "str", + -> "box", + else -> "none", + } == "box", + message: "value can match type condition using is", + ); +} + +test "match fallback equality and inference" { + final boolean_value = true; + final enum_value = MatchEnum.two; + final one = 1; + final zero = 0; + + std\assert( + match (boolean_value) { + true -> "yes", + false -> "no", + } == "yes", + message: "boolean match can be exhaustive without else", + ); + + std\assert( + match (enum_value) { + MatchEnum.one -> "one", + MatchEnum.two -> "two", + else -> "other", + } == "two", + message: "enum values match by equality", + ); + + final same_type = match (one) { + 1 -> "one", + else -> "other", + }; + std\assert(same_type is str, message: "same branch types infer same type"); + + final nullable = match (zero) { + 1 -> "one", + else -> null, + }; + std\assert(nullable is str?, message: "null branch infers optional type"); + + final mixed = match (one) { + 1 -> "one", + else -> 2, + }; + std\assert(mixed is str, message: "mixed branch types produce selected value"); +} diff --git a/tests/behavior/pattern.buzz b/tests/behavior/pattern.buzz index 7f1562f1..91ba885a 100644 --- a/tests/behavior/pattern.buzz +++ b/tests/behavior/pattern.buzz @@ -1,19 +1,19 @@ import "std"; -test "pattern.match" { +test "pattern.matchAgainst" { final pattern = $"hello ([a-z]+)"; - final results = pattern.match("All i want to say is hello joe! hello mundo!"); + final results = pattern.matchAgainst("All i want to say is hello joe! hello mundo!"); std\assert(results?.len() == 2, message: "1 match and 1 capture"); std\assert(results?[0].capture == "hello joe", message: "first is match"); std\assert(results?[1].capture == "joe", message: "second is capture"); } -test "pattern.matchAll" { +test "pattern.matchAllAgainst" { final pattern = $"hello ([a-z]+)"; - final results = pattern.matchAll("All i want to say is hello joe!\nhello mundo!\nAnd hello neighbor..."); + final results = pattern.matchAllAgainst("All i want to say is hello joe!\nhello mundo!\nAnd hello neighbor..."); std\assert(results?.len() == 3, message: "found 3 matches"); std\assert(results![2].len() == 2, message: "1 match and 1 capture"); diff --git a/tests/compile_errors/match-else-missing-arrow.buzz b/tests/compile_errors/match-else-missing-arrow.buzz new file mode 100644 index 00000000..97330e51 --- /dev/null +++ b/tests/compile_errors/match-else-missing-arrow.buzz @@ -0,0 +1,11 @@ +// Expected `->` after `else` +import "std"; + +test "match else missing arrow" { + std\assert( + match (1) { + 1 -> "one", + else "other", + } == "one", + ); +} diff --git a/tests/compile_errors/match-enum-non-exhaustive.buzz b/tests/compile_errors/match-enum-non-exhaustive.buzz new file mode 100644 index 00000000..3fe93318 --- /dev/null +++ b/tests/compile_errors/match-enum-non-exhaustive.buzz @@ -0,0 +1,14 @@ +// Non-exhaustive `match` over enum value, missing enum cases are: two, three +enum MatchExhaustive { + one, + two, + three, +} + +test "match enum non exhaustive" { + final value = MatchExhaustive.one; + + _ = match (value) { + MatchExhaustive.one -> "one", + }; +} diff --git a/tests/compile_errors/match-non-exhaustive.buzz b/tests/compile_errors/match-non-exhaustive.buzz new file mode 100644 index 00000000..2b41a1c2 --- /dev/null +++ b/tests/compile_errors/match-non-exhaustive.buzz @@ -0,0 +1,8 @@ +// Non-exhaustive `match`, `else` branch required +test "match non exhaustive" { + final value = 1; + + _ = match (value) { + 1 -> "one", + }; +} diff --git a/tests/compile_errors/match-number-condition-type.buzz b/tests/compile_errors/match-number-condition-type.buzz new file mode 100644 index 00000000..3ee1af02 --- /dev/null +++ b/tests/compile_errors/match-number-condition-type.buzz @@ -0,0 +1,11 @@ +// `match` condition must be of type `int`, `double`, `rg` or `type` +import "std"; + +test "match number condition type" { + std\assert( + match (1) { + "one" -> "string", + else -> "number", + } == "number", + ); +} diff --git a/tests/compile_errors/match-string-condition-type.buzz b/tests/compile_errors/match-string-condition-type.buzz new file mode 100644 index 00000000..a4792f95 --- /dev/null +++ b/tests/compile_errors/match-string-condition-type.buzz @@ -0,0 +1,11 @@ +// `match` condition must be of type `str`, `pat` or `type` +import "std"; + +test "match string condition type" { + std\assert( + match ("hello") { + 1 -> "number", + else -> "string", + } == "string", + ); +} diff --git a/tests/compile_errors/match-value-condition-type.buzz b/tests/compile_errors/match-value-condition-type.buzz new file mode 100644 index 00000000..3baa2fb9 --- /dev/null +++ b/tests/compile_errors/match-value-condition-type.buzz @@ -0,0 +1,11 @@ +// Bad `match` condition type +import "std"; + +test "match value condition type" { + std\assert( + match (true) { + "true" -> "string", + else -> "bool", + } == "bool", + ); +}