Port full Peppermint language and CLI to C89
This commit is contained in:
@@ -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.
|
||||
|
||||
+885
-139
File diff suppressed because it is too large
Load Diff
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user