rustc: Move unification out of typeck.rs; trans will need it too.

This commit is contained in:
Patrick Walton 2010-12-21 17:47:13 -08:00
parent e06263ff4b
commit 0411132679
2 changed files with 452 additions and 414 deletions

View File

@ -1,5 +1,8 @@
import std._str;
import std._uint;
import std._vec;
import std.map;
import std.map.hashmap;
import std.option;
import std.option.none;
import std.option.some;
@ -8,6 +11,7 @@ import driver.session;
import front.ast;
import front.ast.mutability;
import util.common;
import util.common.append;
import util.common.span;
// Data types
@ -40,6 +44,30 @@ tag sty {
// TODO: ty_fn_arg(@t), for a possibly-aliased function argument
}
// Data structures used in type unification
type unify_handler = obj {
fn resolve_local(ast.def_id id) -> @t;
fn record_local(ast.def_id id, @t ty);
fn unify_expected_param(ast.def_id id, @t expected, @t actual)
-> unify_result;
};
tag type_err {
terr_mismatch;
terr_tuple_size(uint, uint);
terr_tuple_mutability;
terr_record_size(uint, uint);
terr_record_mutability;
terr_record_fields(ast.ident,ast.ident);
terr_arg_count;
}
tag unify_result {
ures_ok(@ty.t);
ures_err(type_err, @ty.t, @ty.t);
}
// Stringification
fn ast_ty_to_str(&@ast.ty ty) -> str {
@ -586,3 +614,400 @@ fn is_lval(@ast.expr expr) -> bool {
}
}
// Type unification
fn unify(@ty.t expected, @ty.t actual, &unify_handler handler)
-> unify_result {
// Wraps the given type in an appropriate cname.
//
// TODO: This doesn't do anything yet. We should carry the cname up from
// the expected and/or actual types when unification results in a type
// identical to one or both of the two. The precise algorithm for this is
// something we'll probably need to develop over time.
// Simple structural type comparison.
fn struct_cmp(@ty.t expected, @ty.t actual) -> unify_result {
if (expected.struct == actual.struct) {
ret ures_ok(expected);
}
ret ures_err(terr_mismatch, expected, actual);
}
fn unify_step(&hashmap[int,@ty.t] bindings, @ty.t expected, @ty.t actual,
&unify_handler handler) -> unify_result {
// TODO: rewrite this using tuple pattern matching when available, to
// avoid all this rightward drift and spikiness.
// If the RHS is a variable type, then just do the appropriate
// binding.
alt (actual.struct) {
case (ty.ty_var(?actual_id)) {
alt (bindings.find(actual_id)) {
case (some[@ty.t](?actual_ty)) {
// FIXME: change the binding here?
// FIXME: "be"
ret unify_step(bindings, expected, actual_ty,
handler);
}
case (none[@ty.t]) {
bindings.insert(actual_id, expected);
ret ures_ok(expected);
}
}
}
case (ty.ty_local(?actual_id)) {
auto actual_ty = handler.resolve_local(actual_id);
auto result = unify_step(bindings,
expected,
actual_ty,
handler);
alt (result) {
case (ures_ok(?result_ty)) {
handler.record_local(actual_id, result_ty);
}
case (_) { /* empty */ }
}
ret result;
}
case (_) { /* empty */ }
}
alt (expected.struct) {
case (ty.ty_nil) { ret struct_cmp(expected, actual); }
case (ty.ty_bool) { ret struct_cmp(expected, actual); }
case (ty.ty_int) { ret struct_cmp(expected, actual); }
case (ty.ty_uint) { ret struct_cmp(expected, actual); }
case (ty.ty_machine(_)) { ret struct_cmp(expected, actual); }
case (ty.ty_char) { ret struct_cmp(expected, actual); }
case (ty.ty_str) { ret struct_cmp(expected, actual); }
case (ty.ty_tag(?expected_id)) {
alt (actual.struct) {
case (ty.ty_tag(?actual_id)) {
if (expected_id._0 == actual_id._0 &&
expected_id._1 == actual_id._1) {
ret ures_ok(expected);
}
}
case (_) { /* fall through */ }
}
ret ures_err(terr_mismatch, expected, actual);
}
case (ty.ty_box(?expected_sub)) {
alt (actual.struct) {
case (ty.ty_box(?actual_sub)) {
auto result = unify_step(bindings,
expected_sub,
actual_sub,
handler);
alt (result) {
case (ures_ok(?result_sub)) {
ret ures_ok(plain_ty(ty.ty_box(result_sub)));
}
case (_) {
ret result;
}
}
}
// TODO: ty_var
case (_) {
ret ures_err(terr_mismatch, expected, actual);
}
}
}
case (ty.ty_vec(?expected_sub)) {
alt (actual.struct) {
case (ty.ty_vec(?actual_sub)) {
auto result = unify_step(bindings,
expected_sub,
actual_sub,
handler);
alt (result) {
case (ures_ok(?result_sub)) {
ret ures_ok(plain_ty(ty.ty_vec(result_sub)));
}
case (_) {
ret result;
}
}
}
// TODO: ty_var
case (_) {
ret ures_err(terr_mismatch, expected, actual);
}
}
}
case (ty.ty_tup(?expected_elems)) {
alt (actual.struct) {
case (ty.ty_tup(?actual_elems)) {
auto expected_len = _vec.len[@ty.t](expected_elems);
auto actual_len = _vec.len[@ty.t](actual_elems);
if (expected_len != actual_len) {
auto err = terr_tuple_size(expected_len,
actual_len);
ret ures_err(err, expected, actual);
}
// TODO: implement an iterator that can iterate over
// two arrays simultaneously.
let vec[@ty.t] result_elems = vec();
auto i = 0u;
while (i < expected_len) {
auto expected_elem = expected_elems.(i);
auto actual_elem = actual_elems.(i);
if (expected_elem.mut != actual_elem.mut) {
auto err = terr_tuple_mutability;
ret ures_err(err, expected, actual);
}
auto result = unify_step(bindings,
expected_elem,
actual_elem,
handler);
alt (result) {
case (ures_ok(?rty)) {
append[@ty.t](result_elems,rty);
}
case (_) {
ret result;
}
}
i += 1u;
}
ret ures_ok(plain_ty(ty.ty_tup(result_elems)));
}
// TODO: ty_var
case (_) {
ret ures_err(terr_mismatch, expected, actual);
}
}
}
case (ty.ty_rec(?expected_fields)) {
alt (actual.struct) {
case (ty.ty_rec(?actual_fields)) {
auto expected_len = _vec.len[field](expected_fields);
auto actual_len = _vec.len[field](actual_fields);
if (expected_len != actual_len) {
auto err = terr_record_size(expected_len,
actual_len);
ret ures_err(err, expected, actual);
}
// TODO: implement an iterator that can iterate over
// two arrays simultaneously.
let vec[field] result_fields = vec();
auto i = 0u;
while (i < expected_len) {
auto expected_field = expected_fields.(i);
auto actual_field = actual_fields.(i);
if (expected_field.ty.mut
!= actual_field.ty.mut) {
auto err = terr_record_mutability;
ret ures_err(err, expected, actual);
}
if (!_str.eq(expected_field.ident,
actual_field.ident)) {
auto err =
terr_record_fields(expected_field.ident,
actual_field.ident);
ret ures_err(err, expected, actual);
}
auto result = unify_step(bindings,
expected_field.ty,
actual_field.ty,
handler);
alt (result) {
case (ures_ok(?rty)) {
append[field]
(result_fields,
rec(ty=rty with expected_field));
}
case (_) {
ret result;
}
}
i += 1u;
}
ret ures_ok(plain_ty(ty.ty_rec(result_fields)));
}
// TODO: ty_var
case (_) {
ret ures_err(terr_mismatch, expected, actual);
}
}
}
case (ty.ty_fn(?expected_inputs, ?expected_output)) {
alt (actual.struct) {
case (ty.ty_fn(?actual_inputs, ?actual_output)) {
auto expected_len = _vec.len[arg](expected_inputs);
auto actual_len = _vec.len[arg](actual_inputs);
if (expected_len != actual_len) {
ret ures_err(terr_arg_count, expected, actual);
}
// TODO: as above, we should have an iter2 iterator.
let vec[arg] result_ins = vec();
auto i = 0u;
while (i < expected_len) {
auto expected_input = expected_inputs.(i);
auto actual_input = actual_inputs.(i);
// This should be safe, I think?
auto result_mode;
if (mode_is_alias(expected_input.mode) ||
mode_is_alias(actual_input.mode)) {
result_mode = ast.alias;
} else {
result_mode = ast.val;
}
auto result = unify_step(bindings,
actual_input.ty,
expected_input.ty,
handler);
alt (result) {
case (ures_ok(?rty)) {
result_ins += vec(rec(mode=result_mode,
ty=rty));
}
case (_) {
ret result;
}
}
i += 1u;
}
// Check the output.
auto result_out;
auto result = unify_step(bindings,
expected_output,
actual_output,
handler);
alt (result) {
case (ures_ok(?rty)) {
result_out = rty;
}
case (_) {
ret result;
}
}
auto t = plain_ty(ty.ty_fn(result_ins, result_out));
ret ures_ok(t);
}
case (_) {
ret ures_err(terr_mismatch, expected, actual);
}
}
}
case (ty.ty_var(?expected_id)) {
alt (bindings.find(expected_id)) {
case (some[@ty.t](?expected_ty)) {
// FIXME: change the binding here?
// FIXME: "be"
ret unify_step(bindings,
expected_ty,
actual,
handler);
}
case (none[@ty.t]) {
bindings.insert(expected_id, actual);
ret ures_ok(actual);
}
}
}
case (ty.ty_local(?expected_id)) {
auto expected_ty = handler.resolve_local(expected_id);
auto result = unify_step(bindings,
expected_ty,
actual,
handler);
alt (result) {
case (ures_ok(?result_ty)) {
handler.record_local(expected_id, result_ty);
}
case (_) { /* empty */ }
}
ret result;
}
case (ty.ty_param(?expected_id)) {
ret handler.unify_expected_param(expected_id,
expected,
actual);
}
}
// TODO: remove me once match-exhaustiveness checking works
fail;
}
fn hash_int(&int x) -> uint { ret x as uint; }
fn eq_int(&int a, &int b) -> bool { ret a == b; }
auto hasher = hash_int;
auto eqer = eq_int;
auto bindings = map.mk_hashmap[int,@ty.t](hasher, eqer);
ret unify_step(bindings, expected, actual, handler);
}
fn type_err_to_str(&ty.type_err err) -> str {
alt (err) {
case (terr_mismatch) {
ret "types differ";
}
case (terr_tuple_size(?e_sz, ?a_sz)) {
ret "expected a tuple with " + _uint.to_str(e_sz, 10u) +
" elements but found one with " + _uint.to_str(a_sz, 10u) +
" elements";
}
case (terr_tuple_mutability) {
ret "tuple elements differ in mutability";
}
case (terr_record_size(?e_sz, ?a_sz)) {
ret "expected a record with " + _uint.to_str(e_sz, 10u) +
" fields but found one with " + _uint.to_str(a_sz, 10u) +
" fields";
}
case (terr_record_mutability) {
ret "record elements differ in mutability";
}
case (terr_record_fields(?e_fld, ?a_fld)) {
ret "expected a record with field '" + e_fld +
"' but found one with field '" + a_fld +
"'";
}
case (terr_arg_count) {
ret "incorrect number of function parameters";
}
}
}

View File

@ -25,7 +25,6 @@ import middle.ty.type_is_scalar;
import std._str;
import std._uint;
import std._vec;
import std.map;
import std.map.hashmap;
import std.option;
import std.option.none;
@ -43,21 +42,6 @@ type fn_ctxt = rec(@ty.t ret_ty,
// Used for ast_ty_to_ty() below.
type ty_getter = fn(ast.def_id) -> @ty.t;
tag type_err {
terr_mismatch;
terr_tuple_size(uint, uint);
terr_tuple_mutability;
terr_record_size(uint, uint);
terr_record_mutability;
terr_record_fields(ast.ident,ast.ident);
terr_arg_count;
}
tag unify_result {
ures_ok(@ty.t);
ures_err(type_err, @ty.t, @ty.t);
}
// Replaces parameter types inside a type with type variables.
fn generalize_ty(@crate_ctxt cx, @ty.t t) -> @ty.t {
state obj ty_generalizer(@crate_ctxt cx,
@ -168,38 +152,6 @@ fn ast_ty_to_ty_crate(@crate_ctxt ccx, &@ast.ty ast_ty) -> @ty.t {
ret ast_ty_to_ty(f, ast_ty);
}
fn type_err_to_str(&type_err err) -> str {
alt (err) {
case (terr_mismatch) {
ret "types differ";
}
case (terr_tuple_size(?e_sz, ?a_sz)) {
ret "expected a tuple with " + _uint.to_str(e_sz, 10u) +
" elements but found one with " + _uint.to_str(a_sz, 10u) +
" elements";
}
case (terr_tuple_mutability) {
ret "tuple elements differ in mutability";
}
case (terr_record_size(?e_sz, ?a_sz)) {
ret "expected a record with " + _uint.to_str(e_sz, 10u) +
" fields but found one with " + _uint.to_str(a_sz, 10u) +
" fields";
}
case (terr_record_mutability) {
ret "record elements differ in mutability";
}
case (terr_record_fields(?e_fld, ?a_fld)) {
ret "expected a record with field '" + e_fld +
"' but found one with field '" + a_fld +
"'";
}
case (terr_arg_count) {
ret "incorrect number of function parameters";
}
}
}
// Item collection - a pair of bootstrap passes:
//
// 1. Collect the IDs of all type items (typedefs) and store them in a table.
@ -511,383 +463,44 @@ fn collect_item_types(@ast.crate crate) -> tup(@ast.crate, @ty_table) {
ret tup(crate_, item_to_ty);
}
// Type unification
fn unify(&@fn_ctxt fcx, @ty.t expected, @ty.t actual) -> unify_result {
// Wraps the given type in an appropriate cname.
//
// TODO: This doesn't do anything yet. We should carry the cname up from
// the expected and/or actual types when unification results in a type
// identical to one or both of the two. The precise algorithm for this is
// something we'll probably need to develop over time.
// Simple structural type comparison.
fn struct_cmp(@ty.t expected, @ty.t actual) -> unify_result {
if (expected.struct == actual.struct) {
ret ures_ok(expected);
fn unify(&@fn_ctxt fcx, @ty.t expected, @ty.t actual) -> ty.unify_result {
obj unify_handler(@fn_ctxt fcx) {
fn resolve_local(ast.def_id id) -> @ty.t {
check (fcx.locals.contains_key(id));
ret fcx.locals.get(id);
}
fn record_local(ast.def_id id, @ty.t t) {
fcx.locals.insert(id, t);
}
fn unify_expected_param(ast.def_id id, @ty.t expected, @ty.t actual)
-> ty.unify_result {
alt (actual.struct) {
case (ty.ty_param(?actual_id)) {
if (id._0 == actual_id._0 && id._1 == actual_id._1) {
ret ty.ures_ok(expected);
}
}
case (_) { /* fall through */ }
}
ret ty.ures_err(ty.terr_mismatch, expected, actual);
}
ret ures_err(terr_mismatch, expected, actual);
}
fn unify_step(&@fn_ctxt fcx, &hashmap[int,@ty.t] bindings, @ty.t expected,
@ty.t actual) -> unify_result {
// TODO: rewrite this using tuple pattern matching when available, to
// avoid all this rightward drift and spikiness.
// If the RHS is a variable type, then just do the appropriate
// binding.
alt (actual.struct) {
case (ty.ty_var(?actual_id)) {
alt (bindings.find(actual_id)) {
case (some[@ty.t](?actual_ty)) {
// FIXME: change the binding here?
// FIXME: "be"
ret unify_step(fcx, bindings, expected, actual_ty);
}
case (none[@ty.t]) {
bindings.insert(actual_id, expected);
ret ures_ok(expected);
}
}
}
case (ty.ty_local(?actual_id)) {
check (fcx.locals.contains_key(actual_id));
auto actual_ty = fcx.locals.get(actual_id);
auto result = unify_step(fcx, bindings, expected, actual_ty);
alt (result) {
case (ures_ok(?result_ty)) {
fcx.locals.insert(actual_id, result_ty);
}
case (_) { /* empty */ }
}
ret result;
}
case (_) { /* empty */ }
}
alt (expected.struct) {
case (ty.ty_nil) { ret struct_cmp(expected, actual); }
case (ty.ty_bool) { ret struct_cmp(expected, actual); }
case (ty.ty_int) { ret struct_cmp(expected, actual); }
case (ty.ty_uint) { ret struct_cmp(expected, actual); }
case (ty.ty_machine(_)) { ret struct_cmp(expected, actual); }
case (ty.ty_char) { ret struct_cmp(expected, actual); }
case (ty.ty_str) { ret struct_cmp(expected, actual); }
case (ty.ty_tag(?expected_id)) {
alt (actual.struct) {
case (ty.ty_tag(?actual_id)) {
if (expected_id._0 == actual_id._0 &&
expected_id._1 == actual_id._1) {
ret ures_ok(expected);
}
}
case (_) { /* fall through */ }
}
ret ures_err(terr_mismatch, expected, actual);
}
case (ty.ty_box(?expected_sub)) {
alt (actual.struct) {
case (ty.ty_box(?actual_sub)) {
auto result = unify_step(fcx,
bindings,
expected_sub,
actual_sub);
alt (result) {
case (ures_ok(?result_sub)) {
ret ures_ok(plain_ty(ty.ty_box(result_sub)));
}
case (_) {
ret result;
}
}
}
// TODO: ty_var
case (_) {
ret ures_err(terr_mismatch, expected, actual);
}
}
}
case (ty.ty_vec(?expected_sub)) {
alt (actual.struct) {
case (ty.ty_vec(?actual_sub)) {
auto result = unify_step(fcx,
bindings,
expected_sub,
actual_sub);
alt (result) {
case (ures_ok(?result_sub)) {
ret ures_ok(plain_ty(ty.ty_vec(result_sub)));
}
case (_) {
ret result;
}
}
}
// TODO: ty_var
case (_) {
ret ures_err(terr_mismatch, expected, actual);
}
}
}
case (ty.ty_tup(?expected_elems)) {
alt (actual.struct) {
case (ty.ty_tup(?actual_elems)) {
auto expected_len = _vec.len[@ty.t](expected_elems);
auto actual_len = _vec.len[@ty.t](actual_elems);
if (expected_len != actual_len) {
auto err = terr_tuple_size(expected_len,
actual_len);
ret ures_err(err, expected, actual);
}
// TODO: implement an iterator that can iterate over
// two arrays simultaneously.
let vec[@ty.t] result_elems = vec();
auto i = 0u;
while (i < expected_len) {
auto expected_elem = expected_elems.(i);
auto actual_elem = actual_elems.(i);
if (expected_elem.mut != actual_elem.mut) {
auto err = terr_tuple_mutability;
ret ures_err(err, expected, actual);
}
auto result = unify_step(fcx,
bindings,
expected_elem,
actual_elem);
alt (result) {
case (ures_ok(?rty)) {
append[@ty.t](result_elems,rty);
}
case (_) {
ret result;
}
}
i += 1u;
}
ret ures_ok(plain_ty(ty.ty_tup(result_elems)));
}
// TODO: ty_var
case (_) {
ret ures_err(terr_mismatch, expected, actual);
}
}
}
case (ty.ty_rec(?expected_fields)) {
alt (actual.struct) {
case (ty.ty_rec(?actual_fields)) {
auto expected_len = _vec.len[field](expected_fields);
auto actual_len = _vec.len[field](actual_fields);
if (expected_len != actual_len) {
auto err = terr_record_size(expected_len,
actual_len);
ret ures_err(err, expected, actual);
}
// TODO: implement an iterator that can iterate over
// two arrays simultaneously.
let vec[field] result_fields = vec();
auto i = 0u;
while (i < expected_len) {
auto expected_field = expected_fields.(i);
auto actual_field = actual_fields.(i);
if (expected_field.ty.mut
!= actual_field.ty.mut) {
auto err = terr_record_mutability;
ret ures_err(err, expected, actual);
}
if (!_str.eq(expected_field.ident,
actual_field.ident)) {
auto err =
terr_record_fields(expected_field.ident,
actual_field.ident);
ret ures_err(err, expected, actual);
}
auto result = unify_step(fcx,
bindings,
expected_field.ty,
actual_field.ty);
alt (result) {
case (ures_ok(?rty)) {
append[field]
(result_fields,
rec(ty=rty with expected_field));
}
case (_) {
ret result;
}
}
i += 1u;
}
ret ures_ok(plain_ty(ty.ty_rec(result_fields)));
}
// TODO: ty_var
case (_) {
ret ures_err(terr_mismatch, expected, actual);
}
}
}
case (ty.ty_fn(?expected_inputs, ?expected_output)) {
alt (actual.struct) {
case (ty.ty_fn(?actual_inputs, ?actual_output)) {
auto expected_len = _vec.len[arg](expected_inputs);
auto actual_len = _vec.len[arg](actual_inputs);
if (expected_len != actual_len) {
ret ures_err(terr_arg_count, expected, actual);
}
// TODO: as above, we should have an iter2 iterator.
let vec[arg] result_ins = vec();
auto i = 0u;
while (i < expected_len) {
auto expected_input = expected_inputs.(i);
auto actual_input = actual_inputs.(i);
// This should be safe, I think?
auto result_mode;
if (mode_is_alias(expected_input.mode) ||
mode_is_alias(actual_input.mode)) {
result_mode = ast.alias;
} else {
result_mode = ast.val;
}
auto result = unify_step(fcx,
bindings,
actual_input.ty,
expected_input.ty);
alt (result) {
case (ures_ok(?rty)) {
result_ins += vec(rec(mode=result_mode,
ty=rty));
}
case (_) {
ret result;
}
}
i += 1u;
}
// Check the output.
auto result_out;
auto result = unify_step(fcx,
bindings,
expected_output,
actual_output);
alt (result) {
case (ures_ok(?rty)) {
result_out = rty;
}
case (_) {
ret result;
}
}
auto t = plain_ty(ty.ty_fn(result_ins, result_out));
ret ures_ok(t);
}
case (_) {
ret ures_err(terr_mismatch, expected, actual);
}
}
}
case (ty.ty_var(?expected_id)) {
alt (bindings.find(expected_id)) {
case (some[@ty.t](?expected_ty)) {
// FIXME: change the binding here?
// FIXME: "be"
ret unify_step(fcx, bindings, expected_ty, actual);
}
case (none[@ty.t]) {
bindings.insert(expected_id, actual);
ret ures_ok(actual);
}
}
}
case (ty.ty_local(?expected_id)) {
check (fcx.locals.contains_key(expected_id));
auto expected_ty = fcx.locals.get(expected_id);
auto result = unify_step(fcx, bindings, expected_ty, actual);
alt (result) {
case (ures_ok(?result_ty)) {
fcx.locals.insert(expected_id, result_ty);
}
case (_) { /* empty */ }
}
ret result;
}
case (ty.ty_param(?expected_id)) {
alt (actual.struct) {
case (ty.ty_param(?actual_id)) {
if (expected_id._0 == actual_id._0 &&
expected_id._1 == actual_id._1) {
ret ures_ok(expected);
}
}
case (_) {
ret ures_err(terr_mismatch, expected, actual);
}
}
}
}
// TODO: remove me once match-exhaustiveness checking works
fail;
}
fn hash_int(&int x) -> uint { ret x as uint; }
fn eq_int(&int a, &int b) -> bool { ret a == b; }
auto hasher = hash_int;
auto eqer = eq_int;
auto bindings = map.mk_hashmap[int,@ty.t](hasher, eqer);
ret unify_step(fcx, bindings, expected, actual);
auto handler = unify_handler(fcx);
ret ty.unify(expected, actual, handler);
}
// Requires that the two types unify, and prints an error message if they
// don't. Returns the unified type.
fn demand(&@fn_ctxt fcx, &span sp, @ty.t expected, @ty.t actual) -> @ty.t {
alt (unify(fcx, expected, actual)) {
case (ures_ok(?ty)) {
ret ty;
}
case (ty.ures_ok(?t)) { ret t; }
case (ures_err(?err, ?expected, ?actual)) {
case (ty.ures_err(?err, ?expected, ?actual)) {
fcx.ccx.sess.span_err(sp, "mismatched types: expected "
+ ty_to_str(expected) + " but found "
+ ty_to_str(actual) + " (" +
type_err_to_str(err) + ")");
ty.type_err_to_str(err) + ")");
// TODO: In the future, try returning "expected", reporting the
// error, and continue.
@ -899,8 +512,8 @@ fn demand(&@fn_ctxt fcx, &span sp, @ty.t expected, @ty.t actual) -> @ty.t {
// Returns true if the two types unify and false if they don't.
fn are_compatible(&@fn_ctxt fcx, @ty.t expected, @ty.t actual) -> bool {
alt (unify(fcx, expected, actual)) {
case (ures_ok(_)) { ret true; }
case (ures_err(_, _, _)) { ret false; }
case (ty.ures_ok(_)) { ret true; }
case (ty.ures_err(_, _, _)) { ret false; }
}
}