The Amiga E language and its v40 module set are the work of Wouter van Oortmerssen, who has kindly granted permission to use the E libraries in this project (from Aminet dev/e/amigae33a). Thank you, Wouter! Every binary this compiler produces carries that credit embedded in it.
A from-scratch compiler for the Amiga E programming language (E v3.3a, 1997), written in zero-dependency JavaScript. It compiles E source to genuine AmigaOS M68K hunk executables and bootable ADF floppy images — usable as a node CLI or embedded as a pure ES-module library (it runs in the browser too).
Faithfulness is the core value: behaviour is validated against the
original 1997 compiler by a differential test suite (115 programs,
byte-identical output), and ecomp's binary .m modules are accepted by
real ec itself.
node tools/ecc.js --source=hello.e --out=hello
node tools/ecc.js --source=hello.e --adf=hello.adf # bootable floppy toooptions:
--out=FILE output path (default: source without .e)
-o FILE same as --out
--adf=FILE.adf also write a bootable 880K floppy image that runs
the program at boot (mount as DF0: in any emulator)
--moduledir=DIR extra directory to search for binary .m modules
(repeatable; like ec's EMODULES: assign)
--warn print semantic warnings
--quiet suppress informational output
--version, --help
A source that declares OPT MODULE produces a binary .m interface
module instead of an executable (and yes, the original Amiga compiler
can import the result).
Note: this repository is the compiler only (CLI + embeddable JS library). A browser IDE that runs the output on an emulated Amiga consumes this as a package and is published separately.
ecomp is fully self-contained: no Kickstart ROM, no Workbench disk, no emulator and no npm packages are needed to compile. Those images are only used by the browser IDE to run your program on the embedded emulated Amiga afterwards. The compiler output is a standard AmigaOS executable you can take to any real Amiga or emulator you like.
Get it (any of):
git clone https://github.com/gin0115/Amiga-E-Compiler-WebBased.git ecomp
# or download a release / the ZIP from GitHub and unpack it anywhereCompile from your own project directory — call the CLI by its path, nothing needs installing:
cd ~/my-amiga-game
node ~/tools/ecomp/tools/ecc.js --source=game.e --out=game --adf=game.adfgame is a ready AmigaOS executable; game.adf is a bootable floppy that
runs it. Your MODULE '*mymodule' files resolve from your project
directory; OS modules ('dos/dos', 'intuition/intuition', …) come from
ecomp's bundled modules/ automatically.
Use it as a JavaScript library (node or browser, pure ES modules):
import { parse } from './ecomp/src/parser.js';
import { analyze } from './ecomp/src/sem.js';
import { compileProgram } from './ecomp/src/codegen.js';
import { bootableAdf } from './ecomp/src/adf.js';
const { program, errors } = parse(eSourceText, 'game.e');
const sem = analyze(program /*, { resolveModule } for MODULE imports */);
const { bin } = compileProgram(program, sem); // Uint8Array: AmigaOS hunk exe
const adf = bootableAdf(bin); // Uint8Array: bootable floppyThe minimal file set for compiling is src/ + tools/ + modules/.
Everything else (the IDE, tests, emulator wiring) is optional.
| You want to… | You need |
|---|---|
| compile E → Amiga executables/ADFs | just these files + node ≥16 |
| run the output | any Amiga emulator (or real hardware) |
| watch it run in a browser | the separately published IDE package |
ecomp follows real E's model — you always compile one main source, and
it pulls its modules in via MODULE statements:
MODULE '*helper' -> the * means: sibling file in the same directory
PROC main()
greet('Amiga')
ENDPROC
-> helper.e
OPT MODULE
EXPORT PROC greet(who)
WriteF('hello, \s!\n', who)
ENDPROC
node tools/ecc.js --source=main.e # helper resolves automaticallyResolution order for MODULE 'name' / MODULE '*name':
- a sibling
name.mbinary interface module - a sibling
name.esource, compiled into your program --moduledir=DIRdirectories (binary.m)- the bundled v40 OS module set (
modules/—'dos/dos','intuition/intuition','exec/lists', …)
The exec/dos/intuition/graphics library calls are preloaded implicitly,
exactly like real E — OpenWindow(), ReadArgs(), MODE_NEWFILE and
friends just work without any MODULE statement.
Everything in the E v3.3a manual, including the famous corners:
- no operator precedence (
1+2*3really is 9), 16-bit runtime*//with ec's exact strength reductions, full-32-bit constant folding - E-strings & E-lists with their length headers,
StrCopy/StringF/… OBJECTs, inheritance, methods withself, constructorsNEW b.init(), destructors viaEND,SIZEOF- exceptions:
HANDLE/EXCEPT/EXCEPT DO(a finally!),Raise,Throw - quoted expressions
`x*x,Eval,MapList/ForAll/Exists/SelectList - LISP cells
<a|b>,Car/Cdr/Cons, unificationexp <=> [1,x,y] - IEEE floats with the
!operator (mathieeesingbas, like the real thing) - inline M68K assembly with E variable access (ch_15)
- windows, screens, graphics and events:
OpenW/OpenS,Line/Box/Plot/TextFonstdrast,WaitIMessage,Mouse - the heap:
NEW,New(),String(),Dispose,Link/Nextchains, auto-free at exit - 100+ builtins, the dos/exec call surface,
CleanUp,CtrlC,Rnd…
Beyond classic EC v3.3a, ecomp has an optional E-VO extension (Darren
Coles' modern Amiga E), enabled with an evo flag and contained entirely in
src/evo/. With the flag off (default) ecomp is faithful EC v3.3a; with it on
it accepts modern E as a strict superset — // comments, <</>>, NOT/~,
compound assignment, ANDALSO/ORELSE, IFN/WHILEN/ELSEWHILE/ALWAYS/…,
ternary ?:, swap :=:, == quick-compare, multi-dim arrays, UNION,
NEW objtype.ctor(), TRY/CATCH, BYTE/WORD types, PSIZEOF/ARRAYSIZE/
OFFSETOF, and the E-VO standard library (List/String/Astr/Mem families).
Validated against the real E-VO compiler (assembled from E-VO.S and run
under vamos as a third oracle): E-VO's own 741-test unittests.e, compiled by
ecomp in evo mode, runs to completion with 732 pass / 9 fail vs EVO's own
731 / 10 (the 9 are vamos-environment quirks EVO also fails). Native EC mode
is byte-identical with the flag on or off (115/115 differential still green).
See docs/evo-support.md for the full feature list,
the src/evo/ architecture, and the three-compiler validation.
ecomp doesn't just read a module's interface — it links the actual 68K
code out of the shipped binary .m files into your executable, the same
way the original ec does. Importing tools/EasyGUI, afc/Parser,
oomodules/* etc. pulls their compiled procedures straight in, no source
required. This is a small linker on top of the code generator:
- Code + relocations — each module's
CODEblob is appended and itsRELOCs applied (internal pointers rebased intoHUNK_RELOC32; the placeholderjsr abs.lifunc sites patched tobsr.linto the runtime). - The E runtime intrinsics the modules call (
WriteF,String,Mod,RealF,SetColour, …) are emitted as thunks ported 1-for-1 from the originalec68kifuncssource and run-verified byte-identical toecunder emulation (tools/ifunc-verify.js, 29/29). - The ABI matches
ec. Precompiled modules bake inec's fixed negative-A4offsets for the standard runtime globals (dosbaseat-44,stdoutat-8, …), so ecomp lays outA4to match — a module callingdos.ReadArgsfinds its base exactly whereecput it. - OOP across the boundary — classes from binary modules dispatch through
their runtime vtable (the per-class "descriptor" the module builds itself):
NEW obj.class()builds the descriptor, allocates, and calls the constructor;obj.method()dispatches via the slot;END objruns the destructor and frees. Reverse-engineered from realecoutput and written up indocs/oop-dispatch.md.
148 of the 149 shipped binary code modules link; the lone holdout
(tools/lisp) needs a conservative mark-sweep GC that assumes ec's
stack-allocated globals — incompatible with ecomp's static-globals model.
tools/oop-corpus.js and tools/lib-bakeoff.js tabulate module linking
against the real ec oracle.
npm test # unit: lexer, parser, semantics, M68K encodings
npm run test:e2e # end-to-end: the ecc CLI on fixture .e filesThe e2e suite runs the CLI as a subprocess and asserts on artifacts:
hunk structure, embedded strings, deterministic output, .m
round-trips, multi-file builds, bootable ADF images, flag handling, and
error diagnostics. Both suites run in CI (.github/workflows/ci.yml) on
node 16/20/22 — with zero dependencies there is no install step.
A third layer runs in CI as well: the behavioral suite compiles fixture programs and executes the resulting Amiga binaries under vamos (open-source m68k emulation), comparing stdout against golden outputs that were verified byte-identical against the original 1997 compiler.
src/lexer.js tokens (E's case-classified identifiers, nested comments)
src/parser.js AST (line-continuation rules, no-precedence chains)
src/sem.js symbol tables, const folding, object layouts, modules
src/codegen.js M68K code generation + hand-assembled E runtime
src/asm68k.js instruction encoder src/asmtext.js inline-asm assembler
src/emod.js binary .m module reader src/emodwrite.js .m writer
src/hunk.js AmigaOS executable src/adf.js bootable OFS floppy
Pure ES modules throughout; a unit test fails the build if anything under
src/ touches a node API, which is what lets the identical code run
in the browser.
GPL-3.0 (see LICENSE). Exception: modules/ — the E v40 binary
modules — are copyright Wouter van Oortmerssen, included with his
explicitly granted permission, and are not relicensed by this project
(see NOTICE).
Runtime * and / are 16-bit (fac(10) really is -303360); only x*2
and x*4 strength-reduce; x++ advances by the pointed-to type's size;
CONST cannot reference other constants; EXIT is illegal in LOOP;
EXCEPT DO clears exception only on normal entry; DisposeLink
returns NIL; immediate lists are static and refilled on every evaluation.
See tools/diff-test.js for the full executable specification.