diff --git a/src/check/Check.zig b/src/check/Check.zig index 4be73fb003..a4414a6416 100644 --- a/src/check/Check.zig +++ b/src/check/Check.zig @@ -99,6 +99,8 @@ bool_var: Var, deferred_static_dispatch_constraints: DeferredConstraintCheck.SafeList, /// Used when looking up static dispatch functions static_dispatch_method_name_buf: std.ArrayList(u8), +/// Map representation of Ident -> Var, used in checking static dispatch constraints +ident_to_var_map: std.AutoHashMap(Ident.Idx, Var), /// A map of rigid variables that we build up during a branch of type checking const FreeVar = struct { ident: base.Ident.Idx, var_: Var }; @@ -159,6 +161,7 @@ pub fn init( .bool_var = undefined, // Will be initialized in copyBuiltinTypes() .deferred_static_dispatch_constraints = try DeferredConstraintCheck.SafeList.initCapacity(gpa, 128), .static_dispatch_method_name_buf = try std.ArrayList(u8).initCapacity(gpa, 32), + .ident_to_var_map = std.AutoHashMap(Ident.Idx, Var).init(gpa), }; } @@ -183,6 +186,7 @@ pub fn deinit(self: *Self) void { self.constraint_origins.deinit(); self.deferred_static_dispatch_constraints.deinit(self.gpa); self.static_dispatch_method_name_buf.deinit(self.gpa); + self.ident_to_var_map.deinit(); } /// Assert that type vars and regions in sync @@ -3749,22 +3753,70 @@ fn checkDeferredStaticDispatchConstraints(self: *Self) std.mem.Allocator.Error!v const dispatcher_content = dispatcher_resolved.desc.content; if (dispatcher_content == .err) { + // If the root type is an error, then skip constraint checking - // Iterate over the constraints const constraints = self.types.sliceStaticDispatchConstraints(deferred_constraint.constraints); for (constraints) |constraint| { + try self.markConstraintFunctionAsError(constraint); + } + try self.updateVar(deferred_constraint.var_, .err, Rank.generalized); + } else if (dispatcher_content == .rigid) { + // Get the rigid variable and the constraints it has defined + const rigid = dispatcher_content.rigid; + const rigid_constraints = self.types.sliceStaticDispatchConstraints(rigid.constraints); + + // Get the deferred constraints to validate against + const deferred_constraints = self.types.sliceStaticDispatchConstraints(deferred_constraint.constraints); + + // First, special case if this rigid has no constraints + if (deferred_constraints.len > 0 and rigid_constraints.len == 0) { + const constraint = deferred_constraints[0]; + try self.reportConstraintError( + deferred_constraint.var_, + constraint, + .{ .missing_method = .rigid }, + ); + continue; + } + + // Build a map of constraints the rigid has + self.ident_to_var_map.clearRetainingCapacity(); + try self.ident_to_var_map.ensureUnusedCapacity(@intCast(rigid_constraints.len)); + for (rigid_constraints) |rigid_constraint| { + self.ident_to_var_map.putAssumeCapacity(rigid_constraint.fn_name, rigid_constraint.fn_var); + } + + // Iterate over the constraints + for (deferred_constraints) |constraint| { // Extract the function and return type from the constraint const resolved_constraint = self.types.resolveVar(constraint.fn_var); const mb_resolved_func = resolved_constraint.desc.content.unwrapFunc(); std.debug.assert(mb_resolved_func != null); const resolved_func = mb_resolved_func.?; - // Set it to be an error - try self.updateVar(resolved_func.ret, .err, Rank.generalized); + // Then, lookup the inferred constraint in the actual list of rigid constraints + if (self.ident_to_var_map.get(constraint.fn_name)) |rigid_var| { + // Unify the actual function var against the inferred var + // + // TODO: For better error messages, we should check if these + // types are functions, unify each arg, etc. This should look + // similar to e_call + const result = try self.unify(rigid_var, constraint.fn_var, Rank.generalized); + if (result.isProblem()) { + try self.updateVar(deferred_constraint.var_, .err, Rank.generalized); + try self.updateVar(resolved_func.ret, .err, Rank.generalized); + } + } else { + try self.reportConstraintError( + deferred_constraint.var_, + constraint, + .{ .missing_method = .nominal }, + ); + continue; + } } - } else if (dispatcher_content == .rigid or dispatcher_content == .flex) { - // If the root type is an flex or rigid, then we there's nothing to check - // since the type is not concrete + } else if (dispatcher_content == .flex) { + // If the root type is aa flex, then we there's nothing to check continue; } else if (dispatcher_content == .structure and dispatcher_content.structure == .nominal_type) { // TODO: Internal types like Str, Result, List, etc are not @@ -3822,36 +3874,24 @@ fn checkDeferredStaticDispatchConstraints(self: *Self) std.mem.Allocator.Error!v // Get the ident of this method in the original env const ident_in_original_env = original_env.getIdentStoreConst().findByString(qualified_name_bytes) orelse { - const snapshot = try self.snapshots.deepCopyVar(self.types, deferred_constraint.var_); - _ = try self.problems.appendProblem(self.cir.gpa, .{ .static_dispach = .{ - .dispatcher_does_not_impl_method = .{ - .dispatcher_var = deferred_constraint.var_, - .dispatcher_snapshot = snapshot, - .fn_var = constraint.fn_var, - .method_name = constraint.fn_name, - }, - } }); - try self.updateVar(resolved_func.ret, .err, Rank.generalized); + try self.reportConstraintError( + deferred_constraint.var_, + constraint, + .{ .missing_method = .nominal }, + ); continue; }; // Get the def index in the original env const node_idx_in_original_env = original_env.getExposedNodeIndexById(ident_in_original_env) orelse { - // This can happen if somehow, the original module has - // an ident that matches the method/type, but it doesn't - // actually have/expose the method. This should be - // impossible, but we handle it gracefully - - const snapshot = try self.snapshots.deepCopyVar(self.types, deferred_constraint.var_); - _ = try self.problems.appendProblem(self.cir.gpa, .{ .static_dispach = .{ - .dispatcher_does_not_impl_method = .{ - .dispatcher_var = deferred_constraint.var_, - .dispatcher_snapshot = snapshot, - .fn_var = constraint.fn_var, - .method_name = constraint.fn_name, - }, - } }); - try self.updateVar(resolved_func.ret, .err, Rank.generalized); + // This can happen if the original module has an ident that + // matches the method/type, but it doesn't actually have + // that method. + try self.reportConstraintError( + deferred_constraint.var_, + constraint, + .{ .missing_method = .nominal }, + ); continue; }; const def_idx: Var = @enumFromInt(@as(u32, @intCast(node_idx_in_original_env))); @@ -3869,6 +3909,7 @@ fn checkDeferredStaticDispatchConstraints(self: *Self) std.mem.Allocator.Error!v // similar to e_call const result = try self.unify(real_method_var, constraint.fn_var, Rank.generalized); if (result.isProblem()) { + try self.updateVar(deferred_constraint.var_, .err, Rank.generalized); try self.updateVar(resolved_func.ret, .err, Rank.generalized); } } @@ -3877,27 +3918,11 @@ fn checkDeferredStaticDispatchConstraints(self: *Self) std.mem.Allocator.Error!v const constraints = self.types.sliceStaticDispatchConstraints(deferred_constraint.constraints); if (constraints.len > 0) { - const constraint = constraints[0]; - - // Snapshot the constraint and add a problem - const snapshot = try self.snapshots.deepCopyVar(self.types, deferred_constraint.var_); - _ = try self.problems.appendProblem(self.cir.gpa, .{ .static_dispach = .{ - .dispatcher_not_nominal = .{ - .dispatcher_var = deferred_constraint.var_, - .dispatcher_snapshot = snapshot, - .fn_var = constraint.fn_var, - .method_name = constraint.fn_name, - }, - } }); - - // Extract the function and return type from the constraint - const resolved_constraint = self.types.resolveVar(constraint.fn_var); - const mb_resolved_func = resolved_constraint.desc.content.unwrapFunc(); - std.debug.assert(mb_resolved_func != null); - const resolved_func = mb_resolved_func.?; - - // Set it to be an error - try self.updateVar(resolved_func.ret, .err, Rank.generalized); + try self.reportConstraintError( + deferred_constraint.var_, + constraints[0], + .not_nominal, + ); } else { // It should be impossible to have a deferred constraint check // that has no constraints. @@ -3906,3 +3931,49 @@ fn checkDeferredStaticDispatchConstraints(self: *Self) std.mem.Allocator.Error!v } } } + +/// Mark a constraint function's return type as error +fn markConstraintFunctionAsError(self: *Self, constraint: StaticDispatchConstraint) !void { + const resolved_constraint = self.types.resolveVar(constraint.fn_var); + const mb_resolved_func = resolved_constraint.desc.content.unwrapFunc(); + std.debug.assert(mb_resolved_func != null); + const resolved_func = mb_resolved_func.?; + try self.updateVar(resolved_func.ret, .err, Rank.generalized); +} + +/// Report a constraint validation error +fn reportConstraintError( + self: *Self, + dispatcher_var: Var, + constraint: StaticDispatchConstraint, + kind: union(enum) { + missing_method: problem.DispatcherDoesNotImplMethod.DispatcherType, + not_nominal, + }, +) !void { + const snapshot = try self.snapshots.deepCopyVar(self.types, dispatcher_var); + + const constraint_problem = switch (kind) { + .missing_method => |dispatcher_type| problem.Problem{ .static_dispach = .{ + .dispatcher_does_not_impl_method = .{ + .dispatcher_var = dispatcher_var, + .dispatcher_snapshot = snapshot, + .dispatcher_type = dispatcher_type, + .fn_var = constraint.fn_var, + .method_name = constraint.fn_name, + }, + } }, + .not_nominal => problem.Problem{ .static_dispach = .{ + .dispatcher_not_nominal = .{ + .dispatcher_var = dispatcher_var, + .dispatcher_snapshot = snapshot, + .fn_var = constraint.fn_var, + .method_name = constraint.fn_name, + }, + } }, + }; + + _ = try self.problems.appendProblem(self.cir.gpa, constraint_problem); + try self.markConstraintFunctionAsError(constraint); + try self.updateVar(dispatcher_var, .err, Rank.generalized); +} diff --git a/src/check/problem.zig b/src/check/problem.zig index 39ca342088..bab4d8c346 100644 --- a/src/check/problem.zig +++ b/src/check/problem.zig @@ -222,8 +222,12 @@ pub const DispatcherNotNominal = struct { pub const DispatcherDoesNotImplMethod = struct { dispatcher_var: Var, dispatcher_snapshot: SnapshotContentIdx, + dispatcher_type: DispatcherType, fn_var: Var, method_name: Ident.Idx, + + /// Type of the dispatcher + pub const DispatcherType = enum { nominal, rigid }; }; // bug // @@ -1695,9 +1699,18 @@ pub const ReportBuilder = struct { try report.document.addLineBreak(); try report.document.addLineBreak(); try report.document.addAnnotated("Hint:", .emphasized); - try report.document.addReflowingText(" Did you forget to define "); - try report.document.addAnnotated(method_name_str, .emphasized); - try report.document.addReflowingText(" in the type's method block?"); + switch (data.dispatcher_type) { + .nominal => { + try report.document.addReflowingText(" Did you forget to define "); + try report.document.addAnnotated(method_name_str, .emphasized); + try report.document.addReflowingText(" in the type's method block?"); + }, + .rigid => { + try report.document.addReflowingText(" Did you forget to specify "); + try report.document.addAnnotated(method_name_str, .emphasized); + try report.document.addReflowingText(" in the type annotation?"); + }, + } return report; } diff --git a/src/check/test/type_checking_integration.zig b/src/check/test/type_checking_integration.zig index 5ae3be5469..2a13af0efe 100644 --- a/src/check/test/type_checking_integration.zig +++ b/src/check/test/type_checking_integration.zig @@ -1218,6 +1218,23 @@ test "check type - static dispatch - concrete - indirection 2" { ); } +test "check type - static dispatch - fail if not in type signature" { + const source = + \\module [] + \\ + \\main : a -> a + \\main = |a| { + \\ _val = a.method() + \\ a + \\} + ; + try checkTypesModule( + source, + .fail, + "MISSING METHOD", + ); +} + // helpers - module // const ModuleExpectation = union(enum) { diff --git a/src/check/test/unify_test.zig b/src/check/test/unify_test.zig index 72b414d2a0..9059cd1f80 100644 --- a/src/check/test/unify_test.zig +++ b/src/check/test/unify_test.zig @@ -4031,7 +4031,7 @@ test "unify - flex with subset of constraints (a subset b)" { try std.testing.expectEqual(2, result_constraints.len()); } -test "unify - flex with constraints vs rigid with subset constraints" { +test "unify - flex with constraints vs rigid with constraints" { const gpa = std.testing.allocator; var env = try TestEnv.init(gpa); defer env.deinit(); @@ -4041,13 +4041,15 @@ test "unify - flex with constraints vs rigid with subset constraints" { // flex has 2 constraints const foo_fn = try env.module_env.types.freshFromContent(try env.mkFuncPure(&[_]Var{str}, str)); + const foo_ident = try env.module_env.getIdentStore().insert(env.module_env.gpa, Ident.for_text("foo")); const foo_constraint = types_mod.StaticDispatchConstraint{ - .fn_name = try env.module_env.getIdentStore().insert(env.module_env.gpa, Ident.for_text("foo")), + .fn_name = foo_ident, .fn_var = foo_fn, }; const bar_fn = try env.module_env.types.freshFromContent(try env.mkFuncPure(&[_]Var{int}, int)); + const bar_ident = try env.module_env.getIdentStore().insert(env.module_env.gpa, Ident.for_text("bar")); const bar_constraint = types_mod.StaticDispatchConstraint{ - .fn_name = try env.module_env.getIdentStore().insert(env.module_env.gpa, Ident.for_text("bar")), + .fn_name = bar_ident, .fn_var = bar_fn, }; const flex_constraints = try env.module_env.types.appendStaticDispatchConstraints(&[_]types_mod.StaticDispatchConstraint{ foo_constraint, bar_constraint }); @@ -4066,10 +4068,20 @@ test "unify - flex with constraints vs rigid with subset constraints" { } }); const result = try env.unify(flex_var, rigid_var); - try std.testing.expectEqual(false, result.isOk()); + try std.testing.expectEqual(true, result.isOk()); + + try std.testing.expectEqual(1, env.scratch.deferred_constraints.len()); + const deferred = env.scratch.deferred_constraints.get(@enumFromInt(0)); + try std.testing.expectEqual(rigid_var, deferred.var_); + + try std.testing.expectEqual(2, deferred.constraints.len()); + const constraint1 = env.module_env.types.static_dispatch_constraints.get(@enumFromInt(0)); + try std.testing.expectEqual(foo_ident, constraint1.fn_name); + const constraint2 = env.module_env.types.static_dispatch_constraints.get(@enumFromInt(1)); + try std.testing.expectEqual(bar_ident, constraint2.fn_name); } -test "unify - flex with constraints vs rigid with superset constraints" { +test "unify - flex with constraints vs rigid constraints 2" { const gpa = std.testing.allocator; var env = try TestEnv.init(gpa); defer env.deinit(); @@ -4079,8 +4091,9 @@ test "unify - flex with constraints vs rigid with superset constraints" { // flex has 1 constraint const foo_fn = try env.module_env.types.freshFromContent(try env.mkFuncPure(&[_]Var{str}, str)); + const foo_ident = try env.module_env.getIdentStore().insert(env.module_env.gpa, Ident.for_text("foo")); const foo_constraint = types_mod.StaticDispatchConstraint{ - .fn_name = try env.module_env.getIdentStore().insert(env.module_env.gpa, Ident.for_text("foo")), + .fn_name = foo_ident, .fn_var = foo_fn, }; const flex_constraints = try env.module_env.types.appendStaticDispatchConstraints(&[_]types_mod.StaticDispatchConstraint{foo_constraint}); @@ -4105,6 +4118,13 @@ test "unify - flex with constraints vs rigid with superset constraints" { const result = try env.unify(flex_var, rigid_var); try std.testing.expectEqual(.ok, result); + + try std.testing.expectEqual(1, env.scratch.deferred_constraints.len()); + const deferred = env.scratch.deferred_constraints.get(@enumFromInt(0)); + try std.testing.expectEqual(rigid_var, deferred.var_); + try std.testing.expectEqual(1, deferred.constraints.len()); + const constraint = env.module_env.types.static_dispatch_constraints.get(@enumFromInt(0)); + try std.testing.expectEqual(foo_ident, constraint.fn_name); } test "unify - empty constraints unify with any" { diff --git a/src/check/unify.zig b/src/check/unify.zig index 96473915af..c7dd1269d8 100644 --- a/src/check/unify.zig +++ b/src/check/unify.zig @@ -533,18 +533,22 @@ const Unifier = struct { } }; - const merged_constraints = try self.unifyStaticDispatchConstraints(a_flex.constraints, b_flex.constraints, .union_all); + const merged_constraints = try self.unifyStaticDispatchConstraints(a_flex.constraints, b_flex.constraints); self.merge(vars, Content{ .flex = .{ .name = mb_ident, .constraints = merged_constraints, } }); }, .rigid => |b_rigid| { - const merged_constraints = try self.unifyStaticDispatchConstraints(a_flex.constraints, b_rigid.constraints, .a_subset_b); - self.merge(vars, Content{ .rigid = .{ - .name = b_rigid.name, - .constraints = merged_constraints, - } }); + if (a_flex.constraints.len() > 0) { + // Record that we need to check constraints later + _ = self.scratch.deferred_constraints.append(self.scratch.gpa, DeferredConstraintCheck{ + .var_ = vars.b.var_, // Since the vars are merge, we arbitrary choose b + .constraints = a_flex.constraints, + }) catch return Error.AllocatorError; + } + + self.merge(vars, .{ .rigid = b_rigid }); }, .alias => |b_alias| { if (a_flex.constraints.len() == 0) { @@ -579,11 +583,15 @@ const Unifier = struct { switch (b_content) { .flex => |b_flex| { - const merged_constraints = try self.unifyStaticDispatchConstraints(a_rigid.constraints, b_flex.constraints, .b_subset_a); - self.merge(vars, Content{ .rigid = .{ - .name = a_rigid.name, - .constraints = merged_constraints, - } }); + if (b_flex.constraints.len() > 0) { + // Record that we need to check constraints later + _ = self.scratch.deferred_constraints.append(self.scratch.gpa, DeferredConstraintCheck{ + .var_ = vars.b.var_, // Since the vars are merge, we arbitrary choose b + .constraints = b_flex.constraints, + }) catch return Error.AllocatorError; + } + + self.merge(vars, .{ .rigid = a_rigid }); }, .rigid => return error.TypeMismatch, .alias => return error.TypeMismatch, @@ -2897,54 +2905,26 @@ const Unifier = struct { // constraints // - const ConstraintMergeStrategy = enum { - /// Take union of all constraints (flex + flex) - union_all, - /// Require a ⊆ b, return b's constraints (flex + rigid) - a_subset_b, - /// Require b ⊆ a, return a's constraints (rigid + flex) - b_subset_a, - }; - fn unifyStaticDispatchConstraints( self: *Self, a_constraints: StaticDispatchConstraint.SafeList.Range, b_constraints: StaticDispatchConstraint.SafeList.Range, - strategy: ConstraintMergeStrategy, ) Error!StaticDispatchConstraint.SafeList.Range { const a_len = a_constraints.len(); const b_len = b_constraints.len(); // Early exits for empty ranges if (a_len == 0 and b_len == 0) { - return StaticDispatchConstraint.SafeList.Range.empty(); - } - if (a_len == 0) return if (strategy == .b_subset_a) a_constraints else b_constraints; - if (b_len == 0) return if (strategy == .a_subset_b) b_constraints else a_constraints; - - // Subset validation - switch (strategy) { - .a_subset_b => if (a_len > b_len) return error.TypeMismatch, - .b_subset_a => if (b_len > a_len) return error.TypeMismatch, - .union_all => {}, + return .empty(); + } else if (a_len == 0 and b_len > 0) { + return b_constraints; + } else if (a_len > 0 and b_len == 0) { + return a_constraints; } // Partition constraints const partitioned = self.partitionStaticDispatchConstraints(a_constraints, b_constraints) catch return Error.AllocatorError; - // Check subset requirements - switch (strategy) { - .a_subset_b => if (partitioned.only_in_a.len() > 0) { - // TODO: Throw custom error message - return error.TypeMismatch; - }, - .b_subset_a => if (partitioned.only_in_b.len() > 0) { - // TODO: Throw custom error message - return error.TypeMismatch; - }, - .union_all => {}, - } - // Unify shared constraints if (partitioned.in_both.len() > 0) { for (self.scratch.in_both_static_dispatch_constraints.sliceRange(partitioned.in_both)) |two_constraints| { @@ -2953,45 +2933,24 @@ const Unifier = struct { } } - // Build result based on strategy const top: u32 = @intCast(self.types_store.static_dispatch_constraints.len()); - const capacity = partitioned.in_both.len() + switch (strategy) { - .union_all => partitioned.only_in_a.len() + partitioned.only_in_b.len(), - .a_subset_b => partitioned.only_in_b.len(), - .b_subset_a => partitioned.only_in_a.len(), - }; - + // Ensure we have enough memory for the new contiguous list + const capacity = partitioned.in_both.len() + partitioned.only_in_a.len() + partitioned.only_in_b.len(); self.types_store.static_dispatch_constraints.items.ensureUnusedCapacity( self.types_store.gpa, capacity, ) catch return Error.AllocatorError; - // Always append shared constraints (using b's version) for (self.scratch.in_both_static_dispatch_constraints.sliceRange(partitioned.in_both)) |two_constraints| { + // Here, we append the constraint's b, but since a & b, it doesn't actually matter self.types_store.static_dispatch_constraints.items.appendAssumeCapacity(two_constraints.b); } - - // Append unique constraints based on strategy - switch (strategy) { - .union_all => { - for (self.scratch.only_in_a_static_dispatch_constraints.sliceRange(partitioned.only_in_a)) |only_a| { - self.types_store.static_dispatch_constraints.items.appendAssumeCapacity(only_a); - } - for (self.scratch.only_in_b_static_dispatch_constraints.sliceRange(partitioned.only_in_b)) |only_b| { - self.types_store.static_dispatch_constraints.items.appendAssumeCapacity(only_b); - } - }, - .a_subset_b => { - for (self.scratch.only_in_b_static_dispatch_constraints.sliceRange(partitioned.only_in_b)) |only_b| { - self.types_store.static_dispatch_constraints.items.appendAssumeCapacity(only_b); - } - }, - .b_subset_a => { - for (self.scratch.only_in_a_static_dispatch_constraints.sliceRange(partitioned.only_in_a)) |only_a| { - self.types_store.static_dispatch_constraints.items.appendAssumeCapacity(only_a); - } - }, + for (self.scratch.only_in_a_static_dispatch_constraints.sliceRange(partitioned.only_in_a)) |only_a| { + self.types_store.static_dispatch_constraints.items.appendAssumeCapacity(only_a); + } + for (self.scratch.only_in_b_static_dispatch_constraints.sliceRange(partitioned.only_in_b)) |only_b| { + self.types_store.static_dispatch_constraints.items.appendAssumeCapacity(only_b); } return self.types_store.static_dispatch_constraints.rangeToEnd(top); diff --git a/test/snapshots/plume_package/Color.md b/test/snapshots/plume_package/Color.md index df4078c2ac..44f7f8b3a1 100644 --- a/test/snapshots/plume_package/Color.md +++ b/test/snapshots/plume_package/Color.md @@ -86,11 +86,6 @@ TYPE MISMATCH - Color.md:51:104:51:105 TYPE DOES NOT HAVE METHODS - Color.md:22:15:22:26 TYPE DOES NOT HAVE METHODS - Color.md:29:13:29:26 TYPE DOES NOT HAVE METHODS - Color.md:35:17:35:41 -TYPE DOES NOT HAVE METHODS - Color.md:36:21:36:45 -TYPE DOES NOT HAVE METHODS - Color.md:37:21:37:45 -TYPE DOES NOT HAVE METHODS - Color.md:38:21:38:45 -TYPE DOES NOT HAVE METHODS - Color.md:39:21:39:45 -TYPE DOES NOT HAVE METHODS - Color.md:40:21:40:45 TYPE DOES NOT HAVE METHODS - Color.md:62:8:62:28 # PROBLEMS **MODULE HEADER DEPRECATED** @@ -204,56 +199,6 @@ You're trying to call the `is_char_in_hex_range` method on a `Num(Int(_size))`: But `Num(Int(_size))` doesn't support methods. -**TYPE DOES NOT HAVE METHODS** -You're trying to call the `is_char_in_hex_range` method on a `Num(Int(_size))`: -**Color.md:36:21:36:45:** -```roc - and b.is_char_in_hex_range() -``` - ^^^^^^^^^^^^^^^^^^^^^^^^ - -But `Num(Int(_size))` doesn't support methods. - -**TYPE DOES NOT HAVE METHODS** -You're trying to call the `is_char_in_hex_range` method on a `Num(Int(_size))`: -**Color.md:37:21:37:45:** -```roc - and c.is_char_in_hex_range() -``` - ^^^^^^^^^^^^^^^^^^^^^^^^ - -But `Num(Int(_size))` doesn't support methods. - -**TYPE DOES NOT HAVE METHODS** -You're trying to call the `is_char_in_hex_range` method on a `Num(Int(_size))`: -**Color.md:38:21:38:45:** -```roc - and d.is_char_in_hex_range() -``` - ^^^^^^^^^^^^^^^^^^^^^^^^ - -But `Num(Int(_size))` doesn't support methods. - -**TYPE DOES NOT HAVE METHODS** -You're trying to call the `is_char_in_hex_range` method on a `Num(Int(_size))`: -**Color.md:39:21:39:45:** -```roc - and e.is_char_in_hex_range() -``` - ^^^^^^^^^^^^^^^^^^^^^^^^ - -But `Num(Int(_size))` doesn't support methods. - -**TYPE DOES NOT HAVE METHODS** -You're trying to call the `is_char_in_hex_range` method on a `Num(Int(_size))`: -**Color.md:40:21:40:45:** -```roc - and f.is_char_in_hex_range() -``` - ^^^^^^^^^^^^^^^^^^^^^^^^ - -But `Num(Int(_size))` doesn't support methods. - **TYPE DOES NOT HAVE METHODS** You're trying to call the `is_named_color` method on a `Str`: **Color.md:62:8:62:28:**