To compile a recipe for how to perform a test.
§1. Instruction set. Intest recipes are written in a mini-language called Delia, which has a fixed set of commands, enumerated as follows.
enum COPY_RCOM from 1 enum DEBUGGER_RCOM enum DEFAULT_RCOM enum ELSE_RCOM enum ENDIF_RCOM enum EXISTS_RCOM enum EXTRACT_RCOM enum FAIL_RCOM enum FAIL_STEP_RCOM enum HASH_RCOM enum IF_RCOM enum IFDEF_RCOM enum IFNDEF_RCOM enum IFFAIL_RCOM enum IFPASS_RCOM enum IF_COMPATIBLE_RCOM enum IF_EXISTS_RCOM enum IF_FORMAT_VALID_RCOM enum IF_SHOWING_RCOM enum MATCH_BINARY_RCOM enum MATCH_FOLDER_RCOM enum MATCH_G_TRANSCRIPT_RCOM enum MATCH_I6_TRANSCRIPT_RCOM enum MATCH_PROBLEM_RCOM enum MATCH_TEXT_RCOM enum MATCH_PLATFORM_TEXT_RCOM enum MATCH_Z_TRANSCRIPT_RCOM enum MKDIR_RCOM enum OR_RCOM enum PASS_RCOM enum REMOVE_RCOM enum SET_RCOM enum SHOW_RCOM enum STEP_RCOM
§2. And here's some metadata about them:
typedef struct recipe_command { int rc_code; one of the *_RCOM codes below inchar32_t *keyword; int tokens_required; or negative for "any number, including none" int supports_or; an or: command can follow int changes_nesting; } recipe_command; recipe_command instruction_set[] = { { COPY_RCOM, U"copy", 2, FALSE, 0 }, { DEBUGGER_RCOM, U"debugger", -1, TRUE, 0 }, { DEFAULT_RCOM, U"default", -1, FALSE, 0 }, { ELSE_RCOM, U"else", 0, FALSE, 0 }, { ENDIF_RCOM, U"endif", 0, FALSE, -1 }, { EXISTS_RCOM, U"exists", 1, TRUE, 0 }, { EXTRACT_RCOM, U"extract", 2, FALSE, 0 }, { FAIL_RCOM, U"fail", -1, FALSE, 0 }, { FAIL_STEP_RCOM, U"fail step", -1, TRUE, 0 }, { HASH_RCOM, U"hash", 1, TRUE, 0 }, { IF_RCOM, U"if", 2, FALSE, 1 }, { IFDEF_RCOM, U"ifdef", 1, FALSE, 1 }, { IFNDEF_RCOM, U"ifndef", 1, FALSE, 1 }, { IFFAIL_RCOM, U"iffail", 0, FALSE, 1 }, { IFPASS_RCOM, U"ifpass", 0, FALSE, 1 }, { IF_COMPATIBLE_RCOM, U"if compatible", 2, FALSE, 1 }, { IF_EXISTS_RCOM, U"if exists", 1, FALSE, 1 }, { IF_FORMAT_VALID_RCOM, U"if format valid", 1, FALSE, 1 }, { IF_SHOWING_RCOM, U"if showing", 1, FALSE, 1 }, { MATCH_BINARY_RCOM, U"match binary", 2, TRUE, 0 }, { MATCH_FOLDER_RCOM, U"match folder", 2, TRUE, 0 }, { MATCH_G_TRANSCRIPT_RCOM, U"match glulxe transcript", 2, TRUE, 0 }, { MATCH_I6_TRANSCRIPT_RCOM, U"match i6 transcript", 2, TRUE, 0 }, { MATCH_PROBLEM_RCOM, U"match problem", 2, TRUE, 0 }, { MATCH_TEXT_RCOM, U"match text", 2, TRUE, 0 }, { MATCH_PLATFORM_TEXT_RCOM, U"match platform text", 2, TRUE, 0 }, { MATCH_Z_TRANSCRIPT_RCOM, U"match frotz transcript", 2, TRUE, 0 }, { MKDIR_RCOM, U"mkdir", 1, FALSE, 0 }, { OR_RCOM, U"or", -1, FALSE, 0 }, { PASS_RCOM, U"pass", 1, FALSE, 0 }, { REMOVE_RCOM, U"remove", 1, FALSE, 0 }, { SET_RCOM, U"set", -1, FALSE, 0 }, { SHOW_RCOM, U"show", -1, TRUE, 0 }, { STEP_RCOM, U"step", -1, TRUE, 0 }, { -1, NULL, 0, FALSE, 0 } };
- The structure recipe_command is accessed in 4/tt and here.
§3. Recipes in memory. Are stored in the following hierarchy, with a recipe being essentially a linked list of lines, and a line being essentially a linked list of tokens.
typedef struct recipe { struct filename *compiled_from; struct linked_list *lines; of recipe_line struct text_stream *recipe_name; int compilation_errors; int conditional_nesting; int end_found; struct recipe_command *last_command; CLASS_DEFINITION } recipe; typedef struct recipe_line { struct recipe_command *command_used; struct linked_list *recipe_tokens; of recipe_token struct text_stream *from_text; CLASS_DEFINITION } recipe_line; typedef struct recipe_token { struct text_stream *token_text; int token_quoted; int token_indirects_to_file; int token_indirects_to_hash; CLASS_DEFINITION } recipe_token;
- The structure recipe is accessed in 4/prp, 2/rf, 4/tt and here.
- The structure recipe_line is accessed in 4/tt and here.
- The structure recipe_token is accessed in 4/tt and here.
void Delia::log_line(OUTPUT_STREAM, void *vL) { recipe_line *L = (recipe_line *) vL; WRITE("%S", L->from_text); }
§5. This looks slow, but there are unlikely to be more than five or so recipes loaded at once.
recipe *Delia::find(text_stream *name) { recipe *R; LOOP_OVER(R, recipe) if (Str::eq(name, R->recipe_name)) return R; return NULL; }
§6. Compilation. The compiler is correspondingly hierarchical. Note that we return only validly compiled recipes.
recipe *Delia::compile(filename *F, text_stream *name) { recipe *R = Delia::begin_compilation(name); R->compiled_from = F; TextFiles::read(F, FALSE, "unable to read recipe file: %f", TRUE, &Delia::compile_line, NULL, (void *) R); return Delia::end_compilation(R); }
recipe *Delia::begin_compilation(text_stream *name) { recipe *R = CREATE(recipe); R->lines = NEW_LINKED_LIST(recipe_line); R->compilation_errors = FALSE; R->compiled_from = NULL; R->conditional_nesting = 0; R->recipe_name = Str::duplicate(name); R->end_found = FALSE; return R; } recipe *Delia::end_compilation(recipe *R) { if (R->conditional_nesting > 0) { TEMPORARY_TEXT(ERM) WRITE_TO(ERM, "'endif' missing at end of recipe"); Errors::in_text_file_S(ERM, NULL); DISCARD_TEXT(ERM) R->compilation_errors = TRUE; } if (R->compilation_errors) return NULL; return R; }
§8. We skip blank lines and comments, then split the line as a command plus some tokens. The divider is a colon, which is optional (in which case, no tokens).
void Delia::compile_line(text_stream *text, text_file_position *tfp, void *state) { recipe *R = (recipe *) state; match_results mr = Regexp::create_mr(); if ((Regexp::string_is_white_space(text)) || (Regexp::match(&mr, text, U" *!%c*"))) { ; } else if (Regexp::match(&mr, text, U" *-end *")) { R->end_found = TRUE; } else if (Regexp::match(&mr, text, U" *(%c*?): *(%c*)")) { Delia::compile_command(R, text, mr.exp[0], mr.exp[1], tfp); } else if (Regexp::match(&mr, text, U" *(%c*?)")) { Delia::compile_command(R, text, mr.exp[0], Str::new(), tfp); } Regexp::dispose_of(&mr); }
§9. The command has to be in the instruction set, and the number of tokens has to be reasonable, but otherwise anything goes.
void Delia::compile_command(recipe *R, text_stream *text, text_stream *command, text_stream *tokens, text_file_position *tfp) { recipe_command *rc = NULL; for (int i=0; ; i++) if (instruction_set[i].keyword == NULL) break; else { if (Str::eq_wide_string(command, instruction_set[i].keyword)) rc = &instruction_set[i]; } if (rc == NULL) { TEMPORARY_TEXT(ERM) WRITE_TO(ERM, "unknown recipe command '%S'", command); Errors::in_text_file_S(ERM, tfp); DISCARD_TEXT(ERM) R->compilation_errors = TRUE; } else { recipe_line *L = CREATE(recipe_line); L->command_used = rc; L->recipe_tokens = NEW_LINKED_LIST(recipe_token); L->from_text = Str::duplicate(text); Delia::tokenise(L->recipe_tokens, tokens); Make sure the number of tokens is reasonable9.1; if (rc->rc_code == OR_RCOM) Make sure the or is allowed9.2; if ((rc->rc_code == SET_RCOM) || (rc->rc_code == DEFAULT_RCOM)) Make sure the set is well-formatted9.3; if ((rc->rc_code == IFDEF_RCOM) || (rc->rc_code == IFNDEF_RCOM)) Make sure the ifdef is well-formatted9.5; if ((rc->rc_code == IFPASS_RCOM) || (rc->rc_code == IFFAIL_RCOM)) Make sure the ifpass is well-formatted9.6; if (rc->rc_code == IF_RCOM) Make sure the if is well-formatted9.4; if (rc->rc_code == SHOW_RCOM) Make sure the show is well-formatted9.8; if (rc->rc_code == FAIL_RCOM) Make sure the fail is well-formatted9.7; R->conditional_nesting += rc->changes_nesting; Make sure the conditional nesting is allowed9.9; R->last_command = rc; ADD_TO_LINKED_LIST(L, recipe_line, R->lines); } }
§9.1. Make sure the number of tokens is reasonable9.1 =
int n = 0; recipe_token *T; LOOP_OVER_LINKED_LIST(T, recipe_token, L->recipe_tokens) n++; if ((rc->tokens_required >= 0) && (n != rc->tokens_required)) { TEMPORARY_TEXT(ERM) WRITE_TO(ERM, "recipe command '%S' takes %d token(s), but %d found", command, rc->tokens_required, n); Errors::in_text_file_S(ERM, tfp); DISCARD_TEXT(ERM) R->compilation_errors = TRUE; }
- This code is used in §9.
§9.2. Make sure the or is allowed9.2 =
if (R->last_command == NULL) { TEMPORARY_TEXT(ERM) WRITE_TO(ERM, "'or' can't be the first command"); Errors::in_text_file_S(ERM, tfp); DISCARD_TEXT(ERM) R->compilation_errors = TRUE; } else if (R->last_command->supports_or == FALSE) { TEMPORARY_TEXT(ERM) WRITE_TO(ERM, "'or' can't follow a '%w' command", R->last_command->keyword); Errors::in_text_file_S(ERM, tfp); DISCARD_TEXT(ERM) R->compilation_errors = TRUE; }
- This code is used in §9.
§9.3. Make sure the set is well-formatted9.3 =
int n = 0; recipe_token *T; LOOP_OVER_LINKED_LIST(T, recipe_token, L->recipe_tokens) n++; if (n == 0) { Errors::in_text_file("nothing to set", tfp); R->compilation_errors = TRUE; } else { recipe_token *first = ENTRY_IN_LINKED_LIST(0, recipe_token, L->recipe_tokens); recipe_token *second = ENTRY_IN_LINKED_LIST(1, recipe_token, L->recipe_tokens); text_stream *K = first->token_text; if (Str::get_first_char(K) == '$') { Str::delete_first_character(K); if ((n < 2) || (Str::eq(second->token_text, I"=") == FALSE)) { Errors::in_text_file("no '=' in set command", tfp); R->compilation_errors = TRUE; } else { DELETE_FROM_LINKED_LIST(1, recipe_token, L->recipe_tokens); } } else { TEMPORARY_TEXT(ERM) WRITE_TO(ERM, "set target '%S' doesn't begin with '$'", K); Errors::in_text_file_S(ERM, tfp); DISCARD_TEXT(ERM) R->compilation_errors = TRUE; } }
- This code is used in §9.
§9.4. Make sure the if is well-formatted9.4 =
recipe_token *second = ENTRY_IN_LINKED_LIST(1, recipe_token, L->recipe_tokens); text_stream *K = second->token_text; if ((Str::get_first_char(K) == DELIA_QUOTE_CHARACTER) && (Str::get_last_char(K) == DELIA_QUOTE_CHARACTER)) { Str::delete_first_character(K); Str::delete_last_character(K); }
- This code is used in §9.
§9.5. Make sure the ifdef is well-formatted9.5 =
recipe_token *first = ENTRY_IN_LINKED_LIST(0, recipe_token, L->recipe_tokens); text_stream *K = first->token_text; if ((Str::get_at(K, 0) == '$') && (Str::get_at(K, 1) == '$')) { leave this alone } else if (Str::get_at(K, 0) == '$') { Str::delete_first_character(K); } else { TEMPORARY_TEXT(ERM) WRITE_TO(ERM, "ifdef test '%S' doesn't begin with '$'", K); Errors::in_text_file_S(ERM, tfp); DISCARD_TEXT(ERM) R->compilation_errors = TRUE; }
- This code is used in §9.
§9.6. Make sure the ifpass is well-formatted9.6 =
if (LinkedLists::len(L->recipe_tokens) > 0) { Errors::in_text_file_S(I"'ifpass' and 'iffail' do not take tokens", tfp); R->compilation_errors = TRUE; } else if (R->last_command == NULL) { TEMPORARY_TEXT(ERM) WRITE_TO(ERM, "'ifpass' or 'iffail' can't be the first command"); Errors::in_text_file_S(ERM, tfp); DISCARD_TEXT(ERM) R->compilation_errors = TRUE; } else if (R->last_command->supports_or == FALSE) { TEMPORARY_TEXT(ERM) WRITE_TO(ERM, "'ifpass' or 'iffail' can't follow a '%w' command", R->last_command->keyword); Errors::in_text_file_S(ERM, tfp); DISCARD_TEXT(ERM) R->compilation_errors = TRUE; }
- This code is used in §9.
§9.7. Make sure the fail is well-formatted9.7 =
if ((LinkedLists::len(L->recipe_tokens) == 0) || (LinkedLists::len(L->recipe_tokens) > 2)) { Errors::in_text_file_S(I"'fail' takes either 1 or 2 tokens", tfp); R->compilation_errors = TRUE; }
- This code is used in §9.
§9.8. Make sure the show is well-formatted9.8 =
switch (LinkedLists::len(L->recipe_tokens)) { case 1: break; case 2: { recipe_token *first = ENTRY_IN_LINKED_LIST(0, recipe_token, L->recipe_tokens); text_stream *K = first->token_text; int bad = FALSE; LOOP_THROUGH_TEXT(pos, K) { inchar32_t c = Str::get(pos); if (((c < 'a') || (c > 'z')) && ((c < '0') || (c > '9')) && (c != '-')) bad = TRUE; } if (bad) { TEMPORARY_TEXT(ERM) WRITE_TO(ERM, "'show' item '%S' should contain only lower-case " "letters, digits and dashes", K); Errors::in_text_file_S(ERM, tfp); DISCARD_TEXT(ERM) R->compilation_errors = TRUE; } break; } default: { Errors::in_text_file_S(I"'show' must take 1 or 2 tokens", tfp); R->compilation_errors = TRUE; break; } }
- This code is used in §9.
§9.9. Make sure the conditional nesting is allowed9.9 =
if ((R->conditional_nesting < 0) || ((R->conditional_nesting == 0) && (rc->rc_code == ELSE_RCOM))) { TEMPORARY_TEXT(ERM) WRITE_TO(ERM, "'%w' misplaced", rc->keyword); Errors::in_text_file_S(ERM, tfp); DISCARD_TEXT(ERM) R->compilation_errors = TRUE; R->conditional_nesting -= rc->changes_nesting; }
- This code is used in §9.
§10. The lowest level of the compiler is the tokeniser, which breaks up a string at white space boundaries, except within the shell quote character.
define DELIA_QUOTE_CHARACTER '\''
void Delia::tokenise(linked_list *L, text_stream *txt) { string_position P = Str::start(txt); while (Characters::is_space_or_tab(Str::get(P))) P = Str::forward(P); inchar32_t first = Str::get(P); if (first == 0) return; recipe_token *T = CREATE(recipe_token); T->token_quoted = FALSE; T->token_indirects_to_file = FALSE; T->token_indirects_to_hash = FALSE; string_position Q = P; the new token begins at position P, and ends just before Q if ((first == '$') && (Str::get(Str::forward(P)) == '[')) Tokenise from a file10.1 else if ((first == '$') && (Str::get(Str::forward(P)) == '{')) Tokenise from a hash10.2 else if (first == '`') Mark to retokenise at expansion time10.3 else if (first == DELIA_QUOTE_CHARACTER) Take this quoted segment as the token10.4 else if (first == SHELL_QUOTE_CHARACTER) Take this shell quoted segment as the token10.5 else Take this unquoted word as the token10.6; T->token_text = Str::new(); Str::substr(T->token_text, P, Q); TEMPORARY_TEXT(tail) if (T->token_indirects_to_file) Q = Str::forward(Str::forward(Q)); if (T->token_indirects_to_hash) Q = Str::forward(Str::forward(Q)); Str::copy_tail(tail, txt, Str::index(Q)); ADD_TO_LINKED_LIST(T, recipe_token, L); Delia::tokenise(L, tail); DISCARD_TEXT(tail) }
§10.1. A token written $[filename$] is expanded into the contents of that file.
Tokenise from a file10.1 =
P = Str::forward(Str::forward(P)); Q = P; while ((Str::in_range(Q)) && !((Str::get(Q) == '$') && (Str::get(Str::forward(Q)) == ']'))) Q = Str::forward(Q); T->token_indirects_to_file = TRUE;
- This code is used in §10.
§10.2. More concisely, ${filename$} expands to the MD5 hash of that file.
Tokenise from a hash10.2 =
P = Str::forward(Str::forward(P)); Q = P; while ((Str::in_range(Q)) && !((Str::get(Q) == '$') && (Str::get(Str::forward(Q)) == '}'))) Q = Str::forward(Q); T->token_indirects_to_hash = TRUE;
- This code is used in §10.
§10.3. A token backticked, like `this, is retokenised before being expanded, and then each individual resulting token is expanded.
Mark to retokenise at expansion time10.3 =
T->token_quoted = NOT_APPLICABLE; P = Str::forward(P); Q = P; while ((Str::in_range(Q)) && (!Characters::is_space_or_tab(Str::get(Q)))) Q = Str::forward(Q);
- This code is used in §10.
§10.4. A token in quotes can include spaces, 'like so'.
Take this quoted segment as the token10.4 =
T->token_quoted = TRUE; int esc = FALSE; Q = Str::forward(Q); while ((Str::in_range(Q)) && ((esc) || (Str::get(Q) != DELIA_QUOTE_CHARACTER))) { if (Str::get(Q) == '\\') esc = TRUE; else esc = FALSE; Q = Str::forward(Q); } if (Str::get(Q) == DELIA_QUOTE_CHARACTER) Q = Str::forward(Q);
- This code is used in §10.
§10.5. Take this shell quoted segment as the token10.5 =
T->token_quoted = TRUE; int esc = FALSE; Q = Str::forward(Q); while ((Str::in_range(Q)) && ((esc) || (Str::get(Q) != SHELL_QUOTE_CHARACTER))) { if (Str::get(Q) == '\\') esc = TRUE; else esc = FALSE; Q = Str::forward(Q); } if (Str::get(Q) == SHELL_QUOTE_CHARACTER) Q = Str::forward(Q);
- This code is used in §10.
§10.6. And otherwise any bare word is a token.
Take this unquoted word as the token10.6 =
while ((Str::in_range(Q)) && (!Characters::is_space_or_tab(Str::get(Q)))) Q = Str::forward(Q);
- This code is used in §10.
void Delia::dequote_first_token(OUTPUT_STREAM, recipe_line *RL) { Str::clear(OUT); recipe_token *first = FIRST_IN_LINKED_LIST(recipe_token, RL->recipe_tokens); if (first) { text_stream *T = first->token_text; int L = Str::len(T); if (first->token_quoted == TRUE) { for (int i=1; i<L-1; i++) PUT(Str::get_at(T, i)); } else { for (int i=0; i<L; i++) PUT(Str::get_at(T, i)); } } }