/** * mds — A micro-display server * Copyright © 2014 Mattias Andrée (maandree@member.fsf.org) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "make-tree.h" #include "raw-data.h" #include #include #include #include #include #include #include #include /** * Wrapper around `asprintf` that makes sure that first * argument gets set to `NULL` on error and that zero is * returned on success rather than the number of printed * characters * * @param VAR:char** The output parameter for the string * @param ...:const char*, ... The format string and arguments * @return :int Zero on success, -1 on error */ #define xasprintf(VAR, ...) \ (asprintf(&(VAR), __VA_ARGS__) < 0 ? (VAR = NULL, -1) : 0) /** * Check whether a value is inside a closed range * * @param LOWER:¿T? The lower bound, inclusive * @param VALUE:¿T? The value to test * @param UPPER:¿T? The upper bound, inclusive * @return :int 1 if `LOWER` ≤ `VALUE` ≤ `UPPER`, otherwise 0 */ #define in_range(LOWER, VALUE, UPPER) \ (((LOWER) <= (VALUE)) && ((VALUE) <= (UPPER))) /** * Check whether a character is a valid callable name character, forward slash is accepted * * @param C:char The character * @return :int Zero if `C` is a valid callable name character or a forward slash, otherwise 0 */ #define is_name_char(C) \ (in_range('a', C, 'z') || in_range('A', C, 'Z') || strchr("0123456789_/", C)) /** * Add an error the to error list * * @param ERROR_IS_IN_FILE:int Whether the error is in the layout code * @param SEVERITY:identifier * in `MDS_KBDC_PARSE_ERROR_*` to indicate severity * @param ...:const char*, ... Error description format string and arguments */ #define NEW_ERROR(ERROR_IS_IN_FILE, SEVERITY, ...) \ if (errors_ptr + 1 >= errors_size) \ { \ errors_size = errors_size ? (errors_size << 1) : 2; \ fail_if (xxrealloc(old_errors, *errors, errors_size, mds_kbdc_parse_error_t*)); \ } \ fail_if (xcalloc(error, 1, mds_kbdc_parse_error_t)); \ (*errors)[errors_ptr + 0] = error; \ (*errors)[errors_ptr + 1] = NULL; \ errors_ptr++; \ error->line = line_i; \ error->severity = MDS_KBDC_PARSE_ERROR_##SEVERITY; \ error->error_is_in_file = ERROR_IS_IN_FILE; \ error->start = (size_t)(line - source_code.lines[line_i]); \ error->end = (size_t)(end - source_code.lines[line_i]); \ fail_if ((error->pathname = strdup(pathname)) == NULL); \ if (ERROR_IS_IN_FILE) \ fail_if ((error->code = strdup(source_code.real_lines[line_i])) == NULL); \ fail_if (xasprintf(error->description, __VA_ARGS__)) /** * Create a new node * * @param LOWERCASE:identifier The keyword, for the node type, in lower case * @param UPPERCASE:identifier The keyword, for the node type, in upper case */ #define NEW_NODE(LOWERCASE, UPPERCASE) \ mds_kbdc_tree_##LOWERCASE##_t* node; \ fail_if (xcalloc(node, 1, mds_kbdc_tree_##LOWERCASE##_t)); \ node->type = MDS_KBDC_TREE_TYPE_##UPPERCASE /** * Update the tip of the tree stack with the current node * and change the pointer at the tip to the pointer to the * current node's down pointer * * This is what should be done when a branch node has * been created and should be added to the result tree * * @param KEYWORD:const char* The keyword for the current node's type */ #define BRANCH(KEYWORD) \ *(tree_stack[stack_ptr]) = (mds_kbdc_tree_t*)node; \ tree_stack[stack_ptr + 1] = &(node->inner); \ keyword_stack[stack_ptr++] = KEYWORD /** * Update the tip of the tree stack with the current node * and change the pointer at the tip to the pointer to the * current node's next pointer * * This is what should be done when a leaf node has been * created and should be added to the result tree */ #define LEAF \ *(tree_stack[stack_ptr]) = (mds_kbdc_tree_t*)node; \ tree_stack[stack_ptr] = &(tree_stack[stack_ptr][0]->next) /** * Check that there are no tokens after a keyword * * @param KEYWORD:const char* The keyword, */ #define NO_PARAMETERS(KEYWORD) \ line += strlen(line); \ *end = prev_end_char, prev_end_char = '\0'; \ while (*line && (*line == ' ')) \ line++; \ do \ if (*line) \ { \ end = line + strlen(line); \ NEW_ERROR(1, ERROR, "extra token after ‘%s’", KEYWORD); \ } \ while (0) /** * Take next parameter, which should be a name of a callable, * and store it in the current node * * @param var:identifier The name of the member variable, for the current * node, where the parameter should be stored */ #define NAMES_1(var) \ line += strlen(line); \ *end = prev_end_char, prev_end_char = '\0'; \ while (*line && (*line == ' ')) \ line++; \ do \ if (*line == '\0') \ { \ line = original, end = line + strlen(line); \ NEW_ERROR(1, ERROR, "a name is expected"); \ } \ else \ { \ char* name_end = line; \ char* test; \ int stray_char = 0; \ while (*name_end && is_name_char(*name_end)) \ name_end++; \ if (*name_end && (*name_end != ' ')) \ { \ char* end_end = name_end + 1; \ while ((*end_end & 0xC0) == 0x80) \ end_end++; \ prev_end_char = *end_end, *end_end = '\0'; \ NEW_ERROR(1, ERROR, "stray ‘%s’ character", name_end); \ error->start = (size_t)(name_end - source_code.lines[line_i]); \ error->end = (size_t)(end_end - source_code.lines[line_i]); \ *end_end = prev_end_char; \ stray_char = 1; \ } \ test = name_end; \ while (*test && (*test == ' ')) \ test++; \ if (*test && !stray_char) \ { \ NEW_ERROR(1, ERROR, "too many parameters"); \ error->start = (size_t)(test - source_code.lines[line_i]); \ error->end = strlen(source_code.lines[line_i]); \ } \ end = name_end; \ prev_end_char = *end; \ *end = '\0'; \ fail_if ((node->var = strdup(line)) == NULL); \ } \ while (0) /** * Take next parameter, which should be a string or numeral, * and store it in the current node * * @param var:identifier The name of the member variable, for the current * node, where the parameter should be stored */ #define CHARS(var) \ do \ { \ if (too_few) \ break; \ line += strlen(line); \ *end = prev_end_char, prev_end_char = '\0'; \ while (*line && (*line == ' ')) \ line++; \ if (*line == '\0') \ { \ line = original, end = line + strlen(line); \ NEW_ERROR(1, ERROR, "too few parameters"); \ line = end, too_few = 1; \ } \ else \ { \ char* arg_end = line; \ char* call_end = arg_end; \ int escape = 0, quote = 0; \ while (*arg_end) \ { \ char c = *arg_end++; \ if (escape) escape = 0; \ else if (arg_end <= call_end) ; \ else if (c == '\\') \ { \ escape = 0; \ call_end = arg_end + get_end_of_call(arg_end, 0, strlen(arg_end)); \ } \ else if (quote) quote = (c != '"'); \ else if (c == ' ') break; \ else quote = (c == '"'); \ } \ prev_end_char = *arg_end, *arg_end = '\0'; \ fail_if ((node->var = strdup(line)) == NULL); \ end = line = arg_end; \ } \ } \ while (0) /** * Test that there are no more parameters */ #define END \ while (*line && (*line == ' ')) \ line++; \ do \ if (*line) \ { \ NEW_ERROR(1, ERROR, "too many parameters"); \ error->end = strlen(source_code.lines[line_i]); \ } \ while (0) /** * Test that the next parameter is in quotes */ #define QUOTES \ do \ { \ char* line_ = line; \ line += strlen(line); \ *end = prev_end_char; \ while (*line && (*line == ' ')) \ line++; \ if (*line && (*line != '"')) \ { \ char* arg_end = line; \ while (*arg_end && (*arg_end != ' ')) \ arg_end++; \ NEW_ERROR(1, ERROR, "parameter must be in quotes"); \ error->end = (size_t)(arg_end - source_code.lines[line_i]); \ } \ *end = '\0'; \ line = line_; \ } \ while (0) /** * Check that there is exactly one parameter, that it is in * quotes, and add it to the current node * * @param var:identifier The name of the member variable, for the current * node, where the parameter should be stored */ #define QUOTES_1(var) \ QUOTES; \ CHARS(var); \ END /** * Check that the next word is a specific keyword * * @parma KEYWORD:const char* The keyword */ #define TEST_FOR_KEYWORD(KEYWORD) \ do \ { \ if (too_few) \ break; \ line += strlen(line); \ *end = prev_end_char, prev_end_char = '\0'; \ while (*line && (*line == ' ')) \ line++; \ if (*line == '\0') \ { \ line = original, end = line + strlen(line); \ NEW_ERROR(1, ERROR, "too few parameters"); \ line = end, too_few = 1; \ } \ else \ { \ int ok = (strstr(line, KEYWORD) == line); \ line += strlen(KEYWORD); \ ok = ok && ((*line == '\0') || (*line == ' ')); \ if (ok) \ { \ end = line; \ prev_end_char = *end, *end = '\0'; \ break; \ } \ line -= strlen(KEYWORD); \ end = line; \ while (*end && (*end != ' ')) \ end++; \ prev_end_char = *end, *end = '\0'; \ NEW_ERROR(1, ERROR, "expecting keyword ‘%s’", KEYWORD); \ } \ } \ while (0) /** * Take next parameter, which should be a key combination or strings, * and store it in the current node * * @param var:identifier The name of the member variable, for the current * node, where the parameter should be stored */ #define KEYS(var) \ do \ { \ if (too_few) \ break; \ line += strlen(line); \ *end = prev_end_char, prev_end_char = '\0'; \ while (*line && (*line == ' ')) \ line++; \ if (*line == '\0') \ { \ line = original, end = line + strlen(line); \ NEW_ERROR(1, ERROR, "too few parameters"); \ line = end, too_few = 1; \ } \ else \ { \ char* arg_end = line; \ char* call_end = arg_end; \ int escape = 0, quote = 0, triangle = (*arg_end == '<'); \ while (*arg_end) \ { \ char c = *arg_end++ ; \ if (escape) escape = 0; \ else if (arg_end <= call_end) ; \ else if (c == '\\') \ { \ escape = 0; \ call_end = arg_end + get_end_of_call(arg_end, 0, strlen(arg_end)); \ } \ else if (quote) quote = (c != '"'); \ else if (c == '\"') quote = 1; \ else if (c == '>') triangle = 0; \ else if ((c == ' ') && !triangle) break; \ } \ prev_end_char = *arg_end, *arg_end = '\0'; \ if (*line == '<') \ { \ mds_kbdc_tree_keys_t* subnode; \ fail_if (xcalloc(subnode, 1, mds_kbdc_tree_keys_t)); \ subnode->type = MDS_KBDC_TREE_TYPE_KEYS; \ node->var = (mds_kbdc_tree_t*)subnode; \ fail_if ((subnode->keys = strdup(line)) == NULL); \ } \ else \ { \ mds_kbdc_tree_string_t* subnode; \ fail_if (xcalloc(subnode, 1, mds_kbdc_tree_string_t)); \ subnode->type = MDS_KBDC_TREE_TYPE_STRING; \ node->var = (mds_kbdc_tree_t*)subnode; \ fail_if ((subnode->string = strdup(line)) == NULL); \ } \ end = line = arg_end; \ } \ } \ while (0) /** * Parse a file into a syntex tree * * @param filename The filename of the file to parse * @param result Output parameter for the root of the tree, `NULL` if -1 is returned * @param errors `NULL`-terminated list of found error, `NULL` if no errors were found or if -1 is returned * @return -1 if an error occursed that cannot be stored in `*errors`, zero otherwise */ int parse_to_tree(const char* restrict filename, mds_kbdc_tree_t** restrict result, mds_kbdc_parse_error_t*** restrict errors) { mds_kbdc_parse_error_t* error; mds_kbdc_parse_error_t** old_errors = NULL; char* pathname; source_code_t source_code; size_t errors_size = 0; size_t errors_ptr = 0; size_t line_i, line_n; const char** keyword_stack = NULL; mds_kbdc_tree_t*** tree_stack = NULL; size_t stack_ptr = 0; int saved_errno, in_array = 0; *result = NULL; *errors = NULL; source_code_initialise(&source_code); /* Get a non-relative pathname for the file, relative filenames * can be misleading as the program can have changed working * directroy to be able to resolve filenames. */ pathname = realpath(filename, NULL); fail_if (pathname == NULL); /* Check that the file exists and can be read. */ if (access(pathname, R_OK) < 0) { saved_errno = errno; fail_if (xmalloc(*errors, 2, mds_kbdc_parse_error_t*)); fail_if (xmalloc(**errors, 1, mds_kbdc_parse_error_t)); (*errors)[1] = NULL; (**errors)->severity = MDS_KBDC_PARSE_ERROR_ERROR; (**errors)->error_is_in_file = 0; (**errors)->pathname = pathname, pathname = NULL; (**errors)->line = 0; (**errors)->start = 0; (**errors)->end = 0; (**errors)->code = NULL; (**errors)->description = strdup(strerror(saved_errno)); fail_if ((**errors)->description == NULL); return 0; } /* Read the file and simplify it a bit. */ fail_if (read_source_lines(pathname, &source_code) < 0); /* TODO '\t':s should be expanded into ' ':s. */ /* Allocate stacks needed to parse the tree. */ fail_if (xmalloc(keyword_stack, source_code.line_count, const char*)); fail_if (xmalloc(tree_stack, source_code.line_count + 1, mds_kbdc_tree_t**)); /* Create a node-slot for the tree root. */ *tree_stack = result; for (line_i = 0, line_n = source_code.line_count; line_i < line_n; line_i++) { char* line = source_code.lines[line_i]; char* end; char prev_end_char; char* original; int too_few = 0; while (*line && (*line == ' ')) line++; end = strchrnul(line, ' '); if (end == line) continue; prev_end_char = *end; *end = '\0'; original = line; redo: if (in_array) { for (;;) { while (*line && (*line == ' ')) line++; if (*line == '\0') break; if (*line == '}') { line++; end = line + strlen(line); END; line = end, prev_end_char = '\0'; stack_ptr--; in_array = 0; break; } end = strchrnul(line, ' '); prev_end_char = *end; *end = '\0'; { #define node subnode NEW_NODE(string, STRING); fail_if ((node->string = strdup(line)) == NULL); LEAF; #undef node } *end = prev_end_char; line = end; } if (*line == '\0') continue; } else if (!strcmp(line, "information")) { NEW_NODE(information, INFORMATION); NO_PARAMETERS("information"); BRANCH("information"); } else if (!strcmp(line, "assumption")) { NEW_NODE(assumption, ASSUMPTION); NO_PARAMETERS("assumption"); BRANCH("assumption"); } else if (!strcmp(line, "return")) { NEW_NODE(return, RETURN); NO_PARAMETERS("return"); LEAF; } else if (!strcmp(line, "continue")) { NEW_NODE(continue, CONTINUE); NO_PARAMETERS("continue"); LEAF; } else if (!strcmp(line, "break")) { NEW_NODE(break, BREAK); NO_PARAMETERS("break"); LEAF; } else if (!strcmp(line, "language")) { NEW_NODE(information_language, INFORMATION_LANGUAGE); QUOTES_1(data); LEAF; } else if (!strcmp(line, "country")) { NEW_NODE(information_country, INFORMATION_COUNTRY); QUOTES_1(data); LEAF; } else if (!strcmp(line, "variant")) { NEW_NODE(information_variant, INFORMATION_VARIANT); QUOTES_1(data); LEAF; } else if (!strcmp(line, "include")) { NEW_NODE(include, INCLUDE); QUOTES_1(filename); LEAF; } else if (!strcmp(line, "function")) { NEW_NODE(function, FUNCTION); NAMES_1(name); BRANCH("function"); } else if (!strcmp(line, "macro")) { NEW_NODE(macro, MACRO); NAMES_1(name); BRANCH("macro"); } else if (!strcmp(line, "if")) { NEW_NODE(if, IF); CHARS(condition); END; BRANCH("if"); } else if (!strcmp(line, "else")) { if (stack_ptr == 0) { NEW_ERROR(1, ERROR, "runaway ‘else’ statement"); goto next; } line += strlen(line); *end = prev_end_char, prev_end_char = '\0'; end = line + strlen(line); stack_ptr--; while (*line && (*line == ' ')) line++; if (strcmp(keyword_stack[stack_ptr], "if")) { line = original, end = line + strlen(line); NEW_ERROR(1, ERROR, "runaway ‘else’ statement"); } else if (*line == '\0') { /* else */ mds_kbdc_tree_if_t* node = &(tree_stack[stack_ptr][0]->if_); tree_stack[stack_ptr + 1] = &(node->otherwise); keyword_stack[stack_ptr++] = "if"; } else if ((strstr(line, "if") == line) && ((line[3] == ' ') || (line[3] == '\0'))) { /* else if */ NEW_NODE(if, IF); tree_stack[stack_ptr][0]->if_.otherwise = (mds_kbdc_tree_t*)node; line += 2; CHARS(condition); END; BRANCH("if"); } else { NEW_ERROR(1, ERROR, "expecting nothing or ‘if’"); } } else if (!strcmp(line, "for")) { NEW_NODE(for, FOR); CHARS(first); TEST_FOR_KEYWORD("to"); CHARS(last); TEST_FOR_KEYWORD("as"); CHARS(variable); END; BRANCH("for"); } else if (!strcmp(line, "let")) { NEW_NODE(let, LET); CHARS(variable); TEST_FOR_KEYWORD(":"); *end = prev_end_char; while (*line && (*line == ' ')) line++; if (*line == '\0') { line = original, end = line + strlen(line), prev_end_char = '\0'; NEW_ERROR(1, ERROR, "too few parameters"); LEAF; } else if (*line != '{') { #define node subnode NEW_NODE(string, STRING); CHARS(string); #undef node node->value = (mds_kbdc_tree_t*)subnode; END; LEAF; } else { #define node subnode NEW_NODE(array, ARRAY); #define inner elements BRANCH("}"); #undef inner #undef node in_array = 1; line++; goto redo; } } else if (!strcmp(line, "have")) { NEW_NODE(assumption_have, ASSUMPTION_HAVE); KEYS(data); END; LEAF; } else if (!strcmp(line, "have_chars")) { NEW_NODE(assumption_have_chars, ASSUMPTION_HAVE_CHARS); QUOTES_1(chars); LEAF; } else if (!strcmp(line, "have_range")) { NEW_NODE(assumption_have_range, ASSUMPTION_HAVE_RANGE); CHARS(first); CHARS(last); END; LEAF; } else if (!strcmp(line, "end")) { if (stack_ptr == 0) { NEW_ERROR(1, ERROR, "runaway ‘end’ statement"); goto next; } line += strlen(line); *end = prev_end_char, prev_end_char = '\0'; stack_ptr--; while (*line && (*line == ' ')) line++; if (*line == '\0') { line = original, end = line + strlen(line); NEW_ERROR(1, ERROR, "expecting a keyword after ‘end’"); } else if (strcmp(line, keyword_stack[stack_ptr])) { NEW_ERROR(1, ERROR, "expected ‘%s’ but got ‘%s’", keyword_stack[stack_ptr], line); } tree_stack[stack_ptr] = &(tree_stack[stack_ptr][0]->next); } else if (strchr("\"<([", *line) || strchr(line, '(')) ; /* TODO */ else { NEW_ERROR(1, ERROR, "invalid syntax ‘%s’", line); } next: *end = prev_end_char; } /* Check that all scopes have been popped. */ if (stack_ptr) { char* line = NULL; char* end = NULL; NEW_ERROR(0, ERROR, "premature end of file"); while (stack_ptr--) /* TODO from where? */ if (!strcmp(keyword_stack[stack_ptr], "}")) { NEW_ERROR(0, NOTE, "missing ‘%s’", keyword_stack[stack_ptr]); } else { NEW_ERROR(0, NOTE, "missing ‘end %s’", keyword_stack[stack_ptr]); } } free(pathname); free(keyword_stack); free(tree_stack); source_code_destroy(&source_code); return 0; pfail: saved_errno = errno; free(pathname); free(keyword_stack); free(tree_stack); source_code_destroy(&source_code); mds_kbdc_parse_error_free_all(old_errors); mds_kbdc_parse_error_free_all(*errors), *errors = NULL; mds_kbdc_tree_free(*result), *result = NULL; return errno = saved_errno, -1; } #undef KEYS #undef TEST_FOR_KEYWORD #undef QUOTES_1 #undef QUOTES #undef END #undef CHARS #undef NAMES_1 #undef NO_PARAMETERS #undef LEAF #undef BRANCH #undef NEW_NODE #undef NEW_ERROR #undef is_name_char #undef in_range #undef xasprintf