ProjectTera is a small, statically typed, interpreted programming language implemented from scratch in TypeScript. Source text goes through a hand-written pipeline — lexer → parser → static type checker → tree-walking interpreter — and the whole toolchain, from the CLI to the error renderer, lives in this repository: no parser generator, no runtime dependencies.
Language documentation lives in the wiki (variables, syntax rules, arithmetic, arrays, control flow, functions, properties). This README covers the toolchain: how to build it, run it, embed it and test it.
Contents
- Quick start
- Requirements
- Running a program
- Embedding the pipeline
- Project structure
- Diagnostics and exit codes
- Testing
- Known limitations
- Documentation
git clone https://github.com/grivia0/projecttera.git
cd projecttera
npm install
npm run build
node dist/bin.js tests/cases/hello.terahello, tera
7
ab
flag is true
npm test builds the project and then runs the whole suite: 46 programs plus 4 CLI checks.
- Node.js 18 or newer — built and tested on v26.11.1. The only runtime
imports are
node:fs,node:path,node:child_processandnode:url. - npm, for the build script and the two dev dependencies (
typescript,@types/node).
npm run build # tsc → dist/
node dist/bin.js tests/cases/hello.tera
node dist/bin.js --help # usage and exit codesnpm start is a shorthand for node dist/bin.js, and because the package declares a tera
binary, npm link makes the interpreter a normal command:
npm start -- tests/cases/hello.tera
npm link
tera tests/cases/hello.teraindex.ts is a self-contained example of driving the same pipeline from another program:
node dist/index.jsArray length is: 4
Last item is: 4
Doubled total: 20
4 is small
16 is big
| Module | Use it for |
|---|---|
Runner.ts |
runSource(source: string): void — lex, parse, type check and interpret a whole program |
Errors.ts |
formatDiagnostic(error, filename?, source?), exitCodeFor(error), the TeraError hierarchy, the EXIT_* constants |
Lexer.ts / Parser.ts / TypeChecker.ts / Interpreter.ts |
the individual stages, when you want tokens or an AST instead of an execution |
runSource never prints anything: failures are thrown as TeraErrors, and the caller decides
how to present them. index.ts is the reference pattern:
import { exitCodeFor, formatDiagnostic } from "./Errors.js";
import { runSource } from "./Runner.js";
const filename = "<demo>";
const source = `let nums: [1, 2] => arr[int]
print(total)`;
try {
runSource(source);
} catch (err) {
console.error(formatDiagnostic(err, filename, source));
process.exit(exitCodeFor(err));
}Passing the source text and a name keeps the snippet working for embedded programs — the same rendering the CLI produces, pointing into the string instead of into a file:
[Type Error] Undefined variable 'total'.
--> <demo>:2:7
|
2 | print(total)
| ^
Two things to know before importing: the codebase is ESM ("type": "module",
module: nodenext), so relative imports carry the compiled .js extension even from TypeScript;
and main points at index.ts's output, which runs the demo when it is imported — import
dist/Runner.js and dist/Errors.js directly, as above.
| File | Role |
|---|---|
TokenType.ts |
token kinds and SourceLocation (line, column) |
Lexer.ts |
source text → tokens, tracking line and column and rejecting illegal characters and unterminated strings |
AST.ts |
AST node types, the expression and statement unions |
Parser.ts |
tokens → AST; every node records the position it came from |
TypeChecker.ts |
static analysis: scopes, signatures, and the built-in member tables for arr[T] and string |
Interpreter.ts |
tree-walking evaluator with lexical environments |
Errors.ts |
the error hierarchy, the diagnostic formatter and the exit-code constants |
Runner.ts |
the whole pipeline (lex → parse → check → interpret) behind one call |
bin.ts |
the tera command line entry point |
index.ts |
example of driving the pipeline from another program |
tests/ |
the suite and its own README |
All four stages fail the same way: a tagged message, then --> file:line:column, then the
offending source line with a caret under the reported column.
[Type Error] Type 'arr[int]' has no member 'length'. Available members: leng, isEmpty, first, last, asString, at, get, rmv, shift, rmShift, add, join, find, findIndex, merge, copyIn, flat, slice, splice.
--> tests/cases/err_unknown_member.tera:2:12
|
2 | print(nums.length)
| ^
The tags are [Lexer Error], [Parser Error], [Type Error] and [Runtime Error]. A failure
that is not a language error is reported as [Internal Error] together with a stack trace,
because that means the interpreter itself is broken.
| Exit code | Meaning |
|---|---|
0 |
the program ran to completion |
1 |
the source was invalid (lexing, parsing, type checking) or the program failed at run time |
2 |
bad usage: wrong arguments, or a missing/unreadable file |
70 |
internal error — a bug in the interpreter |
npm test # build, then the whole suite
node tests/run.mjs # suite only (dist/ must already be built)
node tests/run.mjs arrays # only the cases whose name contains "arrays"The suite runs every program in tests/cases/ through the built CLI and compares stdout, stderr
and the exit code byte for byte against the expectation files stored next to it (.out, .err,
.exit). Four further checks cover the CLI itself — --help, no arguments, a missing file, and
the index.ts example — so npm test ends with 50 passed, 0 failed, 50 total. 36 of the 46
programs are deliberate failures, which is what keeps the diagnostics pinned down as tightly as
the behaviour. tests/README.md explains the layout and how to add a case.
- Numeric literals always type as
int; thefloatannotation cannot be satisfied yet. at,firstandlastfollow JavaScript:at(-1)counts from the end and an out-of-boundsatyieldsundefined. Useget()when a bounds check is wanted.- There is no return-on-all-paths analysis, so a function that skips
returnon some path evaluates toundefined. - A C-style
forloop without an update clause never terminates if its body usescontinue— the same as C. - Members of the built-in types are read-only:
nums.leng = 4is a type error. - Grouping parentheses work; compound assignment operators (
+=,-=, …) do not exist yet. rmShift()is an alias ofshift(): both remove and return the first element.- The language is still moving. The wiki is the reference, and it can lag behind
dist/.
- The wiki — the language reference: variables, syntax rules, arithmetic operators, arrays, control flow, functions and properties.
tests/README.md— the test harness, case layout, and how to add a case. The.terafiles there double as small runnable examples of each feature.index.ts— the smallest complete example of embedding the interpreter.