1
0
Fork 0
mirror of https://github.com/ganelson/inform.git synced 2024-06-29 05:24:57 +03:00
inform7/services/kinds-module/Chapter 2/The Lattice of Kinds.w
2022-05-15 16:46:47 +01:00

487 lines
19 KiB
OpenEdge ABL

[Latticework::] The Lattice of Kinds.
Placing a partial order on kinds according to whether one conforms to another.
@h Conformance.
If we write $K\leq L$ to mean that $K$ conforms to $L$, then $\leq$ provides
an ordering on kinds. For any kinds $K, L, M$ not making use of kind variables[1]
it is true that:
(*) $K \leq K$ -- reflexivity.
(*) If $K\leq L$ and $L\leq M$ then $K\leq M$ -- transitivity.
(*) |K_nil| $\leq K \leq$ |value| -- there are top and bottom elements.[2]
(*) If $K \leq L$ then a value of kind $K$ can always be substituted for a
value of kind $L$ without modification -- the Liskov substitution principle.[3]
(*) There is a join $K\lor L$ and a meet $K\land L$ such that
$K\land L\leq K, L\leq K\lor L$, where $K\lor L$ is minimal and $K\land L$
maximal with that property.
[1] Introducing kind variables complicates the picture, because whether or not
|list of K| conforms to |list of arithmetic values| depends on the current
value of |K| and therefore on the current context.
[2] |K_nil| is a kind which exists only for kind-checking purposes, meaning
"a member of the empty set". Clearly no value can ever have this. This
differs from |K_void|, which means "the absence of a value". Neither can ever
be the kind of a variable, of course.
[3] Also known as strong behavioural subtyping. This only applies to definite
kinds, because no value ever has an indefinite kind.
@ In general there is no guarantee of antisymmetry, i.e., that $K\leq L$
and $L\leq K$ must mean $K = L$, nor that $K\lor L$ or $K\land L$ are unique.
We could imagine creating indefinite kinds |rhyming with lumber value| and
|calculator value| such that |number| and |real number| would conform to both.
If so, then either one of |rhyming with lumber value| and |calculator value|
could equally well serve as $K\lor L$. The set of kinds is therefore not
formally a lattice.
As it happens, however, Inform's choice of indefinite kinds is made in
such a way that this does not happen. The upshot is that although the set of
kinds is not necessarily a lattice, it often will be in practice, and we use
the language of lattices -- hence, words like "join" and "meet" -- as a guide.
@ Conformance is tested with the function //Kinds::conforms_to//, and the
following shows it in action.
= (text from Figures/conformance.txt as REPL)
@ The indefinite |arithmetic kind| used by Inform is a good example of what
in other languages would be called a protocol. Here we see conformance:
= (text from Figures/av-conformance.txt as REPL)
@ Here we see covariance versus contravariance:
= (text from Figures/variance.txt as REPL)
(*) A constructor $\phi$ is "covariant" -- meaning, goes the same way -- if
$K\leq L$ means $\phi(K)\leq\phi(L)$.
(*) It is "contravariant" -- goes the opposite way -- if $K\leq L$ means
$\phi(L)\leq\phi(K)$.
Note that "list of ..." is covariant; "phrase ... -> ..." is contravariant in
the first term, but covariant in the second. To see why, consider a function
$f: K\to L$, and then considering expanding or contracting the sets $K$ and $L$.
It's no problem if $L$ grows larger -- $f$ can still be used -- but if $L$
shrinks then $f$ might map outside it, which is unsafe. But the reverse is true
for $K$. If $K$ shrinks we can still use $f$, but if it grows then we may not
be able to. If $K_1\leq K\leq K_2$ and $L_1\leq L\leq L_2$, then a function
$f:K\to L$ is also a function $K_1\to L_2$, but not $K_2\to L_1$.
See //Latticework::order_relation// for more.
@d COVARIANT 1
@d CONTRAVARIANT -1
@h Lattice operations.
Every $K$ other than |value|, |K_nil| and |K_void| has a minimal element $K^+$
such that $K\leq K^+$ but $K\neq K^+$: we call this the "superkind" of $K$.
Assuming kinds form a lattice, this will be unique. The following function
returns the superkind, or |NULL| for special cases which do not have one.
It is in this function that the basic conformance facts are expressed. We
hard-code that for the built-in indefinite kinds, and give the client tool
a chance to provide other conformances. For Inform, for example, the callback
function is what tells us that the superkind of |man| is |person|.
=
kind *Latticework::super(kind *K) {
if (Kinds::eq(K, K_real_arithmetic_value)) return K_arithmetic_value;
if (Kinds::eq(K, K_enumerated_value)) return K_understandable_value;
if (Kinds::eq(K, K_arithmetic_value)) return K_understandable_value;
if (Kinds::eq(K, K_understandable_value)) return K_sayable_value;
if (Kinds::eq(K, K_pointer_value)) return K_sayable_value;
if (Kinds::eq(K, K_sayable_value)) return K_stored_value;
if (Kinds::eq(K, K_stored_value)) return K_value;
if (Kinds::eq(K, K_value)) return NULL;
if (Kinds::eq(K, K_nil)) return NULL;
if (Kinds::eq(K, K_void)) return NULL;
#ifdef HIERARCHY_GET_SUPER_KINDS_CALLBACK
kind *S = HIERARCHY_GET_SUPER_KINDS_CALLBACK(K);
if (S) return S;
#endif
if (KindConstructors::compatible(K->construct, K_real_arithmetic_value->construct, FALSE))
return K_real_arithmetic_value;
if (KindConstructors::compatible(K->construct, K_enumerated_value->construct, FALSE))
return K_enumerated_value;
if (KindConstructors::compatible(K->construct, K_arithmetic_value->construct, FALSE))
return K_arithmetic_value;
if (KindConstructors::compatible(K->construct, K_pointer_value->construct, FALSE))
return K_pointer_value;
if (KindConstructors::compatible(K->construct, K_understandable_value->construct, FALSE))
return K_understandable_value;
if (KindConstructors::compatible(K->construct, K_sayable_value->construct, FALSE))
return K_sayable_value;
if (KindConstructors::compatible(K->construct, K_stored_value->construct, FALSE))
return K_stored_value;
return K_value;
}
@ Joins are defined above. So are meets, though in practice they are never useful
to us except as a way of calculating joins: the two are dual to each other as
they pass through contravariant constructors.
The main use of this for Inform is to handle literals such as:
>> { 1, 2.1415, "frog" }
The kind of this is by definition the join of the kinds of the values in the
list, which as it happens is |K_sayable_value| -- an indefinite kind, so that
such a list can't be constructed as data.
=
kind *Latticework::join(kind *K1, kind *K2) {
return Latticework::j_or_m(K1, K2, 1);
}
kind *Latticework::meet(kind *K1, kind *K2) {
return Latticework::j_or_m(K1, K2, -1);
}
kind *Latticework::j_or_m(kind *K1, kind *K2, int direction) {
if (K1 == NULL) return K2;
if (K2 == NULL) return K1;
kind_constructor *con = K1->construct;
int a1 = KindConstructors::arity(con);
int a2 = KindConstructors::arity(K2->construct);
if ((a1 > 0) || (a2 > 0)) {
if (K2->construct != con) return K_value;
kind *ka[MAX_KIND_CONSTRUCTION_ARITY] = { NULL, };
for (int i=0; i<a1; i++)
if (con->variance[i] == COVARIANT)
ka[i] = Latticework::j_or_m(K1->kc_args[i], K2->kc_args[i], direction);
else
ka[i] = Latticework::j_or_m(K1->kc_args[i], K2->kc_args[i], -direction);
if (a1 == 1) return Kinds::unary_con(con, ka[0]);
else return Kinds::binary_con(con, ka[0], ka[1]);
} else {
@<Deal with nil@>;
@<Deal with floating-point promotions@>;
if (Kinds::conforms_to(K1, K2)) return (direction > 0)?K2:K1;
if (Kinds::conforms_to(K2, K1)) return (direction > 0)?K1:K2;
if (direction > 0) {
for (kind *K = K1; K; K = Latticework::super(K))
if (Kinds::conforms_to(K2, K))
return K;
return K_value;
} else {
return K_nil;
}
}
}
@ This ensures that $K\land$ |K_nil| $ = $ |K_nil|, and that
$K\lor$ |K_nil| $ = K$.
@<Deal with nil@> =
if ((Kinds::eq(K1, K_nil))) return (direction > 0)?K2:K1;
if ((Kinds::eq(K2, K_nil))) return (direction > 0)?K1:K2;
@ If one of $K_1$, $K_2$ uses floating-point and the other doesn't, then
we promote the one which doesn't before taking the join. This is useful
when inferring the kind of a constant list or column which mixes floating-point
and integer literals; recall that |number| $\not\leq$ |real number|, so
without this promotion there would be no way to join such kinds.
@<Deal with floating-point promotions@> =
if ((Kinds::FloatingPoint::uses_floating_point(K1) == FALSE) &&
(Kinds::FloatingPoint::uses_floating_point(K2)) &&
(Kinds::eq(Kinds::FloatingPoint::real_equivalent(K1), K2)))
return (direction > 0)?K2:K1;
if ((Kinds::FloatingPoint::uses_floating_point(K2) == FALSE) &&
(Kinds::FloatingPoint::uses_floating_point(K1)) &&
(Kinds::eq(Kinds::FloatingPoint::real_equivalent(K2), K1)))
return (direction > 0)?K1:K2;
@h Compatibility.
A related but different question is "compatibility". This asks whether a
value of kind $K$ can be used where $L$ is expected, but:
(i) It is now okay if explicit code to perform a conversion would be needed; and
(ii) There are now three possible answers -- always, never and sometimes, where
"sometimes" means that code can be compiled which would test compatibility at
run time rather than compile time.
Conformance implies compatibility but not vice versa. For example:
= (text from Figures/compatibility.txt as REPL)
Note that |number| is compatible with |real number|. Run-time code will be
needed to convert the value, but the answer is "always". We also see that
|device| is always compatible with |thing| -- every device is a thing --
but also that |thing| is sometimes compatible with |device|. If we pass a
thing to a function expecting to see a device, run-time code can check whether
the value passed is indeed a device, and reject the call with a run-time error
if not.
@ Kind checking can happen for many reasons, but the most difficult case
occurs when matching phrase prototypes. This is difficult because it involves
kind variables:
>> To add (new entry - K) to (L - list of values of kind K) ...
This matches "add 23 to L" if "L" is a list of numbers, but not if it
is a list of times.
Since kind variables can be changed only when matching phrase prototypes,
and this is not a recursive process, we can store the current definitions
of A to Z in a single array.
@d MAX_KIND_VARIABLES 27 /* we number them from 1 (A) to 26 (Z) */
= (early code)
kind *values_of_kind_variables[MAX_KIND_VARIABLES];
@ In theory the kind-checker only needs to read the values of |A| to |Z|, not
to write them. If |Q| is currently equal to |number|, then it should check
kinds against |Q| exactly as if it were checking against |number|. It can
indeed do this -- that's the |MATCH_KIND_VARIABLES_AS_VALUES| mode. It can
also ignore the values of kind variables and treat them as names, so that
|Q| matches only against |Q|, regardless of its current value; that's the
|MATCH_KIND_VARIABLES_AS_SYMBOLS| mode. Or it can match anything
against |Q|, which is |MATCH_KIND_VARIABLES_AS_UNIVERSAL| mode.
More interestingly, the kind-checker can also set the values of |A| to |Z|.
This is convenient since checking their correctness is more or less the same
thing as inferring what they seem to be in a given situation. So this is
|MATCH_KIND_VARIABLES_INFERRING_VALUES| mode.
@d MATCH_KIND_VARIABLES_AS_SYMBOLS 0
@d MATCH_KIND_VARIABLES_INFERRING_VALUES 1
@d MATCH_KIND_VARIABLES_AS_VALUES 2
@d MATCH_KIND_VARIABLES_AS_UNIVERSAL 3
=
int kind_checker_mode = MATCH_KIND_VARIABLES_AS_SYMBOLS;
@ When the kind-checker does choose a value for a variable, it not only
sets the relevant entry in the above array but also creates a note that
this has been done. (If a phrase is correctly matched by the specification
matcher, a linked list of these notes is attached to the results: thus
a match of "add 23 to L" in the example above would produce a successful
result plus a note that |K| has to be |list of numbers|.)
=
typedef struct kind_variable_declaration {
int kv_number; /* must be from 1 to 26 */
struct kind *kv_value; /* must be a definite non-|NULL| kind */
struct kind_variable_declaration *next;
CLASS_DEFINITION
} kind_variable_declaration;
@ And this little function unpacks a list of KVDs into an array of values:
=
void Latticework::unpack_kvd(kind **vals, kind_variable_declaration *kvd) {
for (int i=0; i<27; i++) vals[i] = NULL;
for (; kvd; kvd=kvd->next) vals[kvd->kv_number] = kvd->kv_value;
}
@h Common code.
So the following routine tests conformance if |comp| is |FALSE|, returning
|ALWAYS_MATCH| if and only if |from| $\leq$ |to| holds; and otherwise it tests
compatibility, returning |ALWAYS_MATCH|, |SOMETIMES_MATCH| or |NEVER_MATCH|
as appropriate.
=
int Latticework::order_relation(kind *from, kind *to, int allow_casts) {
if (Kinds::get_variable_number(to) > 0) {
kind *var_k = to, *other_k = from;
@<Deal separately with matches against kind variables@>;
}
if (Kinds::get_variable_number(from) > 0) {
kind *var_k = from, *other_k = to;
@<Deal separately with matches against kind variables@>;
}
@<Deal separately with the sayability of lists@>;
@<Deal separately with the top and bottom of the lattice@>;
@<Deal separately with the special role of the unknown kind@>;
@<The general case of compatibility@>;
}
@ A list is sayable if and only if it is empty, or its contents are sayable.
@<Deal separately with the sayability of lists@> =
if ((Kinds::get_construct(from) == CON_list_of) &&
(Kinds::eq(to, K_sayable_value))) {
kind *CK = Kinds::unary_construction_material(from);
if (Kinds::eq(CK, K_nil)) return ALWAYS_MATCH;
if (CK == NULL) return ALWAYS_MATCH; /* for an internal test case making dodgy lists */
return Latticework::order_relation(CK, K_sayable_value, allow_casts);
}
@<Deal separately with the top and bottom of the lattice@> =
if (Kinds::eq(to, K_value)) return ALWAYS_MATCH;
if (Kinds::eq(from, K_nil)) return ALWAYS_MATCH;
@ |NULL| as a kind means "unknown". It's compatible only with itself
and, of course, "value".
@<Deal separately with the special role of the unknown kind@> =
if ((to == NULL) && (from == NULL)) return ALWAYS_MATCH;
if ((to == NULL) || (from == NULL)) return NEVER_MATCH;
@<The general case of compatibility@> =
int f_a = KindConstructors::arity(from->construct);
int t_a = KindConstructors::arity(to->construct);
int arity = (f_a < t_a)?f_a:t_a;
int o = ALWAYS_MATCH;
if (from->construct != to->construct)
o = Latticework::construct_compatible(from, to, allow_casts);
int i, this_o = NEVER_MATCH, fallen = FALSE;
for (i=0; i<arity; i++) {
if ((Latticework::vacuous(from->kc_args[i])) && (Latticework::vacuous(to->kc_args[i])))
continue;
if (KindConstructors::variance(from->construct, i) == COVARIANT)
this_o = Latticework::order_relation(from->kc_args[i], to->kc_args[i], allow_casts);
else {
this_o = Latticework::order_relation(to->kc_args[i], from->kc_args[i], allow_casts);
}
switch (this_o) {
case NEVER_MATCH: o = this_o; break;
case SOMETIMES_MATCH: if (o != NEVER_MATCH) { o = this_o; fallen = TRUE; } break;
}
}
if ((fallen) && (to->construct != CON_list_of)) return NEVER_MATCH;
return o;
@ =
int Latticework::vacuous(kind *K) {
if ((Kinds::eq(K, K_nil)) || (Kinds::eq(K, K_void))) return TRUE;
return FALSE;
}
@ =
int Latticework::construct_compatible(kind *from, kind *to, int allow_casts) {
kind *K = from;
while (K) {
if (Kinds::eq(K, to)) return ALWAYS_MATCH;
K = Latticework::super(K);
}
if ((allow_casts) && (KindConstructors::find_cast(from->construct, to->construct)))
return ALWAYS_MATCH;
K = to;
while (K) {
if (Kinds::eq(K, from)) {
#ifdef HIERARCHY_ALLOWS_SOMETIMES_MATCH_KINDS_CALLBACK
if (HIERARCHY_ALLOWS_SOMETIMES_MATCH_KINDS_CALLBACK(from))
return SOMETIMES_MATCH;
#endif
#ifndef HIERARCHY_ALLOWS_SOMETIMES_MATCH_KINDS_CALLBACK
return SOMETIMES_MATCH;
#endif
}
K = Latticework::super(K);
}
return NEVER_MATCH;
}
@ Recall that kind variables are identified by a number in the range 1 ("A")
to 26 ("Z"), and that it is also possible to assign them a domain, or a
"declaration". This marks that they are free to take on a value, within
that domain. For example, in
>> To add (new entry - K) to (L - list of values of kind K) ...
the first appearance of |K| will be an ordinary use of a kind variable,
whereas the second has the declaration |value| -- meaning that it can
become any kind matching |value|. (It could alternatively have had a
more restrictive declaration like |arithmetic value|.)
@<Deal separately with matches against kind variables@> =
switch(kind_checker_mode) {
case MATCH_KIND_VARIABLES_AS_SYMBOLS:
if (Kinds::get_variable_number(other_k) ==
Kinds::get_variable_number(var_k)) return ALWAYS_MATCH;
return NEVER_MATCH;
case MATCH_KIND_VARIABLES_AS_UNIVERSAL: return ALWAYS_MATCH;
default: {
int vn = Kinds::get_variable_number(var_k);
if (Kinds::get_variable_stipulation(var_k))
@<Act on a declaration usage, where inference is allowed@>
else
@<Act on an ordinary usage, where inference is not allowed@>;
}
}
@ When the specification matcher works on matching text such as
>> add 23 to the scores list;
it works through the prototype:
>> To add (new entry - K) to (L - list of values of kind K) ...
in two passes. On the first pass, it tries to match the tokens "23" and
"scores list" against |K| and |list of values of kind K| respectively
in |MATCH_KIND_VARIABLES_INFERRING_VALUES| mode; on the second pass, it
does the same in |MATCH_KIND_VARIABLES_AS_VALUES| mode. In this example,
on the first pass we infer (from the kind of "scores list", which is
indeed a list of numbers) that |K| must be |number|; on the second pass
we verify that "23" is a |K|.
The following shows what happens matching |values of kind K|, which is
a declaration usage of the variable |K|.
@<Act on a declaration usage, where inference is allowed@> =
switch(kind_checker_mode) {
case MATCH_KIND_VARIABLES_INFERRING_VALUES:
if (Latticework::order_relation(other_k,
Kinds::get_variable_stipulation(var_k), allow_casts) != ALWAYS_MATCH)
return NEVER_MATCH;
LOGIF(KIND_CHECKING, "Inferring kind variable %d from %u (declaration %u)\n",
vn, other_k, Kinds::get_variable_stipulation(var_k));
values_of_kind_variables[vn] = other_k;
kind_variable_declaration *kvd = CREATE(kind_variable_declaration);
kvd->kv_number = vn;
kvd->kv_value = other_k;
kvd->next = NULL;
return ALWAYS_MATCH;
case MATCH_KIND_VARIABLES_AS_VALUES: return ALWAYS_MATCH;
}
@ Whereas this is what happens when matching just |K|. On the inference pass,
we always make a match, which is legitimate because we know we are going to
make a value-checking pass later.
@<Act on an ordinary usage, where inference is not allowed@> =
switch(kind_checker_mode) {
case MATCH_KIND_VARIABLES_INFERRING_VALUES: return ALWAYS_MATCH;
case MATCH_KIND_VARIABLES_AS_VALUES:
LOGIF(KIND_CHECKING, "Checking %u against kind variable %d (=%u)\n",
other_k, vn, values_of_kind_variables[vn]);
if (Latticework::order_relation(other_k,
values_of_kind_variables[vn], allow_casts) == NEVER_MATCH)
return NEVER_MATCH;
else
return ALWAYS_MATCH;
}
@ It's easy to confused when writing a type checker, especially with variables
getting in the way, so these logging functions can be helpful:
=
void Latticework::show_variables(void) {
LOG("Variables: ");
for (int i=1; i<=26; i++) {
kind *K = values_of_kind_variables[i];
if (K == NULL) continue;
LOG("%c=%u ", 'A'+i-1, K);
}
LOG("\n");
}
void Latticework::show_frame_variables(void) {
int shown = 0;
for (int i=1; i<=26; i++) {
kind *K = Kinds::variable_from_context(i);
if (K) {
if (shown++ == 0) LOGIF(MATCHING, "Stack frame uses kind variables: ");
LOGIF(MATCHING, "%c=%u ", 'A'+i-1, K);
}
}
if (shown == 0) LOGIF(MATCHING, "Stack frame sets no kind variables");
LOGIF(MATCHING, "\n");
}