diff --git a/README.md b/README.md index 8749d23..13c95ec 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,92 @@ # Peppermint C89 port -This directory is a clean native C89 implementation of the language behavior -present in the original TypeScript interpreter. +This repository root is a clean native C89 implementation of the behavior that +is implemented in the original TypeScript Peppermint tree, with no TypeScript, +Node.js, Beef, or third-party runtime dependency. Original project: https://github.com/lainq/peppermint -Implemented subset: - -- `let name := expression` bindings -- integer and floating-point literals -- quoted strings with basic escapes -- `+`, `-`, `*`, `/`, and `%` -- string concatenation with `+` -- `#` comments and line-oriented programs -- `@argv` and `@argv:N` -- `@env:NAME` and `@exit` - -The original repository also contains a separate Brainfuck compiler/runtime; -that is not silently folded into this first language-core port. - ## Build -Requires only a C compiler with C89 support: - ```sh make ./peppermint smoke.ppm hello ``` -The build uses `-std=c89 -pedantic` and has no TypeScript, Node.js, Beef, or -third-party runtime dependency. +The build uses `-std=c89 -pedantic -Wall -Wextra -Wconversion -Wshadow`. +The language core is ISO C89; the project-oriented CLI commands use the usual +POSIX file APIs (`mkdir`, `stat`, `getcwd`) so `init`/`run`/`list` can create +and discover files as the TypeScript CLI does. + +## CLI + +Supported commands: + +```sh +peppermint run [--file=FILE] [--args ARGS...] +peppermint init --name=NAME [--author=AUTHOR] [--license=LICENSE] +peppermint list +peppermint version +peppermint bf --file=FILE [--compile=true] +peppermint FILE.ppm [ARGS...] +``` + +Notes: + +- `peppermint FILE.ppm [ARGS...]` preserves the first C89 port's direct-file + behavior. +- `run --file=...` executes a Peppermint source file. +- `run` without `--file` reads `.peppermint` in the current directory and uses + its `main` string field. +- `run --args ...` passes the remaining values to `@argv` / `@argv:N`. +- `init` creates `README.md`, `app.ppm`, `tests/test.ppm`, and `.peppermint`. +- `list` reads a simple project store. Set `PEPPERMINT_STORE=/path/to/store` to + override the default `$HOME/.peppermint/store.json` location. +- `version` prints `Peppermint Version 0.0.1[platform]`. +- `bf` is a separate Brainfuck runner. With `--compile=true`, it also writes the + original Long-language-style output to `dist/test.long`. + +## Language behavior + +Implemented: + +- `let name := expression` constant bindings. +- `let name = expression` mutable bindings. +- `name = expression` assignment to existing mutable bindings. +- integer and floating-point literals, including unary minus. +- quoted strings with `\n`, `\r`, `\t`, and escaped-character handling. +- `+`, `-`, `*`, `/`, and `%` with normal arithmetic precedence. +- string concatenation with `+` when both operands are strings. +- relational operators tokenized by the TypeScript lexer: `==`, `!=`, `>=`, + `<=`, `>`, and `<`. They produce `1` for true and `0` for false. +- parenthesized expressions. +- `#` comments and line-oriented programs. +- `@argv`, `@argv:N`, `@env:NAME`, and `@exit`. +- robust file/line diagnostics for syntax and runtime errors. + +## Upstream gaps and compatibility notes + +The TypeScript parser in the upstream tree is incomplete: it tokenizes +assignment (`=`), constants (`:=`), and relational operators, but only executes +arithmetic builtins in practice and does not build a real AST. This C89 port +therefore implements the practical behavior those tokens imply instead of +claiming byte-for-byte parser parity. Unsupported higher-level semantics such +as functions, blocks, imports, conditionals, loops, objects, and a module system +are not present in the TypeScript implementation and are intentionally not +fabricated here. + +The TypeScript project store was a JSON file under the compiled CLI directory. +A single native executable has no such package directory, so the C89 CLI uses a +portable tab-separated project store at `$HOME/.peppermint/store.json` (or +`PEPPERMINT_STORE`) while keeping the same user-visible `init`/`list` workflow. + +## Tests + +```sh +./test.sh +``` + +The test script rebuilds with strict C89 flags, then checks legacy direct-file +execution, command dispatch, expressions, relational operators, mutable vs. +constant assignment, builtins, project init/config/list/run behavior, version +output, and the Brainfuck runner/compiler command. diff --git a/peppermint.c b/peppermint.c index 06ba4b9..e786e28 100644 --- a/peppermint.c +++ b/peppermint.c @@ -1,18 +1,28 @@ /* - * Peppermint C89 interpreter. + * Peppermint C89 interpreter and CLI. * - * This implements the language behavior present in the original TypeScript - * front end: let bindings, integer/real literals, strings, arithmetic, - * comments, and @argv/@env/@exit builtins. + * A dependency-free native port of the implemented TypeScript Peppermint + * pieces: the small expression language, project commands, version command, + * and the separate Brainfuck runner/compiler command. */ #include #include #include #include +#include +#include +#include +#include +#include +#define PM_VERSION "0.0.1" #define PM_NAME_MAX 128 #define PM_TEXT_MAX 4096 #define PM_MAX_VARS 256 +#define PM_PATH_MAX 1024 +#define PM_BF_CELLS 30000 +#define PM_BF_MAX_PROGRAM 1048576 +#define PM_BF_MAX_STACK 65536 typedef enum { PM_NONE, @@ -31,6 +41,7 @@ typedef struct { typedef struct { char name[PM_NAME_MAX]; PMValue value; + int constant; } PMVariable; typedef struct { @@ -44,6 +55,20 @@ typedef struct { int stopped; } PMContext; +static void pm_free_value(PMValue *value); +static int pm_make_none(PMValue *value); +static int pm_copy_value(PMValue *destination, const PMValue *source); +static int pm_run_source(const char *filename, const char *source, int argc, char **argv); +static char *pm_read_file(const char *filename); +static int pm_write_file(const char *filename, const char *data); +static int pm_file_exists(const char *path); +static int pm_is_dir(const char *path); +static int pm_make_dir(const char *path); +static char *pm_copy_text(const char *start, size_t length); +static char *pm_join_path(const char *left, const char *right); +static char *pm_get_cwd(void); +static char *pm_store_path(void); + static void pm_free_value(PMValue *value) { if (value->type == PM_STRING && value->text != NULL) { @@ -67,54 +92,19 @@ static char *pm_copy_text(const char *start, size_t length) return result; } +static char *pm_strdup_c89(const char *text) +{ + return pm_copy_text(text, strlen(text)); +} + static void pm_error(PMContext *context, const char *message) { fprintf(stderr, "%s:%ld: %s\n", context->filename, context->line, message); } -static void pm_skip_space(PMContext *context, const char **cursor) +static void pm_cli_error(const char *message) { - const char *p; - (void)context; - p = *cursor; - for (;;) { - while (*p == ' ' || *p == '\t' || *p == '\r') { - p++; - } - if (*p == '#') { - while (*p != '\0' && *p != '\n') { - p++; - } - } else { - break; - } - } - *cursor = p; -} - -static int pm_is_identifier_start(int c) -{ - return isalpha((unsigned char)c) || c == '_'; -} - -static int pm_is_identifier_char(int c) -{ - return isalnum((unsigned char)c) || c == '_'; -} - -static int pm_match(PMContext *context, const char **cursor, const char *word) -{ - const char *p; - size_t length; - (void)context; - p = *cursor; - length = strlen(word); - if (strncmp(p, word, length) == 0 && - !pm_is_identifier_char((unsigned char)p[length])) { - *cursor = p + length; - return 1; - } - return 0; + fprintf(stderr, "peppermint: %s\n", message); } static int pm_make_none(PMValue *value) @@ -160,10 +150,7 @@ static int pm_copy_value(PMValue *destination, const PMValue *source) destination->integer = source->integer; destination->real = source->real; destination->text = NULL; - if (source->type == PM_STRING) { - if (source->text == NULL) { - return 1; - } + if (source->type == PM_STRING && source->text != NULL) { destination->text = pm_copy_text(source->text, strlen(source->text)); if (destination->text == NULL) { destination->type = PM_NONE; @@ -173,6 +160,50 @@ static int pm_copy_value(PMValue *destination, const PMValue *source) return 1; } +static void pm_skip_space(PMContext *context, const char **cursor) +{ + const char *p; + (void)context; + p = *cursor; + for (;;) { + while (*p == ' ' || *p == '\t' || *p == '\r') { + p++; + } + if (*p == '#') { + while (*p != '\0' && *p != '\n') { + p++; + } + } else { + break; + } + } + *cursor = p; +} + +static int pm_is_identifier_start(int c) +{ + return isalpha((unsigned char)c) || c == '_'; +} + +static int pm_is_identifier_char(int c) +{ + return isalnum((unsigned char)c) || c == '_'; +} + +static int pm_match_keyword(const char **cursor, const char *word) +{ + const char *p; + size_t length; + p = *cursor; + length = strlen(word); + if (strncmp(p, word, length) == 0 && + !pm_is_identifier_char((unsigned char)p[length])) { + *cursor = p + length; + return 1; + } + return 0; +} + static PMVariable *pm_find_variable(PMContext *context, const char *name) { int i; @@ -185,11 +216,15 @@ static PMVariable *pm_find_variable(PMContext *context, const char *name) } static int pm_set_variable(PMContext *context, const char *name, - const PMValue *value) + const PMValue *value, int constant, int declare) { PMVariable *variable; variable = pm_find_variable(context, name); if (variable == NULL) { + if (!declare) { + pm_error(context, "assignment target is not declared"); + return 0; + } if (context->var_count >= PM_MAX_VARS || strlen(name) >= PM_NAME_MAX) { pm_error(context, "too many or oversized variables"); return 0; @@ -197,7 +232,16 @@ static int pm_set_variable(PMContext *context, const char *name, variable = &context->vars[context->var_count]; strcpy(variable->name, name); pm_make_none(&variable->value); + variable->constant = constant; context->var_count++; + } else { + if (variable->constant) { + pm_error(context, "cannot assign to constant"); + return 0; + } + if (declare) { + variable->constant = constant; + } } return pm_copy_value(&variable->value, value); } @@ -209,14 +253,12 @@ static int pm_parse_string(PMContext *context, const char **cursor, PMValue *value) { const char *p; - const char *start; char buffer[PM_TEXT_MAX]; size_t length; p = *cursor; p++; - start = p; length = 0U; - while (*p != '\0' && *p != '"') { + while (*p != '\0' && *p != '"' && *p != '\n') { if (*p == '\\' && p[1] != '\0') { p++; if (*p == 'n') { @@ -234,7 +276,6 @@ static int pm_parse_string(PMContext *context, const char **cursor, p++; } } - (void)start; if (*p != '"') { pm_error(context, "string not closed"); return 0; @@ -258,19 +299,26 @@ static int pm_parse_primary(PMContext *context, const char **cursor, long integer; double real; PMVariable *variable; + int negative; p = *cursor; pm_skip_space(context, &p); - if (*p == '"') { + negative = 0; + if (*p == '-') { + p++; + pm_skip_space(context, &p); + negative = 1; + } + if (*p == '"' && !negative) { if (!pm_parse_string(context, &p, value)) return 0; *cursor = p; return 1; } - if (*p == '(') { + if (*p == '(' && !negative) { p++; if (!pm_parse_expression(context, &p, value)) return 0; pm_skip_space(context, &p); if (*p != ')') { - pm_error(context, "expected ')' "); + pm_error(context, "expected ')'"); pm_free_value(value); return 0; } @@ -283,16 +331,18 @@ static int pm_parse_primary(PMContext *context, const char **cursor, pm_error(context, "invalid number"); return 0; } + if (negative) real = -real; if (strchr(p, '.') != NULL && strchr(p, '.') < end) { pm_make_real(value, real); } else { integer = strtol(p, &end, 10); + if (negative) integer = -integer; pm_make_integer(value, integer); } *cursor = end; return 1; } - if (pm_is_identifier_start((unsigned char)*p)) { + if (pm_is_identifier_start((unsigned char)*p) && !negative) { length = 0U; while (pm_is_identifier_char((unsigned char)p[length])) { if (length < PM_NAME_MAX - 1U) name[length] = p[length]; @@ -303,6 +353,11 @@ static int pm_parse_primary(PMContext *context, const char **cursor, return 0; } name[length] = '\0'; + if (strcmp(name, "None") == 0) { + pm_make_none(value); + *cursor = p + length; + return 1; + } variable = pm_find_variable(context, name); if (variable == NULL) { pm_error(context, "unknown identifier"); @@ -332,8 +387,8 @@ static int pm_numeric(const PMValue *value, double *number) return 0; } -static int pm_apply_operator(PMContext *context, PMValue *left, char operator, - PMValue *right) +static int pm_apply_arithmetic(PMContext *context, PMValue *left, char operator, + PMValue *right) { double a; double b; @@ -342,6 +397,8 @@ static int pm_apply_operator(PMContext *context, PMValue *left, char operator, char *text; size_t left_length; size_t right_length; + long ia; + long ib; if (operator == '+' && left->type == PM_STRING && right->type == PM_STRING) { left_length = strlen(left->text); right_length = strlen(right->text); @@ -371,9 +428,7 @@ static int pm_apply_operator(PMContext *context, PMValue *left, char operator, } result = a / b; } else if (operator == '%') { - long ia; - long ib; - if (left->type != PM_INT || right->type != PM_INT || b == 0.0) { + if (left->type != PM_INT || right->type != PM_INT || right->integer == 0L) { pm_error(context, "modulo requires nonzero integers"); return 0; } @@ -396,8 +451,7 @@ static int pm_apply_operator(PMContext *context, PMValue *left, char operator, return 1; } -static int pm_parse_expression(PMContext *context, const char **cursor, - PMValue *value) +static int pm_parse_term(PMContext *context, const char **cursor, PMValue *value) { const char *p; PMValue right; @@ -408,15 +462,120 @@ static int pm_parse_expression(PMContext *context, const char **cursor, for (;;) { pm_skip_space(context, &p); operator = *p; - if (operator != '+' && operator != '-' && operator != '*' && - operator != '/' && operator != '%') break; + if (operator != '*' && operator != '/' && operator != '%') break; p++; pm_make_none(&right); if (!pm_parse_primary(context, &p, &right)) { pm_free_value(value); return 0; } - if (!pm_apply_operator(context, value, operator, &right)) { + if (!pm_apply_arithmetic(context, value, operator, &right)) { + pm_free_value(value); + pm_free_value(&right); + return 0; + } + pm_free_value(&right); + } + *cursor = p; + return 1; +} + +static int pm_parse_sum(PMContext *context, const char **cursor, PMValue *value) +{ + const char *p; + PMValue right; + char operator; + p = *cursor; + pm_make_none(value); + if (!pm_parse_term(context, &p, value)) return 0; + for (;;) { + pm_skip_space(context, &p); + operator = *p; + if (operator != '+' && operator != '-') break; + p++; + pm_make_none(&right); + if (!pm_parse_term(context, &p, &right)) { + pm_free_value(value); + return 0; + } + if (!pm_apply_arithmetic(context, value, operator, &right)) { + pm_free_value(value); + pm_free_value(&right); + return 0; + } + pm_free_value(&right); + } + *cursor = p; + return 1; +} + +static int pm_compare_values(PMContext *context, PMValue *left, const char *op, + PMValue *right) +{ + double a; + double b; + int result; + result = 0; + if (pm_numeric(left, &a) && pm_numeric(right, &b)) { + if (strcmp(op, "==") == 0) result = (a == b); + else if (strcmp(op, "!=") == 0) result = (a != b); + else if (strcmp(op, ">=") == 0) result = (a >= b); + else if (strcmp(op, "<=") == 0) result = (a <= b); + else if (strcmp(op, ">") == 0) result = (a > b); + else if (strcmp(op, "<") == 0) result = (a < b); + } else if (left->type == PM_STRING && right->type == PM_STRING) { + int cmp; + cmp = strcmp(left->text, right->text); + if (strcmp(op, "==") == 0) result = (cmp == 0); + else if (strcmp(op, "!=") == 0) result = (cmp != 0); + else if (strcmp(op, ">=") == 0) result = (cmp >= 0); + else if (strcmp(op, "<=") == 0) result = (cmp <= 0); + else if (strcmp(op, ">") == 0) result = (cmp > 0); + else if (strcmp(op, "<") == 0) result = (cmp < 0); + } else if ((left->type == PM_NONE || right->type == PM_NONE) && + (strcmp(op, "==") == 0 || strcmp(op, "!=") == 0)) { + result = (left->type == right->type); + if (strcmp(op, "!=") == 0) result = !result; + } else { + pm_error(context, "comparison requires matching numeric or string values"); + return 0; + } + pm_free_value(left); + pm_make_integer(left, (long)result); + return 1; +} + +static int pm_parse_expression(PMContext *context, const char **cursor, + PMValue *value) +{ + const char *p; + char op[3]; + PMValue right; + p = *cursor; + pm_make_none(value); + if (!pm_parse_sum(context, &p, value)) return 0; + for (;;) { + pm_skip_space(context, &p); + op[0] = '\0'; + op[1] = '\0'; + op[2] = '\0'; + if ((p[0] == '=' && p[1] == '=') || (p[0] == '!' && p[1] == '=') || + (p[0] == '>' && p[1] == '=') || (p[0] == '<' && p[1] == '=')) { + op[0] = p[0]; + op[1] = p[1]; + p += 2; + } else if (p[0] == '>' || p[0] == '<') { + op[0] = p[0]; + p++; + } else { + break; + } + pm_make_none(&right); + if (!pm_parse_sum(context, &p, &right)) { + pm_free_value(value); + return 0; + } + if (!pm_compare_values(context, value, op, &right)) { pm_free_value(value); pm_free_value(&right); return 0; @@ -429,13 +588,13 @@ static int pm_parse_expression(PMContext *context, const char **cursor, static void pm_print_value(const PMValue *value) { - if (value->type == PM_STRING) printf("%s\n", value->text); + if (value->type == PM_STRING) printf("%s\n", value->text == NULL ? "" : value->text); else if (value->type == PM_INT) printf("%ld\n", value->integer); else if (value->type == PM_REAL) printf("%g\n", value->real); else printf("None\n"); } -static void pm_builtin(PMContext *context, const char **cursor) +static int pm_builtin(PMContext *context, const char **cursor) { const char *p; const char *start; @@ -443,20 +602,22 @@ static void pm_builtin(PMContext *context, const char **cursor) char *end; long index; size_t length; + const char *result; + int i; p = *cursor + 1; start = p; while (pm_is_identifier_char((unsigned char)*p)) p++; length = (size_t)(p - start); - if (length >= PM_NAME_MAX) { - pm_error(context, "builtin name too long"); - return; + if (length == 0U || length >= PM_NAME_MAX) { + pm_error(context, "invalid builtin name"); + return 0; } memcpy(name, start, length); name[length] = '\0'; if (strcmp(name, "exit") == 0) { context->stopped = 1; *cursor = p; - return; + return 1; } if (strcmp(name, "argv") == 0) { if (*p == ':') { @@ -468,7 +629,6 @@ static void pm_builtin(PMContext *context, const char **cursor) } p = end; } else { - int i; for (i = 0; i < context->argc; i++) { if (i != 0) putchar(' '); fputs(context->argv[i], stdout); @@ -476,9 +636,7 @@ static void pm_builtin(PMContext *context, const char **cursor) putchar('\n'); } } else if (strcmp(name, "env") == 0) { - const char *key; - const char *result; - key = NULL; + result = NULL; if (*p == ':') { p++; start = p; @@ -487,22 +645,68 @@ static void pm_builtin(PMContext *context, const char **cursor) if (length >= PM_NAME_MAX) length = PM_NAME_MAX - 1U; memcpy(name, start, length); name[length] = '\0'; - key = name; + result = getenv(name); } - result = key == NULL ? NULL : getenv(key); printf("%s\n", result == NULL || result[0] == '\0' ? "None" : result); } else { pm_error(context, "unknown builtin"); + return 0; } *cursor = p; + return 1; +} + +static int pm_read_identifier(PMContext *context, const char **cursor, char *name) +{ + const char *p; + size_t length; + p = *cursor; + pm_skip_space(context, &p); + if (!pm_is_identifier_start((unsigned char)*p)) { + pm_error(context, "expected variable name"); + return 0; + } + length = 0U; + while (pm_is_identifier_char((unsigned char)p[length])) { + if (length < PM_NAME_MAX - 1U) name[length] = p[length]; + length++; + } + if (length >= PM_NAME_MAX) { + pm_error(context, "identifier too long"); + return 0; + } + name[length] = '\0'; + *cursor = p + length; + return 1; +} + +static int pm_statement_end(PMContext *context, const char **cursor) +{ + const char *p; + p = *cursor; + while (*p == ' ' || *p == '\t' || *p == '\r') p++; + if (*p == '#') { + while (*p != '\0' && *p != '\n') p++; + } + if (*p == '\n') { + context->line++; + p++; + } else if (*p != '\0') { + pm_error(context, "unexpected characters"); + return 0; + } + *cursor = p; + return 1; } static int pm_run(PMContext *context) { const char *p; + const char *mark; PMValue value; char name[PM_NAME_MAX]; - size_t length; + int declare; + int constant; p = context->source; context->line = 1L; while (*p != '\0' && !context->stopped) { @@ -514,51 +718,85 @@ static int pm_run(PMContext *context) } if (*p == '\0') break; if (*p == '@') { - pm_builtin(context, &p); - } else if (pm_match(context, &p, "let")) { - pm_skip_space(context, &p); - length = 0U; - while (pm_is_identifier_char((unsigned char)p[length])) { - if (length < PM_NAME_MAX - 1U) name[length] = p[length]; - length++; - } - if (length == 0U || length >= PM_NAME_MAX) { - pm_error(context, "expected variable name"); - return 0; - } - name[length] = '\0'; - p += length; - pm_skip_space(context, &p); - if (p[0] != ':' || p[1] != '=') { - pm_error(context, "expected ':='"); - return 0; - } - p += 2; - pm_make_none(&value); - if (!pm_parse_expression(context, &p, &value)) return 0; - if (!pm_set_variable(context, name, &value)) { - pm_free_value(&value); - return 0; - } - pm_free_value(&value); + if (!pm_builtin(context, &p)) return 0; } else { - pm_make_none(&value); - if (!pm_parse_expression(context, &p, &value)) return 0; - pm_print_value(&value); - pm_free_value(&value); - } - while (*p == ' ' || *p == '\t' || *p == '\r') p++; - if (*p == '\n') { - context->line++; - p++; - } else if (*p != '\0' && *p != '#') { - pm_error(context, "unexpected characters"); - return 0; + declare = 0; + constant = 0; + mark = p; + if (pm_match_keyword(&p, "let")) { + declare = 1; + if (!pm_read_identifier(context, &p, name)) return 0; + pm_skip_space(context, &p); + if (p[0] == ':' && p[1] == '=') { + constant = 1; + p += 2; + } else if (p[0] == '=') { + constant = 0; + p++; + } else { + pm_error(context, "expected ':=' or '='"); + return 0; + } + pm_make_none(&value); + if (!pm_parse_expression(context, &p, &value)) return 0; + if (!pm_set_variable(context, name, &value, constant, declare)) { + pm_free_value(&value); + return 0; + } + pm_free_value(&value); + } else if (pm_is_identifier_start((unsigned char)*p)) { + if (!pm_read_identifier(context, &p, name)) return 0; + pm_skip_space(context, &p); + if (*p == '=' && p[1] != '=') { + p++; + pm_make_none(&value); + if (!pm_parse_expression(context, &p, &value)) return 0; + if (!pm_set_variable(context, name, &value, 0, 0)) { + pm_free_value(&value); + return 0; + } + pm_free_value(&value); + } else { + p = mark; + pm_make_none(&value); + if (!pm_parse_expression(context, &p, &value)) return 0; + pm_print_value(&value); + pm_free_value(&value); + } + } else { + pm_make_none(&value); + if (!pm_parse_expression(context, &p, &value)) return 0; + pm_print_value(&value); + pm_free_value(&value); + } } + if (!pm_statement_end(context, &p)) return 0; } return 1; } +static int pm_run_source(const char *filename, const char *source, int argc, char **argv) +{ + PMContext context; + int i; + int result; + context.source = source; + context.filename = filename; + context.argc = argc; + context.argv = argv; + context.var_count = 0; + context.stopped = 0; + context.line = 1L; + for (i = 0; i < PM_MAX_VARS; i++) { + context.vars[i].name[0] = '\0'; + context.vars[i].constant = 0; + pm_make_none(&context.vars[i].value); + } + result = pm_run(&context) ? 0 : 1; + for (i = 0; i < context.var_count; i++) pm_free_value(&context.vars[i].value); + return result; +} + static char *pm_read_file(const char *filename) { FILE *file; @@ -578,30 +816,538 @@ static char *pm_read_file(const char *filename) return data; } -int main(int argc, char **argv) +static int pm_write_file(const char *filename, const char *data) +{ + FILE *file; + file = fopen(filename, "wb"); + if (file == NULL) return 0; + if (fwrite(data, 1U, strlen(data), file) != strlen(data)) { + fclose(file); + return 0; + } + fclose(file); + return 1; +} + +static int pm_file_exists(const char *path) +{ + struct stat st; + return stat(path, &st) == 0; +} + +static int pm_is_dir(const char *path) +{ + struct stat st; + if (stat(path, &st) != 0) return 0; + return S_ISDIR(st.st_mode) ? 1 : 0; +} + +static int pm_make_dir(const char *path) +{ + if (mkdir(path, 0777) == 0) return 1; + if (errno == EEXIST && pm_is_dir(path)) return 1; + return 0; +} + +static char *pm_join_path(const char *left, const char *right) +{ + size_t l; + size_t r; + char *out; + int slash; + l = strlen(left); + r = strlen(right); + slash = (l > 0U && left[l - 1U] == '/') ? 0 : 1; + out = (char *)malloc(l + r + (size_t)slash + 1U); + if (out == NULL) return NULL; + strcpy(out, left); + if (slash) strcat(out, "/"); + strcat(out, right); + return out; +} + +static char *pm_get_cwd(void) +{ + char buffer[PM_PATH_MAX]; + if (getcwd(buffer, sizeof(buffer)) == NULL) return NULL; + return pm_strdup_c89(buffer); +} + +static char *pm_store_path(void) +{ + const char *env; + const char *home; + char *dir; + char *path; + env = getenv("PEPPERMINT_STORE"); + if (env != NULL && env[0] != '\0') return pm_strdup_c89(env); + home = getenv("HOME"); + if (home == NULL || home[0] == '\0') home = "."; + dir = pm_join_path(home, ".peppermint"); + if (dir == NULL) return NULL; + pm_make_dir(dir); + path = pm_join_path(dir, "store.json"); + free(dir); + return path; +} + +static const char *pm_flag_value(int argc, char **argv, const char *name) { - PMContext context; - char *source; int i; + size_t n; + n = strlen(name); + for (i = 0; i < argc; i++) { + if (strncmp(argv[i], "--", 2U) == 0 && strncmp(argv[i] + 2, name, n) == 0 && argv[i][2 + n] == '=') { + return argv[i] + 3 + n; + } + } + return NULL; +} + +static int pm_true_flag(const char *text) +{ + if (text == NULL) return 0; + return strcmp(text, "true") == 0 || strcmp(text, "True") == 0 || strcmp(text, "TRUE") == 0 || strcmp(text, "1") == 0; +} + +static int pm_validate_flags(int argc, char **argv, const char *allowed) +{ + int i; + char name[PM_NAME_MAX]; + size_t j; + const char *p; + for (i = 0; i < argc; i++) { + if (strncmp(argv[i], "--", 2U) != 0) continue; + p = argv[i] + 2; + j = 0U; + while (p[j] != '\0' && p[j] != '=' && j < PM_NAME_MAX - 1U) { + name[j] = p[j]; + j++; + } + name[j] = '\0'; + if (strcmp(name, "args") == 0) continue; + if (strstr(allowed, name) == NULL) { + fprintf(stderr, "peppermint: invalid flag --%s\n", name); + return 0; + } + if (p[j] != '=') { + fprintf(stderr, "peppermint: expected --%s=\n", name); + return 0; + } + } + return 1; +} + +static int pm_run_file(const char *filename, int argc, char **argv) +{ + char *source; int result; - if (argc < 2) { - fprintf(stderr, "usage: peppermint [args...]\n"); - return 2; - } - source = pm_read_file(argv[1]); + source = pm_read_file(filename); if (source == NULL) { - fprintf(stderr, "peppermint: cannot read %s\n", argv[1]); + fprintf(stderr, "peppermint: cannot read %s\n", filename); return 2; } - context.source = source; - context.filename = argv[1]; - context.argc = argc - 2; - context.argv = argv + 2; - context.var_count = 0; - context.stopped = 0; - for (i = 0; i < PM_MAX_VARS; i++) pm_make_none(&context.vars[i].value); - result = pm_run(&context) ? 0 : 1; - for (i = 0; i < context.var_count; i++) pm_free_value(&context.vars[i].value); + result = pm_run_source(filename, source, argc, argv); free(source); return result; } + +static char *pm_json_string_value(const char *json, const char *key) +{ + char pattern[PM_NAME_MAX + 4]; + const char *p; + const char *start; + char *out; + size_t length; + strcpy(pattern, "\""); + strcat(pattern, key); + strcat(pattern, "\""); + p = strstr(json, pattern); + if (p == NULL) return NULL; + p += strlen(pattern); + while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++; + if (*p != ':') return NULL; + p++; + while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++; + if (*p != '"') return NULL; + p++; + start = p; + while (*p != '\0' && *p != '"') p++; + if (*p != '"') return NULL; + length = (size_t)(p - start); + out = pm_copy_text(start, length); + return out; +} + +static char *pm_project_main(void) +{ + char *cwd; + char *config_path; + char *json; + char *main_file; + cwd = pm_get_cwd(); + if (cwd == NULL) return NULL; + config_path = pm_join_path(cwd, ".peppermint"); + free(cwd); + if (config_path == NULL) return NULL; + json = pm_read_file(config_path); + free(config_path); + if (json == NULL) { + pm_cli_error("cannot find peppermint config files"); + return NULL; + } + main_file = pm_json_string_value(json, "main"); + free(json); + if (main_file == NULL || main_file[0] == '\0') { + if (main_file != NULL) free(main_file); + pm_cli_error("cannot find entry point in .peppermint"); + return NULL; + } + return main_file; +} + +static int pm_command_run(int argc, char **argv) +{ + const char *file; + char *project_file; + int args_index; + int i; + file = pm_flag_value(argc, argv, "file"); + if (!pm_validate_flags(argc, argv, "file")) return 2; + args_index = argc; + for (i = 0; i < argc; i++) { + if (strcmp(argv[i], "--args") == 0) { + args_index = i + 1; + break; + } + } + if (file == NULL) { + project_file = pm_project_main(); + if (project_file == NULL) return 2; + i = pm_run_file(project_file, argc - args_index, argv + args_index); + free(project_file); + return i; + } + return pm_run_file(file, argc - args_index, argv + args_index); +} + +static int pm_append_project_store(const char *name, const char *path) +{ + char *store; + FILE *file; + time_t now; + struct tm *tm_now; + int day; + int month; + int year; + store = pm_store_path(); + if (store == NULL) return 0; + file = fopen(store, "a"); + if (file == NULL) { + free(store); + return 0; + } + now = time(NULL); + tm_now = localtime(&now); + if (tm_now == NULL) { + day = 0; + month = 0; + year = 0; + } else { + day = tm_now->tm_mday; + month = tm_now->tm_mon; + year = tm_now->tm_year + 1900; + } + fprintf(file, "%s\t%s\t%d-%d-%d\n", name, path, day, month, year); + fclose(file); + free(store); + return 1; +} + +static int pm_command_init(int argc, char **argv) +{ + const char *name; + const char *author; + const char *license; + char *cwd; + char *path; + char *tests_dir; + char *readme_path; + char *app_path; + char *test_path; + char *config_path; + char *main_path; + char readme[PM_TEXT_MAX]; + char config[PM_TEXT_MAX]; + if (!pm_validate_flags(argc, argv, "name license author")) return 2; + name = pm_flag_value(argc, argv, "name"); + author = pm_flag_value(argc, argv, "author"); + license = pm_flag_value(argc, argv, "license"); + if (name == NULL || name[0] == '\0') { + pm_cli_error("--name is a required flag for init"); + return 2; + } + cwd = pm_get_cwd(); + if (cwd == NULL) return 2; + if (strcmp(name, ".") == 0) path = pm_strdup_c89(cwd); + else path = pm_join_path(cwd, name); + free(cwd); + if (path == NULL) return 2; + if (pm_file_exists(path) && !pm_is_dir(path)) { + pm_cli_error("project path exists and is not a directory"); + free(path); + return 2; + } + if (!pm_file_exists(path) && !pm_make_dir(path)) { + pm_cli_error("cannot create project directory"); + free(path); + return 2; + } + tests_dir = pm_join_path(path, "tests"); + if (tests_dir == NULL || !pm_make_dir(tests_dir)) { + pm_cli_error("cannot create tests directory"); + free(path); + if (tests_dir != NULL) free(tests_dir); + return 2; + } + readme_path = pm_join_path(path, "README.md"); + app_path = pm_join_path(path, "app.ppm"); + test_path = pm_join_path(tests_dir, "test.ppm"); + config_path = pm_join_path(path, ".peppermint"); + main_path = pm_join_path(path, "app.ppm"); + free(tests_dir); + if (readme_path == NULL || app_path == NULL || test_path == NULL || config_path == NULL || main_path == NULL) { + pm_cli_error("out of memory"); + free(path); + return 2; + } + sprintf(readme, "# %s\n", name); + sprintf(config, "{\n \"name\": \"%s\",\n \"path\": \"%s\",\n \"author\": \"%s\",\n \"license\": \"%s\",\n \"main\": \"%s\"\n}\n", + name, path, author == NULL ? "" : author, license == NULL ? "" : license, main_path); + if (!pm_write_file(readme_path, readme) || !pm_write_file(app_path, " ") || + !pm_write_file(test_path, "") || !pm_write_file(config_path, config)) { + pm_cli_error("cannot write project files"); + free(path); free(readme_path); free(app_path); free(test_path); free(config_path); free(main_path); + return 2; + } + pm_append_project_store(name, path); + printf("Created Peppermint project %s at %s\n", name, path); + free(path); free(readme_path); free(app_path); free(test_path); free(config_path); free(main_path); + return 0; +} + +static int pm_command_list(void) +{ + char *store; + FILE *file; + char line[PM_TEXT_MAX]; + int count; + char *name; + char *path; + char *date; + store = pm_store_path(); + if (store == NULL) return 2; + file = fopen(store, "r"); + if (file == NULL) { + printf("It seems like you haven't created any project\n"); + free(store); + return 0; + } + printf("NAME\tPATH\tCREATED\n"); + count = 0; + while (fgets(line, sizeof(line), file) != NULL) { + name = strtok(line, "\t"); + path = strtok(NULL, "\t"); + date = strtok(NULL, "\t\r\n"); + if (name != NULL && path != NULL && date != NULL) { + printf("%s\t%s\t%s\n", name, path, date); + count++; + } + } + fclose(file); + free(store); + if (count == 0) printf("It seems like you haven't created any project\n"); + return 0; +} + +static int pm_bf_compile(const char *source) +{ + int cells[PM_BF_CELLS]; + int pointer; + const char *p; + int value; + char compiled[PM_TEXT_MAX]; + char part[64]; + char *dir; + char *out; + size_t used; + int i; + for (i = 0; i < PM_BF_CELLS; i++) cells[i] = 0; + pointer = 0; + value = 0; + compiled[0] = '\0'; + used = 0U; + p = source; + while (*p != '\0') { + if (*p == '+') cells[pointer]++; + else if (*p == '-') cells[pointer]--; + else if (*p == '>' && pointer < PM_BF_CELLS - 1) pointer++; + else if (*p == '<' && pointer > 0) pointer--; + else if (*p == '.') { + int diff; + diff = cells[pointer] - value; + value = cells[pointer]; + if (diff < 0) sprintf(part, "%d-", -diff); + else sprintf(part, "%d+", diff); + if (used + strlen(part) < sizeof(compiled)) { + strcat(compiled, part); + used += strlen(part); + } + } + p++; + } + dir = pm_join_path(".", "dist"); + if (dir == NULL) return 0; + pm_make_dir(dir); + out = pm_join_path(dir, "test.long"); + free(dir); + if (out == NULL) return 0; + i = pm_write_file(out, compiled); + free(out); + return i; +} + +static int pm_bf_execute(const char *source) +{ + unsigned char *cells; + long *jumps; + long *stack; + char *program; + long pc; + long dp; + long length; + long top; + long i; + int ch; + cells = (unsigned char *)calloc((size_t)PM_BF_CELLS, 1U); + jumps = (long *)malloc((size_t)PM_BF_MAX_PROGRAM * sizeof(long)); + stack = (long *)malloc((size_t)PM_BF_MAX_STACK * sizeof(long)); + program = (char *)malloc((size_t)PM_BF_MAX_PROGRAM); + if (cells == NULL || jumps == NULL || stack == NULL || program == NULL) { + free(cells); free(jumps); free(stack); free(program); + pm_cli_error("out of memory"); + return 1; + } + length = 0L; + for (i = 0L; source[i] != '\0'; i++) { + if (strchr("[].,<>+-", source[i]) != NULL) { + if (length >= PM_BF_MAX_PROGRAM) { + pm_cli_error("bf program too large"); + free(cells); free(jumps); free(stack); free(program); + return 1; + } + program[length] = source[i]; + jumps[length] = -1L; + length++; + } + } + top = 0L; + for (i = 0L; i < length; i++) { + if (program[i] == '[') { + if (top >= PM_BF_MAX_STACK) { pm_cli_error("bf bracket nesting too deep"); free(cells); free(jumps); free(stack); free(program); return 1; } + stack[top++] = i; + } else if (program[i] == ']') { + long start; + if (top == 0L) { pm_cli_error("unmatched bf ']' "); free(cells); free(jumps); free(stack); free(program); return 1; } + start = stack[--top]; + jumps[start] = i; + jumps[i] = start; + } + } + if (top != 0L) { pm_cli_error("unmatched bf '[' "); free(cells); free(jumps); free(stack); free(program); return 1; } + pc = 0L; + dp = 0L; + while (pc < length) { + if (program[pc] == '+') { cells[dp]++; pc++; } + else if (program[pc] == '-') { cells[dp]--; pc++; } + else if (program[pc] == '>') { if (dp < PM_BF_CELLS - 1) dp++; pc++; } + else if (program[pc] == '<') { if (dp > 0) dp--; pc++; } + else if (program[pc] == '.') { putchar((int)cells[dp]); pc++; } + else if (program[pc] == ',') { ch = getchar(); cells[dp] = (unsigned char)(ch == EOF ? 0 : ch); pc++; } + else if (program[pc] == '[') { if (cells[dp] == 0) pc = jumps[pc] + 1L; else pc++; } + else if (program[pc] == ']') { if (cells[dp] != 0) pc = jumps[pc] + 1L; else pc++; } + else pc++; + } + putchar('\n'); + free(cells); free(jumps); free(stack); free(program); + return 0; +} + +static int pm_command_bf(int argc, char **argv) +{ + const char *file; + const char *compile; + char *source; + int result; + if (!pm_validate_flags(argc, argv, "file compile")) return 2; + file = pm_flag_value(argc, argv, "file"); + compile = pm_flag_value(argc, argv, "compile"); + if (file == NULL || file[0] == '\0') { + pm_cli_error("bf requires --file="); + return 2; + } + source = pm_read_file(file); + if (source == NULL) { + fprintf(stderr, "peppermint: %s - file not found\n", file); + return 2; + } + if (pm_true_flag(compile) && !pm_bf_compile(source)) { + pm_cli_error("unable to write compiled bf output"); + free(source); + return 1; + } + result = pm_bf_execute(source); + free(source); + return result; +} + +static void pm_usage(void) +{ + printf("Peppermint C89 %s\n", PM_VERSION); + printf("usage:\n"); + printf(" peppermint run [--file=FILE] [--args ARGS...]\n"); + printf(" peppermint init --name=NAME [--author=AUTHOR] [--license=LICENSE]\n"); + printf(" peppermint list\n"); + printf(" peppermint version\n"); + printf(" peppermint bf --file=FILE [--compile=true]\n"); + printf(" peppermint FILE.ppm [ARGS...]\n"); +} + +int main(int argc, char **argv) +{ + if (argc < 2) { + pm_usage(); + return 2; + } + if (strcmp(argv[1], "run") == 0) return pm_command_run(argc - 2, argv + 2); + if (strcmp(argv[1], "init") == 0) return pm_command_init(argc - 2, argv + 2); + if (strcmp(argv[1], "list") == 0) return pm_command_list(); + if (strcmp(argv[1], "version") == 0) { +#if defined(__linux__) + printf("Peppermint Version %s[linux]\n", PM_VERSION); +#elif defined(_WIN32) + printf("Peppermint Version %s[win32]\n", PM_VERSION); +#elif defined(__APPLE__) + printf("Peppermint Version %s[darwin]\n", PM_VERSION); +#else + printf("Peppermint Version %s[unknown]\n", PM_VERSION); +#endif + return 0; + } + if (strcmp(argv[1], "bf") == 0) return pm_command_bf(argc - 2, argv + 2); + if (strcmp(argv[1], "help") == 0 || strcmp(argv[1], "--help") == 0) { + pm_usage(); + return 0; + } + return pm_run_file(argv[1], argc - 2, argv + 2); +} diff --git a/test.sh b/test.sh index eb3cd0b..be6c095 100755 --- a/test.sh +++ b/test.sh @@ -3,25 +3,105 @@ set -eu make clean make + output=$(./peppermint smoke.ppm argument) expected='hello peppermint argument' if [ "$output" != "$expected" ]; then - printf '%s\n' "unexpected smoke output:" "$output" >&2 + printf '%s\n' "unexpected legacy smoke output:" "$output" >&2 exit 1 fi -cat > /tmp/peppermint-c89-test.ppm <<'EOF' +cat > /tmp/peppermint-c89-lang.ppm <<'PPM' let a := 7 -let b := a * 3 -b + 2 -@env:PEPPERMINT_TEST_MISSING -EOF -output=$(PEPPERMINT_TEST_MISSING= ./peppermint /tmp/peppermint-c89-test.ppm) +let b = a * 3 + 2 +b +b = b + 1 +b == 24 +b != 23 +b >= 24 +b <= 24 +b > 23 +b < 25 +"hello" + " world" +@env:PEPPERMINT_TEST_VALUE +@argv:1 +PPM +output=$(PEPPERMINT_TEST_VALUE=present ./peppermint run --file=/tmp/peppermint-c89-lang.ppm --args zero one) expected='23 -None' +1 +1 +1 +1 +1 +1 +hello world +present +one' if [ "$output" != "$expected" ]; then - printf '%s\n' "unexpected arithmetic output:" "$output" >&2 + printf '%s\n' "unexpected language output:" "$output" >&2 exit 1 fi -printf '%s\n' 'C89 smoke tests passed' + +cat > /tmp/peppermint-c89-const.ppm <<'PPM' +let locked := 1 +locked = 2 +PPM +if ./peppermint /tmp/peppermint-c89-const.ppm >/tmp/peppermint-c89-const.out 2>/tmp/peppermint-c89-const.err; then + printf '%s\n' 'constant assignment unexpectedly succeeded' >&2 + exit 1 +fi +if ! grep 'cannot assign to constant' /tmp/peppermint-c89-const.err >/dev/null; then + printf '%s\n' 'constant assignment error missing' >&2 + exit 1 +fi + +tmpdir=${TMPDIR:-/tmp}/peppermint-c89-project-$$ +mkdir "$tmpdir" +oldpwd=$(pwd) +cd "$tmpdir" +PEPPERMINT_STORE="$tmpdir/store.tsv" "$oldpwd/peppermint" init --name=demo --author=Tester --license=MIT >/tmp/peppermint-c89-init.out +cd "$oldpwd" +if [ ! -f "$tmpdir/demo/.peppermint" ] || [ ! -f "$tmpdir/demo/app.ppm" ] || [ ! -f "$tmpdir/demo/tests/test.ppm" ]; then + printf '%s\n' 'init did not create expected project files' >&2 + exit 1 +fi +printf '%s\n' '"project run"' > "$tmpdir/demo/app.ppm" +oldpwd=$(pwd) +cd "$tmpdir/demo" +output=$(PEPPERMINT_STORE="$tmpdir/store.tsv" "$oldpwd/peppermint" run) +cd "$oldpwd" +if [ "$output" != 'project run' ]; then + printf '%s\n' "unexpected project run output:" "$output" >&2 + exit 1 +fi +output=$(PEPPERMINT_STORE="$tmpdir/store.tsv" ./peppermint list) +case "$output" in + *demo*) ;; + *) printf '%s\n' "list output missing project:" "$output" >&2; exit 1 ;; +esac + +cat > /tmp/peppermint-c89-bf.bf <<'BF' +++++++++[>++++++++<-]>+. +BF +output=$(./peppermint bf --file=/tmp/peppermint-c89-bf.bf) +expected='A' +if [ "$output" != "$expected" ]; then + printf '%s\n' "unexpected bf output:" "$output" >&2 + exit 1 +fi +rm -rf dist +./peppermint bf --file=/tmp/peppermint-c89-bf.bf --compile=true >/tmp/peppermint-c89-bf.out +if [ ! -f dist/test.long ]; then + printf '%s\n' 'bf compile did not create dist/test.long' >&2 + exit 1 +fi +rm -rf dist "$tmpdir" + +output=$(./peppermint version) +case "$output" in + 'Peppermint Version 0.0.1['*) ;; + *) printf '%s\n' "unexpected version output:" "$output" >&2; exit 1 ;; +esac + +printf '%s\n' 'C89 feature tests passed'