Merge pull request #8335 from roc-lang/jared/push-rlpsnsmkslnm

Fix rigid type static dispatch validation
This commit is contained in:
Luke Boswell 2025-10-27 09:33:44 +11:00 committed by GitHub
commit e89ca2d318
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 214 additions and 189 deletions

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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) {

View file

@ -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" {

View file

@ -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);

View file

@ -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:**