mirror of
https://github.com/ganelson/inform.git
synced 2024-04-28 15:19:34 +03:00
3632 lines
148 KiB
OpenEdge ABL
3632 lines
148 KiB
OpenEdge ABL
[Dash::] Dash.
|
|
|
|
The part of Inform most nearly like a typechecker in a conventional compiler.
|
|
|
|
@ Dash is the second typechecking algorithm to be used in Inform, installed in
|
|
early 2015: the first had served since 2003, but became unwieldy after so many
|
|
exceptional cases had been added to it, and was impossible to adapt to the
|
|
redesigned parse tree. Dash is not so called because it's faster (it's
|
|
actually a few percent slower), but because at one stage Inform was running
|
|
both typecheckers side by side: TC and TC-dash, or Dash for short. TC-dash
|
|
won, it's still called Dash, and TC is no more.
|
|
|
|
Because Dash also deals with text which entirely fails to make sense, which in
|
|
other compilers would be rejected at a lower level, it has to issue basic
|
|
syntax errors as well as type mismatch errors. This is arguably a good thing,
|
|
though, because it means they can be issued using the same generally helpful
|
|
system as more sophisticated problems.
|
|
|
|
Partly because of the need to do this, the type-checker has a top-down
|
|
approach. It aims to prove that the node found can match what's expected,
|
|
making selections from alternative readings, and in limited cases actually
|
|
making changes to the parse tree, in order to do this. For instance,
|
|
consider checking the tree for:
|
|
|
|
>> let the score be the score plus 10
|
|
|
|
Dash takes the view that the phrase usage can be proved correct, so long as
|
|
the arguments can also be proved. There are several valid interpretations of
|
|
"let ... be ...", and these are all present in the parse tree as alternative
|
|
interpretations, so the typechecker tries each in turn, accepting one (or
|
|
more) if the arguments can be proved to be of the right type. This means
|
|
proving that argument 0 ("the score") is an lvalue and also that argument 1
|
|
("the score plus 10") is an rvalue. A further rule requires that the kind of
|
|
value of argument 1 must match the kind of value stored in the variable, here
|
|
a "number", so we must prove that too. Now "plus" is polymorphic and can
|
|
produce different kinds of value depending on the kinds of value it acts upon,
|
|
so again we must check all possible interpretations. But we finally succeed in
|
|
showing that "score" is an lvalue, "10" is a number, "score" is also a number,
|
|
and that "plus" on two numbers gives a number, so we complete the proof and
|
|
the phrase is proved correct.
|
|
|
|
@ When issuing problems, we show a form of backtrace so that the user can
|
|
see what we've considered, and this is used to accumulate data for that.
|
|
|
|
=
|
|
typedef struct inv_token_problem_token {
|
|
struct wording problematic_text;
|
|
struct parse_node *as_parsed;
|
|
int already_described;
|
|
int new_name; /* found in context of a name not yet defined */
|
|
CLASS_DEFINITION
|
|
} inv_token_problem_token;
|
|
|
|
@h The Dashboard.
|
|
Dash uses a small suite of global variables to keep track of two decidedly
|
|
global side-effects of checking: the issuing of problem messages, and the
|
|
setting of kind variables. This suite is called the "dashboard".
|
|
|
|
First, we keep track of the problem messages we will issue, if any, using
|
|
a bitmap made up of the following modes:
|
|
|
|
@d BEGIN_DASH_MODE int s_dm = dash_mode;
|
|
kind **s_kvc = kind_of_var_to_create;
|
|
parse_node *s_invl = Dash_ambiguity_list;
|
|
@d DASH_MODE_ENTER(mode) dash_mode |= mode;
|
|
@d DASH_MODE_CREATE(K) kind_of_var_to_create = K;
|
|
@d DASH_MODE_EXIT(mode) dash_mode &= (~mode);
|
|
@d END_DASH_MODE dash_mode = s_dm;
|
|
kind_of_var_to_create = s_kvc;
|
|
Dash_ambiguity_list = s_invl;
|
|
@d TEST_DASH_MODE(mode) (dash_mode & mode)
|
|
|
|
@d ISSUE_PROBLEMS_DMODE 0x00000001 /* rather than keep silent about them */
|
|
@d ISSUE_LOCAL_PROBLEMS_DMODE 0x00000002 /* at the end, that is */
|
|
@d ISSUE_GROSS_PROBLEMS_DMODE 0x00000004 /* at the end, that is */
|
|
@d ISSUE_INTERESTING_PROBLEMS_DMODE 0x00000008 /* unless casting to text */
|
|
@d ABSOLUTE_SILENCE_DMODE 0x00000010 /* say nothing at all */
|
|
|
|
=
|
|
int dash_mode = ISSUE_PROBLEMS_DMODE; /* default */
|
|
kind **kind_of_var_to_create = NULL;
|
|
int dash_recursion_count = 0;
|
|
|
|
@ Three grades of problem can appear: "ordinary", "gross" and "grosser than
|
|
gross". We distinguish these in order to produce a Problem message which
|
|
reflects the biggest thing wrong, rather than being so esoteric that it misses
|
|
the main point. Changing a particular error condition from an ordinary to a
|
|
gross problem, or vice versa, has no effect on the result returned by Dash,
|
|
only on the Problem messages given to the user.
|
|
|
|
@d THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM
|
|
no_gross_problems_thrown++; /* problems this gross cannot be suppressed */
|
|
|
|
@d THIS_IS_A_GROSS_PROBLEM
|
|
no_gross_problems_thrown++; /* this increments even if the message is suppressed */
|
|
if ((TEST_DASH_MODE(ISSUE_PROBLEMS_DMODE) == FALSE) &&
|
|
(TEST_DASH_MODE(ISSUE_GROSS_PROBLEMS_DMODE) == FALSE)) return NEVER_MATCH;
|
|
|
|
@d THIS_IS_AN_ORDINARY_PROBLEM
|
|
if (TEST_DASH_MODE(ISSUE_PROBLEMS_DMODE) == FALSE) return NEVER_MATCH;
|
|
|
|
=
|
|
int no_gross_problems_thrown = 0;
|
|
int no_interesting_problems_thrown = 0;
|
|
int initial_problem_count = 0;
|
|
int backtraced_problem_count = 0;
|
|
|
|
int Dash::problems_have_been_issued(void) {
|
|
if (initial_problem_count < problem_count) return TRUE;
|
|
return FALSE;
|
|
}
|
|
|
|
@ Next, we keep track of the most recent set of meanings attached to the
|
|
kind variables A, B, C, ..., Z, and the most recently looked-at list of
|
|
invocations.
|
|
|
|
=
|
|
kind_variable_declaration *most_recent_interpretation = NULL;
|
|
parse_node *Dash_ambiguity_list = NULL;
|
|
|
|
@ We need careful debug logging of what Dash does. During Inform's infancy, the
|
|
type checker was the hardest thing to debug, but that wasn't so much because
|
|
this was the great habitat and breeding ground for bugs; it was more that those
|
|
bugs which were here were by far the hardest to root out. So careful logging
|
|
on demand is vital.
|
|
|
|
Each call to the recursive Dash has its own unique ID number, to make logging
|
|
more legible.
|
|
|
|
@d LOG_DASH_LEFT
|
|
LOGIF(MATCHING, "[%d%s] ",
|
|
unique_DR_call_identifier,
|
|
(TEST_DASH_MODE(ISSUE_PROBLEMS_DMODE))?"":"-silent");
|
|
@d LOG_DASH(stage)
|
|
LOGIF(MATCHING, "[%d%s] %s $P\n",
|
|
unique_DR_call_identifier,
|
|
(TEST_DASH_MODE(ISSUE_PROBLEMS_DMODE))?"":"-silent", stage, p);
|
|
|
|
=
|
|
int unique_DR_call_identifier = 0, DR_call_counter = 0; /* solely to make the log more legible */
|
|
|
|
@h Return values.
|
|
Dash records the outcome of checking as one of three states.
|
|
|
|
It is perhaps telling that we never need a |Dash::best_case| routine.
|
|
Typecheckers are not allowed to be optimistic.
|
|
|
|
=
|
|
int Dash::worst_case(int rv1, int rv2) {
|
|
if ((rv1 == NEVER_MATCH) || (rv2 == NEVER_MATCH)) return NEVER_MATCH;
|
|
if ((rv1 == SOMETIMES_MATCH) || (rv2 == SOMETIMES_MATCH)) return SOMETIMES_MATCH;
|
|
return ALWAYS_MATCH;
|
|
}
|
|
|
|
@h (1) Entering Dash.
|
|
Dash is structured into levels and this is level 1, the topmost.
|
|
|
|
Dash has three points of entry: to check a condition, check a value, or check
|
|
an invocation list for a phrase used in a routine.
|
|
|
|
These top-level routines do not look recursive, but in fact some can be,
|
|
because Dash needs to call the predicate calculus engine to typecheck
|
|
propositions: and these in turn call Dash to check that constant values
|
|
are used correctly.
|
|
|
|
All of these funnel downwards into level 2:
|
|
|
|
=
|
|
int Dash::check_condition(parse_node *p) {
|
|
parse_node *cn = Node::new(CONDITION_CONTEXT_NT);
|
|
cn->down = p;
|
|
LOGIF(MATCHING, "Dash (1): condition\n");
|
|
return Dash::funnel_to_level_2(cn, FALSE);
|
|
}
|
|
|
|
int Dash::check_value(parse_node *p, kind *K) {
|
|
parse_node *vn = Node::new(RVALUE_CONTEXT_NT);
|
|
if (K) Node::set_kind_required_by_context(vn, K);
|
|
vn->down = p;
|
|
if (K) LOGIF(MATCHING, "Dash (1): value of kind %u\n", K);
|
|
if (K == NULL) LOGIF(MATCHING, "Dash (1): value\n");
|
|
return Dash::funnel_to_level_2(vn, FALSE);
|
|
}
|
|
|
|
int Dash::check_value_silently(parse_node *p, kind *K) {
|
|
parse_node *vn = Node::new(RVALUE_CONTEXT_NT);
|
|
if (K) Node::set_kind_required_by_context(vn, K);
|
|
vn->down = p;
|
|
if (K) LOGIF(MATCHING, "Dash (1): value of kind %u\n", K);
|
|
if (K == NULL) LOGIF(MATCHING, "Dash (1): value\n");
|
|
return Dash::funnel_to_level_2(vn, TRUE);
|
|
}
|
|
|
|
int Dash::check_invl(parse_node *p) {
|
|
LOGIF(MATCHING, "Dash (1): invocation list '%W'\n", Node::get_text(p));
|
|
LOGIF(MATCHING, "p = $T\n", p);
|
|
return Dash::funnel_to_level_2(p, FALSE);
|
|
}
|
|
|
|
int Dash::funnel_to_level_2(parse_node *p, int silently) {
|
|
no_gross_problems_thrown = 0;
|
|
dash_recursion_count = 0;
|
|
BEGIN_DASH_MODE;
|
|
if (!silently) DASH_MODE_ENTER(ISSUE_PROBLEMS_DMODE);
|
|
initial_problem_count = problem_count;
|
|
DASH_MODE_CREATE(NULL);
|
|
Latticework::show_frame_variables();
|
|
int rv = Dash::typecheck_recursive(p, NULL, TRUE);
|
|
END_DASH_MODE;
|
|
return rv;
|
|
}
|
|
|
|
@h (2) Recursion point.
|
|
Loosely speaking, Dash works by visiting every node in the parse tree being
|
|
examined with the following routine, which is therefore recursive as Dash
|
|
heads ever downward.
|
|
|
|
The routine itself is really just an outer shell, though, and has two
|
|
functions: it keeps the debugging log tidy (see above) and it produces
|
|
the backtrace if the inner routine should throw a problem message.
|
|
|
|
The recursion limit below is clearly arbitrary, but is there to prevent the
|
|
algorithm from slowing Inform unacceptably in the event of something like
|
|
|
|
>> say g + g + g + g + g + g + g + g + g + g + g + g + g + g + g + g + g + g + g + g + g + g + g;
|
|
|
|
where "g" is a term Inform doesn't recognise, because otherwise this will
|
|
recurse through every possible interpretation of the plus sign (i.e. every
|
|
possible order of operations).
|
|
|
|
@d MAX_DASH_RECURSION 10000
|
|
|
|
=
|
|
int Dash::typecheck_recursive(parse_node *p, parse_node *context, int consider_alternatives) {
|
|
if (p == NULL) internal_error("Dash on null node");
|
|
|
|
if (dash_recursion_count >= MAX_DASH_RECURSION) return NEVER_MATCH;
|
|
dash_recursion_count++;
|
|
|
|
int outer_id = unique_DR_call_identifier;
|
|
int problem_count_before = problem_count;
|
|
unique_DR_call_identifier = DR_call_counter++;
|
|
LOG_INDENT;
|
|
LOG_DASH("(2)");
|
|
int return_value = Dash::typecheck_recursive_inner(p, context, consider_alternatives);
|
|
|
|
switch(return_value) {
|
|
case ALWAYS_MATCH: LOG_DASH_LEFT; LOGIF(MATCHING, "== always\n"); break;
|
|
case SOMETIMES_MATCH: LOG_DASH_LEFT; LOGIF(MATCHING, "== sometimes\n"); break;
|
|
case NEVER_MATCH: LOG_DASH_LEFT; LOGIF(MATCHING, "== never\n"); break;
|
|
default: internal_error("impossible verdict from Dash");
|
|
}
|
|
LOG_OUTDENT;
|
|
|
|
if ((problem_count > problem_count_before) && (consider_alternatives))
|
|
@<Consider adding a backtrace of what the type-checker was up to@>;
|
|
|
|
unique_DR_call_identifier = outer_id;
|
|
return return_value;
|
|
}
|
|
|
|
@ The backtrace is added to problem messages only if we have just been checking
|
|
a phrase, and if it produced problems not previously seen. The trick here is
|
|
to ensure that if we have
|
|
|
|
>> let X be a random wibble bibble spong;
|
|
|
|
then it will be the "random ..." phrase which is backtraced, and not the
|
|
"let ..." phrase, even though that also goes wrong in turn.
|
|
|
|
@<Consider adding a backtrace of what the type-checker was up to@> =
|
|
if (problem_count > backtraced_problem_count) {
|
|
if ((p) && (p->down) &&
|
|
(Node::get_type(p) == INVOCATION_LIST_NT)) {
|
|
TextSubstitutions::it_is_not_worth_adding();
|
|
@<Backtrace what phrase definitions the type-checker was looking at@>;
|
|
TextSubstitutions::it_is_worth_adding();
|
|
backtraced_problem_count = problem_count;
|
|
}
|
|
}
|
|
|
|
@ We skip proven invocations, and those never needed because of them, since
|
|
those aren't in dispute; and we also skip groups not even reached, since they
|
|
aren't where the problem lies. (This can happen when checking a compound "say",
|
|
from a text substitution.)
|
|
|
|
@<Backtrace what phrase definitions the type-checker was looking at@> =
|
|
parse_node *inv;
|
|
LOOP_THROUGH_ALTERNATIVES(inv, p->down) LOG("$e\n", inv);
|
|
|
|
int to_show = 0;
|
|
LOOP_THROUGH_ALTERNATIVES(inv, p->down) {
|
|
id_body *idb = Node::get_phrase_invoked(inv);
|
|
if (IDTypeData::is_a_spare_say_X_phrase(&(idb->type_data))) continue;
|
|
to_show++;
|
|
}
|
|
|
|
int announce = TRUE;
|
|
text_stream *latest = Problems::latest_sigil();
|
|
if (Str::eq_wide_string(latest, L"PM_AllInvsFailed")) announce = FALSE;
|
|
|
|
if (announce) @<Produce the I was trying... banner@>;
|
|
@<Produce the list of possibilities@>;
|
|
int real_found = FALSE;
|
|
@<Produce the tokens which were recognisable as something@>;
|
|
@<Produce the tokens which weren't recognisable as something@>;
|
|
@<Produce the tokens which were intentionally not recognisable as something@>;
|
|
if (real_found) @<Produce a note about real versus integer@>;
|
|
|
|
@<Produce the I was trying... banner@> =
|
|
Problems::issue_problem_begin(Task::syntax_tree(), "*");
|
|
if (to_show > 1)
|
|
Problems::issue_problem_segment("I was trying to match one of these phrases:");
|
|
else
|
|
Problems::issue_problem_segment("I was trying to match this phrase:");
|
|
Problems::issue_problem_end();
|
|
|
|
@<Produce the list of possibilities@> =
|
|
int shown = 0;
|
|
LOOP_THROUGH_ALTERNATIVES(inv, p->down) {
|
|
id_body *idb = Node::get_phrase_invoked(inv);
|
|
if (IDTypeData::is_a_spare_say_X_phrase(&(idb->type_data))) continue;
|
|
shown++;
|
|
Problems::quote_number(1, &shown);
|
|
Problems::quote_invocation(2, inv);
|
|
if (announce == FALSE) {
|
|
Problems::issue_problem_begin(Task::syntax_tree(), "***");
|
|
announce = TRUE;
|
|
} else {
|
|
Problems::issue_problem_begin(Task::syntax_tree(), "****");
|
|
}
|
|
if (to_show > 1) Problems::issue_problem_segment("%1. %2");
|
|
else Problems::issue_problem_segment("%2");
|
|
Problems::issue_problem_end();
|
|
}
|
|
|
|
@<Produce the tokens which were recognisable as something@> =
|
|
int any = FALSE;
|
|
inv_token_problem_token *itpt;
|
|
LOOP_OVER(itpt, inv_token_problem_token)
|
|
if (Node::is(itpt->as_parsed, UNKNOWN_NT) == FALSE)
|
|
if (itpt->already_described == FALSE) {
|
|
itpt->already_described = TRUE;
|
|
if (any == FALSE) {
|
|
any = TRUE;
|
|
Problems::issue_problem_begin(Task::syntax_tree(), "*");
|
|
Problems::issue_problem_segment("I recognised:");
|
|
Problems::issue_problem_end();
|
|
}
|
|
@<Produce this token@>;
|
|
}
|
|
|
|
@<Produce this token@> =
|
|
Problems::quote_wording_tinted_green(1, itpt->problematic_text);
|
|
Problems::quote_spec(2, itpt->as_parsed);
|
|
Problems::issue_problem_begin(Task::syntax_tree(), "****");
|
|
if (Specifications::is_value(itpt->as_parsed)) {
|
|
kind *K = Specifications::to_kind(itpt->as_parsed);
|
|
int changed = FALSE;
|
|
K = Kinds::substitute(K, NULL, &changed, FALSE);
|
|
Problems::quote_kind(3, K);
|
|
if (Kinds::eq(K, K_real_number)) real_found = TRUE;
|
|
if (Lvalues::is_lvalue(itpt->as_parsed))
|
|
@<Produce the token for an lvalue@>
|
|
else if (Node::is(itpt->as_parsed, PHRASE_TO_DECIDE_VALUE_NT))
|
|
@<Produce the token for a phrase deciding a value@>
|
|
else
|
|
@<Produce the token for a constant rvalue@>;
|
|
} else Problems::issue_problem_segment("%1 = %2");
|
|
Problems::issue_problem_end();
|
|
|
|
@<Produce the token for an lvalue@> =
|
|
Problems::issue_problem_segment("%1 = %2, holding %3");
|
|
|
|
@<Produce the token for a phrase deciding a value@> =
|
|
char *seg = "%1 = an instruction to work out %3";
|
|
if (K == NULL) seg = "%1 = a phrase";
|
|
parse_node *found_invl = itpt->as_parsed->down;
|
|
parse_node *inv;
|
|
LOOP_THROUGH_ALTERNATIVES(inv, found_invl) {
|
|
LOG("$e\n", inv);
|
|
if (Dash::reading_passed(inv) == FALSE) {
|
|
seg = "%1 = an instruction I think should work out %3, "
|
|
"but which I can't make sense of";
|
|
for (int i=0; i<Invocations::get_no_tokens(inv); i++) {
|
|
parse_node *tok = Invocations::get_token_as_parsed(inv, i);
|
|
if (Node::is(tok, UNKNOWN_NT)) {
|
|
Problems::quote_wording(4, Node::get_text(tok));
|
|
seg = "%1 = an instruction I think should work out %3, "
|
|
"but which I can't perform because '%4' doesn't make sense here";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Problems::issue_problem_segment(seg);
|
|
|
|
@<Produce the token for a constant rvalue@> =
|
|
char *seg = "%1 = %3";
|
|
if (Rvalues::is_CONSTANT_construction(itpt->as_parsed, CON_property)) {
|
|
property *prn = Node::get_constant_property(itpt->as_parsed);
|
|
if (Properties::is_value_property(prn)) {
|
|
binary_predicate *bp = ValueProperties::get_stored_relation(prn);
|
|
if (bp) {
|
|
seg = "%1 = %3, which is used to store %4, "
|
|
"but is not the same thing as the relation itself";
|
|
Problems::quote_relation(4, bp);
|
|
}
|
|
}
|
|
}
|
|
Problems::issue_problem_segment(seg);
|
|
|
|
@<Produce the tokens which were intentionally not recognisable as something@> =
|
|
int unknowns = 0;
|
|
inv_token_problem_token *itpt;
|
|
LOOP_OVER(itpt, inv_token_problem_token)
|
|
if ((Node::is(itpt->as_parsed, UNKNOWN_NT)) && (itpt->new_name))
|
|
if (itpt->already_described == FALSE) {
|
|
itpt->already_described = TRUE;
|
|
if (unknowns < 5) {
|
|
Problems::quote_wording_tinted_red(++unknowns,
|
|
itpt->problematic_text);
|
|
}
|
|
}
|
|
if (unknowns > 0) {
|
|
Problems::issue_problem_begin(Task::syntax_tree(), "*");
|
|
char *chunk = "";
|
|
switch (unknowns) {
|
|
case 1: chunk = "The name '%1' doesn't yet exist."; break;
|
|
case 2: chunk = "The names '%1' and '%2' don't yet exist."; break;
|
|
case 3: chunk = "The names '%1', '%2' and '%3' don't yet exist."; break;
|
|
case 4: chunk = "The names '%1', '%2', '%3' and '%4' don't yet exist."; break;
|
|
default: chunk = "The names '%1', '%2', '%3', '%4', and so on, don't yet exist."; break;
|
|
}
|
|
Problems::issue_problem_segment(chunk);
|
|
Problems::issue_problem_end();
|
|
}
|
|
|
|
@<Produce the tokens which weren't recognisable as something@> =
|
|
int unknowns = 0;
|
|
inv_token_problem_token *itpt;
|
|
LOOP_OVER(itpt, inv_token_problem_token)
|
|
if ((Node::is(itpt->as_parsed, UNKNOWN_NT)) &&
|
|
(itpt->new_name == FALSE))
|
|
if (itpt->already_described == FALSE) {
|
|
itpt->already_described = TRUE;
|
|
if (unknowns < 5) {
|
|
Problems::quote_wording_tinted_red(++unknowns,
|
|
itpt->problematic_text);
|
|
}
|
|
}
|
|
if (unknowns > 0) {
|
|
Problems::issue_problem_begin(Task::syntax_tree(), "*");
|
|
char *chunk = "";
|
|
switch (unknowns) {
|
|
case 1: chunk = "But I didn't recognise '%1'."; break;
|
|
case 2: chunk = "But I didn't recognise '%1' or '%2'."; break;
|
|
case 3: chunk = "But I didn't recognise '%1', '%2' or '%3'."; break;
|
|
case 4: chunk = "But I didn't recognise '%1', '%2', '%3' or '%4'."; break;
|
|
default: chunk = "But I didn't recognise '%1', '%2', '%3', '%4' and so on."; break;
|
|
}
|
|
Problems::issue_problem_segment(chunk);
|
|
Problems::issue_problem_end();
|
|
}
|
|
|
|
@<Produce a note about real versus integer@> =
|
|
Problems::issue_problem_begin(Task::syntax_tree(), "*");
|
|
Problems::issue_problem_segment(
|
|
" %PNote that Inform's kinds 'number' and 'real number' are not "
|
|
"interchangeable. A 'number' like 7 can be used where a 'real "
|
|
"number' is expected - it becomes 7.000 - but not vice versa. "
|
|
"Use 'R to the nearest whole number' if you want to make a "
|
|
"conversion.");
|
|
Problems::issue_problem_end();
|
|
|
|
@h (3) Context switching.
|
|
After those epic preliminaries, we finally do some typechecking.
|
|
|
|
The scheme here is that our expectations of |p| depend on the context, and
|
|
this is defined by some node higher in the current subtree than |p|, which
|
|
we will call |context|. Most of the time this is the parent of |p|, but
|
|
sometimes the grandparent or great-grandparent; and at the start of the
|
|
recursion, when no context has appeared yet, it will be null. In effect,
|
|
then, the tree we're checking contains its own instructions on how it
|
|
should be checked. For example, the subtree
|
|
= (text)
|
|
CONDITION_CONTEXT_NT
|
|
p
|
|
=
|
|
tells us that when we reach |p| it should be checked as a condition.
|
|
|
|
=
|
|
int Dash::typecheck_recursive_inner(parse_node *p, parse_node *context, int consider_alternatives) {
|
|
LOG_DASH("(3)");
|
|
switch (p->node_type) {
|
|
case CONDITION_CONTEXT_NT: @<Switch context@>;
|
|
|
|
case RVALUE_CONTEXT_NT: @<Switch context@>;
|
|
case MATCHING_RVALUE_CONTEXT_NT: @<Switch context to an rvalue matching a description@>;
|
|
case SPECIFIC_RVALUE_CONTEXT_NT: @<Switch context to an rvalue matching a value@>;
|
|
case VOID_CONTEXT_NT: @<Switch to a void context@>;
|
|
|
|
case LVALUE_CONTEXT_NT: @<Switch context to an lvalue@>;
|
|
case LVALUE_TR_CONTEXT_NT: @<Switch context to a table reference lvalue@>;
|
|
case LVALUE_LOCAL_CONTEXT_NT: @<Switch context to an existing local variable lvalue@>;
|
|
|
|
case NEW_LOCAL_CONTEXT_NT: @<Deal with a new local variable name@>;
|
|
|
|
default: @<Typecheck within current context@>;
|
|
}
|
|
return NEVER_MATCH; /* to prevent compiler warnings: unreachable in fact */
|
|
}
|
|
|
|
@ When we find a node like |CONDITION_CONTEXT_NT|, that becomes the new context
|
|
and we move down to its only child.
|
|
|
|
@d SWITCH_CONTEXT_AND_RECURSE(p) Dash::typecheck_recursive(p->down, p, TRUE)
|
|
|
|
@<Switch context@> =
|
|
return SWITCH_CONTEXT_AND_RECURSE(p);
|
|
|
|
@ Other context switches are essentially the same thing, plus a check that
|
|
the value meets some extra requirement. For example:
|
|
|
|
@<Switch context to an lvalue@> =
|
|
int rv = SWITCH_CONTEXT_AND_RECURSE(p);
|
|
if (Lvalues::is_lvalue(p->down) == FALSE)
|
|
@<Issue problem for not being an lvalue@>;
|
|
return rv;
|
|
|
|
@ More specifically:
|
|
|
|
@<Switch context to a table reference lvalue@> =
|
|
int rv = SWITCH_CONTEXT_AND_RECURSE(p);
|
|
if (Node::is(p->down, TABLE_ENTRY_NT) == FALSE)
|
|
@<Issue problem for not being a table reference@>;
|
|
return rv;
|
|
|
|
@<Switch context to an existing local variable lvalue@> =
|
|
int rv = SWITCH_CONTEXT_AND_RECURSE(p);
|
|
if (Node::is(p->down, LOCAL_VARIABLE_NT) == FALSE)
|
|
@<Issue problem for not being an existing local@>;
|
|
return rv;
|
|
|
|
@ Suppose we are matching the parameter of a phrase like this:
|
|
|
|
>> To inspect (D - an open door): ...
|
|
|
|
and typechecking the following invocation:
|
|
|
|
>> inspect the Marble Portal;
|
|
|
|
Then we would have |p| set to some value -- here "the Marble Portal" --
|
|
and the |MATCHING_RVALUE_CONTEXT_NT| node would point to a description node
|
|
for open doors. We must see if |p| matches that. Any match can be at best at
|
|
the "sometimes" level. We can prove the Marble Portal is a door at compile
|
|
time, but we can't prove it's open until run-time.
|
|
|
|
Note that we switch context and recurse first, then make the supplementary
|
|
check afterwards, when we know the kinds at least must be right.
|
|
|
|
@<Switch context to an rvalue matching a description@> =
|
|
int rv = SWITCH_CONTEXT_AND_RECURSE(p);
|
|
if (rv != NEVER_MATCH)
|
|
rv = Dash::worst_case(rv,
|
|
Dash::compatible_with_description(p->down,
|
|
Node::get_token_to_be_parsed_against(p)));
|
|
return rv;
|
|
|
|
@ This is something else that wouldn't appear in a typical typechecker.
|
|
Here we are dealing with a phrase specification such as:
|
|
|
|
>> To attract (N - 10) things: ...
|
|
|
|
where the "N" argument will be accepted if and only if it's the value 10.
|
|
The fact that Inform allows this is further evidence of the slippery way
|
|
that natural language doesn't distinguish values from types; early designs
|
|
of Inform didn't allow it, but many people reported this as a bug.
|
|
|
|
Again we switch context and recurse first. We can't safely test pointer
|
|
values, such as texts, for equality at compile time -- for one thing, we
|
|
don't know what text substitutions will then expand to -- so the value
|
|
test only forces us towards never or always when the constants being
|
|
compared are word values.
|
|
|
|
@<Switch context to an rvalue matching a value@> =
|
|
int rv = SWITCH_CONTEXT_AND_RECURSE(p);
|
|
if (rv != NEVER_MATCH) {
|
|
kind *K = Specifications::to_kind(p->down);
|
|
if ((Kinds::Behaviour::uses_block_values(K) == FALSE) &&
|
|
(Node::is(p->down, CONSTANT_NT))) {
|
|
parse_node *val = Node::get_token_to_be_parsed_against(p);
|
|
if (!(Rvalues::compare_CONSTANT(p->down, val)))
|
|
@<Issue problem for being the wrong rvalue@>;
|
|
} else {
|
|
rv = Dash::worst_case(rv, SOMETIMES_MATCH);
|
|
LOGIF(MATCHING, "dropping to sometimes level for value comparison\n");
|
|
}
|
|
}
|
|
return rv;
|
|
|
|
@ I would ideally like to remove void contexts from Dash entirely, but was
|
|
forced to retain them by the popularity of the Hypothetical Questions
|
|
extension, which made use of the old undocumented |phrase| token.
|
|
|
|
@<Switch to a void context@> =
|
|
int rv = SWITCH_CONTEXT_AND_RECURSE(p);
|
|
if (rv != NEVER_MATCH) {
|
|
if (!(Node::is(p->down, PHRASE_TO_DECIDE_VALUE_NT))) {
|
|
@<Issue problem for not being a phrase@>;
|
|
}
|
|
}
|
|
return rv;
|
|
|
|
@ A whole set of problem messages arise out of contextual failures:
|
|
|
|
@<Issue problem for not being an lvalue@> =
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p->down));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_ValueAsStorageItem));
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but '%2' is a value, not a place where a value is "
|
|
"stored. "
|
|
"%PFor example, if 'The tally is a number that varies.', then "
|
|
"I can 'increment the tally', but I can't 'increment 37' - the "
|
|
"number 37 is always what it is. Similarly, I can't 'increment "
|
|
"the number of people'. Phrases like 'increment' work only on "
|
|
"stored values, like values that vary, or table entries.");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
|
|
@<Issue problem for not being a table reference@> =
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p->down));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_ValueAsTableReference));
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but '%2' is a value, not a reference to an entry "
|
|
"in a table.");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
|
|
@<Issue problem for not being an existing local@> =
|
|
if (TEST_DASH_MODE(ISSUE_LOCAL_PROBLEMS_DMODE)) {
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p));
|
|
if (Specifications::is_kind_like(p->down))
|
|
Problems::quote_text(3, "a kind of value");
|
|
else
|
|
Problems::quote_kind_of(3, p->down);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_ExistingVarNotFound));
|
|
Problems::issue_problem_segment(
|
|
"In the sentence %1, I was expecting that '%2' would be the "
|
|
"name of a temporary value, but it turned out to be %3.");
|
|
Problems::issue_problem_end();
|
|
}
|
|
return NEVER_MATCH;
|
|
|
|
@<Issue problem for being the wrong rvalue@> =
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p->down));
|
|
Problems::quote_spec(3, p->down);
|
|
Problems::quote_spec(4, val);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_NotExactValueWanted));
|
|
Problems::issue_problem_segment(
|
|
"In the sentence %1, I was expecting that '%2' would be the specific "
|
|
"value '%4'.");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
|
|
@<Issue problem for not being a phrase@> =
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p->down));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(...));
|
|
Problems::issue_problem_segment(
|
|
"In the sentence %1, I was expecting that '%2' would be a phrase.");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
|
|
@h New variables.
|
|
The following doesn't switch context and recurse down: there's nothing
|
|
to recurse down to, since all we have is a name for a new variable. Instead
|
|
we deal with that right away.
|
|
|
|
It might seem rather odd that the typechecker should be the part of Inform
|
|
which creates local variables. Surely that's a sign that the parsing went
|
|
wrong, so how did things get to this stage?
|
|
|
|
In a C-like language, where variables are predeclared, that would be true.
|
|
But in Inform, a phrase like:
|
|
|
|
>> let the monster be a random pterodactyl;
|
|
|
|
can be valid even where "the monster" is text not known to the S-parser
|
|
as yet -- indeed, that's how local variables are made. It's the typechecker
|
|
which sorts this out, because only the typechecker can decide which of the
|
|
subtly different forms of "let" is being used.
|
|
|
|
@<Deal with a new local variable name@> =
|
|
kind *K = Node::get_kind_required_by_context(p);
|
|
parse_node *check = p->down;
|
|
if (Node::is(check, AMBIGUITY_NT)) check = check->down;
|
|
if (LocalVariables::permit_as_new_local(check, FALSE)) {
|
|
if (kind_of_var_to_create) *kind_of_var_to_create = K;
|
|
return ALWAYS_MATCH;
|
|
}
|
|
@<Issue a problem for an inappropriate variable name@>;
|
|
return NEVER_MATCH;
|
|
|
|
@ This problem message is never normally seen using the definitions in the
|
|
Standard Rules because the definitions made there are such that other
|
|
problems appear first. So the only way to see this message is to declare an
|
|
unambiguous phrase with one of its tokens requiring a variable of a
|
|
species; and then to misuse that phrase.
|
|
|
|
@<Issue a problem for an inappropriate variable name@> =
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p));
|
|
if (Specifications::is_kind_like(p->down))
|
|
Problems::quote_text(3, "a kind of value");
|
|
else
|
|
Problems::quote_kind_of(3, p->down);
|
|
Problems::quote_kind(4, K);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_KindOfVariable));
|
|
Problems::issue_problem_segment(
|
|
"In the sentence %1, I was expecting that '%2' would be a new "
|
|
"variable name (to hold %4), but it turned out to be %3.");
|
|
Problems::issue_problem_end();
|
|
|
|
@h (4) Typechecking within current context.
|
|
Everything else, then, passes through here, with the context now set either
|
|
to |NULL| (meaning no expectations) or to some ancestor of |p| in the parse
|
|
tree.
|
|
|
|
Level 4 forks rapidly into three branches: (4A), for ambiguous readings;
|
|
(4I), for single invocations; and (4S), for single readings other than
|
|
invocations. Here's the code which does the switching:
|
|
|
|
@<Typecheck within current context@> =
|
|
kind *kind_needed = NULL;
|
|
int condition_context = FALSE;
|
|
if (context) {
|
|
kind_needed = Node::get_kind_required_by_context(context);
|
|
if ((Node::is(context, CONDITION_CONTEXT_NT)) ||
|
|
(Node::is(context, LOGICAL_AND_NT)) ||
|
|
(Node::is(context, LOGICAL_OR_NT)) ||
|
|
(Node::is(context, LOGICAL_NOT_NT)) ||
|
|
(Node::is(context, LOGICAL_TENSE_NT)))
|
|
condition_context = TRUE;
|
|
}
|
|
LOG_DASH("(4)");
|
|
|
|
int outcome = ALWAYS_MATCH;
|
|
if ((consider_alternatives) && (p->next_alternative))
|
|
@<Resolve an ambiguous reading@>
|
|
else
|
|
@<Verify an unambiguous reading@>;
|
|
return outcome;
|
|
|
|
@ For a phrase node, we pass the buck down to its invocation list. For an
|
|
invocation list, we pass the buck down to its invocation (which may or
|
|
may not be the first in a chain of alternatives), which means we end up
|
|
in (4I) either directly or via (4A). For everything else, it's (4S) for us.
|
|
|
|
@<Verify an unambiguous reading@> =
|
|
switch (p->node_type) {
|
|
case PHRASE_TO_DECIDE_VALUE_NT:
|
|
outcome = Dash::typecheck_recursive(p->down, context, TRUE);
|
|
break;
|
|
|
|
case INVOCATION_LIST_NT: case INVOCATION_LIST_SAY_NT: case AMBIGUITY_NT:
|
|
if (p->down == NULL) @<Unknown found text occurs as a command@>;
|
|
BEGIN_DASH_MODE;
|
|
Dash_ambiguity_list = p;
|
|
outcome = Dash::typecheck_recursive(p->down, context, TRUE);
|
|
END_DASH_MODE;
|
|
break;
|
|
|
|
case INVOCATION_NT: @<Step (4I) Verify an invocation@>; break;
|
|
|
|
default: @<Step (4S) Verify anything else@>; break;
|
|
}
|
|
|
|
@ (4A) Ambiguities.
|
|
Ambiguities presently consist of chains of invocation nodes listed in
|
|
the tree as alternatives.
|
|
|
|
@<Resolve an ambiguous reading@> =
|
|
LOG_DASH("(4A)");
|
|
parse_node *list_of_possible_readings[MAX_INVOCATIONS_PER_PHRASE];
|
|
int no_of_possible_readings = 0;
|
|
int no_of_passed_readings = 0;
|
|
|
|
@<Step (4A.a) Set up the list of readings to test@>;
|
|
@<Step (4A.b) Recurse Dash to try each reading in turn@>;
|
|
if (Dash::problems_have_been_issued()) return NEVER_MATCH;
|
|
if (no_of_passed_readings > 0) @<Step (4A.c) Preserve successful readings@>
|
|
else @<Step (4A.d) Give up with no readings possible@>;
|
|
LOGIF(MATCHING, "Ambiguity resolved to: $E", p);
|
|
|
|
@ Phrase definitions are kept in a linked list with a total ordering which
|
|
properly contains the partial ordering in which $P_1\leq P_2$ if they are
|
|
lexically identical and if each parameter of $P_1$ provably, at compile time,
|
|
also satisfies the requirements for the corresponding parameter of $P_2$.
|
|
They have already been lexically parsed in that order, so the list of
|
|
invocations (which will have accumulated during parsing) is also in that
|
|
same order. Now this is nearly the correct order for type-checking. But we
|
|
make one last adjustment: the phrase being compiled is moved to the back of
|
|
the list. This is to make recursion always the last thing checked, so that
|
|
later rules can override earlier ones but still make use of them.
|
|
|
|
@<Step (4A.a) Set up the list of readings to test@> =
|
|
LOG_DASH("(4A.a)");
|
|
parse_node *alt;
|
|
LOOP_THROUGH_ALTERNATIVES(alt, p)
|
|
if ((Node::is(alt, INVOCATION_NT)) &&
|
|
(Node::get_phrase_invoked(alt) != Functions::defn_being_compiled()))
|
|
@<Add this reading to the list of test cases@>;
|
|
LOOP_THROUGH_ALTERNATIVES(alt, p)
|
|
if (!((Node::is(alt, INVOCATION_NT)) &&
|
|
(Node::get_phrase_invoked(alt) != Functions::defn_being_compiled())))
|
|
@<Add this reading to the list of test cases@>;
|
|
LOGIF(MATCHING, "Resolving %d possible readings:\n", no_of_possible_readings);
|
|
for (int i=0; i<no_of_possible_readings; i++)
|
|
LOGIF(MATCHING, "Possibility (P%d) $e\n", i, list_of_possible_readings[i]);
|
|
|
|
@ In general, it's not great for typecheckers in compilers to put an upper bound
|
|
on complexity, because although human-written code seldom hits such maxima, there's
|
|
always the possibility of mechanically-generated code which does. On the other hand,
|
|
the result of that doctrine is that a lot of modern compilers (Swift, for example)
|
|
slow to a painful crawl and allocate gigabytes of memory trying to understand
|
|
strange type constraints in two or three lines of code. So, for now at least,
|
|
let's be pragmatic.
|
|
|
|
@<Add this reading to the list of test cases@> =
|
|
if (no_of_possible_readings >= MAX_INVOCATIONS_PER_PHRASE) {
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
Problems::quote_wording(1, Node::get_text(p));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_AmbiguitiesTooDeep));
|
|
Problems::issue_problem_segment(
|
|
"The phrase %1 is too complicated for me to disentangle without "
|
|
"running very, very slowly as I check many ambiguities in it. There "
|
|
"ought to be some way to simplify things for me?");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
}
|
|
list_of_possible_readings[no_of_possible_readings++] = alt;
|
|
Dash::clear_flags(alt);
|
|
|
|
@ Now we work through the list of tests. We must produce at least one reading
|
|
passing at least at the "sometimes" level marked by the |UNPROVEN_DASHFLAG|, or
|
|
else the whole specification fails its match. The first proven match stops our
|
|
work, since we can never need lower-priority interpretations.
|
|
|
|
@<Step (4A.b) Recurse Dash to try each reading in turn@> =
|
|
LOG_DASH("(4A.b)");
|
|
for (int ref = 0; ref<no_of_possible_readings; ref++) {
|
|
parse_node *inv = list_of_possible_readings[ref];
|
|
|
|
@<Test the current reading and set its results flags accordingly@>;
|
|
LOGIF(MATCHING, "(P%d) %s: $e\n", ref, Dash::verdict_to_text(inv), inv);
|
|
|
|
if (Dash::test_flag(inv, PASSED_DASHFLAG)) {
|
|
no_of_passed_readings++;
|
|
if (Dash::test_flag(inv, UNPROVEN_DASHFLAG) == FALSE) break;
|
|
}
|
|
if (Dash::problems_have_been_issued()) break; /* to prevent duplication of problem messages */
|
|
}
|
|
LOGIF(MATCHING, "List %s: ", (no_of_passed_readings > 0)?"passed":"failed");
|
|
for (int i=0; i<no_of_possible_readings; i++) {
|
|
parse_node *inv = list_of_possible_readings[i];
|
|
LOGIF(MATCHING, "%s ", Dash::quick_verdict_to_text(inv));
|
|
}
|
|
LOGIF(MATCHING, "|\n");
|
|
|
|
@ We tell Dash to run silently unless grosser-than-gross problems arise, and
|
|
also tell it to check the reading with no alternatives considered. (If we
|
|
let it consider alternatives, that would be circular: we'd end up here
|
|
again, and so on forever.)
|
|
|
|
@<Test the current reading and set its results flags accordingly@> =
|
|
LOGIF(MATCHING, "(P%d) Trying <%W>: $e\n", ref, Node::get_text(inv), inv);
|
|
|
|
BEGIN_DASH_MODE;
|
|
DASH_MODE_EXIT(ISSUE_PROBLEMS_DMODE);
|
|
int rv = Dash::typecheck_recursive(inv, context, FALSE);
|
|
END_DASH_MODE;
|
|
|
|
Dash::set_flag(inv, TESTED_DASHFLAG);
|
|
if (rv != NEVER_MATCH) {
|
|
Dash::set_flag(inv, PASSED_DASHFLAG);
|
|
outcome = Dash::worst_case(outcome, rv);
|
|
}
|
|
|
|
@ This is the happy ending, in which the list can probably be passed, though
|
|
there are still a handful of pitfalls.
|
|
|
|
@<Step (4A.c) Preserve successful readings@> =
|
|
LOG_DASH("(4A.c)");
|
|
@<Step (4A.c.1) Winnow the reading list down to the survivors@>;
|
|
@<Step (4A.c.2) Infer the kind of any requested local variable@>;
|
|
|
|
@ To recap, after checking through the possible readings we have something
|
|
like this as the result:
|
|
= (text)
|
|
f ? f g ? ? p - - -
|
|
=
|
|
We can now throw away the |f|, |g| and |-| readings -- failed, grossly failed,
|
|
or never reached -- to leave just those which will be compiled:
|
|
= (text)
|
|
? ? ? p
|
|
=
|
|
If compiled this will result in run-time code to check if the arguments
|
|
allow the first invocation and run it if so; then the second; then the third;
|
|
and, if those three fell through, run the fourth invocation without further
|
|
checking.
|
|
|
|
@<Step (4A.c.1) Winnow the reading list down to the survivors@> =
|
|
LOG_DASH("(4A.c.1)");
|
|
int invocational = TRUE;
|
|
if (Node::is(Dash_ambiguity_list, AMBIGUITY_NT)) invocational = FALSE;
|
|
|
|
LOGIF(MATCHING, "Winnow %s from $T\n",
|
|
(invocational)?"invocationally":"regularly", Dash_ambiguity_list);
|
|
|
|
if (invocational) {
|
|
int dubious = FALSE;
|
|
for (int ref = 0; ref<no_of_possible_readings; ref++) {
|
|
parse_node *inv = list_of_possible_readings[ref];
|
|
if (Node::is(inv, INVOCATION_NT) == FALSE)
|
|
dubious = TRUE;
|
|
}
|
|
if (dubious) @<Issue the dubious ambiguity problem message@>;
|
|
}
|
|
|
|
if (invocational) Dash_ambiguity_list->down = NULL;
|
|
|
|
parse_node *last_survivor = NULL;
|
|
for (int ref = 0; ref<no_of_possible_readings; ref++) {
|
|
parse_node *inv = list_of_possible_readings[ref];
|
|
inv->next_alternative = NULL;
|
|
if (Dash::test_flag(inv, PASSED_DASHFLAG)) {
|
|
if (invocational) {
|
|
if (last_survivor) last_survivor->next_alternative = inv;
|
|
else Dash_ambiguity_list->down = inv;
|
|
last_survivor = inv;
|
|
} else {
|
|
parse_node *link = Dash_ambiguity_list->next;
|
|
Node::copy(Dash_ambiguity_list, inv);
|
|
Dash_ambiguity_list->next = link;
|
|
Dash_ambiguity_list->next_alternative = NULL;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (invocational) {
|
|
p = Dash_ambiguity_list->down;
|
|
int nfi = -1, number_ambiguity = FALSE;
|
|
parse_node *inv;
|
|
LOOP_THROUGH_ALTERNATIVES(inv, p)
|
|
if (Node::is(inv, INVOCATION_NT)) {
|
|
int nti = Invocations::get_no_tokens(inv);
|
|
if (nfi == -1) nfi = nti;
|
|
else if (nfi != nti) number_ambiguity = TRUE;
|
|
}
|
|
|
|
if (number_ambiguity) @<Issue the number ambiguity problem message@>;
|
|
}
|
|
LOGIF(MATCHING, "After winnowing, CS is $T\n", current_sentence);
|
|
|
|
@ This is a last-throw-of-the-dice problem message, designed to pick up just
|
|
a few really awkward ambiguities which have been missed elsewhere in the parser
|
|
or in Dash.
|
|
|
|
@<Issue the dubious ambiguity problem message@> =
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_number(2, &no_of_possible_readings);
|
|
Problems::quote_wording(3, Node::get_text(list_of_possible_readings[0]));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_DubiousAmbiguity));
|
|
Problems::issue_problem_segment(
|
|
"The phrase %1 is ambiguous in a way that I can't sort out. "
|
|
"I can see %2 different meanings of '%3', and no good way to choose.");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
|
|
@ This is another sort of error which couldn't happen with a conventional
|
|
programming language -- in C, for instance, it's syntactically obvious
|
|
how many arguments a function call has, because the brackets and commas
|
|
are unambiguous. But in Inform, there are no reserved tokens of syntax
|
|
acting like that. So we could easily have two accepted invocations in the
|
|
list which have different numbers of arguments to each other, and there's
|
|
no way safely to adjudicate that at run-time.
|
|
|
|
@<Issue the number ambiguity problem message@> =
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_UnequalValueAmbiguity));
|
|
Problems::issue_problem_segment(
|
|
"The phrase %1 is ambiguous in a way that I can't disentangle. "
|
|
"It has more than one plausible interpretation, such that it "
|
|
"would only be possible to tell which is valid at run-time: "
|
|
"ordinarily that would be fine, but because the different "
|
|
"interpretations are so different (and involve different "
|
|
"numbers of values being used) there's no good way to cope. "
|
|
"Try rewording one of the phrases which caused this clash: "
|
|
"there's a good chance the problem will then go away.");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
|
|
@ If an invocation passes, and asks to create a local variable, we need
|
|
to mark the tree accordingly. If there's just one invocation then (4I)
|
|
handles this, but if there's ambiguity, we handle it here, and only
|
|
for the surviving nodes.
|
|
|
|
@<Step (4A.c.2) Infer the kind of any requested local variable@> =
|
|
LOG_DASH("(4A.c.2)");
|
|
parse_node *inv;
|
|
LOOP_THROUGH_ALTERNATIVES(inv, p)
|
|
if (Node::is(inv, INVOCATION_NT))
|
|
if (Dash::set_up_any_local_required(inv) == NEVER_MATCH)
|
|
return NEVER_MATCH;
|
|
|
|
@ And this is the unhappy ending:
|
|
|
|
@<Step (4A.d) Give up with no readings possible@> =
|
|
LOG_DASH("(4A.d)");
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
if (InvocationLists::length(p) == 0) return NEVER_MATCH;
|
|
|
|
LOGIF(MATCHING, "All possibilities failed: issuing problem\n");
|
|
return Dash::failed(list_of_possible_readings, no_of_possible_readings,
|
|
context, kind_needed);
|
|
|
|
@ (4I) Invocations.
|
|
Invocations are the hardest nodes to check, but here at least we can forget
|
|
all about the ambiguities arising from multiple possibilities, and look at
|
|
just a single one.
|
|
|
|
In the event of an interesting problem message, we mark an invocation as being
|
|
interestingly problematic, but we keep going, since other invocations might be
|
|
better. Only if everything fails will we retrace our steps and actually throw
|
|
the problem.
|
|
|
|
@<Step (4I) Verify an invocation@> =
|
|
LOG_DASH("(4I)");
|
|
int no_gross_problems_thrown_before = no_gross_problems_thrown;
|
|
int no_interesting_problems_thrown_before = no_interesting_problems_thrown;
|
|
int qualified = FALSE;
|
|
parse_node *inv = p;
|
|
ParseInvocations::parse_within_inv(inv);
|
|
Dash::set_flag(inv, TESTED_DASHFLAG);
|
|
id_body *idb = Node::get_phrase_invoked(inv);
|
|
if (idb) {
|
|
Node::set_kind_resulting(inv, IDTypeData::get_return_kind(&(idb->type_data)));
|
|
|
|
/* are the arguments of the right kind? */
|
|
if (outcome != NEVER_MATCH) @<Step (4I.a) Take care of arithmetic phrases@>;
|
|
if (outcome != NEVER_MATCH) @<Step (4I.b) Take care of non-arithmetic phrases@>;
|
|
if (outcome != NEVER_MATCH) @<Step (4I.c) Match type templates in the argument specifications@>;
|
|
if (outcome != NEVER_MATCH) @<Step (4I.d) Match kinds in assignment phrases@>;
|
|
|
|
/* if this evaluates something, is it a value of the right kind? */
|
|
if (outcome != NEVER_MATCH) @<Step (4I.e) Check kind of value returned@>;
|
|
|
|
/* are there any special rules about invoking this phrase? */
|
|
if (outcome != NEVER_MATCH) @<Step (4I.f) Check any phrase options@>;
|
|
if (outcome != NEVER_MATCH) @<Step (4I.g) Worry about self in say property of@>;
|
|
if (outcome != NEVER_MATCH) @<Step (4I.h) Worry about using a phrase outside of the control structure it belongs to@>;
|
|
if (outcome != NEVER_MATCH) @<Step (4I.i) Disallow any phrases which are now deprecated@>;
|
|
|
|
/* should we mark to create a let variable here? */
|
|
if ((outcome != NEVER_MATCH) && (consider_alternatives))
|
|
outcome = Dash::worst_case(outcome, Dash::set_up_any_local_required(inv));
|
|
}
|
|
|
|
/* the outcome is now definitely known */
|
|
if (outcome == NEVER_MATCH) @<Step (4I.j) Cope with failure@>
|
|
else @<Step (4I.k) Cope with success@>;
|
|
|
|
@ Most problem messages issued by (4I) will be of a sort called "interesting",
|
|
and will use the following macro.
|
|
|
|
@d THIS_IS_AN_INTERESTING_PROBLEM
|
|
outcome = NEVER_MATCH;
|
|
no_interesting_problems_thrown++;
|
|
if (TEST_DASH_MODE(ISSUE_INTERESTING_PROBLEMS_DMODE))
|
|
|
|
@ "Polymorphic" here means that the phrase (i) produces a value, and (ii) that
|
|
the kind of this value depends on the kinds of its arguments. Inform supports
|
|
only a few polymorphic phrases, all clearly declared as such in the Standard
|
|
Rules, and they come in two sorts: those marked with a "polymorphism exception",
|
|
and those marked as "arithmetic operations".
|
|
|
|
@<Step (4I.a) Take care of arithmetic phrases@> =
|
|
LOG_DASH("(4I.a)");
|
|
if (IDTypeData::arithmetic_operation(idb) == TOTAL_OPERATION)
|
|
@<Step (4I.a.1) "Total P of O" has kind the kind of P@>
|
|
else if (IDTypeData::is_arithmetic_phrase(idb)) @<Step (4I.a.2) Dimension-check arithmetic phrases@>;
|
|
|
|
@ For instance, the kind of "total carrying capacity of people in the Dining
|
|
Room" is a number, because the kind of the property "carrying capacity" is
|
|
"number".
|
|
|
|
@<Step (4I.a.1) "Total P of O" has kind the kind of P@> =
|
|
LOG_DASH("(4I.a.1)");
|
|
parse_node *P = Invocations::get_token_as_parsed(inv, 0);
|
|
int rv = Dash::typecheck_recursive(P, NULL, TRUE);
|
|
if ((rv != NEVER_MATCH) && (Rvalues::is_CONSTANT_construction(P, CON_property))) {
|
|
property *prn = Rvalues::to_property(P);
|
|
if (Properties::is_value_property(prn))
|
|
Node::set_kind_resulting(inv, ValueProperties::kind(prn));
|
|
else {
|
|
THIS_IS_AN_INTERESTING_PROBLEM {
|
|
StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_TotalEitherOr),
|
|
"this seems to be an attempt to total up an either/or property",
|
|
"and by definition such a property has nothing to total.");
|
|
}
|
|
}
|
|
} else @<Fail the invocation for totalling something other than a property@>;
|
|
|
|
@ The problem message here is to help what turns out to be quite a popular
|
|
mistake. (Perhaps we should simply implement column-totalling and be done
|
|
with it.)
|
|
|
|
@<Fail the invocation for totalling something other than a property@> =
|
|
LOG_DASH("(4I.a.1) failed as nonproperty");
|
|
if (Kinds::get_construct(Node::get_kind_of_value(P)) == CON_table_column) {
|
|
THIS_IS_AN_INTERESTING_PROBLEM {
|
|
StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_TotalTableColumn),
|
|
"this seems to be an attempt to total up the column of a table",
|
|
"whereas it's only legal to use 'total' for properties.");
|
|
}
|
|
}
|
|
outcome = NEVER_MATCH;
|
|
|
|
@ For instance, the following blocks an attempt to add a number to a text.
|
|
|
|
@<Step (4I.a.2) Dimension-check arithmetic phrases@> =
|
|
LOG_DASH("(4I.a.2)");
|
|
int op_number = IDTypeData::arithmetic_operation(idb);
|
|
LOGIF(MATCHING, "Arithmetic operation <op-%d>\n", op_number);
|
|
|
|
parse_node *L, *R;
|
|
kind *kind_wanted, *left_kind, *right_kind, *kind_produced;
|
|
@<Work out the kinds of the operands, and what we want, and what we get@>;
|
|
LOGIF(MATCHING, "%u (~) %u = %u\n", left_kind, right_kind, kind_produced);
|
|
|
|
if (kind_produced) Node::set_kind_resulting(inv, kind_produced);
|
|
else @<Fail the invocation for a dimensional problem@>;
|
|
|
|
@ For the way this is actually worked out, see the section on "Dimensions".
|
|
|
|
@<Work out the kinds of the operands, and what we want, and what we get@> =
|
|
L = Invocations::get_token(inv, 0);
|
|
left_kind = Dash::fix_arithmetic_operand(L);
|
|
if (Kinds::Dimensions::arithmetic_op_is_unary(op_number)) {
|
|
R = NULL; right_kind = NULL;
|
|
} else {
|
|
R = Invocations::get_token(inv, 1);
|
|
right_kind = Dash::fix_arithmetic_operand(R);
|
|
}
|
|
if (((left_kind) && (Kinds::Behaviour::is_quasinumerical(left_kind) == FALSE)) ||
|
|
((right_kind) && (Kinds::Behaviour::is_quasinumerical(right_kind) == FALSE)))
|
|
kind_produced = NULL;
|
|
else
|
|
kind_produced = Kinds::Dimensions::arithmetic_on_kinds(left_kind, right_kind, op_number);
|
|
kind_wanted = kind_needed;
|
|
|
|
@ Note that "value" -- the vaguest kind of all -- might come up here as
|
|
a result of some problem evaluating one of the operands, which has already been
|
|
reported in a problem message; so we only issue this problem message when
|
|
L and R are more definite.
|
|
|
|
@<Fail the invocation for a dimensional problem@> =
|
|
if ((left_kind) && (Kinds::eq(left_kind, K_value) == FALSE) &&
|
|
(right_kind) && (Kinds::eq(right_kind, K_value) == FALSE)) {
|
|
THIS_IS_AN_INTERESTING_PROBLEM {
|
|
LOG("So the inv subtree is:\n$T\n", inv);
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(L));
|
|
Problems::quote_wording(3, Node::get_text(R));
|
|
Problems::quote_kind(4, left_kind);
|
|
Problems::quote_kind(5, right_kind);
|
|
switch(op_number) {
|
|
case PLUS_OPERATION: Problems::quote_text(6, "adding"); Problems::quote_text(7, "to"); break;
|
|
case MINUS_OPERATION: Problems::quote_text(6, "subtracting"); Problems::quote_text(7, "from"); break;
|
|
case TIMES_OPERATION: Problems::quote_text(6, "multiplying"); Problems::quote_text(7, "by"); break;
|
|
case DIVIDE_OPERATION:
|
|
case REMAINDER_OPERATION: Problems::quote_text(6, "dividing"); Problems::quote_text(7, "by"); break;
|
|
case APPROXIMATION_OPERATION: Problems::quote_text(6, "rounding"); Problems::quote_text(7, "to"); break;
|
|
}
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_BadArithmetic));
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but that seems to involve %6 %4 ('%2') %7 %5 ('%3'), "
|
|
"which is not good arithmetic.");
|
|
Problems::issue_problem_end();
|
|
}
|
|
}
|
|
outcome = NEVER_MATCH;
|
|
|
|
@ This is the general case: almost all phrases fall into this category,
|
|
including all phrases created outside the Standard Rules.
|
|
|
|
The deal is simply that every argument must match its specification. For
|
|
instance, if |inv| is an invocation of this phrase:
|
|
|
|
>> To truncate (L - a list of values) to (N - a number) entries: ...
|
|
|
|
...then token 0 must match "list of values", and token 1 must match "number".
|
|
|
|
@<Step (4I.b) Take care of non-arithmetic phrases@> =
|
|
if (IDTypeData::is_arithmetic_phrase(idb) == FALSE) {
|
|
LOG_DASH("(4I.b)");
|
|
int i, exit_at_once = FALSE;
|
|
for (i=0; i<Invocations::get_no_tokens(inv); i++) {
|
|
LOGIF(MATCHING, "(4I.b) trying argument %d (prior to this, best possible: %d)\n",
|
|
i, outcome);
|
|
Invocations::set_token_check_to_do(inv, i, NULL);
|
|
@<Type-check a single token from the list@>;
|
|
if (exit_at_once) break;
|
|
}
|
|
LOGIF(MATCHING, "(4I.b) argument type matching %s\n",
|
|
(outcome==NEVER_MATCH)?"failed":"passed");
|
|
}
|
|
|
|
@<Type-check a single token from the list@> =
|
|
parse_node *ith_spec = idb->type_data.token_sequence[i].to_match;
|
|
if ((idb->type_data.token_sequence[i].construct == KIND_NAME_IDTC) && (outcome != NEVER_MATCH))
|
|
@<Cautiously reparse this as a name of a kind of value@>
|
|
else {
|
|
int save_kcm = kind_checker_mode;
|
|
kind_checker_mode = MATCH_KIND_VARIABLES_AS_UNIVERSAL;
|
|
kind *create = NULL;
|
|
BEGIN_DASH_MODE;
|
|
DASH_MODE_EXIT(ISSUE_PROBLEMS_DMODE);
|
|
DASH_MODE_CREATE(&create);
|
|
int rv = Dash::typecheck_recursive(Invocations::get_token(inv, i), context, TRUE);
|
|
END_DASH_MODE;
|
|
switch(rv) {
|
|
case NEVER_MATCH:
|
|
LOGIF(MATCHING, "(4I.b) on %W failed at token %d\n", Node::get_text(p), i);
|
|
outcome = NEVER_MATCH;
|
|
if (Dash::problems_have_been_issued()) exit_at_once = TRUE;
|
|
break;
|
|
case SOMETIMES_MATCH:
|
|
LOGIF(MATCHING, "(4I.b) on %W qualified at token %d\n", Node::get_text(p), i);
|
|
Invocations::set_token_check_to_do(inv, i, ith_spec);
|
|
qualified = TRUE;
|
|
break;
|
|
}
|
|
kind_checker_mode = save_kcm;
|
|
if (create) {
|
|
if ((CompileBlocksAndLines::compiling_single_line_block()) &&
|
|
(IDTypeData::is_a_let_assignment(idb))) {
|
|
THIS_IS_AN_INTERESTING_PROBLEM {
|
|
Problems::quote_source(1, current_sentence);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_LetCreatedInIf));
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but when a temporary value is created "
|
|
"inside an 'if ..., ...' or an 'otherwise ...', it only "
|
|
"lasts until that line is complete - which means it "
|
|
"can never be used for anything, because it goes away "
|
|
"as soon as created. To make something more durable, "
|
|
"create it before the 'if' or 'otherwise'.");
|
|
Problems::issue_problem_end();
|
|
}
|
|
}
|
|
Invocations::set_token_variable_kind(inv, i, create);
|
|
}
|
|
}
|
|
|
|
@ The following is a delicate manoeuvre, but luckily it takes action only very
|
|
rarely and in very specific circumstances. We allow a very limited use
|
|
of second-order logic in using the name of a kind as if it were a value,
|
|
even though Inform is really not set up for this. The point is to allow:
|
|
|
|
>> let (name - nonexisting variable) be (K - name of kind of word value);
|
|
|
|
where the "K" parameter would match (1) but not (2), (3) or (4) from:
|
|
|
|
>> (1) let X be a number;
|
|
>> (2) let X be text;
|
|
>> (3) let X be 21;
|
|
>> (4) let X be \{1, 2, 3\};
|
|
|
|
What all of this has to do with being |UNKNOWN_NT| is that text parsed in the
|
|
expectation of a value will usually not recognise something like "a list
|
|
of numbers", so that would be here as |UNKNOWN_NT|. We take the otherwise
|
|
unheard-of measure of reparsing the text, but we only impose the result
|
|
if the match can definitely be made successfully.
|
|
|
|
We have to be very careful to take action only on (5) and not (6):
|
|
|
|
>> (5) let L be a list of scenes;
|
|
>> (6) let L be the list of scenes;
|
|
|
|
(5) creates L as an empty list, whereas (6) creates it as the list made up
|
|
of all scenes. We can tell these apart since (6) will have a valid phrase
|
|
in |ith_token|, an invocation of "the list of K", whereas (5) won't.
|
|
|
|
@<Cautiously reparse this as a name of a kind of value@> =
|
|
outcome = NEVER_MATCH;
|
|
parse_node *ith_token = Invocations::get_token_as_parsed(inv, i);
|
|
LOGIF(MATCHING, "(4I.b) thinking about reparsing: $P\n", ith_token);
|
|
int warned_already = FALSE;
|
|
if (Node::is(ith_token, AMBIGUITY_NT)) ith_token = ith_token->down;
|
|
if ((Node::is(ith_token, UNKNOWN_NT)) ||
|
|
(Specifications::is_description(ith_token)) ||
|
|
(Rvalues::is_CONSTANT_construction(ith_token, CON_property)) ||
|
|
(Specifications::is_kind_like(ith_token)) ||
|
|
((IDTypeData::is_a_let_assignment(idb) == FALSE) &&
|
|
(Node::is(ith_token, PHRASE_TO_DECIDE_VALUE_NT)))) {
|
|
wording W = Node::get_text(ith_token);
|
|
|
|
kind *K = NULL;
|
|
parse_node *reparsed = NULL;
|
|
if (<s-type-expression>(W)) reparsed = <<rp>>;
|
|
if (Specifications::is_kind_like(reparsed))
|
|
K = Specifications::to_kind(reparsed);
|
|
if ((K == NULL) && (<k-kind>(W))) K = <<rp>>;
|
|
if (K == NULL) {
|
|
if ((<value-property-name>(W)) &&
|
|
(ValueProperties::coincides_with_kind(<<rp>>)))
|
|
K = ValueProperties::kind(<<rp>>);
|
|
}
|
|
|
|
LOGIF(MATCHING, "(4I.b) reparsed as: %u (vs spec $P)\n", K, ith_spec);
|
|
if ((K) && (Specifications::is_kind_like(ith_spec))) {
|
|
kind *ikind = Specifications::to_kind(ith_spec);
|
|
if (Kinds::Behaviour::definite(K)) {
|
|
if (Kinds::compatible(K, ikind) == ALWAYS_MATCH) {
|
|
LOGIF(MATCHING, "(4I.b) allows name-of token: $P\n", reparsed);
|
|
Invocations::set_token_as_parsed(inv, i, Node::duplicate(reparsed));
|
|
outcome = ALWAYS_MATCH;
|
|
} else {
|
|
THIS_IS_AN_ORDINARY_PROBLEM {
|
|
warned_already = TRUE;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, W);
|
|
Problems::quote_kind(3, ikind);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_NameOfKindMismatch));
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but although '%2' is the name of a kind, "
|
|
"it isn't the name of a kind of %3, which this phrase needs.");
|
|
Problems::issue_problem_end();
|
|
}
|
|
}
|
|
} else {
|
|
THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM {
|
|
warned_already = TRUE;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, W);
|
|
Problems::quote_kind(3, ikind);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_BadLocalKOV));
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but although '%2' is the name of a kind, "
|
|
"it isn't a definite kind and is instead a general "
|
|
"description which might apply to many different kinds. "
|
|
"(For example, 'let R be a relation' is vague because it doesn't "
|
|
"make clear what R will relate - 'let R be a relation of numbers' "
|
|
"would be fine.)");
|
|
Problems::issue_problem_end();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ith_token = Invocations::get_token_as_parsed(inv, i);
|
|
if ((!Specifications::is_kind_like(ith_token)) && (warned_already == FALSE)) {
|
|
THIS_IS_AN_ORDINARY_PROBLEM {
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(ith_token));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_NameOfKindIsnt));
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but although '%2' does have a meaning, "
|
|
"it isn't the name of a kind, which this phrase needs.");
|
|
Problems::issue_problem_end();
|
|
}
|
|
}
|
|
|
|
@ For templates and the meaning of |kind_checker_mode|, see the section
|
|
on "Kind Checking". But basically this handles the matching of an invocation
|
|
against a definition like:
|
|
|
|
>> To remove (N - value of kind K) from (L - list of Ks): ...
|
|
|
|
@<Step (4I.c) Match type templates in the argument specifications@> =
|
|
LOG_DASH("(4I.c)");
|
|
int exit_at_once = FALSE;
|
|
|
|
kind_variable_declaration *kvd_marker = LAST_OBJECT(kind_variable_declaration);
|
|
|
|
if (IDTypeData::contains_variables(&(idb->type_data))) {
|
|
kind_variable_declaration *save_most_recent_interpretation = most_recent_interpretation;
|
|
int pass, save_kcm = kind_checker_mode;
|
|
for (pass = 1; pass <= 2; pass++) {
|
|
LOGIF(MATCHING, "(4I.c) prototype check pass %d\n", pass);
|
|
if (Log::aspect_switched_on(MATCHING_DA)) Latticework::show_variables();
|
|
if (pass == 1) kind_checker_mode = MATCH_KIND_VARIABLES_INFERRING_VALUES;
|
|
else kind_checker_mode = MATCH_KIND_VARIABLES_AS_VALUES;
|
|
int i;
|
|
for (i=0; i<Invocations::get_no_tokens(inv); i++) {
|
|
kind *Kt = IDTypeData::token_kind(&(idb->type_data), i);
|
|
if ((Kt) &&
|
|
(idb->type_data.token_sequence[i].construct != NEW_LOCAL_IDTC)) {
|
|
parse_node *token_spec = Invocations::get_token_as_parsed(inv, i);
|
|
kind *kind_read = Specifications::to_kind(token_spec);
|
|
LOGIF(MATCHING, "Token %d: $P: kind %u: template %u\n", i,
|
|
token_spec, kind_read, Kt);
|
|
switch(Kinds::compatible(kind_read, Kt)) {
|
|
case NEVER_MATCH:
|
|
LOGIF(MATCHING, "(4I.c) failed at token %d\n", i);
|
|
outcome = NEVER_MATCH;
|
|
if (Dash::problems_have_been_issued()) exit_at_once = TRUE;
|
|
break;
|
|
case SOMETIMES_MATCH:
|
|
outcome = Dash::worst_case(outcome, SOMETIMES_MATCH);
|
|
/* we won't use |with_qualifications| -- we don't know exactly what they are */
|
|
LOGIF(MATCHING, "(4I.c) dropping to sometimes at token %d\n", i);
|
|
break;
|
|
case ALWAYS_MATCH:
|
|
break;
|
|
}
|
|
}
|
|
if (exit_at_once) break;
|
|
}
|
|
if (exit_at_once) break;
|
|
if ((pass == 1) && (outcome != NEVER_MATCH)) {
|
|
LOGIF(MATCHING, "(4I.c) prototype check passed\n");
|
|
most_recent_interpretation = NULL;
|
|
kind_variable_declaration *kvdm = kvd_marker;
|
|
if (kvdm) kvdm = NEXT_OBJECT(kvdm, kind_variable_declaration);
|
|
else kvdm = FIRST_OBJECT(kind_variable_declaration);
|
|
while (kvdm) {
|
|
kvdm->next = most_recent_interpretation;
|
|
most_recent_interpretation = kvdm;
|
|
kvdm = NEXT_OBJECT(kvdm, kind_variable_declaration);
|
|
}
|
|
}
|
|
}
|
|
kind_checker_mode = save_kcm;
|
|
if (outcome != NEVER_MATCH) Node::set_kind_variable_declarations(inv, most_recent_interpretation);
|
|
most_recent_interpretation = save_most_recent_interpretation;
|
|
}
|
|
|
|
if (Kinds::contains(Node::get_kind_resulting(inv), CON_KIND_VARIABLE)) {
|
|
int changed = FALSE;
|
|
kind *K = Kinds::substitute(Node::get_kind_resulting(inv), NULL, &changed, FALSE);
|
|
if (changed) {
|
|
LOGIF(MATCHING, "(4I.c) amended kind returned to %u\n", K);
|
|
Node::set_kind_resulting(inv, K);
|
|
} else @<Disallow an undeclared kind variable as return kind@>;
|
|
}
|
|
|
|
@<Disallow an undeclared kind variable as return kind@> =
|
|
THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible));
|
|
Problems::issue_problem_segment(
|
|
"In the line %1, you seem to be using '%2' to produce a value, but "
|
|
"it's not clear what kind of value this will be. It seems to use "
|
|
"a phrase which has been declared wrongly, because the kind it decides "
|
|
"is given only by a symbol which isn't otherwise defined.");
|
|
Problems::issue_problem_end();
|
|
outcome = NEVER_MATCH;
|
|
|
|
@ Although we don't implement it with prototypes as such, there's a
|
|
similar constraint on the arguments of an assignment. If we are checking an
|
|
invocation against:
|
|
|
|
>> To let (t - existing variable) be (u - value): ...
|
|
|
|
then we have so far checked that argument 0 is indeed the name of a variable
|
|
which already exists. But suppose the invocation is
|
|
|
|
>> let N be "there'll be no mutant enemy";
|
|
|
|
where N has already been created as a variable of kind "number". This clearly
|
|
has to be rejected, as it would violate type-safety. Step (4I.d) therefore
|
|
makes sure that all assignments match the kind of the new value against the
|
|
kind of the storage item to which it is being written.
|
|
|
|
A reasonable question might be why we don't implement this using the prototype
|
|
system of (4I.c), thus removing a rule from this already-complex algorithm, say by
|
|
|
|
>> To let (var - K variable) be (val - value of kind K): ...
|
|
|
|
The answer is that this would indeed work nicely for valid source text, but that
|
|
we would get less helpful problem messages in the all-too-likely case of a
|
|
mistake having been made.
|
|
|
|
@<Step (4I.d) Match kinds in assignment phrases@> =
|
|
LOG_DASH("(4I.d)");
|
|
if (IDTypeData::is_assignment_phrase(idb)) {
|
|
parse_node *target = Invocations::get_token_as_parsed(inv, 0);
|
|
parse_node *new_value = Invocations::get_token_as_parsed(inv, 1);
|
|
|
|
parse_node *target_spec = idb->type_data.token_sequence[0].to_match;
|
|
parse_node *new_value_spec = idb->type_data.token_sequence[1].to_match;
|
|
|
|
local_variable *lvar = Lvalues::get_local_variable_if_any(target);
|
|
if ((lvar) && (LocalVariables::protected(lvar)))
|
|
outcome = NEVER_MATCH;
|
|
else {
|
|
if (Kinds::Behaviour::is_object(Specifications::to_kind(target_spec)))
|
|
@<Step (4I.d.1) Police an assignment to an object@>;
|
|
|
|
if (idb->type_data.token_sequence[0].construct != NEW_LOCAL_IDTC)
|
|
@<Step (4I.d.2) Police an assignment to a storage item@>;
|
|
}
|
|
}
|
|
|
|
@ It doesn't always look like an assignment, but a phrase such as:
|
|
|
|
>> change the Marble Door to open;
|
|
|
|
has similar type-checking needs.
|
|
|
|
@<Step (4I.d.1) Police an assignment to an object@> =
|
|
LOG_DASH("(4I.d.1)");
|
|
instance *target_wo = Rvalues::to_object_instance(target);
|
|
property *prn = NULL;
|
|
int make_check = FALSE;
|
|
if (Kinds::eq(Node::get_kind_of_value(new_value_spec), K_value))
|
|
@<Maybe we're changing an object to a value of a kind coinciding with a property@>;
|
|
if (Rvalues::is_CONSTANT_construction(new_value_spec, CON_property))
|
|
@<Maybe we're changing an object to a named either/or property or condition state@>;
|
|
if (make_check)
|
|
@<Check that the property exists and that the object is allowed to have it@>;
|
|
|
|
@ There are actually two definitions like this in the Standard Rules:
|
|
|
|
>> (1) To change (o - object) to (w - value): ...
|
|
>> (2) To change (o - object) to (p - property): ...
|
|
|
|
Here's the code for (1), the less obvious case. This is needed for something like
|
|
|
|
>> change the canvas to blue;
|
|
|
|
where "blue" is a constant colour, and "colour" is both a kind and also a
|
|
property. (This case really is an assignment -- it assigns the value "blue"
|
|
to the colour property of the canvas.)
|
|
|
|
@<Maybe we're changing an object to a value of a kind coinciding with a property@> =
|
|
LOG_DASH("(4I.d.1.a)");
|
|
instance *I = Rvalues::to_instance(new_value);
|
|
if (I == NULL) outcome = NEVER_MATCH;
|
|
else {
|
|
prn = Properties::property_with_same_name_as(Instances::to_kind(I));
|
|
if (prn == NULL) outcome = NEVER_MATCH;
|
|
else make_check = TRUE;
|
|
}
|
|
|
|
@ And here's the simpler case, (2). A small quirk here is that it will also pick
|
|
up "change the Atrium to spiffy" in the following:
|
|
|
|
>> Atrium is a room. The Atrium can be spiffy, cool or lame.
|
|
>> When play begins: change the Atrium to spiffy.
|
|
|
|
...where "spiffy" is deemed a property rather than a constant value of a kind
|
|
because of the way the condition of the Atrium is declared. This is a little
|
|
bit horrid, but works fine in practice. (If we try to accommodate this case
|
|
within (1.2.4.1a), which might seem more logical, we run into trouble because
|
|
the property name is cast to a property value of |self| when being typechecked
|
|
against "value".)
|
|
|
|
@<Maybe we're changing an object to a named either/or property or condition state@> =
|
|
LOG_DASH("(4I.d.1.b)");
|
|
if (Rvalues::is_CONSTANT_construction(new_value, CON_property))
|
|
prn = Rvalues::to_property(new_value);
|
|
else if (Descriptions::number_of_adjectives_applied_to(new_value) == 1) {
|
|
adjective *aph = AdjectivalPredicates::to_adjective(Descriptions::first_unary_predicate(new_value));
|
|
if (AdjectiveAmbiguity::has_enumerative_meaning(aph))
|
|
prn = Properties::property_with_same_name_as(Instances::to_kind(AdjectiveAmbiguity::has_enumerative_meaning(aph)));
|
|
else if (AdjectiveAmbiguity::has_either_or_property_meaning(aph, NULL))
|
|
prn = AdjectiveAmbiguity::has_either_or_property_meaning(aph, NULL);
|
|
}
|
|
make_check = TRUE;
|
|
|
|
@ We do something quite interesting here, if the object is not explicitly
|
|
named: we deliberately allow an assignment which may not be type-safe, and
|
|
without even dropping to the "sometimes" level. This is for phrases like so:
|
|
|
|
>> change the item to closed;
|
|
|
|
Here the author seems to know what he's doing, and is pretty sure that the
|
|
current contents of "item" will accept closure. All we can prove is that
|
|
"item" contains an object (or perhaps |nothing|, the non-object). But
|
|
we allow the assignment because it will compile to code which will issue
|
|
a helpful run-time problem message if it goes wrong.
|
|
|
|
Now that "change" has been removed, as of January 2011, it looks as if
|
|
this case in the type-checker is never exercised.
|
|
|
|
@<Check that the property exists and that the object is allowed to have it@> =
|
|
LOGIF(MATCHING, "Property appears to be $Y\n", prn);
|
|
if (prn == NULL) {
|
|
THIS_IS_AN_INTERESTING_PROBLEM {
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(target));
|
|
Problems::quote_wording(3, Node::get_text(new_value));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible)); /* the parser seems not to allow these */
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, asking to change the object '%2'. This would "
|
|
"make sense if '%3' were an either/or property like 'open' "
|
|
"(or perhaps a named property value like 'blue') - but it "
|
|
"isn't, so the change makes no sense.");
|
|
Problems::issue_problem_end();
|
|
}
|
|
}
|
|
if ((target_wo) && (prn) &&
|
|
(PropertyPermissions::find(Instances::as_subject(target_wo), prn, TRUE) == NULL)) {
|
|
THIS_IS_AN_INTERESTING_PROBLEM {
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(target));
|
|
Problems::quote_property(3, prn);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible));
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but '%2' is not allowed to have the property '%3'.");
|
|
Problems::issue_problem_end();
|
|
}
|
|
}
|
|
|
|
@ This is more straightforward, with just a tiny glitch to make the rules
|
|
tougher on variables which hold text to be parsed. (Because the regular rules
|
|
for exchanging the subtly different forms of text which a double-quoted
|
|
literal can mean are too generous.)
|
|
|
|
@<Step (4I.d.2) Police an assignment to a storage item@> =
|
|
kind *kind_wanted, *kind_found;
|
|
LOGIF(MATCHING, "Check assignment of $P to $P\n", new_value, target);
|
|
switch(Lvalues::get_storage_form(target_spec)) {
|
|
case LOCAL_VARIABLE_NT: Problems::quote_text(6, "the name of"); break;
|
|
case PROPERTY_VALUE_NT: Problems::quote_text(6, "a property whose kind of value is"); break;
|
|
case NONLOCAL_VARIABLE_NT: Problems::quote_text(6, "a variable whose kind of value is"); break;
|
|
case TABLE_ENTRY_NT: Problems::quote_text(6, "a table entry whose kind of value is"); break;
|
|
case LIST_ENTRY_NT: Problems::quote_text(6, "an entry in a list whose kind of value is"); break;
|
|
default: Problems::quote_text(6, "a stored value holding"); break;
|
|
}
|
|
kind_wanted = Specifications::to_kind(target);
|
|
kind_found = Specifications::to_kind(new_value);
|
|
|
|
parse_node *new_invl = new_value->down;
|
|
if (Node::is(new_invl, INVOCATION_LIST_NT)) {
|
|
parse_node *new_inv;
|
|
LOOP_THROUGH_ALTERNATIVES(new_inv, new_invl)
|
|
if (Dash::test_flag(new_inv, PASSED_DASHFLAG)) break;
|
|
if (new_inv) kind_found = Node::get_kind_resulting(new_inv);
|
|
}
|
|
LOGIF(MATCHING, "Kinds found: %u, wanted: %u\n", kind_found, kind_wanted);
|
|
|
|
if (((K_understanding) && (Kinds::eq(kind_wanted, K_understanding)) &&
|
|
(Kinds::eq(kind_found, K_understanding) == FALSE))
|
|
|| (Kinds::compatible(kind_found, kind_wanted) == NEVER_MATCH)) {
|
|
THIS_IS_AN_INTERESTING_PROBLEM {
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(target));
|
|
Problems::quote_kind(3, Specifications::to_kind(target));
|
|
Problems::quote_wording(4, Node::get_text(new_value));
|
|
Problems::quote_kind(5, Specifications::to_kind(new_value));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_ChangeToWrongValue));
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but '%2' is supposed to be "
|
|
"%6 %3, so it cannot be set equal to %4, whose kind is %5.");
|
|
Problems::issue_problem_end();
|
|
}
|
|
}
|
|
|
|
@ Suppose we have something like this:
|
|
|
|
>> award the current action points;
|
|
|
|
and we are typechecking |found| as "the current action" (a phrase deciding
|
|
a value) against |expected| as "number", the parameter expected in
|
|
"award (N - a number) points".
|
|
|
|
No matter how peculiar this invocation of |found| was, we have now successfully
|
|
worked out the kind of the value it would return if compiled, and this is
|
|
stored in |inv->kind_resulting|. We now check to see if this matches the kind
|
|
expected -- in this example, it won't, because a stored action does not cast
|
|
to a number.
|
|
|
|
@<Step (4I.e) Check kind of value returned@> =
|
|
LOG_DASH("(4I.e)");
|
|
int outcome_test = ALWAYS_MATCH;
|
|
if (kind_needed) {
|
|
LOGIF(MATCHING, "Checking returned %u against desired %u\n",
|
|
Node::get_kind_resulting(inv), kind_needed);
|
|
outcome_test = Kinds::compatible(
|
|
Node::get_kind_resulting(inv), kind_needed);
|
|
}
|
|
switch (outcome_test) {
|
|
case NEVER_MATCH: outcome = NEVER_MATCH; break;
|
|
case SOMETIMES_MATCH: outcome = Dash::worst_case(outcome, SOMETIMES_MATCH); break;
|
|
}
|
|
|
|
@ The final stage in type-checking a phrase is to ensure that any phrase
|
|
options are properly used.
|
|
|
|
@<Step (4I.f) Check any phrase options@> =
|
|
LOG_DASH("(4I.f)");
|
|
if ((outcome != NEVER_MATCH) && (Node::get_phrase_options_invoked(inv))) {
|
|
int cso = PhraseOptions::parse_invoked_options(
|
|
inv, (TEST_DASH_MODE(ISSUE_PROBLEMS_DMODE))?FALSE:TRUE);
|
|
if (cso == FALSE) outcome = NEVER_MATCH;
|
|
}
|
|
|
|
@ A say phrase which involves a property of something implicitly changes
|
|
the scope for any vaguely described properties within the text supplied
|
|
as that property (if it is indeed text). We have to mark any such
|
|
property, and any such say. For instance, suppose we are typechecking
|
|
|
|
>> (1) "Oh, look: [initial appearance of the escritoire]"
|
|
|
|
and the initial appearance in question is:
|
|
|
|
>> (2) "A small, portable writing desk holding up to [carrying capacity] letters."
|
|
|
|
Printing text (2), it's important for the |self| object to be the
|
|
escritoire, which might not be the case otherwise; so during the printing
|
|
of (1), we have to change |self| temporarily and restore it afterwards.
|
|
|
|
@<Step (4I.g) Worry about self in say property of@> =
|
|
LOG_DASH("(4I.g)");
|
|
if ((IDTypeData::is_a_say_phrase(idb)) &&
|
|
(Invocations::get_no_tokens(inv) == 1) &&
|
|
(Lvalues::get_storage_form(Invocations::get_token_as_parsed(inv, 0)) == PROPERTY_VALUE_NT)) {
|
|
Annotations::write_int(Invocations::get_token_as_parsed(inv, 0), record_as_self_ANNOT, TRUE);
|
|
Invocations::mark_to_save_self(inv);
|
|
}
|
|
|
|
@ Some phrases are defined with a notation making them allowable only inside
|
|
loops, or other control structures; for instance,
|
|
|
|
>> To break -- in loop: ...
|
|
|
|
And here is where we check that "break" is indeed used only in a loop.
|
|
|
|
@<Step (4I.h) Worry about using a phrase outside of the control structure it belongs to@> =
|
|
LOG_DASH("(4I.h)");
|
|
if (idb) {
|
|
wchar_t *required = IDTypeData::only_in(idb);
|
|
if (required) {
|
|
if (Wide::cmp(required, L"loop") == 0) {
|
|
LOGIF(MATCHING, "Required to be inside loop body\n");
|
|
if (CodeBlocks::inside_a_loop_body() == FALSE) {
|
|
THIS_IS_AN_INTERESTING_PROBLEM {
|
|
Problems::quote_source(1, current_sentence);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_CantUseOutsideLoop));
|
|
Problems::issue_problem_segment(
|
|
"%1 makes sense only inside a 'while' or 'repeat' loop.");
|
|
Problems::issue_problem_end();
|
|
}
|
|
}
|
|
} else {
|
|
LOGIF(MATCHING, "Required to be inside block '%w'\n", required);
|
|
wchar_t *actual = CodeBlocks::name_of_current_block();
|
|
if ((actual) && (Wide::cmp(actual, L"unless") == 0)) actual = L"if";
|
|
if ((actual == NULL) || (Wide::cmp(required, actual) != 0)) {
|
|
THIS_IS_AN_INTERESTING_PROBLEM {
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wide_text(2, required);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_CantUseOutsideStructure));
|
|
Problems::issue_problem_segment(
|
|
"%1 makes sense only inside a '%2' block.");
|
|
Problems::issue_problem_end();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@<Step (4I.i) Disallow any phrases which are now deprecated@> =
|
|
if (global_compilation_settings.no_deprecated_features) {
|
|
LOG_DASH("(4I.i)");
|
|
if ((idb) && (idb->type_data.now_deprecated)) {
|
|
THIS_IS_AN_INTERESTING_PROBLEM {
|
|
Problems::quote_source(1, current_sentence);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible)); /* too moving a target to test */
|
|
Problems::issue_problem_segment(
|
|
"'%1' uses a phrase which is now deprecated: you should rephrase "
|
|
"to avoid the need for it. I'd normally allow this, but you have "
|
|
"the 'Use no deprecated features' option set.");
|
|
Problems::issue_problem_end();
|
|
}
|
|
}
|
|
}
|
|
|
|
@<Step (4I.j) Cope with failure@> =
|
|
LOG_DASH("(4I.j) failure");
|
|
if (no_gross_problems_thrown > no_gross_problems_thrown_before)
|
|
Dash::set_flag(inv, GROSSLY_FAILED_DASHFLAG);
|
|
else if (no_interesting_problems_thrown > no_interesting_problems_thrown_before)
|
|
Dash::set_flag(inv, INTERESTINGLY_FAILED_DASHFLAG);
|
|
if ((consider_alternatives) && (TEST_DASH_MODE(ISSUE_PROBLEMS_DMODE)))
|
|
Dash::failed_one(inv, context, kind_needed);
|
|
|
|
@ Usage statistics are mainly interesting to the writers of Inform, to help us
|
|
to get some picture of how much phrases are used across a large corpus of
|
|
existing source text (e.g., the documentation examples, or the public
|
|
extensions).
|
|
|
|
@<Step (4I.k) Cope with success@> =
|
|
LOG_DASH("(4I.k) success");
|
|
Dash::set_flag(inv, PASSED_DASHFLAG);
|
|
if (qualified) {
|
|
Dash::set_flag(inv, UNPROVEN_DASHFLAG);
|
|
Invocations::mark_unproven(inv);
|
|
}
|
|
if (idb) {
|
|
wording NW = ToPhraseFamily::doc_ref(idb->head_of_defn);
|
|
if (Wordings::nonempty(NW)) {
|
|
TEMPORARY_TEXT(pds)
|
|
WRITE_TO(pds, "%+W", Wordings::one_word(Wordings::first_wn(NW)));
|
|
if (Log::aspect_switched_on(PHRASE_USAGE_DA)) {
|
|
DocReferences::doc_mark_used(pds,
|
|
Wordings::first_wn(Node::get_text(inv)));
|
|
}
|
|
DISCARD_TEXT(pds)
|
|
}
|
|
}
|
|
|
|
@h (4S) Verifying single non-invocation readings.
|
|
This is much easier, though that's because a lot of the work is delegated
|
|
to level 5.
|
|
|
|
@<Step (4S) Verify anything else@> =
|
|
LOG_DASH("(4S.a)");
|
|
LOG_INDENT;
|
|
|
|
outcome = Dash::typecheck_single_node(p, kind_needed, condition_context);
|
|
LOG_OUTDENT;
|
|
@<Allow listed-in table references only where these are expected@>;
|
|
|
|
LOG_DASH("(4S.b)");
|
|
for (parse_node *arg = p->down; arg; arg = arg->next)
|
|
outcome =
|
|
Dash::worst_case(outcome,
|
|
Dash::typecheck_recursive(arg, p, TRUE));
|
|
if ((outcome != NEVER_MATCH) && (p->down)) {
|
|
if (Node::is(p, LIST_ENTRY_NT))
|
|
@<Step (4S.c) Check arguments of a list entry@>;
|
|
if (Node::is(p, PROPERTY_VALUE_NT))
|
|
@<Step (4S.d) Check arguments of a property value@>;
|
|
if (Node::is(p, TABLE_ENTRY_NT))
|
|
@<Step (4S.e) Check arguments of a table reference@>;
|
|
}
|
|
|
|
@ The "C listed in T" form of table reference is illegal as a general value,
|
|
and allowed only in phrases using the |table-reference| token.
|
|
|
|
@<Allow listed-in table references only where these are expected@> =
|
|
if ((Node::is(p, TABLE_ENTRY_NT)) &&
|
|
(Node::no_children(p) == 2) &&
|
|
(kind_needed) &&
|
|
(!(Node::is(context, LVALUE_TR_CONTEXT_NT)))) {
|
|
THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
|
|
StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_InexplicitTableEntryAsValue),
|
|
"this form of table entry can only be used in certain special phrases",
|
|
"because it doesn't explicitly refer to a single value. (You can see "
|
|
"which phrases in the Phrasebook index: it's allowed wherever a 'table "
|
|
"entry' is wanted.)");
|
|
return NEVER_MATCH;
|
|
}
|
|
|
|
@ For a list entry, we have to have a list and an index.
|
|
|
|
@<Step (4S.c) Check arguments of a list entry@> =
|
|
LOG_DASH("(4S.c)");
|
|
kind *K1 = Specifications::to_kind(p->down);
|
|
kind *K2 = Specifications::to_kind(p->down->next);
|
|
if (Kinds::unary_construction_material(K1) == NULL) {
|
|
THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
|
|
StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_EntryOfNonList),
|
|
"that doesn't make sense to me as a list entry",
|
|
"since the entry is taken from something which isn't a list.");
|
|
return NEVER_MATCH;
|
|
}
|
|
if (Kinds::eq(K2, K_number) == FALSE) {
|
|
THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
|
|
StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_NonNumericListEntry),
|
|
"that doesn't make sense to me as a list entry",
|
|
"because the indication of which entry is not a number. "
|
|
"For instance, 'entry 3 of L' is allowed, but not 'entry "
|
|
"\"six\" of L'. (List entries are numbered 1, 2, 3, ...)");
|
|
return NEVER_MATCH;
|
|
}
|
|
|
|
@ For a property value, we have to have a property and an owner (perhaps an
|
|
object, perhaps a value). If the owner is a value, we need to police the
|
|
availability of the property carefully, since no run-time checking can help
|
|
us there.
|
|
|
|
@<Step (4S.d) Check arguments of a property value@> =
|
|
LOG_DASH("(4S.d)");
|
|
parse_node *the_property = p->down;
|
|
kind *K1 = Specifications::to_kind(the_property);
|
|
if (Kinds::get_construct(K1) != CON_property) @<Issue a "not a property" problem message@>;
|
|
property *prn = Rvalues::to_property(the_property);
|
|
if (prn == NULL)
|
|
internal_error("null property name in type checking");
|
|
if (Properties::is_either_or(prn)) @<Issue a "not a value property" problem message@>;
|
|
|
|
parse_node *the_owner = p->down->next;
|
|
kind *K2 = Specifications::to_kind(the_owner);
|
|
if ((K2 == NULL) || (Specifications::is_description(the_owner)))
|
|
@<Issue a problem message for being too vague about the owner@>;
|
|
|
|
inference_subject *owning_subject = InferenceSubjects::from_specification(the_owner);
|
|
if (owning_subject == NULL) owning_subject = KindSubjects::from_kind(K2);
|
|
if (PropertyPermissions::find(owning_subject, prn, TRUE) == NULL) {
|
|
if ((Kinds::Behaviour::is_object(K2) == FALSE) ||
|
|
((Rvalues::is_object(the_owner)) &&
|
|
(Rvalues::is_self_object_constant(the_owner) == FALSE)))
|
|
@<Issue a problem message for not being allowed this property@>;
|
|
}
|
|
|
|
@ Inform constructs property-value specifications quite carefully, and I think
|
|
it's only possible for the typechecker to see one where the property isn't
|
|
a property when recovering from other problems.
|
|
|
|
@<Issue a "not a property" problem message@> =
|
|
THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p));
|
|
Problems::quote_wording(3, Node::get_text(the_property));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible));
|
|
Problems::issue_problem_segment(
|
|
"In the sentence %1, it looks as if you intend '%2' to be a property "
|
|
"of something, but there is no such property as '%3'.");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
|
|
@<Issue a "not a value property" problem message@> =
|
|
THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_EitherOrAsValue));
|
|
Problems::issue_problem_segment(
|
|
"In the sentence %1, it looks as if you intend '%2' to be the value "
|
|
"of a property of something, but that property has no value: it's "
|
|
"something which an object either is or is not.");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
|
|
@<Issue a problem message for being too vague about the owner@> =
|
|
THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p));
|
|
int owner_quoted = TRUE;
|
|
if ((Specifications::to_kind(the_owner)) &&
|
|
(Descriptions::get_quantifier(the_owner) == NULL))
|
|
Problems::quote_kind(3, Specifications::to_kind(the_owner));
|
|
else if (Wordings::nonempty(Node::get_text(the_owner)))
|
|
Problems::quote_wording(3, Node::get_text(the_owner));
|
|
else owner_quoted = FALSE;
|
|
LOG("Owner tree is $T\n", the_owner);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_PropertyOfKind2));
|
|
if (owner_quoted) {
|
|
if (Wordings::nonempty(Node::get_text(p)))
|
|
Problems::issue_problem_segment(
|
|
"In the sentence %1, it looks as if you intend '%2' to be a property, "
|
|
"but '%3' is not specific enough about who or what the owner is. ");
|
|
else
|
|
Problems::issue_problem_segment(
|
|
"In the sentence %1, it looks as if you intend to look up a property "
|
|
"of something, but '%3' is not specific enough about who or what "
|
|
"the owner is. ");
|
|
} else {
|
|
if (Wordings::nonempty(Node::get_text(p)))
|
|
Problems::issue_problem_segment(
|
|
"In the sentence %1, it looks as if you intend '%2' to be a property, "
|
|
"but you're not specific enough about who or what the owner is. ");
|
|
else
|
|
Problems::issue_problem_segment(
|
|
"In the sentence %1, it looks as if you intend to look up a property "
|
|
"of something, but you're not specific enough about who or what "
|
|
"the owner is. ");
|
|
}
|
|
Problems::issue_problem_segment(
|
|
"%PSometimes this mistake is made because Inform mostly doesn't understand "
|
|
"the English language habit of referring to something indefinite by a "
|
|
"common noun - for instance, writing 'change the carrying capacity of "
|
|
"the container to 10' throws Inform because it doesn't understand "
|
|
"that 'the container' means one which has been discussed recently.");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
|
|
@<Issue a problem message for not being allowed this property@> =
|
|
THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, prn->name);
|
|
Problems::quote_subject(3, owning_subject);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_LookedUpForbiddenProperty));
|
|
Problems::issue_problem_segment(
|
|
"In the sentence %1, you seem to be looking up the '%2' property, "
|
|
"but '%3' is not allowed to have that property. ");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
|
|
@ For a table entry, we have to have a list and an index.
|
|
|
|
@<Step (4S.e) Check arguments of a table reference@> =
|
|
LOG_DASH("(4S.e)");
|
|
if (Node::no_children(p) == 4) {
|
|
kind *col_kind = Specifications::to_kind(p->down->next);
|
|
kind *col_contents_kind = Kinds::unary_construction_material(col_kind);
|
|
kind *key_kind = Specifications::to_kind(p->down->next->next);
|
|
LOGIF(MATCHING, "Kinds: col %u, contents %u, key %u\n",
|
|
col_kind, col_contents_kind, key_kind);
|
|
if ((Kinds::get_construct(col_kind) != CON_table_column) ||
|
|
(col_contents_kind == NULL)) {
|
|
THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
|
|
StandardProblems::sentence_problem(Task::syntax_tree(), _p_(BelievedImpossible),
|
|
"that doesn't make sense to me as a table entry",
|
|
"since the entry is taken from something which isn't a table.");
|
|
return NEVER_MATCH;
|
|
}
|
|
if ((K_snippet) &&
|
|
(Kinds::eq(key_kind, K_snippet)) && (Kinds::eq(col_contents_kind, K_text))) {
|
|
THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_kind(2, col_contents_kind);
|
|
Problems::quote_kind(3, key_kind);
|
|
Problems::quote_wording(4, Node::get_text(p->down->next->next));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_TableCorrFruitless2));
|
|
Problems::issue_problem_segment(
|
|
"In the sentence %1, you seem to be looking up a corresponding "
|
|
"entry in a table: but you're looking up a snippet of a command "
|
|
"(%3) in a column of text. Although those look the same, they "
|
|
"really aren't, and no match can be made. (You might be able to "
|
|
"fix matters by converting the snippet to text, say writing '\"[%4]\"' "
|
|
"in place of '%4'.)");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
}
|
|
if (Kinds::compatible(key_kind, col_contents_kind) == NEVER_MATCH) {
|
|
THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_kind(2, col_contents_kind);
|
|
Problems::quote_kind(3, key_kind);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_TableCorrFruitless));
|
|
Problems::issue_problem_segment(
|
|
"In the sentence %1, you seem to be looking up a corresponding "
|
|
"entry in a table: but it's fruitless to go looking for %3 "
|
|
"in a column where each entry contains %2.");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
}
|
|
}
|
|
|
|
@<Unknown found text occurs as a command@> =
|
|
THIS_IS_A_GROSS_PROBLEM;
|
|
if (<structural-phrase-problem-diagnosis>(Node::get_text(p)) == FALSE) {
|
|
if (Wordings::mismatched_brackets(Node::get_text(p))) {
|
|
StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_UnpairedBrackets),
|
|
"this is a phrase which I don't recognise",
|
|
"perhaps because it uses brackets '(' and ')' or braces '{' and '}' "
|
|
"in a way that doesn't make sense to me. Each open '(' or '{' has "
|
|
"to have a matching ')' or '}'.");
|
|
} else {
|
|
StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_UnknownPhrase),
|
|
"this is a phrase which I don't recognise",
|
|
"possibly because it is one you meant to define but never got round "
|
|
"to, or because the wording is wrong (see the Phrasebook section of "
|
|
"the Index to check). Alternatively, it may be that the text "
|
|
"immediately previous to this was a definition whose ending, normally "
|
|
"a full stop, is missing?");
|
|
}
|
|
}
|
|
return NEVER_MATCH;
|
|
|
|
@ "Diagnosis" nonterminals are used to parse syntax which is already known
|
|
to be invalid: they simply choose between problem messages. This one picks
|
|
up on misuse of structural phrases.
|
|
|
|
=
|
|
<structural-phrase-problem-diagnosis> ::=
|
|
continue ==> @<Issue PM_WrongContinue problem@>
|
|
|
|
@<Issue PM_WrongContinue problem@> =
|
|
StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_WrongContinue),
|
|
"this is a phrase which I don't recognise",
|
|
"and which isn't defined. Perhaps you wanted the phrase which "
|
|
"would skip to the next repetition of a loop, since that's "
|
|
"written 'continue' in some programming languages (such as C "
|
|
"and Inform 6)? If so, what you want is 'next'.");
|
|
|
|
@h Arithmetic operands.
|
|
The following works out the kind of an operand for an arithmetic operation,
|
|
which because of polymorphism is not as straightforward as it looks.
|
|
|
|
=
|
|
kind *Dash::fix_arithmetic_operand(parse_node *operand) {
|
|
if (Node::is(operand->down, UNKNOWN_NT)) return NULL;
|
|
if (Node::get_type(operand) != RVALUE_CONTEXT_NT)
|
|
internal_error("arithmetic operand not an rvalue");
|
|
kind *expected = NULL;
|
|
parse_node *check = operand->down;
|
|
if (Node::is(check, AMBIGUITY_NT)) check = check->down;
|
|
if (Rvalues::is_CONSTANT_construction(check, CON_property)) {
|
|
property *prn = Rvalues::to_property(check);
|
|
if (Properties::is_either_or(prn) == FALSE)
|
|
expected = ValueProperties::kind(prn);
|
|
}
|
|
kind *K = Node::get_kind_required_by_context(operand);
|
|
Node::set_kind_required_by_context(operand, expected);
|
|
BEGIN_DASH_MODE;
|
|
DASH_MODE_EXIT(ISSUE_PROBLEMS_DMODE);
|
|
DASH_MODE_CREATE(NULL);
|
|
int rv = Dash::typecheck_recursive(operand, NULL, TRUE);
|
|
END_DASH_MODE;
|
|
Node::set_kind_required_by_context(operand, K);
|
|
if (rv == NEVER_MATCH) return NULL;
|
|
return Specifications::to_kind(operand->down);
|
|
}
|
|
|
|
@h Local variable markers.
|
|
Branches (4A) and (4I) both make use of the following code, which is
|
|
applied to any invocation surviving Dash.
|
|
|
|
Here's the usual way a local variable is made. One invocation we matched is
|
|
for the phrase whose prototype reads:
|
|
|
|
>> To let (T - nonexisting variable) be (V - value): ...
|
|
|
|
To be definite, let's suppose we are working on:
|
|
|
|
>> let the magic word be "Shazam [turn count] times!";
|
|
|
|
The checking code above accepted "magic word" as a new name, and marked
|
|
token 0 in the invocation as one where a new variable will need to be
|
|
created -- which is done if and when the invocation is ever compiled.
|
|
|
|
=
|
|
int Dash::set_up_any_local_required(parse_node *inv) {
|
|
for (int i=0, N = Invocations::get_no_tokens(inv); i<N; i++) {
|
|
kind *K = Invocations::get_token_variable_kind(inv, i);
|
|
if (K) {
|
|
if ((i == 0) && (N >= 2) && (Kinds::eq(K, K_value)) &&
|
|
(IDTypeData::is_a_let_assignment(Node::get_phrase_invoked(inv))))
|
|
@<Infer the kind of the new variable@>;
|
|
|
|
int changed = FALSE;
|
|
K = Kinds::substitute(K, NULL, &changed, FALSE);
|
|
if (changed) LOGIF(MATCHING, "(4A.c.1) Local var amended to %u\n", K);
|
|
Invocations::set_token_variable_kind(inv, i, K);
|
|
}
|
|
}
|
|
return ALWAYS_MATCH;
|
|
}
|
|
|
|
@ The following code is used to work out a good kind for the new variable,
|
|
instead of "value", based on looking at token 1 -- the value being assigned.
|
|
In the example above, we look at this initial value,
|
|
|
|
>> "Shazam [turn count] times!"
|
|
|
|
and decide that K should be "text".
|
|
|
|
@<Infer the kind of the new variable@> =
|
|
parse_node *val = Invocations::get_token_as_parsed(inv, i);
|
|
wording W = Node::get_text(val);
|
|
parse_node *initial_value = Invocations::get_token_as_parsed(inv, 1);
|
|
parse_node *iv_spec = Node::get_phrase_invoked(inv)->type_data.token_sequence[1].to_match;
|
|
if (initial_value)
|
|
@<Where no kind was explicitly stated, infer this from the supplied initial value@>;
|
|
|
|
@ Unusually, it's legal for the initial value to be a kind --
|
|
|
|
>> let the magic digraph be a text;
|
|
|
|
This doesn't give us an initial value as such, but it explicitly tells us the
|
|
kind, which is good enough.
|
|
|
|
Otherwise, we either know the kind already from polymorphism calculations, or
|
|
we can work it out by seeing what the initial value evaluates to. Note that
|
|
with values which are objects, we guess the kind as the broadest subkind of
|
|
"object" to which the value belongs: in practice that means usually a thing,
|
|
a room or a region.
|
|
|
|
We make one exception to allow lines like --
|
|
|
|
>> let X be a one-to-one relation of numbers to men;
|
|
|
|
where the adjective "one-to-one" forces the right hand side to be description
|
|
of a relation.
|
|
|
|
@<Where no kind was explicitly stated, infer this from the supplied initial value@> =
|
|
kind *seems_to_be = NULL;
|
|
if ((Specifications::is_kind_like(iv_spec)) &&
|
|
(Node::is(initial_value, CONSTANT_NT))) {
|
|
kind *K = Node::get_kind_of_value(initial_value);
|
|
if (Kinds::get_construct(K) == CON_description) {
|
|
kind *DK = Kinds::unary_construction_material(K);
|
|
if (Kinds::get_construct(DK) == CON_relation) seems_to_be = DK;
|
|
}
|
|
}
|
|
if (seems_to_be == NULL) seems_to_be = Specifications::to_kind(initial_value);
|
|
|
|
LOGIF(LOCAL_VARIABLES, "New variable %W from $P ($P) seems to be: %u\n",
|
|
W, initial_value, iv_spec, seems_to_be);
|
|
if (seems_to_be == NULL) @<Fail: the initial value of the local is unknown@>;
|
|
|
|
if ((Kinds::get_construct(seems_to_be) == CON_list_of) &&
|
|
(Kinds::eq(Kinds::unary_construction_material(seems_to_be), K_nil)))
|
|
@<Fail: the initial value of the local is the empty list@>;
|
|
if (Kinds::Behaviour::definite(seems_to_be) == FALSE)
|
|
@<Fail: the initial value can't be stored@>;
|
|
LOGIF(MATCHING, "(4A.c.1) Local variable seems to have kind: %u (kind-like: %d)\n",
|
|
seems_to_be, Specifications::is_kind_like(initial_value));
|
|
|
|
K = seems_to_be;
|
|
if (Specifications::is_kind_like(initial_value) == FALSE)
|
|
if (Kinds::Behaviour::is_subkind_of_object(K))
|
|
while (Kinds::eq(Latticework::super(K), K_object) == FALSE)
|
|
K = Latticework::super(K);
|
|
LOGIF(MATCHING, "(4A.c.1) Local variable inferred to have kind: %u\n", K);
|
|
|
|
@<Fail: the initial value of the local is unknown@> =
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(initial_value));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible));
|
|
Problems::issue_problem_segment(
|
|
"The phrase %1 tries to use 'let' to give a temporary name to a value, "
|
|
"but the value ('%2') is one that I can't understand.");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
|
|
@ Bet you didn't think of this one. Actually, the kind of the list can also
|
|
collapse to just "value" if the entries are incompatible, so we call the
|
|
relevant code to issue a better problem message if it can.
|
|
|
|
@<Fail: the initial value of the local is the empty list@> =
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
int pc = problem_count;
|
|
Lists::check_one(Node::get_text(initial_value));
|
|
if (pc == problem_count) {
|
|
Problems::quote_source(1, current_sentence);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_CantLetEmptyList));
|
|
Problems::issue_problem_segment(
|
|
"The phrase %1 tries to use 'let' to give a temporary name to the "
|
|
"empty list '{ }', but because it's empty, I can't tell what kind of "
|
|
"value the list should have. Try 'let X be a list of numbers' (or "
|
|
"whatever) instead.");
|
|
Problems::issue_problem_end();
|
|
}
|
|
return NEVER_MATCH;
|
|
|
|
@ And some kinds are just forbidden in storage:
|
|
|
|
@<Fail: the initial value can't be stored@> =
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
StandardProblems::sentence_problem(Task::syntax_tree(), _p_(BelievedImpossible),
|
|
"this isn't a definite kind",
|
|
"and is instead a general description which might apply to many "
|
|
"different kinds, so I can't see how to create this named value. "
|
|
"(For example, 'let R be a relation' is vague because it doesn't "
|
|
"make clear what R will relate - 'let R be a relation of numbers' "
|
|
"would be fine.)");
|
|
return NEVER_MATCH;
|
|
|
|
@h Problems, problems, problems.
|
|
We are now in a situation where Dash has certainly failed, and on every
|
|
possible alternative reading, so it would be legitimate to return |NEVER_MATCH|
|
|
here, which would likely result in some anodyne problem message from higher
|
|
up in Dash.
|
|
|
|
But we want to produce more helpful problem messages than that. It's not
|
|
entirely clear how best to do this. Often, when a node fails, it fails for
|
|
seven different reasons -- each different possibility fails for a different
|
|
cause. We want, somehow, to guess which was the most likely to have been
|
|
intended and to report the problem with that one.
|
|
|
|
=
|
|
int Dash::failed_one(parse_node *inv, parse_node *context, kind *kind_needed) {
|
|
parse_node *list[1];
|
|
list[0] = inv;
|
|
return Dash::failed(list, 1, context, kind_needed);
|
|
}
|
|
|
|
wording PM_BadIntermediateKind_wording = EMPTY_WORDING_INIT;
|
|
|
|
int Dash::failed(parse_node **list_of_possible_readings, int no_of_possible_readings,
|
|
parse_node *context, kind *kind_needed) {
|
|
if (Dash::problems_have_been_issued()) return NEVER_MATCH;
|
|
|
|
parse_node *first_inv_in_group = NULL;
|
|
parse_node *first_failing_interestingly = NULL;
|
|
parse_node *first_not_failing_grossly = NULL;
|
|
wording SW = EMPTY_WORDING;
|
|
int list_includes_lets = FALSE;
|
|
int nongross_count = 0;
|
|
@<Scan through the invocations in the problematic group, gathering information@>;
|
|
|
|
parse_node *most_likely_to_have_been_intended = NULL;
|
|
@<Decide which invocation is the one most likely to have been intended@>;
|
|
|
|
int pc_before = problem_count;
|
|
if (first_failing_interestingly)
|
|
@<Re-type-check the first interesting invocation, allowing interesting problems this time@>
|
|
else if (most_likely_to_have_been_intended)
|
|
@<Re-type-check the tokens of the most likely invocation with silence off@>
|
|
if (problem_count == pc_before) {
|
|
if (Wordings::nonempty(SW)) <failed-text-substitution-diagnosis>(SW);
|
|
else @<Issue a problem for a regular phrase with multiple failed possibilities@>;
|
|
}
|
|
return NEVER_MATCH;
|
|
}
|
|
|
|
@<Scan through the invocations in the problematic group, gathering information@> =
|
|
for (int ref=0; ref<no_of_possible_readings; ref++) {
|
|
parse_node *inv = list_of_possible_readings[ref];
|
|
if ((Dash::test_flag(inv, INTERESTINGLY_FAILED_DASHFLAG)) &&
|
|
(first_failing_interestingly == NULL)) first_failing_interestingly = inv;
|
|
if (first_inv_in_group == NULL) first_inv_in_group = inv;
|
|
id_body *idb = Node::get_phrase_invoked(inv);
|
|
if (idb) {
|
|
if (IDTypeData::is_a_let_assignment(idb)) list_includes_lets = TRUE;
|
|
if (Dash::test_flag(inv, GROSSLY_FAILED_DASHFLAG) == FALSE) {
|
|
first_not_failing_grossly = inv;
|
|
if (IDTypeData::is_a_say_X_phrase(&(idb->type_data))) SW = Node::get_text(inv);
|
|
nongross_count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
@<Decide which invocation is the one most likely to have been intended@> =
|
|
if (first_failing_interestingly)
|
|
most_likely_to_have_been_intended = first_failing_interestingly;
|
|
else if ((nongross_count > 1) && (list_includes_lets))
|
|
most_likely_to_have_been_intended = first_not_failing_grossly;
|
|
else if (nongross_count == 1)
|
|
most_likely_to_have_been_intended = first_not_failing_grossly;
|
|
else if ((nongross_count == 0) && (first_inv_in_group))
|
|
most_likely_to_have_been_intended = first_inv_in_group;
|
|
|
|
@<Re-type-check the first interesting invocation, allowing interesting problems this time@> =
|
|
BEGIN_DASH_MODE;
|
|
DASH_MODE_ENTER(ISSUE_INTERESTING_PROBLEMS_DMODE);
|
|
DASH_MODE_CREATE(NULL);
|
|
Dash::typecheck_recursive(first_failing_interestingly, context, FALSE);
|
|
END_DASH_MODE;
|
|
|
|
@<Re-type-check the tokens of the most likely invocation with silence off@> =
|
|
int ec = problem_count;
|
|
BEGIN_DASH_MODE;
|
|
DASH_MODE_ENTER(ISSUE_PROBLEMS_DMODE);
|
|
DASH_MODE_CREATE(NULL);
|
|
for (int i=0; i<Invocations::get_no_tokens(most_likely_to_have_been_intended); i++) {
|
|
Dash::typecheck_recursive(Invocations::get_token(most_likely_to_have_been_intended, i), context, TRUE);
|
|
if (problem_count > ec) break;
|
|
}
|
|
if (problem_count == ec) {
|
|
LOGIF(MATCHING, "Try again in local problems mode\n");
|
|
BEGIN_DASH_MODE;
|
|
DASH_MODE_ENTER(ISSUE_LOCAL_PROBLEMS_DMODE);
|
|
DASH_MODE_CREATE(NULL);
|
|
for (int i=0; i<Invocations::get_no_tokens(most_likely_to_have_been_intended); i++) {
|
|
Dash::typecheck_recursive(Invocations::get_token(most_likely_to_have_been_intended, i), context, TRUE);
|
|
if (problem_count > ec) break;
|
|
}
|
|
END_DASH_MODE;
|
|
}
|
|
END_DASH_MODE;
|
|
if (problem_count == ec) {
|
|
kind *K = Node::get_kind_resulting(most_likely_to_have_been_intended);
|
|
kind *W = kind_needed;
|
|
if ((K) && (W) && (Kinds::compatible(K, W) == NEVER_MATCH)) {
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
wording PW = Node::get_text(list_of_possible_readings[0]);
|
|
if (!(Wordings::eq(PM_BadIntermediateKind_wording, PW))) {
|
|
PM_BadIntermediateKind_wording = PW;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, PW);
|
|
Problems::quote_kind(3, K);
|
|
Problems::quote_kind(4, W);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_BadIntermediateKind));
|
|
Problems::issue_problem_segment(
|
|
"In %1, the phrase '%2' doesn't seem to fit: I was hoping it would "
|
|
"be %4, but in fact it's %3.");
|
|
Problems::issue_problem_end();
|
|
}
|
|
return NEVER_MATCH;
|
|
}
|
|
if (problem_count == ec) {
|
|
LOGIF(MATCHING, "Try again in gross problems mode\n$T\n", most_likely_to_have_been_intended);
|
|
BEGIN_DASH_MODE;
|
|
DASH_MODE_ENTER(ISSUE_GROSS_PROBLEMS_DMODE);
|
|
DASH_MODE_CREATE(NULL);
|
|
Dash::typecheck_recursive(most_likely_to_have_been_intended, context, FALSE);
|
|
END_DASH_MODE;
|
|
}
|
|
if (problem_count == 0)
|
|
StandardProblems::sentence_problem(Task::syntax_tree(), _p_(BelievedImpossible),
|
|
"the ingredients in this phrase do not fit it",
|
|
"and I am confused enough by this that I can't give a very helpful "
|
|
"problem message. Sorry about that.");
|
|
}
|
|
|
|
@<Issue a problem for a regular phrase with multiple failed possibilities@> =
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_AllInvsFailed));
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, which I tried to match against several possible phrase "
|
|
"definitions. None of them worked.");
|
|
Problems::issue_problem_end();
|
|
|
|
@ The following chooses a problem message for a text substitution which is
|
|
unrecognised.
|
|
|
|
=
|
|
<failed-text-substitution-diagnosis> ::=
|
|
a list of ... | ==> @<Issue PM_SayAList problem@>
|
|
... ==> @<Issue last-resort failed ts problem@>
|
|
|
|
@<Issue PM_SayAList problem@> =
|
|
StandardProblems::sentence_in_detail_problem(Task::syntax_tree(), _p_(PM_SayAList), W,
|
|
"this asked to say 'a list of...'",
|
|
"which I read as being a general description applying to some "
|
|
"lists and not others, so it's not something which can be said. "
|
|
"(Maybe you meant 'the list of...' instead? That normally makes "
|
|
"a definite list of whatever matches the '...' part.)");
|
|
|
|
@<Issue last-resort failed ts problem@> =
|
|
StandardProblems::sentence_in_detail_problem(Task::syntax_tree(), _p_(BelievedImpossible), W,
|
|
"this asked to say something which I do not recognise",
|
|
"either as a value or as one of the possible text substitutions.");
|
|
|
|
@ In the final checklist of doomed possibility, the code to quote an
|
|
invocation in a problem message will call the following routine for each
|
|
parsed token. This remembers the token so that it can be explained in notes
|
|
at the end of the big list; but each word range is remembered only once,
|
|
for brevity. We don't gloss the meanings of literal constants like |26|
|
|
or |"frog"| since these are glaringly obvious.
|
|
|
|
=
|
|
void Dash::note_inv_token_text(parse_node *p, int new_name) {
|
|
inv_token_problem_token *itpt;
|
|
LOOP_OVER(itpt, inv_token_problem_token)
|
|
if (Wordings::eq(itpt->problematic_text, Node::get_text(p))) {
|
|
if (new_name) itpt->new_name = TRUE;
|
|
return;
|
|
}
|
|
itpt = CREATE(inv_token_problem_token);
|
|
itpt->problematic_text = Node::get_text(p);
|
|
itpt->new_name = new_name;
|
|
if (Node::is(p, AMBIGUITY_NT)) p = p->down;
|
|
itpt->as_parsed = p; itpt->already_described = FALSE;
|
|
if ((Rvalues::is_CONSTANT_of_kind(p, K_number)) ||
|
|
(Rvalues::is_CONSTANT_of_kind(p, K_text))) itpt->already_described = TRUE;
|
|
}
|
|
|
|
@ This last little grammar diagnoses problems with a condition, and helps to
|
|
construct a problem message which will (usually) show which part of a compound
|
|
condition caused the trouble:
|
|
|
|
=
|
|
<condition-problem-diagnosis> ::=
|
|
<condition-problem-part> <condition-problem-part-tail> | ==> { R[1] | R[2], - }
|
|
<condition-problem-part> ==> { pass 1 }
|
|
|
|
<condition-problem-part-tail> ::=
|
|
, and/or <condition-problem-diagnosis> | ==> { pass 1 }
|
|
,/and/or <condition-problem-diagnosis> ==> { pass 1 }
|
|
|
|
<condition-problem-part> ::=
|
|
<s-condition> | ==> { 0, - }; @<Quote this-condition-okay segment@>;
|
|
<s-value> | ==> { INVALID_CP_BIT, - }; @<Quote this-condition-value segment@>;
|
|
... begins/ends | ==> { WHENWHILE_CP_BIT+INVALID_CP_BIT, - }; @<Quote scene-begins-or-ends segment@>;
|
|
when/while *** | ==> { WHENWHILE_CP_BIT+INVALID_CP_BIT, - }; @<Quote this-condition-bad segment@>;
|
|
... ==> { INVALID_CP_BIT, - }; @<Quote this-condition-bad segment@>;
|
|
|
|
@<Quote this-condition-okay segment@> =
|
|
if (preform_lookahead_mode == FALSE) {
|
|
Problems::quote_wording(4, W);
|
|
Problems::issue_problem_segment("'%4' was okay; ");
|
|
}
|
|
|
|
@<Quote this-condition-value segment@> =
|
|
if (preform_lookahead_mode == FALSE) {
|
|
Problems::quote_wording(4, W);
|
|
Problems::issue_problem_segment(
|
|
"'%4' only made sense as a value, which can't be used as a condition; ");
|
|
}
|
|
|
|
@<Quote scene-begins-or-ends segment@> =
|
|
if (preform_lookahead_mode == FALSE) {
|
|
Problems::quote_wording(4, W);
|
|
Problems::issue_problem_segment(
|
|
"'%4' did not make sense as a condition, but looked as if it might "
|
|
"be a way to specify a beginning or end for a scene - but such things "
|
|
"can't be divided by 'or'; ");
|
|
}
|
|
|
|
@<Quote this-condition-bad segment@> =
|
|
if (preform_lookahead_mode == FALSE) {
|
|
Problems::quote_wording(4, W);
|
|
Problems::issue_problem_segment("'%4' did not make sense; ");
|
|
}
|
|
|
|
@h (5) Single nodes.
|
|
Here we typecheck a single non-invocation node on its own terms, ignoring
|
|
any children it may have.
|
|
|
|
=
|
|
int Dash::typecheck_single_node(parse_node *p, kind *kind_expected, int condition_context) {
|
|
LOG_DASH("(5)");
|
|
LOGIF(MATCHING, "Kind expected: %u, condition expected: %d\n", kind_expected, condition_context);
|
|
int outcome = ALWAYS_MATCH; /* drops to |SOMETIMES_MATCH| if a need for run-time checking is realised */
|
|
|
|
if ((Rvalues::is_nothing_object_constant(p)) &&
|
|
(kind_expected) && (Kinds::Behaviour::is_subkind_of_object(kind_expected)))
|
|
@<Disallow "nothing" as a match for a description requiring a kind of object@>;
|
|
|
|
@<Step (5.a) Deal with the UNKNOWN_NT@>;
|
|
@<Step (5.b) Deal with bare property names@>;
|
|
@<Step (5.c) Deal with any attached proposition@>;
|
|
@<Step (5.d) Apply miscellaneous other coercions@>;
|
|
@<Step (5.e) The Main Rule of Type-Checking@>;
|
|
return outcome;
|
|
}
|
|
|
|
@ "You can't have/ Something for nothing," as Canadian power-trio Rush tell
|
|
us with the air of having just made a great discovery; well, you can't have
|
|
"nothing" for something, either --
|
|
|
|
@<Disallow "nothing" as a match for a description requiring a kind of object@> =
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p));
|
|
Problems::quote_kind(3, kind_expected);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_NothingForSomething));
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but '%2' is literally no thing, and it consequently does "
|
|
"not count as %3.");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
|
|
@h Rule (5.a).
|
|
In all cases, unknown text in |found| is incorrect. We can produce
|
|
any of more than twenty different problem messages here, in an attempt to be
|
|
helpful about what exactly is wrong.
|
|
|
|
@d SAY_UTSHAPE 1
|
|
@d LIST_UTSHAPE 2
|
|
@d NO_UTSHAPE 3
|
|
|
|
@<Step (5.a) Deal with the UNKNOWN_NT@> =
|
|
LOG_DASH("(5.a)");
|
|
if (Node::is(p, UNKNOWN_NT)) {
|
|
THIS_IS_A_GROSS_PROBLEM;
|
|
LOG("(5.a) problem message:\nfound: $Texpected: %u", p, kind_expected);
|
|
#ifdef IF_MODULE
|
|
if (Kinds::eq(kind_expected, K_stored_action))
|
|
@<Unknown found text occurs as an action to try@>;
|
|
#endif
|
|
Problems::quote_source_eliding_begin(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p));
|
|
if (condition_context)
|
|
Problems::quote_text(3, "a condition");
|
|
else if (kind_expected == NULL)
|
|
Problems::quote_kind(3, K_value);
|
|
else
|
|
Problems::quote_kind(3, kind_expected);
|
|
|
|
int shape = NO_UTSHAPE;
|
|
if (current_sentence) {
|
|
<unknown-text-shape>(Node::get_text(current_sentence));
|
|
shape = <<r>>;
|
|
}
|
|
if (shape == NO_UTSHAPE) {
|
|
<unknown-text-shape>(Node::get_text(p));
|
|
shape = <<r>>;
|
|
}
|
|
|
|
int preceding = Wordings::first_wn(Node::get_text(p)) - 1;
|
|
if ((preceding >= 0) && (current_sentence) &&
|
|
(((TextSubstitutions::currently_compiling()) || (shape == SAY_UTSHAPE))) &&
|
|
((preceding == Wordings::first_wn(Node::get_text(current_sentence)))
|
|
|| (Lexer::word(preceding) == COMMA_V)))
|
|
@<Unknown found text occurs as a text substitution@>
|
|
else if ((condition_context) && (shape == LIST_UTSHAPE))
|
|
@<Issue a problem message for a compound condition which has gone bad@>
|
|
else
|
|
@<Issue a problem message for miscellaneous suspicious wordings@>;
|
|
|
|
return NEVER_MATCH;
|
|
}
|
|
|
|
@<Unknown found text occurs as an action to try@> =
|
|
parse_node *spec = NULL;
|
|
kind *K = NULL, *K2 = NULL;
|
|
Dash::clear_validation_case();
|
|
<action-pattern>(Node::get_text(p));
|
|
if (Dash::get_validation_case(&spec, &K, &K2)) {
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_UnknownTryAction1));
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p));
|
|
Problems::quote_wording(3, Node::get_text(spec));
|
|
Problems::quote_kind(4, K);
|
|
Problems::quote_kind(5, K2);
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but '%2' is not an action I can try. This looks as "
|
|
"if it might be because it contains something of the wrong kind. "
|
|
"My best try involved seeing if '%3' could be %4, which might have "
|
|
"made sense, but it turned out to be %5.");
|
|
Problems::issue_problem_end();
|
|
} else {
|
|
StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_UnknownTryAction2),
|
|
"this is not an action I recognise",
|
|
"or else is malformed in a way I can't see how to sort out.");
|
|
}
|
|
return NEVER_MATCH;
|
|
|
|
@ The <unknown-text-shape> is used purely in diagnosing problems; it helps
|
|
to decide, for instance, whether the errant phrase was intended to be a text
|
|
substitution or not.
|
|
|
|
=
|
|
<unknown-text-shape> ::=
|
|
say ... | ==> { SAY_UTSHAPE, - }
|
|
... and/or ... | ==> { LIST_UTSHAPE, - }
|
|
... ==> { NO_UTSHAPE, - }
|
|
|
|
<unknown-text-substitution-problem-diagnosis> ::=
|
|
, ... | ==> @<Issue PM_SayComma problem@>
|
|
unicode ... | ==> @<Issue PM_SayUnicode problem@>
|
|
... condition | ==> @<Issue PM_SayUnknownCondition problem@>
|
|
otherwise/else *** | ==> @<Issue PM_SayElseMisplaced problem@>
|
|
... ==> @<Issue PM_SayUnknown problem@>
|
|
|
|
@<Issue PM_SayComma problem@> =
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_SayComma));
|
|
Problems::issue_problem_segment(
|
|
"In the line %1, I was expecting that '%2' would be something to "
|
|
"'say', but unexpectedly it began with a comma. The usual form is "
|
|
"just 'say \"text\"', perhaps with some substitutions in square "
|
|
"brackets within the quoted text, but no commas.");
|
|
Problems::issue_problem_end();
|
|
|
|
@<Issue PM_SayUnicode problem@> =
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_SayUnicode));
|
|
Problems::issue_problem_segment(
|
|
"In the line %1, I was expecting that '%2' would be something to "
|
|
"'say', but it didn't look like any form of 'say' that I know. "
|
|
"So I tried to read '%2' as a Unicode character, which seemed "
|
|
"likely because of the word 'unicode', but that didn't work either. "
|
|
"%PUnicode characters can be written either using their decimal "
|
|
"numbers - for instance, 'Unicode 2041' - or with their standard "
|
|
"names - 'Unicode Latin small ligature oe'. For efficiency reasons "
|
|
"these names are only available if you ask for them; to make them "
|
|
"available, you need to 'Include Unicode Character Names by Graham "
|
|
"Nelson' or, if you really need more, 'Include Unicode Full "
|
|
"Character Names by Graham Nelson'.");
|
|
Problems::issue_problem_end();
|
|
|
|
@<Issue PM_SayElseMisplaced problem@> =
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_SayElseMisplaced));
|
|
Problems::issue_problem_segment(
|
|
"In the line %1, I was expecting that '%2' would be something to "
|
|
"'say', but unexpectedly I found an 'otherwise' (or 'else'). That "
|
|
"would be fine inside an '[if ...]' part of the text, but doesn't "
|
|
"make sense on its own.");
|
|
Problems::issue_problem_end();
|
|
|
|
@<Issue PM_SayUnknownCondition problem@> =
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_SayUnknownCondition));
|
|
Problems::issue_problem_segment(
|
|
"In the line %1, I was expecting that '%2' would be something to "
|
|
"'say', but it didn't look like any form of 'say' that I know. So "
|
|
"I tried to read '%2' as a value of some kind (because it's legal "
|
|
"to say values), but couldn't make sense of it that way either. "
|
|
"%PSometimes this happens because punctuation has gone wrong - "
|
|
"for instance, if you've omitted a semicolon or full stop at the "
|
|
"end of the 'say' phrase.");
|
|
Problems::issue_problem_segment(
|
|
"%PNames which end in 'condition' often represent the current "
|
|
"state of something which can be in any one of three or more "
|
|
"states. This will only be the case if you have explicitly said "
|
|
"so, with a line like 'The rocket is either dry, fuelled or launched.' - "
|
|
"in which case the value 'rocket condition' will always be one "
|
|
"of 'dry', 'fuelled' or 'launched'. Note that all of this only "
|
|
"applies to a list of three or more possibilities - a thing can "
|
|
"have any number of either/or properties. For instance, a "
|
|
"container is open or closed, but it also transparent or opaque. "
|
|
"Neither of these counts as its 'condition'.");
|
|
Problems::issue_problem_end();
|
|
|
|
@<Issue PM_SayUnknown problem@> =
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_SayUnknown));
|
|
Problems::issue_problem_segment(
|
|
"In the line %1, I was expecting that '%2' would be something to "
|
|
"'say', but it didn't look like any form of 'say' that I know. So "
|
|
"I tried to read '%2' as a value of some kind (because it's legal "
|
|
"to say values), but couldn't make sense of it that way either. "
|
|
"%PSometimes this happens because punctuation has gone wrong - "
|
|
"for instance, if you've omitted a semicolon or full stop at the "
|
|
"end of the 'say' phrase.");
|
|
Problems::issue_problem_end();
|
|
|
|
@<Unknown found text occurs as a text substitution@> =
|
|
<unknown-text-substitution-problem-diagnosis>(Node::get_text(p));
|
|
|
|
@ It's a bit unenlightening when an entire condition is rejected as
|
|
unknown if, in fact, only one of perhaps many clauses is broken. We
|
|
therefore produce quite an elaborate problem message which goes through
|
|
the clauses, summing up their status in turn:
|
|
|
|
@d INVALID_CP_BIT 1
|
|
@d WHENWHILE_CP_BIT 2
|
|
|
|
@<Issue a problem message for a compound condition which has gone bad@> =
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_CompoundConditionFailed));
|
|
Problems::issue_problem_segment(
|
|
"In the sentence %1, I was expecting that '%2' would be a condition. "
|
|
"It didn't make sense as one long phrase, but because it was divided up by "
|
|
"'and'/'or', I tried breaking it down into smaller conditions, but "
|
|
"that didn't work either. ");
|
|
<condition-problem-diagnosis>(Node::get_text(p));
|
|
int dubious = <<r>>;
|
|
if (dubious & INVALID_CP_BIT)
|
|
Problems::issue_problem_segment(
|
|
"so I ran out of ideas.");
|
|
else
|
|
Problems::issue_problem_segment(
|
|
"but that combination of conditions isn't allowed to be joined "
|
|
"together with 'and' or 'or', because that would just be too confusing. "
|
|
"%PFor example, 'if the player is carrying a container or a "
|
|
"supporter' has an obvious meaning in English, but Inform reads "
|
|
"it as two different conditions glued together: 'if the player is "
|
|
"carrying a container', and also 'a supporter'. The meaning of "
|
|
"the first is obvious. The second part is true if the current "
|
|
"item under discussion is a supporter - for instance, the noun of "
|
|
"the current action, or the item to which a definition applies. "
|
|
"Both of these conditions are useful in different circumstances, "
|
|
"but combining them in one condition like this makes a very "
|
|
"misleading line of text. So Inform disallows it.");
|
|
if (dubious & WHENWHILE_CP_BIT)
|
|
Problems::issue_problem_segment(
|
|
"%PI notice there's a 'when' or 'while' being used as the opening "
|
|
"word of one of those conditions, though; maybe that's the problem?");
|
|
Problems::issue_problem_end();
|
|
|
|
@ These are cases where the wording used in the source text suggests some
|
|
common misunderstanding.
|
|
|
|
=
|
|
<unknown-value-problem-diagnosis> ::=
|
|
turns | ==> @<Issue PM_NumberOfTurns problem@>
|
|
... is/are out of play | ==> @<Issue PM_OutOfPlay problem@>
|
|
unicode ... | ==> @<Issue PM_MidTextUnicode problem@>
|
|
... condition | ==> @<Issue PM_UnknownCondition problem@>
|
|
... ==> @<Issue PM_Unknown problem@>
|
|
|
|
<unknown-use-option-diagnosis> ::=
|
|
... ^option | ==> @<Issue PM_OptionlessOption problem@>
|
|
... ==> @<Issue PM_Unknown problem@>
|
|
|
|
<unknown-activity-diagnosis> ::=
|
|
... of | ==> @<Issue PM_ActivityOf problem@>
|
|
... for | ==> @<Issue PM_ActivityWithFor problem@>
|
|
... ==> @<Issue PM_Unknown problem@>
|
|
|
|
@<Issue PM_NumberOfTurns problem@> =
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_NumberOfTurns));
|
|
@<Issue the generic unknown wording message@>;
|
|
Problems::issue_problem_segment(
|
|
"%PPerhaps by 'turns' you meant the number of turns of play to date? "
|
|
"If so, try 'turn count' instead.");
|
|
Problems::issue_problem_end();
|
|
|
|
@<Issue PM_OutOfPlay problem@> =
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_OutOfPlay));
|
|
@<Issue the generic unknown wording message@>;
|
|
Problems::issue_problem_segment(
|
|
"%PPeople sometimes say that things or people removed from all "
|
|
"rooms are 'out of play', but Inform uses the adjective "
|
|
"'off-stage' - for instance, 'if the ball is off-stage'. "
|
|
"If you would like 'out of play' to work, you could always "
|
|
"write 'Definition: A thing is out of play if it is off-stage.' "
|
|
"Then the two would be equivalent.");
|
|
Problems::issue_problem_end();
|
|
|
|
@<Issue PM_OptionlessOption problem@> =
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_OptionlessOption));
|
|
@<Issue the generic unknown wording message@>;
|
|
Problems::issue_problem_segment(
|
|
"%PThe names of use options, on the rare occasions when they "
|
|
"appear as values, always end with the word 'option' - for "
|
|
"instance, we have to write 'American dialect option' not "
|
|
"'American dialect'. As your text here doesn't end with the "
|
|
"word 'option', perhaps you've forgotten this arcane rule?");
|
|
Problems::issue_problem_end();
|
|
|
|
@<Issue PM_ActivityOf problem@> =
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_ActivityOf));
|
|
@<Issue the generic unknown wording message@>;
|
|
Problems::issue_problem_segment(
|
|
"%PActivity names rarely end with 'of': for instance, when we talk "
|
|
"about 'printing the name of something', properly speaking "
|
|
"the activity is called 'printing the name'. Maybe that's it?");
|
|
Problems::issue_problem_end();
|
|
|
|
@<Issue PM_MidTextUnicode problem@> =
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_MidTextUnicode));
|
|
@<Issue the generic unknown wording message@>;
|
|
Problems::issue_problem_segment(
|
|
"%PMaybe you intended this to produce a Unicode character? "
|
|
"Unicode characters can be written either using their decimal "
|
|
"numbers - for instance, 'Unicode 2041' - or with their standard "
|
|
"names - 'Unicode Latin small ligature oe'. For the full list of "
|
|
"those names, see the Unicode standard version 15.0.0.");
|
|
Problems::issue_problem_end();
|
|
|
|
@<Issue PM_UnknownCondition problem@> =
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_UnknownCondition));
|
|
@<Issue the generic unknown wording message@>;
|
|
Problems::issue_problem_segment(
|
|
"%PNames which end in 'condition' often represent the current "
|
|
"state of something which can be in any one of three or more "
|
|
"states. Names like this only work if you've declared them, with "
|
|
"a line like 'The rocket is either dry, fuelled or launched.' - "
|
|
"in which case the value 'rocket condition' will always be one "
|
|
"of 'dry', 'fuelled' or 'launched'. Maybe you forgot to declare "
|
|
"something like this, or mis-spelled the name of the owner?");
|
|
Problems::issue_problem_end();
|
|
|
|
@<Issue PM_ActivityWithFor problem@> =
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_ActivityWithFor));
|
|
@<Issue the generic unknown wording message@>;
|
|
Problems::issue_problem_segment(
|
|
"%PWere you by any chance meaning to refer to an activity by name, "
|
|
"and used the word 'for' at the end of that name? If so, try removing "
|
|
"just the word 'for'.");
|
|
Problems::issue_problem_end();
|
|
|
|
@<Issue PM_Unknown problem@> =
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_Unknown));
|
|
@<Issue the generic unknown wording message@>;
|
|
Problems::issue_problem_end();
|
|
|
|
@<Issue the generic unknown wording message@> =
|
|
Problems::issue_problem_segment(
|
|
"In the sentence %1, I was expecting to read %3, but instead found some "
|
|
"text that I couldn't understand - '%2'. ");
|
|
|
|
@<Issue a problem message for miscellaneous suspicious wordings@> =
|
|
if (Kinds::eq(kind_expected, K_use_option)) {
|
|
<unknown-use-option-diagnosis>(Node::get_text(p));
|
|
} else if (Kinds::get_construct(kind_expected) == CON_activity) {
|
|
<unknown-activity-diagnosis>(Node::get_text(p));
|
|
} else {
|
|
<unknown-value-problem-diagnosis>(Node::get_text(p));
|
|
}
|
|
|
|
@h Rule (5.b).
|
|
This is all concerned with a shorthand far more convenient to an Inform author
|
|
than it is to us -- where a property's name is used without any indication of
|
|
its owner.
|
|
|
|
@<Step (5.b) Deal with bare property names@> =
|
|
LOG_DASH("(5.b)");
|
|
if (Kinds::get_construct(kind_expected) != CON_property) {
|
|
parse_node *check = p;
|
|
if (Node::is(check, AMBIGUITY_NT)) check = check->down;
|
|
if (Rvalues::is_CONSTANT_construction(check, CON_property)) {
|
|
property *prn = Rvalues::to_property(check);
|
|
@<Step (5.b.2) If a bare property name is used where we expect a value, coerce it if the kinds allow@>;
|
|
}
|
|
}
|
|
|
|
@ But more often we want a value which just happens in this case to come
|
|
from a property. For instance, in a text routine printing a description
|
|
like "The cedarwood box could hold [carrying capacity in words]
|
|
item[s].", we want "carrying capacity" to be a number value, and we
|
|
treat it as if it read "carrying capacity of the cedarwood box".
|
|
|
|
We don't coerce if the property holds a relation, because letting a variable
|
|
be a description of a relation tries to create a local relation on the
|
|
stack frame, and this is unlikely to be what anyone wanted.
|
|
|
|
@<Step (5.b.2) If a bare property name is used where we expect a value, coerce it if the kinds allow@> =
|
|
LOG_DASH("(5.b.2)");
|
|
if (kind_expected) {
|
|
LOG_DASH("(5.b.2a)");
|
|
if (Properties::is_value_property(prn)) {
|
|
LOG_DASH("(5.b.2b)");
|
|
kind *kind_if_coerced = ValueProperties::kind(prn);
|
|
int verdict = Kinds::compatible(kind_if_coerced, kind_expected);
|
|
if (verdict != NEVER_MATCH) {
|
|
LOG_DASH("(5.b.2c)");
|
|
@<Coerce into a property of the "self" object@>;
|
|
return verdict;
|
|
}
|
|
if ((Kinds::get_construct(kind_expected) == CON_description) &&
|
|
(Kinds::get_construct(kind_if_coerced) != CON_relation)) {
|
|
LOGIF(MATCHING, "(5.b.2) coercing to description\n");
|
|
parse_node *become = Specifications::from_kind(kind_if_coerced);
|
|
Node::set_text(become, Node::get_text(p));
|
|
Node::copy_in_place(p, Descriptions::to_rvalue(become));
|
|
p->down = NULL;
|
|
} else {
|
|
LOGIF(MATCHING, "(5.b.2) declining to cast into property value form\n");
|
|
return verdict;
|
|
}
|
|
}
|
|
}
|
|
|
|
@ The tricky part is working out what the implicitly meant object is, a
|
|
classic donkey anaphora-style problem in linguistics. We don't even begin
|
|
to solve that here: indeed the decision is taken rather indirectly, because
|
|
we simply compile code which uses Inform 6's |self| variable to refer to
|
|
the owner. The I6 library and our own run-time code conspire to ensure that
|
|
|self| is always equal to something sensible.
|
|
|
|
@<Coerce into a property of the "self" object@> =
|
|
parse_node *was = p->next_alternative;
|
|
parse_node *pr = Node::duplicate(p);
|
|
pr->next_alternative = NULL;
|
|
parse_node *become = Lvalues::new_PROPERTY_VALUE(pr, Rvalues::new_self_object_constant());
|
|
Node::copy(p, become);
|
|
p->next_alternative = was;
|
|
|
|
LOGIF(MATCHING, "(5.b) coercing PROPERTY to PROPERTY VALUE: $P\n", p);
|
|
|
|
@h Rule (5.c).
|
|
An unchecked SP can contain a proposition which, though valid as a
|
|
predicate calculus sentence, makes no sense for type reasons: for instance,
|
|
"the Orange Room is 10" compiles to a valid sentence but one in which the
|
|
binary predicate for equality is applied to incomparable SPs. To type-check,
|
|
we must prove that any proposition needed is valid on these grounds, and we
|
|
delegate that to "Type Check Propositions.w".
|
|
|
|
@<Step (5.c) Deal with any attached proposition@> =
|
|
LOG_DASH("(5.c)");
|
|
char *desired_to = NULL;
|
|
if (Node::is(p, TEST_PROPOSITION_NT)) desired_to = "be a condition";
|
|
if (Descriptions::is_complex(p)) desired_to = "be a description";
|
|
|
|
if (desired_to) {
|
|
if (TypecheckPropositions::type_check(Specifications::to_proposition(p),
|
|
TypecheckPropositions::tc_no_problem_reporting())
|
|
== NEVER_MATCH) {
|
|
LOGIF(MATCHING, "(5.c) on $P failed proposition type-checking: $D\n",
|
|
p, Specifications::to_proposition(p));
|
|
THIS_IS_A_GROSS_PROBLEM;
|
|
TypecheckPropositions::type_check(Specifications::to_proposition(p),
|
|
TypecheckPropositions::tc_problem_reporting(Node::get_text(p), desired_to));
|
|
return NEVER_MATCH;
|
|
} else { LOG_DASH("(5.c) Okay!"); }
|
|
}
|
|
|
|
@h Rule (5.d).
|
|
Something of a grab-bag, this one. What these three situations have in common
|
|
is that all use the typechecker to clarify ambiguities in syntax.
|
|
|
|
@<Step (5.d) Apply miscellaneous other coercions@> =
|
|
#ifdef IF_MODULE
|
|
@<Step (5.d.1) Coerce TEST ACTION to constant action@>;
|
|
#endif
|
|
@<Step (5.d.2) Coerce constant TEXT and TEXT ROUTINE to UNDERSTANDING@>;
|
|
@<Step (5.d.3) Coerce a description to a value, if we expect a noun-like description@>;
|
|
@<Step (5.d.4) Reject plausible but wrong uses due to use of inline-only types in phrases@>;
|
|
|
|
@ An action pattern can be an action if specific enough, and this is
|
|
crucial: it enables phrases such as "try taking the box" to work. When
|
|
such phrases are type-checked, they expect the argument to be a constant
|
|
action value, which is a specific action.
|
|
|
|
@<Step (5.d.1) Coerce TEST ACTION to constant action@> =
|
|
LOG_DASH("(5.d.1)");
|
|
if ((AConditions::is_action_TEST_VALUE(p)) && (kind_expected) &&
|
|
(Kinds::compatible(K_stored_action, kind_expected))) {
|
|
explicit_action *ea = Node::get_constant_explicit_action(p->down);
|
|
if (ea == NULL) {
|
|
action_pattern *ap = Node::get_constant_action_pattern(p->down);
|
|
int failure_code = 0;
|
|
ea = ExplicitActions::from_action_pattern(ap, &failure_code);
|
|
|
|
if (failure_code == UNDERSPECIFIC_EA_FAILURE) {
|
|
THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_ActionNotSpecific));
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but '%2' is too vague to describe a specific action. "
|
|
"%PIt has to be an exact instruction about what is being done, and "
|
|
"to what. For instance, 'taking the box' is fine, but 'dropping or "
|
|
"taking something openable' is not.");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
}
|
|
if (failure_code == OVERSPECIFIC_EA_FAILURE) {
|
|
THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_ActionTooSpecific));
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but '%2' imposes too many restrictions on the "
|
|
"action to be carried out, by saying something about the "
|
|
"circumstances which you can't guarantee will be true. "
|
|
"%PSometimes this problem appears because I've misread text like "
|
|
"'in ...' as a clause saying that the action takes place in a "
|
|
"particular room, when in fact it was part of the name of one of "
|
|
"the items involved. If that's the problem, try using 'let' to "
|
|
"create a simpler name for it, and then rewrite the 'try' to use "
|
|
"that simpler name - the ambiguity should then vanish.");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
}
|
|
}
|
|
Node::copy_in_place(p, p->down);
|
|
p->down = NULL;
|
|
Node::set_kind_of_value(p, K_stored_action);
|
|
Node::set_constant_explicit_action(p, ea);
|
|
Node::set_constant_action_pattern(p, NULL);
|
|
LOGIF(MATCHING, "Coerced to sa: $P\n", p);
|
|
return ALWAYS_MATCH;
|
|
}
|
|
if ((condition_context) &&
|
|
(Node::is(p, CONSTANT_NT))) {
|
|
kind *E = Specifications::to_kind(p);
|
|
if (Kinds::compatible(E, K_stored_action)) {
|
|
parse_node *val = Node::duplicate(p);
|
|
Node::copy_in_place(p, Node::new(TEST_VALUE_NT));
|
|
Node::set_text(p, Node::get_text(val));
|
|
p->down = val;
|
|
LOGIF(MATCHING, "Coerced back again to sa: $P\n", p);
|
|
return ALWAYS_MATCH;
|
|
}
|
|
}
|
|
|
|
@ The following applies only to literal text in double-quotes, which might
|
|
or might not include text substitutions in square brackets: if we check it
|
|
against "understanding", then we are trying to interpret it as a grammar
|
|
to parse rather than text to print. We need to coerce since these have very
|
|
different representations at run-time.
|
|
|
|
@<Step (5.d.2) Coerce constant TEXT and TEXT ROUTINE to UNDERSTANDING@> =
|
|
LOG_DASH("(5.d.2)");
|
|
if ((Rvalues::is_CONSTANT_of_kind(p, K_text)) &&
|
|
(K_understanding) && (Kinds::eq(kind_expected, K_understanding))) {
|
|
Node::set_kind_of_value(p, K_understanding);
|
|
}
|
|
|
|
@ Another ambiguity is that the text "women who are in lighted rooms" in:
|
|
|
|
>> let N be the number of women who are in lighted rooms;
|
|
|
|
...is parsed as a description, a condition. But in
|
|
fact it's a noun here -- it has to be a value, in fact, which can go into
|
|
the "number of..." phrase as an argument. We make this happen by coercing
|
|
it to a constant value, using the "description of..." constructor.
|
|
|
|
@<Step (5.d.3) Coerce a description to a value, if we expect a noun-like description@> =
|
|
LOG_DASH("(5.d.3)");
|
|
kind *domain = NULL;
|
|
if (Kinds::get_construct(kind_expected) == CON_description) {
|
|
domain = Kinds::unary_construction_material(kind_expected);
|
|
}
|
|
if ((domain) && (Specifications::is_description(p))) {
|
|
LOGIF(MATCHING, "(5.d.3) requiring description of %u\n", domain);
|
|
kind *K = Specifications::to_kind(p);
|
|
if (K == NULL) K = K_object;
|
|
LOGIF(MATCHING, "(5.d.3) finding description of %u\n", K);
|
|
int made_match = TRUE;
|
|
if (Kinds::compatible(K, domain) == NEVER_MATCH) made_match = FALSE;
|
|
@<Throw out the wrong sort of description with a seldom-seen problem message@>;
|
|
quantifier *q = Descriptions::get_quantifier(p);
|
|
if ((q) && (q != not_exists_quantifier) && (q != for_all_quantifier))
|
|
@<Issue a problem message for a quantified proposition in the description@>;
|
|
parse_node *as_con = Descriptions::to_rvalue(p);
|
|
if (as_con == NULL)
|
|
@<Issue a problem message for a malformed proposition in the description@>;
|
|
Node::copy_in_place(p, as_con);
|
|
p->down = NULL;
|
|
return ALWAYS_MATCH;
|
|
}
|
|
|
|
@ This is for undescriptive descriptions, really.
|
|
|
|
@<Issue a problem message for a quantified proposition in the description@> =
|
|
THIS_IS_AN_INTERESTING_PROBLEM {
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_BadQuantifierInDescription));
|
|
Problems::issue_problem_segment(
|
|
"In %1 you wrote the description '%2' in the context of a value, "
|
|
"but descriptions used that way are not allowed to talk about "
|
|
"quantities. For example, it's okay to write 'an even number' "
|
|
"as a description value, but not 'three numbers' or 'most numbers'.");
|
|
Problems::issue_problem_end();
|
|
}
|
|
return NEVER_MATCH;
|
|
|
|
@ The following message is seldom seen since most phrases using descriptions
|
|
are set up with two parallel versions. As every description matches exactly one
|
|
of these, there won't be a problem. But just in case the user has intentionally
|
|
defined a phrase for only one case:
|
|
|
|
@<Throw out the wrong sort of description with a seldom-seen problem message@> =
|
|
if (made_match == FALSE) {
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p));
|
|
Problems::quote_kind(3, K);
|
|
Problems::quote_kind(4, domain);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible));
|
|
Problems::issue_problem_segment(
|
|
"In the line %1, the text '%2' seems to be a description of %3, but "
|
|
"a description of %4 was required.");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
}
|
|
|
|
@ I can't see an easy proof that this can never occur, but nor can I make it
|
|
happen. The problem message is just in case someone finds a way. It appears
|
|
if the description has a proposition with other than one free variable, once
|
|
any universal quantifier ("all", etc.) is removed.
|
|
|
|
@<Issue a problem message for a malformed proposition in the description@> =
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible));
|
|
Problems::issue_problem_segment(
|
|
"In the line %1, the text '%2' is given where a description of a collection "
|
|
"of things or values was required. For instance, 'rooms which contain "
|
|
"something', or 'closed containers' - note that there is no need to say "
|
|
"'all' or 'every' in this context, as that is understood already.");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
|
|
@ It might look as if this ought to be checked when phrase definitions are
|
|
made; the trouble is, "action", "condition" and so on are valid
|
|
in phrase definitions, but only in inline-defined ones. We don't want to get
|
|
into all that here, because the message is aimed more at Inform novices who
|
|
have made an understandable confusion.
|
|
|
|
@<Step (5.d.4) Reject plausible but wrong uses due to use of inline-only types in phrases@> =
|
|
if (Lvalues::is_lvalue(p)) {
|
|
kind *K = Specifications::to_kind(p);
|
|
if (K == NULL) {
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible)); /* screened out at definition time */
|
|
Problems::issue_problem_segment(
|
|
"In the line %1, '%2' ought to be a value, but isn't - there must be "
|
|
"something fishy about the way it was created. %P"
|
|
"Usually this happens because it is one of the named items in "
|
|
"a phrase definition, but stood for a chunk of text which can't "
|
|
"be a value - for instance, 'To marvel at (feat - an action)' "
|
|
"doesn't make 'feat' a value. (Calling it a 'stored action' "
|
|
"would have been fine; and similarly, if you want something "
|
|
"which is either true or false, use 'truth state' not 'condition'.)");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
}
|
|
}
|
|
|
|
@h Rule (5.e).
|
|
The "main rule" is, as we shall see, that |p| should have the same
|
|
species as |expected|, or if |expected| give no species then at least it
|
|
should have the same family. The two exceptional cases are when |expected|
|
|
is a description such as "an even number", or the name of a kind of value
|
|
such as "a scene", in which case we allow |p| if it's a value which
|
|
meets these requirements.
|
|
|
|
@<Step (5.e) The Main Rule of Type-Checking@> =
|
|
int exceptional_case = FALSE;
|
|
@<Step (5.e.2) Exception: when expecting a generic or actual CONSTANT@>;
|
|
if (exceptional_case == FALSE) @<Step (5.e.3) Main rule@>;
|
|
|
|
@ Now for the related, but slightly simpler, case of matching the name of a
|
|
kind. Suppose we are parsing "award 5 points" against
|
|
|
|
>> To award (N - a number) points: ...
|
|
|
|
Here |p| will be the actual constant value 5, and |expected| the
|
|
generic constant value with kind "number".
|
|
|
|
A phrase which returns a value must have its own return value's kind
|
|
checked. Unfortunately we can't do that yet: we want to wait until
|
|
recursive type-checking has removed incorrect invocations before drawing a
|
|
conclusion about the return kind of the phrase.
|
|
|
|
@<Step (5.e.2) Exception: when expecting a generic or actual CONSTANT@> =
|
|
LOG_DASH("(5.e.2)");
|
|
if ((kind_expected) && (Specifications::is_value(p))) {
|
|
if (Node::is(p, PHRASE_TO_DECIDE_VALUE_NT)) {
|
|
LOGIF(MATCHING, "(5.e.2) exempting phrase from return value checking for now\n");
|
|
} else {
|
|
switch (Kinds::compatible(
|
|
Specifications::to_kind(p),
|
|
kind_expected)) {
|
|
case NEVER_MATCH:
|
|
@<Fail with a mismatched value problem message@>;
|
|
case SOMETIMES_MATCH:
|
|
outcome = SOMETIMES_MATCH;
|
|
LOGIF(MATCHING, "dropping to sometimes level\n");
|
|
return outcome;
|
|
break;
|
|
case ALWAYS_MATCH: break;
|
|
}
|
|
}
|
|
exceptional_case = TRUE;
|
|
}
|
|
|
|
@ This is the error message a typical C compiler's type-checker would issue;
|
|
it says the value has the wrong kind.
|
|
|
|
@<Fail with a mismatched value problem message@> =
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
LOG("Offending subtree: $T\n", p);
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p));
|
|
Problems::quote_kind(3, kind_expected);
|
|
|
|
if (Node::is(p, LOCAL_VARIABLE_NT)) {
|
|
local_variable *lvar = Node::get_constant_local_variable(p);
|
|
Problems::quote_kind(4, LocalVariables::kind(lvar));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_LocalMismatch));
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but '%2' is a temporary name for %4 (created by 'let' "
|
|
"or 'repeat'), whereas I was expecting to find %3 there.");
|
|
Problems::issue_problem_end();
|
|
} else if (Kinds::eq(kind_expected, K_sayable_value)) {
|
|
Problems::quote_kind(4, Specifications::to_kind(p));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_AllSayInvsFailed));
|
|
if (Wordings::empty(Node::get_text(p)))
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but that only works for sayable values, that is, "
|
|
"values which I can display in text form. '%2' isn't one of those "
|
|
"values: it's %4, a kind which isn't sayable.");
|
|
else
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but that only works for sayable values, that is, "
|
|
"values which I can display in text form. This isn't one of those "
|
|
"values: it's %4, a kind which isn't sayable.");
|
|
Problems::issue_problem_end();
|
|
} else {
|
|
LOG("Found: %u; Expected: %u\n", Specifications::to_kind(p),
|
|
kind_expected);
|
|
Problems::quote_kind(4, Specifications::to_kind(p));
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_TypeMismatch));
|
|
if (Wordings::empty(Node::get_text(p)))
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but that has the wrong kind of value: %4 rather than %3.");
|
|
else
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but '%2' has the wrong kind of value: %4 rather than %3.");
|
|
Problems::issue_problem_end();
|
|
}
|
|
return NEVER_MATCH;
|
|
|
|
@ We now apply the main rule, supposing that neither of the exceptional cases
|
|
has intervened to stop us getting here. The found and expected specifications
|
|
must have the same family and, unless the expected species is |UNKNOWN_NT|, the
|
|
same species as well.
|
|
|
|
@<Step (5.e.3) Main rule@> =
|
|
LOG_DASH("(5.e.3)");
|
|
if ((kind_expected) || (condition_context)) {
|
|
int condition_found = FALSE;
|
|
if (Specifications::is_condition(p)) condition_found = TRUE;
|
|
if (condition_found != condition_context) {
|
|
if ((Specifications::is_description(p)) && (kind_expected))
|
|
@<Fail with a warning about literal descriptions@>
|
|
else
|
|
@<Fail with a catch-all typechecking problem message@>;
|
|
}
|
|
}
|
|
|
|
@<Fail with a warning about literal descriptions@> =
|
|
if (Descriptions::is_complex(p)) {
|
|
THIS_IS_AN_INTERESTING_PROBLEM {
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p));
|
|
Problems::quote_kind(3, kind_expected);
|
|
Problems::quote_kind_of(4, p);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_GenericDescription));
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but '%2' is used in a context where I'd expect to see "
|
|
"a (single) specific example of %3. Although what you wrote did "
|
|
"make sense as a description, it could refer to many different "
|
|
"values or to none, so it wasn't specific enough.");
|
|
Problems::issue_problem_end();
|
|
}
|
|
} else {
|
|
THIS_IS_AN_INTERESTING_PROBLEM {
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p));
|
|
Problems::quote_kind(3, kind_expected);
|
|
Problems::quote_kind_of(4, p);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_LiteralDescriptionAsValue));
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but '%2' is used in a context where I'd expect to see "
|
|
"a (single) specific example of %3, not a description.");
|
|
if ((Specifications::is_kind_like(p)) &&
|
|
(Kinds::eq(Specifications::to_kind(p), K_time)))
|
|
Problems::issue_problem_segment(
|
|
" %P(If you meant the current time, this is called 'time "
|
|
"of day' in Inform to avoid confusing it with the various "
|
|
"other meanings that the word 'time' can have.)");
|
|
Problems::issue_problem_end();
|
|
}
|
|
}
|
|
return NEVER_MATCH;
|
|
|
|
@ This is the general-purpose Problem message to which the type-checker
|
|
resorts when it has nothing more specific to say.
|
|
|
|
@<Fail with a catch-all typechecking problem message@> =
|
|
THIS_IS_AN_ORDINARY_PROBLEM;
|
|
|
|
Problems::quote_source(1, current_sentence);
|
|
Problems::quote_wording(2, Node::get_text(p));
|
|
Problems::quote_kind(3, kind_expected);
|
|
Problems::quote_kind_of(4, p);
|
|
StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible)); /* at any rate I haven't seen it lately */
|
|
Problems::issue_problem_segment(
|
|
"You wrote %1, but '%2' seems to be %4, whereas I was expecting to "
|
|
"find %3 there.");
|
|
Problems::issue_problem_end();
|
|
return NEVER_MATCH;
|
|
|
|
@h Ambiguity testing flags.
|
|
To avoid filling the parse tree with unnecessary annotations, we apply these
|
|
only when resolving ambiguities, in (4A) above.
|
|
|
|
@d PASSED_DASHFLAG 0x00000001 /* once type-checked: did this pass type checking? */
|
|
@d UNPROVEN_DASHFLAG 0x00000002 /* once type-checked: will this need run-time checking? */
|
|
@d GROSSLY_FAILED_DASHFLAG 0x00000004 /* once type-checked: oh, this one failed big time */
|
|
@d TESTED_DASHFLAG 0x00000008 /* has been type-checked */
|
|
@d INTERESTINGLY_FAILED_DASHFLAG 0x00000010 /* an interesting problem message could be produced about the way this failed */
|
|
|
|
=
|
|
int Dash::reading_passed(parse_node *p) {
|
|
if (Dash::test_flag(p, PASSED_DASHFLAG)) return TRUE;
|
|
else if (Dash::test_flag(p, TESTED_DASHFLAG)) return FALSE;
|
|
return NOT_APPLICABLE;
|
|
}
|
|
|
|
char *Dash::verdict_to_text(parse_node *p) {
|
|
if (p == NULL) return "(no node)";
|
|
char *verdict = "untested";
|
|
if (Dash::test_flag(p, TESTED_DASHFLAG)) verdict = "failed";
|
|
if (Dash::test_flag(p, INTERESTINGLY_FAILED_DASHFLAG)) verdict = "interesting";
|
|
if (Dash::test_flag(p, GROSSLY_FAILED_DASHFLAG)) verdict = "gross";
|
|
if (Dash::test_flag(p, PASSED_DASHFLAG)) verdict = "proven";
|
|
if (Dash::test_flag(p, UNPROVEN_DASHFLAG)) verdict = "unproven";
|
|
return verdict;
|
|
}
|
|
|
|
char *Dash::quick_verdict_to_text(parse_node *p) {
|
|
if (p == NULL) return "?";
|
|
char *verdict = "-";
|
|
if (Dash::test_flag(p, TESTED_DASHFLAG)) verdict = "f";
|
|
if (Dash::test_flag(p, INTERESTINGLY_FAILED_DASHFLAG)) verdict = "i";
|
|
if (Dash::test_flag(p, GROSSLY_FAILED_DASHFLAG)) verdict = "g";
|
|
if (Dash::test_flag(p, PASSED_DASHFLAG)) verdict = "p";
|
|
if (Dash::test_flag(p, UNPROVEN_DASHFLAG)) verdict = "u";
|
|
return verdict;
|
|
}
|
|
|
|
@ The bitmap holding the results of typechecking:
|
|
|
|
=
|
|
void Dash::set_flag(parse_node *p, int flag) {
|
|
if (p == NULL) internal_error("tried to set flag for null p");
|
|
int bm = Annotations::read_int(p, epistemological_status_ANNOT);
|
|
Annotations::write_int(p, epistemological_status_ANNOT, bm | flag);
|
|
}
|
|
|
|
void Dash::clear_flags(parse_node *p) {
|
|
if (p == NULL) internal_error("tried to clear flags for null p");
|
|
Annotations::write_int(p, epistemological_status_ANNOT, 0);
|
|
}
|
|
|
|
void Dash::clear_flag(parse_node *p, int flag) {
|
|
if (p == NULL) internal_error("tried to clear flag for null p");
|
|
int bm = Annotations::read_int(p, epistemological_status_ANNOT);
|
|
Annotations::write_int(p, epistemological_status_ANNOT, bm & (~flag));
|
|
}
|
|
|
|
int Dash::test_flag(parse_node *p, int flag) {
|
|
int bm = (p)?(Annotations::read_int(p, epistemological_status_ANNOT)):0;
|
|
if (bm & flag) return TRUE;
|
|
return FALSE;
|
|
}
|
|
|
|
@ A convenience sometimes needed for checking conditional clauses, like the
|
|
"when..." attached to action patterns:
|
|
|
|
=
|
|
int Dash::validate_conditional_clause(parse_node *spec) {
|
|
if (spec == NULL) return TRUE;
|
|
if (Node::is(spec, UNKNOWN_NT)) return FALSE;
|
|
if (Dash::check_condition(spec) == NEVER_MATCH) return FALSE;
|
|
return TRUE;
|
|
}
|
|
|
|
@ The exceptional treatment of the "property" kind below is to allow
|
|
"examining scenery" to be an action pattern, where an either/or property
|
|
has a name which is really a noun rather than an adjective, luring people
|
|
into treating it as such.
|
|
|
|
=
|
|
parse_node *last_spec_failing_to_validate = NULL;
|
|
kind *last_kind_failing_to_validate = NULL;
|
|
kind *last_kind_found_failing_to_validate = NULL;
|
|
|
|
void Dash::clear_validation_case(void) {
|
|
last_spec_failing_to_validate = NULL;
|
|
last_kind_failing_to_validate = NULL;
|
|
last_kind_found_failing_to_validate = NULL;
|
|
}
|
|
|
|
int Dash::get_validation_case(parse_node **spec, kind **set_K,
|
|
kind **set_K2) {
|
|
*spec = last_spec_failing_to_validate;
|
|
*set_K = last_kind_failing_to_validate;
|
|
*set_K2 = last_kind_found_failing_to_validate;
|
|
if ((*spec == NULL) || (*set_K == NULL)) return FALSE;
|
|
return TRUE;
|
|
}
|
|
|
|
int Dash::validate_parameter(parse_node *spec, kind *K) {
|
|
parse_node *vts;
|
|
kind *kind_found = NULL;
|
|
if (spec == NULL) return TRUE;
|
|
if (Node::is(spec, UNKNOWN_NT)) goto DontValidate;
|
|
|
|
if (Specifications::is_description(spec)) {
|
|
pcalc_prop *prop = Descriptions::to_proposition(spec);
|
|
if ((prop) && (Binding::number_free(prop) != 1)) return FALSE;
|
|
}
|
|
|
|
if (Specifications::is_description(spec)) Dash::check_condition(spec);
|
|
else Dash::check_value(spec, NULL); /* to force a generic return kind to be evaluated */
|
|
kind_found = Specifications::to_kind(spec);
|
|
if ((Kinds::get_construct(kind_found) == CON_property) && (Kinds::Behaviour::is_object(K)))
|
|
return TRUE;
|
|
if ((K_understanding) && (Kinds::eq(kind_found, K_snippet)) &&
|
|
(Kinds::eq(K, K_understanding)))
|
|
return TRUE;
|
|
if ((K_understanding) && (Kinds::eq(K, K_understanding)) &&
|
|
(Node::is(spec, CONSTANT_NT) == FALSE) &&
|
|
(Kinds::eq(kind_found, K_text)))
|
|
goto DontValidate;
|
|
vts = Specifications::from_kind(K);
|
|
if (Dash::compatible_with_description(spec, vts) == NEVER_MATCH) {
|
|
if ((K_understanding) && (Kinds::eq(K, K_understanding)) && (Node::is(spec, CONSTANT_NT))) {
|
|
vts = Specifications::from_kind(K_snippet);
|
|
if (Dash::compatible_with_description(spec, vts) != NEVER_MATCH) return TRUE;
|
|
}
|
|
if (Kinds::eq(kind_found, K_value)) return TRUE; /* pick up later in type-checking */
|
|
goto DontValidate;
|
|
}
|
|
return TRUE;
|
|
|
|
DontValidate:
|
|
last_spec_failing_to_validate = Node::duplicate(spec);
|
|
last_kind_failing_to_validate = K;
|
|
last_kind_found_failing_to_validate = kind_found;
|
|
return FALSE;
|
|
}
|
|
|
|
@ This is the state of the |***| pseudo-phrase used for debugging:
|
|
|
|
=
|
|
int verbose_checking_state = FALSE;
|
|
linked_list *packages_to_log_inter_from = NULL;
|
|
|
|
void Dash::tracing_phrases(wchar_t *text) {
|
|
if ((text) && (text[0])) {
|
|
TEMPORARY_TEXT(LT)
|
|
WRITE_TO(LT, "%w", text);
|
|
if (Str::eq_insensitive(LT, I"inter")) {
|
|
inter_package *pack = Functions::package_being_compiled();
|
|
if (pack) {
|
|
if (packages_to_log_inter_from == NULL)
|
|
packages_to_log_inter_from = NEW_LINKED_LIST(inter_package);
|
|
ADD_TO_LINKED_LIST(pack, inter_package, packages_to_log_inter_from);
|
|
}
|
|
} else {
|
|
Log::set_aspect_from_command_line(LT, FALSE);
|
|
}
|
|
DISCARD_TEXT(LT)
|
|
verbose_checking_state = TRUE;
|
|
} else {
|
|
verbose_checking_state = (verbose_checking_state)?FALSE:TRUE;
|
|
if (verbose_checking_state == FALSE) {
|
|
Log::set_all_aspects(FALSE);
|
|
} else {
|
|
Log::set_aspect(MATCHING_DA, TRUE);
|
|
Log::set_aspect(KIND_CHECKING_DA, TRUE);
|
|
Log::set_aspect(LOCAL_VARIABLES_DA, TRUE);
|
|
}
|
|
}
|
|
}
|
|
|
|
linked_list *Dash::phrases_to_log(void) {
|
|
return packages_to_log_inter_from;
|
|
}
|
|
|
|
@h Value checking.
|
|
The following adapts the above test to attempt to match two specifications
|
|
together: for example, to match "12" against "even number". This, rather
|
|
surprisingly, returns |SOMETIMES_MATCH|, since we find that the kinds
|
|
are guaranteed -- 12 is indeed a number -- but Inform doesn't "know" the
|
|
meaning of the word "even", only that it's a test which will be applied
|
|
at run time.
|
|
|
|
=
|
|
int Dash::compatible_with_description(parse_node *from_spec, parse_node *to_spec) {
|
|
LOGIF(KIND_CHECKING, "[Can we match from: $P to: $P?]\n", from_spec, to_spec);
|
|
|
|
kind *from = Specifications::to_kind(from_spec);
|
|
kind *to = Specifications::to_kind(to_spec);
|
|
|
|
int result = NEVER_MATCH;
|
|
if ((from) && (to)) result = Kinds::compatible(from, to);
|
|
else if (to) result = SOMETIMES_MATCH;
|
|
|
|
if ((Descriptions::is_qualified(to_spec)) || (Descriptions::to_instance(to_spec)))
|
|
result = Dash::worst_case(result, SOMETIMES_MATCH);
|
|
|
|
switch(result) {
|
|
case ALWAYS_MATCH: LOGIF(KIND_CHECKING, "[Always]\n"); break;
|
|
case SOMETIMES_MATCH: LOGIF(KIND_CHECKING, "[Sometimes]\n"); break;
|
|
case NEVER_MATCH: LOGIF(KIND_CHECKING, "[Never]\n"); break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@h Ambiguous alternatives.
|
|
|
|
@d AMBIGUITY_JOIN_SYNTAX_CALLBACK Dash::ambiguity_join
|
|
|
|
=
|
|
int Dash::ambiguity_join(parse_node *existing, parse_node *reading) {
|
|
if ((Specifications::is_phrasal(reading)) &&
|
|
(Node::get_type(reading) == Node::get_type(existing))) {
|
|
Dash::add_pr_inv(existing, reading);
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
void Dash::add_pr_inv(parse_node *E, parse_node *reading) {
|
|
for (parse_node *N = reading->down->down, *next_N = (N)?(N->next_alternative):NULL; N;
|
|
N = next_N, next_N = (N)?(N->next_alternative):NULL)
|
|
Dash::add_single_pr_inv(E, N);
|
|
}
|
|
|
|
void Dash::add_single_pr_inv(parse_node *E, parse_node *N) {
|
|
E = E->down->down;
|
|
if (Invocations::same_phrase_and_tokens(E, N)) return;
|
|
while ((E) && (E->next_alternative)) {
|
|
E = E->next_alternative;
|
|
if (Invocations::same_phrase_and_tokens(E, N)) return;
|
|
}
|
|
E->next_alternative = N; N->next_alternative = NULL;
|
|
}
|
|
|
|
@h Internal testing.
|
|
|
|
=
|
|
void Dash::perform_dash_internal_test(OUTPUT_STREAM, struct internal_test_case *itc) {
|
|
int full = FALSE;
|
|
wording W = itc->text_supplying_the_case;
|
|
@<Perform a Dash internal test@>;
|
|
}
|
|
|
|
void Dash::perform_dashlog_internal_test(OUTPUT_STREAM, struct internal_test_case *itc) {
|
|
int full = TRUE;
|
|
wording W = itc->text_supplying_the_case;
|
|
@<Perform a Dash internal test@>;
|
|
}
|
|
|
|
@<Perform a Dash internal test@> =
|
|
kind *K = NULL;
|
|
parse_node *test_tree = NULL, *last_alt = NULL;
|
|
<s-value-uncached>->multiplicitous = TRUE;
|
|
<s-value-uncached>->ins.watched = TRUE;
|
|
int n = 0;
|
|
while (Wordings::nonempty(W)) {
|
|
wording T = W;
|
|
if (<phrase-with-comma-notation>(W)) {
|
|
T = GET_RW(<phrase-with-comma-notation>, 1);
|
|
W = GET_RW(<phrase-with-comma-notation>, 2);
|
|
} else W = EMPTY_WORDING;
|
|
if (<k-kind>(T)) K = <<rp>>;
|
|
else if (<s-value-uncached>(T)) {
|
|
parse_node *p = <<rp>>;
|
|
if (last_alt) last_alt->next_alternative = p;
|
|
else test_tree = p;
|
|
last_alt = p;
|
|
n++;
|
|
} else LOG("Failed to parse: %W\n", T);
|
|
}
|
|
<s-value-uncached>->multiplicitous = FALSE;
|
|
<s-value-uncached>->ins.watched = FALSE;
|
|
if (n > 1) {
|
|
parse_node *holder = Node::new(AMBIGUITY_NT);
|
|
holder->down = test_tree;
|
|
test_tree = holder;
|
|
}
|
|
LOG("$m\n", test_tree);
|
|
if (K) {
|
|
LOG("Dash: value of kind %u\n", K);
|
|
if (full) Dash::tracing_phrases(NULL);
|
|
int rv = Dash::check_value(test_tree, K);
|
|
char *trv = "ALWAYS";
|
|
if (rv == SOMETIMES_MATCH) trv = "SOMETIMES";
|
|
if (rv == NEVER_MATCH) trv = "NEVER";
|
|
LOG("Result: %s\n", trv);
|
|
if (full) Dash::tracing_phrases(NULL);
|
|
LOG("$m\n", test_tree);
|
|
}
|