Two Logo systems — one from a 1981 Apple II, one from Berkeley — living in one static web page, sharing a virtual machine and nothing a user can see.
https://buildx100.github.io/turtleshell/ — nothing to install; it is a
static page. Type REPEAT 4 [FD 100 RT 90], then press APPLE ][ and watch
the same square land on a one-bit NTSC screen.
One of the buildx100 projects.
| Apple Logo II | Berkeley Logo |
|---|---|
![]() |
![]() |
npm run serve # http://localhost:16005Double-clicking web/index.html will not work. The page is built from native
ES modules and module loading is blocked by CORS on the file:// protocol. It
has to be served over HTTP. There is no build step — npm run serve is a
40-line static file server, and Node alone runs the page and the tests.
npm test # node --test, no frameworkTwenty-four of those tests drive a real browser, because four bugs walked straight past the other 272: a stylesheet rule that lost on file order, a canvas reporting its pixel buffer as its height, two elements answering to the same name, and a field you could not type into because something invisible sat in front of it. None of those are visible to Node. They skip themselves when there is no browser to drive, so a fresh clone is still green:
npm install --save-dev playwright-core
npx playwright install chromiumThat is a driver for a browser, not a library the page loads. web/ still
imports nothing from npm and never will.
Logo was never one language, it was a family, and its dialects were shaped by the machines under them. The Apple II's colours are a side effect of NTSC decoding rather than a palette: ask for purple on an odd column and you get something else. That is not a defect to smooth over — it is what the language felt like on that machine, and the retro mode models the real one-bit framebuffer rather than faking six colours, so the artefacts come out for free.
- Not an Apple II emulator. No 6502, no ROM, no disk images. The interpreter
is original; only the look and the dialect are borrowed, and neither of those
is copyrightable. See
NOTICE. - Not a UCBLogo compatibility project. jslogo, Calormen Logo and Turtle Academy already do browser Logo well, and this is not entering that parity race. The implemented primitive set is written down and anything outside it says so instead of pretending.
- Not one language with a theme switch. Two dialects means two primitive registries, two error catalogues, two renderers, two terminals. Only the reader, the machine and the turtle are shared — see docs/architecture.md.
web/js/lang/ reader, virtual machine, workspace, primitive implementations
web/js/dialect/ per-dialect names, arities, precedence, error wording
web/js/turtle/ turtle state and the event log
web/js/render/ one renderer per mode
web/js/shell/ terminal
test/ node --test
docs/ why it behaves that way, and what was not verified
web/js/main.js wires these together and contains no logic of its own.
Every milestone done; from here it is iteration. Measured by npm test and
npm run check-manual:
| tests passing | 355 of 355 — 311 in Node, 44 driving Chromium |
| acceptance programs | 22 of 22, simplest first — square, star, rosette, circle, five notes, templates, spiral, colour bars, Sierpinski, Koch snowflake, binary tree, dragon curve, Hilbert curve, an L-system, a word reversed, towers of Hanoi, merge sort, an arithmetic evaluator, a countdown, primes, an interactive dialogue, a doctor. Four of them run on one dialect only and say so |
| primitives checked against a manual | 372 of 372 across both dialects |
| dialects | both, switchable at runtime, each with its own renderer |
They are the same programs the page offers from its examples dialog — one copy,
in web/js/examples.js — so nothing on the menu is untested. That dialog shows
what each one draws, and the picture is the program: run by the same interpreter
the page runs, in whichever dialect is showing, rather than a screenshot in a
folder that nothing checks. They are in the order a reader should meet them: six that define no procedure,
then the ones that define a procedure and draw with it, then six that
compute with words and lists instead of drawing, and last the two that stop and
ask you something. The later ones are there because Logo is not a drawing
language with a list feature: an L-system whose rule rewrites a list and whose
letters are then RUN, a merge sort, an evaluator that knows times comes before
plus, and thirty lines of Weizenbaum. Eighteen of the twenty-two are written in the subset both dialects share — no
IFELSE, no MINUS, nothing either machine is missing — and a test runs those
through both machines. The other four are the point made the other way round:
each names the machine it needs, on its row in the menu, and the test asserts
both halves — that it runs there and that the other machine refuses it. Two are
Apple's (a tune, because that manual has a loudspeaker and the other has none;
and a loop made of GO and LABEL, which no other Logo has) and two are Berkeley's
(templates, and a sieve made of one FILTER). Choosing one switches the page to
its machine. They are
ordinary textbook constructions written for this project, not listings copied
out of a book; each one is placed to fit inside 280 by 192 without running off.
Every primitive's name, aliases and input count is recorded in docs/manual-tables/ and asserted against the code in both directions, so neither a table nor a registry can be edited alone. Doing this found eight defects on the Berkeley side alone: three primitives its manual does not document, three missing parenthesised forms, half the error wording mis-capitalised, and a "division by zero" error that dialect does not have.
The two tables are not equally strong and say so: the Berkeley manual is machine-readable text, so its arities are verified against the manual itself; the Apple manual survives only as OCR of two scans, so its arities are hand-checked and only its coverage is machine-verified.
The Apple screen is the real one: 280x192 one-bit pixels, seven to a byte, with
the colour decoded out of the bit stream rather than stored. Which means a white
vertical line comes out violet, a green diagonal comes out dotted, and an orange
pixel re-colours the six neighbours that share its byte. Those are assertions in
test/hires.test.js, not descriptions. A monochrome-monitor mode shows the same
bits with no decoding at all, which is the clearest way to see that the colour
was never in the data.
npm run preview -- 'REPEAT 60 [FD 60 RT 174]' # draws it in the terminalIts transcript is on that screen, not beside it. The machine had one glass and
one raster, so the character grid is composed into the bottom of the same 280x192
picture that the turtle draws on, in the manual's proportion of twenty rows of
graphics over four of text. Which means a split crops the drawing rather than
shrinking it: the bottom 32 lines are still there and still being drawn into, and
FULLSCREEN reveals them rather than enlarging anything.
The line you type is on that screen too, in the same glyphs, on the row the
machine would have put it on — and it is still an ordinary <input>, placed over
that row with only its text made transparent. The browser keeps the caret, the
selection, the input method and the phone keyboard; the screen keeps the
letterforms. docs/architecture.md sets out why the field itself may never be
hidden, which is a lesson this project paid for.
The glass is 4:3, which is the shape the raster filled, so a turtle square comes
out at 1.14 to 1. That is the machine and not a defect — it is the reason Apple
Logo shipped SETSCRUNCH — and the one other shape it offers is the one that
primitive could produce, for anyone who minds. The correction is applied to the display, never to the frame
buffer, so no colour behaviour changes with it. docs/architecture.md sets out
why that is not the same kind of thing as the artefacts.
READWORD makes a program stop mid-procedure and wait for you to type, then
carry on from the same instruction. That needs no async anywhere: the machine
owns its frame stack, so suspending is simply not advancing it. It was the last
of the seven things that decision was made for at M0.
PAUSE is the same trick with the keyboard handed back. A procedure stops where
it stands, the prompt becomes its name, and what you type runs in that
procedure's scope — :N at a WALK? prompt is WALK's N, because nothing had to
be unwound to stop there. A mistake at the pause prompt costs the line and not
the program, and CO puts it back on its feet.
Your work is kept with SAVE and LOAD, the language's own primitives, against
localStorage — named workspaces you can list with CATALOG, rather than an
invisible autosave a program cannot see. My Work shows that disk, hands any
of it back as a .logo file, and takes one in: importing puts the file on the
disk the way sliding a floppy in does, and loading it is still LOAD. A saved workspace is Logo source text,
so LOAD is nothing but running what SAVE wrote: no format to version, and a
file a person can read. A browser that refuses storage is reported to the
program as trouble with the disk, which is what the manuals call it and, for
once, exactly true.
Working: the reader; the machine with infix precedence, interruption, procedure
definition, dynamic scoping, OUTPUT/STOP, tail-call elimination,
suspend-for-input and the interactive pause; CATCH/THROW, the template tools
(MAP, FILTER, REDUCE, FOREACH and ?), arrays, property lists, the loop
words and the bit operations; contents lists, burying, tracing, stacks and
queues, PARSE/RUNPARSE, and reading the keyboard a character at a time; the
turtle model with wrapping and a bounded field; the event log; both dialects,
both renderers and both terminals; a forty- or eighty-column character screen;
the examples menu, HELP, saving, importing and exporting.
Everything either manual documents is now either implemented or written down as missing with the subsystem it waits on. The "nothing in the way, just not written yet" list is empty for both dialects. What is left needs something this does not have — file streams, macros, an addressable text screen, a reader that knows vertical bars — or is deliberately out of scope, and docs/manual-tables/ names which for every word.
Not planned: multiple turtles and sprites, which were always a stretch goal.
Not verified: only Chromium has ever loaded this page. No Firefox, no Safari,
no phone. The error wording matches the manual's table but has not been compared
against a running UCBLogo. docs/licensing.md lists what else is unconfirmed.
The toolbar is available in English, 简体中文 and 日本語. The terminal is not translated, in either mode, and that is deliberate rather than unfinished.
It is the machine's output surface, not chrome. Its command names and error messages reproduce historical systems, so they are English the way the systems were. And in Apple mode it is a 40-column screen with a font of sixty-four shapes drawn for this project — Chinese cannot be drawn on it at all, and would arrive as a row of solid blocks. Typography the machine never had, such as an em dash, is folded to what it did have rather than blocked.
Apache-2.0 — see LICENSE. The project bundles no third-party code, fonts, ROMs
or disk images at all, so there is no boundary to state: everything here is under
that one licence. NOTICE records what is deliberately absent, and
docs/licensing.md records the provenance chain — in
particular that UCBLogo and its manual are GPL, that no UCBLogo source was read,
and that the manual is not vendored here.

