{"slug": "bringing-swift-to-the-apple", "title": "Bringing Swift to the Apple ][", "summary": "A developer has created SwiftII, a Swift-flavored mini development environment for the Apple II series of computers, bringing a subset of the modern Swift language to the vintage 1977 machine. The project includes a launcher, file selector, text editor, REPL, and compiler, all fitting within the Apple II's 40KB memory constraint. Built with heavy AI assistance, SwiftII aims to provide a familiar Swift-like syntax while overcoming the hardware limitations of the 1 MHz MOS 6502 CPU.", "body_md": "Swift is the modern programming language behind many apps that run on modern Apple platforms. I thought it would be fun to bring a small taste of it back to Apple’s early days, the Apple II. It was Apple’s first mass-market series of machines, initially released in 1977 with a 1 MHz MOS 6502 CPU.\n\nI built **SwiftII**, a Swift-flavored mini development environment for the original Apple II to a //e and up.\n\nTo be honest, I built it with heavy AI assistance in my spare time. I will be candid later how it helped because that workflow was as interesting to me as the retrocomputing work itself.\n\nOne big caveat, this is not modern Swift. It could never be. The Swift full standard library will not fit on this machine. SwiftII is a deliberate subset, much closer in spirit to [Embedded Swift](https://www.swift.org/get-started/embedded/) than the SDK meant for modern platforms like iOS or macOS.\n\nMy goal was to fit as much of Swift into the Apple II as I can while still reading like Swift on sight. If you know Swift, you should be able to read a SwiftII-compatible program and immediately understand it.\n\nOther than the interpreter, I want my user to still have a relatively good user/developer experience. Because of this, I also had to build a launcher, file selector and text editor due to limitations I encountered on the Apple II.\n\n## Contents\n\nThis post is quite long, so here are the main sections for you to jump straight to:\n\n[The App](/2026/06/swift-on-apple-ii/#the-app)[Motivation](/2026/06/swift-on-apple-ii/#motivation)[Target Hardware](/2026/06/swift-on-apple-ii/#target-hardware)[The language](/2026/06/swift-on-apple-ii/#the-language)[The main constraint: 40,704 bytes](/2026/06/swift-on-apple-ii/#the-main-constraint-40704-bytes)[Development setup](/2026/06/swift-on-apple-ii/#development-setup)[How the language works](/2026/06/swift-on-apple-ii/#how-the-language-works)[Two families of binary](/2026/06/swift-on-apple-ii/#two-families-of-binary)[“What you type” vs “what you see” vs “what is stored”](/2026/06/swift-on-apple-ii/#what-you-type-vs-what-you-see-vs-what-is-stored)[Developing Swift on the machine itself](/2026/06/swift-on-apple-ii/#developing-swift-on-the-machine-itself)[The trouble with memory banking](/2026/06/swift-on-apple-ii/#the-trouble-with-memory-banking)[Why this project ships as 8x disks](/2026/06/swift-on-apple-ii/#why-this-project-ships-as-8x-disks)[Testing too many machines](/2026/06/swift-on-apple-ii/#testing-too-many-machines)[How it was built with AI](/2026/06/swift-on-apple-ii/#how-it-was-built-with-ai)[Try it](/2026/06/swift-on-apple-ii/#try-it)[Conclusion](/2026/06/swift-on-apple-ii/#conclusion)\n\n## The App\n\nBoot the appropriate disk and you land in a launcher. You can select an interactive REPL, a file browser that runs `.swift`\n\nprograms straight from the disk, a full-screen editor, all self-contained on one bootable floppy.\n\nHere is a demo video of the app running on my original Apple II Plus.\n\nIt goes through booting the system, running some sample programs and usage of the file selector, editor, REPL and compiler. The compiler is on a separate disk from the REPL.\n\nThe original II Plus keyboard has no lowercase and no `\\`\n\nkey, so the program is typed with [digraphs](https://en.wikipedia.org/wiki/Digraphs_and_trigraphs_%28programming%29). `??/`\n\nbecomes `\\`\n\nwhich SwiftII interprets as canonical Swift. More on that quirk later.\n\nHere are some sample screenshots:\n\nIf you are too excited to continue reading and want to dive into the code and disk images, here is the GitHub repo [https://github.com/yeokm1/swiftii](https://github.com/yeokm1/swiftii).\n\n## Motivation\n\nI recently restored an Apple II Plus that was generously donated to me. I wondered what modern abilities I can make it do.\n\nYears ago, I wrote a [DOS ChatGPT client](/2023/03/building-a-dos-chatgpt-client-in-2023/) and a [Slack client for Windows 3.1](/2019/12/building-a-new-win-3-1-app-in-2019-part-1-slack-client/). Those projects were about squeezing a modern network service into a vintage machine. This time I wanted to know if I can do the same for a modern programming language, one from Apple itself no less!\n\nThe inspiration for this app is [Apple Pascal](https://en.wikipedia.org/wiki/Apple_Pascal). Back in 1979, Apple Pascal brought the [UCSD p-System](https://en.wikipedia.org/wiki/UCSD_Pascal) to the Apple II. Rather than compile Pascal to native 6502 machine code, it compiled to bytecode that ran on a virtual machine similar to how Java does it.\n\nI wanted to do the same for Swift. The compiler emits bytecode and a virtual machine executes it. Almost half a century apart, both languages reach the 6502 by avoiding native 6502 code generation.\n\nI also wanted a REPL too if some users want to test their code in an interactive manner.\n\n## Target Hardware\n\nThe baseline target is the original 1977 Apple II with appropriate hardware upgrades applied. If SwiftII runs here, it runs on pretty much any future Apple II.\n\nThe machine in the photo above is a 1979 Apple II Plus, an incremental iteration over the original Apple II with features like Applesoft BASIC, more RAM and disk Autostart. Otherwise, it is largely similar to the original Apple II.\n\nMy II Plus has the following high-level specifications:\n\n- MOS 6502 CPU 1 MHz\n- 48 KB main RAM\n- Clone Saturn 128K RAM card (backward compatible as 16K Language Card)\n- Clone Videx Videoterm 80-column card\n- Yellowstone universal disk controller card (can emulate original Disk II mode)\n- Floppy Emu to emulate Disk II drives\n\nMore details on my own Apple II Plus setup are documented here: [https://github.com/yeokm1/retro-configs-apple/tree/main/apple-ii-plus](https://github.com/yeokm1/retro-configs-apple/tree/main/apple-ii-plus).\n\nThe original 1977 Apple II is supported as long as it has been upgraded to 48 KB RAM plus a 16 KB language card, giving the same 64 KB total. On a non-Autostart ROM machine you start the Disk II manually with `C600G`\n\nor `PR#6`\n\n. The Apple //e is the nicer machine to use, because it has lowercase support, a better display ROM, and a proper ASCII keyboard. On a //e, you can type SwiftII source much more naturally instead of relying on the Apple II Plus digraph and case-marker input layer.\n\nI used ProDOS 2.4.3 because it is the modern still-maintained ProDOS 8 revival that is well documented and still runs on an original Apple II. Using a single ProDOS reference image also made the disk build and testing much simpler than supporting multiple DOS/ProDOS variants. A minimum 16K Language Card is mandatory for ProDOS.\n\nThe 6502 is an 8-bit CPU. Its A, X and Y registers are all 8 bits wide and there is no general-purpose 16-bit register. There is **no floating-point unit**, and not even a hardware multiply or divide instruction. The hardware stack is only 256 bytes which limits function call depth.\n\nAs for memory, 64 KB is tiny by today’s standards more akin to a microcontroller.\n\n## The language\n\nHere is a taste of what SwiftII looks like. If you know Swift, you should find this very familiar.\n\n``` js\nlet answer = 42\nvar count = 0\n\nlet maybe: Int? = 5\nif let x = maybe { print(x) }\nlet n = maybe ?? 0\n\nwhile count < 10 { count += 1 }\nfor i in 0..<5 { print(i) }\n\nfunc greet(name: String) -> String {\n    return \"Hello, \\(name)!\"\n}\n\nvar xs = [1, 2, 3]\nxs.append(4)\nprint(\"\\(xs.count) items\")\n```\n\n### SwiftII supported features\n\nThe core reads exactly like Swift:\n\n`let`\n\nand`var`\n\nwith type inference`if`\n\n/`else if`\n\n/`else`\n\n,`while`\n\n, and`for-in`\n\nover ranges- Top-level functions with\n`return`\n\n- Optionals with\n`if let`\n\n,`??`\n\n, and force-unwrap`!`\n\n- Arrays with\n`append`\n\n,`count`\n\n,`isEmpty`\n\n, and subscripting - Strings with\n`+`\n\nconcatenation,`\\(...)`\n\ninterpolation, and`String(n)`\n\nBeyond that, additional capabilities depend on which disk you boot:\n\n- the\n**extras disks** add integer parsing (`Int(s)`\n\n), byte/string helpers (`asc`\n\n,`chr`\n\n), more array methods (`removeLast`\n\n,`removeAll`\n\n,`contains`\n\n),`peek`\n\n/`poke`\n\n, cursor/text control, and lo-res graphics (`gr`\n\n,`color`\n\n,`plot`\n\n,`hlin`\n\n,`vlin`\n\n); - the\n**compiler disks** go further with`switch`\n\n,`for-in`\n\ndirectly over arrays,`random(in:)`\n\n, speaker`tone`\n\n, and file I/O.\n\nSo the same source can be a compile error on one disk and run fine on another depending on which features you try on which disk.\n\n### Where the types differ from Swift\n\nTypes behave differently from conventional Swift:\n\n-\n`Int`\n\nis 16-bit and signed due to the cc65 limit. It ranges −32,768 to 32,767 and it wraps silently on overflow. (Conventional Swift is 64-bit on modern system) It is also the only numeric type, no`Double`\n\nand no`Float`\n\n. I will cover more on this in the[section below](/2026/06/swift-on-apple-ii/#no-floating-point-at-all). -\n`String`\n\nis just bytes, not Unicode. Modern Swift treats a string as a collection of`Character`\n\nobjects. SwiftII strings are ASCII byte sequences, closer to C-style string arrays. -\nItems in arrays have to be all the same type.\n\n-\nIdentifiers are capped at 11 characters to save space. A longer name is a hard compile error rather than a silent truncation, so two long names can never collide on a shared prefix.\n\nThere are also no no closures, no dictionaries, no error-handling or `throws`\n\n, no concurrency (`async`\n\n/`await`\n\n), no macros, and no call-site argument labels. Each would cost memory or single-pass-compiler complexity the machine obviously cannot spare.\n\n## The main constraint: 40,704 bytes\n\nTo understand the memory budget, you first have to understand the Apple II’s memory map. The 6502 can address 65536 bytes with its 16-bit address bus. SwiftII runs under [ProDOS](https://en.wikipedia.org/wiki/Apple_ProDOS) as a `SYS`\n\nbinary, which starts at `$2000`\n\n.\n\nSwiftII binaries are `SYS`\n\nprograms and do not leave room for `BASIC.SYSTEM`\n\nabove it, so the practical load-image ceiling is the contiguous range from `$2000`\n\nup to the ProDOS global page at `$BF00`\n\n-> `\\$2000-$BEFF`\n\n.\n\nI got AI to help me to draw up this infographic.\n\nThat region is exactly **40,704 bytes**. It is the entire main-RAM my binary can use.\n\n### Memory beyond 64K\n\n64KB is just enough to run core features of SwiftII but is not sufficient for more functions or bigger programs. However, memory beyond 64 KB is not directly addressable.\n\nMemory upgrades of that time period like Saturn 128K card or the //e’s 64K of auxiliary RAM does not extend the address space like modern linear memory. It sits behind a window, and software banks pieces of it in and out through soft switches. If you come from the DOS PC world, it is conceptually similar to [Expanded Memory Specification (EMS)](https://en.wikipedia.org/wiki/Expanded_memory). The extra RAM is there, but the CPU can only see the selected page at the selected address range.\n\nOn the Apple II this is especially awkward because the language-card window is also where ROM, ProDOS [ProDOS Machine Language Interface (MLI)](https://prodos8.com/docs/techref/calls-to-the-mli/) code also lives. Only one bank can be visible there at a time.\n\nI will come back to this topic later.\n\n## Development setup\n\nThe entire project is written largely in C90 and is compiled in two ways:\n\n**clang** on my Mac for the unit tests.**cc65** for the Apple II, where it actually ships.[cc65](https://cc65.github.io/)is a C cross-compiler for 6502 systems.\n\nWhy C and not Assembly? Getting AI to write in Assembly or directly into machine code binary itself could probably squeeze out more efficiency gains. But a project filled with 6502 assembly would be much harder for me to inspect and change directly.\n\nC keeps the project at a level where, if the AI tools are wrong/unavailable or I have to debug the last mile myself, I can still understand and modify the code in a feasible way. I think the tradeoff is justified.\n\n## How the language works\n\nA SwiftII program flows through a conventional-looking pipeline:\n\n``` php\nSwift source ---> lexer    ---> compiler ---> bytecode ---> VM ---> output\n                  (tokens)      (Pratt)       (.swb)\n```\n\nMost language implementations parse source into an [Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) (AST), then walk that tree to generate code. However SwiftII cannot afford the tree as there is no memory to hold one.\n\nInstead it uses a single-pass [Pratt parser](https://en.wikipedia.org/wiki/Operator-precedence_parser) that emits bytecode directly as it reads the source. There is no intermediate representation. This is inspired by Robert Nystrom’s book [Crafting Interpreters](https://craftinginterpreters.com/).\n\n### From source text to bytecode\n\nFor readers who have not written a compiler before, the important point is that SwiftII does not “understand” the whole program at once. It reads a token, decides what small piece of language it is looking at then appends VM instructions to a byte buffer.\n\n(Given the complexity of this section and to ensure accuracy, I got AI to significantly draft and vet this part)\n\nTake this line:\n\n``` js\nlet x = 1 + 2\n```\n\nThe lexer first turns the characters into a stream of tokens:\n\n```\nLET  IDENT(x)  EQUAL  INT(1)  PLUS  INT(2)  EOF\n```\n\nThe statement parser sees `LET`\n\n, so it knows it is compiling a variable declaration. It records `x`\n\nin the globals table, then asks the expression parser to compile the right-hand side.\n\nThe expression parser is where Pratt parsing helps. Each operator has a precedence. When it sees `1 + 2`\n\n, it emits “push 1”, sees `+`\n\n, notices that `+`\n\nbinds to the next value, emits “push 2”, then emits “add”. There is no `BinaryExpr(left: 1, op: +, right: 2)`\n\nobject sitting in memory. The output bytes are produced as the parser goes:\n\n```\nsource fragment     bytecode emitted\n---------------     ----------------\n1                   OP_INT_U8 1\n2                   OP_INT_U8 2\n+                   OP_ADD\nlet x = ...         OP_DEFINE_GLOBAL #0\n```\n\nThe VM is a stack machine. So this bytecode pushes the operands, runs an opcode that pops them, pushes the result then stores the result into global slot 0.\n\nTop-level functions are also just bytecode ranges. When the compiler sees `func greet(...)`\n\n, it records the function’s start offset, parameter count and return flag in a small function table, then emits the function body into the bytecode arena. A later call does not carry a function pointer, it carries the function-table index.\n\nStrings take a slightly different path. The compiler copies each literal into the constant heap and emits `OP_STR <offset>`\n\n. Therefore the `.swb`\n\nfile has to carry both bytecode and the constant pool. The instructions say “load the string at heap offset N”, and the runner must recreate the same constant heap layout before executing the program.\n\n### What the bytecode actually looks like\n\nEach instruction is **1 byte of opcode followed by 0 to 2 bytes of inline operand**. There are no multi-byte opcodes or prefixes.\n\nThe opcodes are grouped by rough function: constants, stack manipulation, variables, arithmetic, comparison, control flow, calls, optionals and heap objects. The upper range is left reserved for things SwiftII does not have yet, such as closures, structs, dictionaries, classes and protocols.\n\nHere is `let x = 1 + 2`\n\nfollowed by `print(x)`\n\n, assuming `x`\n\nis global #0 and `print`\n\nis builtin #0, compiling to twelve bytes:\n\n```\n03 01       OP_INT_U8 1          ; push the small int 1\n03 02       OP_INT_U8 2          ; push the small int 2\n30          OP_ADD               ; pop two, push their sum\n22 00       OP_DEFINE_GLOBAL #0  ; bind it to x\n20 00       OP_GET_GLOBAL #0     ; push x back\n63 00 01    OP_CALL_BUILTIN print, argc=1\n```\n\nBecause the single-pass compiler does not track operand types deeply, several opcodes are polymorphic at runtime. `OP_ADD`\n\ndoes integer addition when both operands are ints, but dispatches to heap string concatenation when both are strings, so the language gets `\"a\" + \"b\"`\n\nwithout a second concat instruction.\n\nString interpolation works the same way, one opcode inspects the value’s tag and renders an `Int`\n\n, `Bool`\n\nor `nil`\n\nto text.\n\n### No floating point at all\n\nSwiftII has no `Double`\n\n, no `Float`\n\nand no decimals anywhere. `Int`\n\nis a 16-bit signed integer and that is the only numeric type available.\n\nThe cc65 compiler itself does not support float. The 6502 CPU itself does not support float. Adding in a float library would severely bloat the binary size.\n\nOn a side note, Apple’s own [Embedded Swift](https://www.swift.org/blog/embedded-swift-examples/), the trimmed-down dialect aimed at microcontrollers, also treats floating point carefully. For a long time you could not even print a `Double`\n\n, because the standard-library routine to do so was not available. That was only [filled in with an all-Swift implementation in Swift 6.3](https://www.swift.org/blog/embedded-swift-improvements-coming-in-swift-6.3/).\n\nIf even constrained Swift treats floating point as something to handle carefully, an Apple II from the 1970s would face more difficulty.\n\nApplesoft BASIC that comes with the II Plus does support float through its floating point routines. However this was done by holding it directly in the ROM which I cannot do. I also understand floating point performance on the Apple II is not that good too.\n\n## Two families of binary\n\nThe same source tree ships as **two families** of binary.\n\nThe reason is the design and usage of Apple II’s 16 KB [language card](https://en.wikipedia.org/wiki/Language_Card), which lives at `\\$D000-$FFFF`\n\n. ProDOS keeps its MLI in that same region.\n\n-\n**Family A** cointains the REPL. To fit, the interpreter spills cold code into the language card. This overwrites the ProDOS MLI, so Family A cannot do general file I/O after the interpreter starts. It can run programs staged by the launcher, but it cannot freely open and read arbitrary files. -\n**Family B** contains the**compiler plus runner**. These are MAIN-only tools, so they leave the language card free and the ProDOS MLI stays alive.\n\nBoth family disks contain the launcher which integrates the file browser and editor.\n\nThe Family B compiler reads in a `.swift`\n\nsource file and emits `.swb`\n\nbytecode onto disk for the runner to execute.\n\nThe `.swb`\n\nfile has a 12-byte header (the three-character magic `SWB`\n\n, a one-byte format version, the entry program counter, and the section lengths), followed by bytecode, the constant pool, and a 4-byte record per function.\n\nFamily B itself ships in three tiers depending on how much spare RAM the machine has.\n\n| Tier | Machine | Max total bytecode |\n|---|---|---|\n| Tier 1 (flat) | II Plus 64K | 1,834 bytes |\n| Tier 2 (Saturn) | II Plus + Saturn 128K | ~36 KB |\n| Tier 3 (aux) | //e + 64K aux | ~36 KB |\n\nOn a stock machine you get **1,834 bytes of bytecode**. To go bigger, a Saturn 128K or //e 64K auxiliary RAM card (such as 80-column Extended Text card) is used to page completed function bytecode bodies out of main memory and into spare RAM.\n\nThe extra capacity mostly helps function-heavy programs, since each individual function and the top-level `main`\n\nstill have to fit a resident window.\n\n### Why quitting the REPL reboots the machine\n\nIf you watched the above video carefully, you would notice that upon quiting the REPL, the system has to go through a reboot process before going back to the launcher menu.\n\nThis looks heavy-handed, but it is forced. To return cleanly, the interpreter would have to read the launcher’s `SYS`\n\nfile from disk and jump into it. Reading a file requires the ProDOS MLI, but the interpreter has overwritten the MLI with its own code. The routine needed to load the next program no longer exists in memory.\n\nThe full-screen editor is compiled into the launcher rather than being a separate program. The launcher is Main-only and keeps the ProDOS MLI alive, so the editor can open and save `.swift`\n\nfiles directly.\n\nSince the file selector and editor are still the same running program, moving from browser to editor and back is much faster than chaining a separate `SYS`\n\nfile each time.\n\n- Ctrl-Q returns to the file browser with no reboot.\n- Ctrl-R saves your file, stages the source, chains the interpreter to run it\n\n## “What you type” vs “what you see” vs “what is stored”\n\nOn modern computers, three things are usually the same.\n\n**what you type** on the keyboard**what you see** on screen**what gets stored** in the file\n\nYou press `[`\n\n, a `[`\n\nappears on the display, and the byte for `[`\n\nlands in the file. On the original Apple II and II Plus, those three paths diverge.\n\nThe II/II+ keyboard produces only uppercase and lacks keys for characters Swift needs. The display has only the original character ROM glyphs, so it cannot draw lowercase or braces directly. The file on disk, however, still needs to be normal ASCII SwiftII source so the same `.swift`\n\nfile works no matter which Apple II wrote it.\n\nSwiftII therefore keeps storage canonical and translates at the edges on display and keyboard.\n\n| You type (on a II Plus) | You see on screen | Stored on disk |\n|---|---|---|\n`PRINT` |\n`PRINT` (normal video) |\n`print` (lowercase) |\n`'INT` |\ninverse-video `I` , then `nt` |\n`Int` |\n`<:` `:>` |\n`[` `]` |\n`[` `]` |\n`??/` |\n`\\` |\n`\\` |\n`<%` |\n`<%` |\n`{` (one byte, `$7B` ) |\n\nThe next two sections describe the input and output sides of that translation.\n\n### Typing Swift on a 1977 keyboard for II and II+\n\nSwift source is lowercase ASCII full of punctuation. The II/II+ keyboard cannot type lowercase, backslash, `{ } [ ]`\n\n, `_`\n\nor backtick.\n\nSo SwiftII puts an input translation layer between the keyboard and the saved file. It auto-lowercases letters, uses `'`\n\nas a one-shot uppercase marker (`''`\n\nfor a full word), maps `Ctrl-W`\n\nto `_`\n\n, and borrows the [C-standard digraphs and trigraphs](https://en.wikipedia.org/wiki/Digraphs_and_trigraphs_%28programming%29) for missing punctuation:\n\nIn the screenshot, `'INT`\n\nbecomes `Int`\n\nin storage, and inverse video marks actual uppercase letters where the II Plus cannot draw lowercase shapes. The same rule handles camelCase too. `readLine`\n\nis typed as `READ'LINE`\n\n, and a double `''`\n\nmarks a whole run of capitals at once.\n\nThat apostrophe marker is important because Swift mixes upper and lower case inside identifiers. A type like `Int`\n\nis typed `'INT`\n\n, and `String`\n\nis typed `'STRING`\n\n.\n\nOn a //e with a normal keyboard, you type the program as-is. The `.swift`\n\nfile on disk is the same regardless of which machine authored it.\n\n### Text output on the Apple II\n\nIf there is one part of this project where the effort outweighed what users actually see, it is putting characters on the screen.\n\nI had to keep in mind 4 different output paths to interface the display, character set and column count.\n\n| Path | Machine / mode | How SwiftII writes text |\n|---|---|---|\n| Pre-IIe 40-column | Original Apple II / II Plus | Lowercase becomes normal uppercase, uppercase becomes inverse-video uppercase, and missing glyphs such as `{` are displayed as digraphs like `<%` . |\n| Videx 80-column | II Plus with Videx Videoterm | Supports all display characters. |\n| //e 40-column | Apple //e text mode | Supports all display characters |\n| //e 80-column | Apple //e with 80-column firmware | Supports all display characters |\n\nThe way to handle the 4 screen types are not the same even for those that support all display characters but it’s too much to cover here.\n\n## Developing Swift on the machine itself\n\nFor a development environment to be useful, one cannot just write the interpreter. There needs to be a means of file handling as well as to edit the files.\n\nThese tools did not exist in a satisfactory way for me on this machine so I had to build them.\n\nBoth exist inside the same launcher binary so they get real file access and a reboot-free return to the menu.\n\n### The file browser\n\nThe file browser is a small three-pane file manager. The left pane shows the parent directory for context, the right pane is the current directory with the highlight. A details line reports each entry’s ProDOS type and size.\n\nBottom pane is the preview. When the user dwells the cursor for at least 1.5s, the pane will display a scrollable preview of the file. The delay reduces unnecessary disk reads. The pane renders `.swift`\n\nsource with exactly the same case and digraph rules the editor uses, so a program looks identical whether you are previewing or editing it.\n\nBecause the II Plus keyboard has no up/down arrow keys, navigation uses `I`\n\nand `M`\n\nfor up and down.\n\n### The editor\n\nThe REPL takes one line at a time, which is fine for trying some short expressions without saving but infeasible for writing a real multi-line program. So I need a proper editor that is aware of the digraph and case limitations and is able to save canonical Swift to disk.\n\nCooked / DGR mode: Digraphs and case markers |\nRaw mode: Shown on II+ that cannot display lowercase |\n\nIt has a **cooked mode and a raw mode**. A `.swift`\n\nfile opens “cooked”, with the digraph and case-marker support. A plain text file opens byte-for-byte verbatim, because the typing model is a Swift-source convention that would only mangle a README. `Ctrl-G`\n\nflips between them, reloading the file so what you see and what is stored stay consistent, and a `[DGR]`\n\nor `[RAW]`\n\ntag on the top status line tells you which you are in.\n\nThe cursor is careful about the display mismatch from earlier. A `{`\n\nis one byte in the buffer but shows as `<%`\n\n, two columns wide, on a II Plus. Arrowing across it moves one byte at a time in memory even as the cursor visibly jumps two columns, with a digraph-aware width calculation keeping the column maths honest.\n\nThe key bindings deliberately follow **Apple Pascal** style for familiarity:\n\n| Action | Key Binding | Notes |\n|---|---|---|\nMove Left / Right |\n`Left Arrow` / `Right Arrow` |\nII+ keyboard has this |\nMove Up |\n`Ctrl-O` |\nStandard UCSD / Apple Pascal |\nMove Down |\n`Ctrl-L` |\nStandard UCSD / Apple Pascal |\nSave |\n`Ctrl-S` |\n|\nSave and Run |\n`Ctrl-R` |\n|\nQuit |\n`Ctrl-Q` |\nDrops back to the file browser |\n\nOn a IIe with no similar display limitations, everything can be display as-is.\n\n## The trouble with memory banking\n\nThis memory banking caused a lot of pain.\n\nThe Apple II does not treat extra RAM as one larger flat address space. The 6502 can only address 64 KB, so extra memory appears by replacing `\\$D000-$FFFF`\n\nwith another physical backing. It can hold:\n\n- Motherboard ROM\n- ProDOS’s MLI code\n- SwiftII’s language-card code\n- Selected Saturn bank\n\nThat makes banking both useful and dangerous. It gives SwiftII somewhere to put code that will not fit in main RAM, but it also means a routine can accidentally hide the ROM service or ProDOS MLI code it is about to call.\n\nThe Family A REPL is far larger than what can remain resident in main RAM so some cold code has to be banked.\n\nMain RAM below `$C000`\n\nremains visible, but code in the `\\$D000-$FFFF`\n\nlanguage-card window depends on the current bank state, so every call into or out of that window needs a carefully managed bank switch.\n\nThe split of what code is in main RAM or banked is based on my expected execution frequency. I classify the cold code as graphics, `peek`\n\n/`poke`\n\netc. These are parked into the Saturn 128K card or the //e’s 64K of aux RAM.\n\nThe two cards need different methods to utilise the banked data:\n\n- Saturn bank-select allows code in the banked window to execute in place from the card.\n- //e’s aux RAM code cannot run in place so the cold code has to be first copied into a main-RAM staging buffer and run from there.\n\nFamily B’s compiler and runner use the same spare RAM differently to store bytecode. The runner streams through a small main-RAM window as it executes rather than running in place.\n\nAll of this is transparent to the SwiftII programmer.\n\n## Why this project ships as 8x disks\n\nAll of these constraints result in one outcome, SwiftII is not a single download. It is **8x disk images**, and the reasons trace straight back to the sections above.\n\nAs mentioned previously, memory limits constrain which features can be banked.\n\nA Family A REPL overrides the ProDOS MLI IO functions but the Family B compiler-runner needs them to read/write bytecode to disk. So both cannot be same binary, because one evicts ProDOS and the other needs it.\n\nSo the release is 4x Family A REPL disks, 3x Family B compiler disks, and 1x shared data disk.\n\n### Family A: REPL\n\n| Disk | For | Features |\n|---|---|---|\n`iip-lite-repl` |\nany II / II Plus (64K) | core-language REPL, editor and file browser |\n`iip-sat-repl` |\nII Plus with a Saturn 128K card | core + graphics, `peek` /`poke` , speaker, and 80-column via Videx if fitted |\n`iie-lite-repl` |\n//e | core + native lowercase, 80-column text if an 80-column firmware card is present |\n`iie-aux-repl` |\n//e with 64K aux RAM | core + graphics, `peek` /`poke` , speaker, and 80-column |\n\n### Family B: Compiler and Runner\n\nAll 3x disks compile a `.swift`\n\nfile to a `.swb`\n\nbytecode file and then run it, and they share the biggest feature set in the project. On top of the core language they add:\n\n- file I/O —\n`readFile`\n\n,`writeFile`\n\n,`appendFile`\n\n,`listDirectory`\n\n`switch`\n\n,`for-in`\n\ndirectly over arrays, and`random(in:)`\n\n- sound (\n`tone`\n\n) and timing (`wait`\n\n) `abs`\n\n/`sgn`\n\n, and string`hasPrefix`\n\n/`hasSuffix`\n\n- the full graphics and\n`peek`\n\n/`poke`\n\nset\n\nThe three disks differ only in how large a program they can hold, which is set by where the bytecode is paged:\n\n| Disk | For | ~Bytecode size |\n|---|---|---|\n`iip-compiler` |\nany II / II Plus | 1.8 KB |\n`iip-sat-compiler` |\nII Plus with a Saturn 128K card | 36 KB |\n`iie-compiler` |\n//e with 64K aux RAM | 36 KB |\n\nThe eighth disk is `data`\n\n, it contains sample programs and the test suite, mounted in drive 2 alongside any of the above.\n\nI thought of auto-detecting the machine at boot and adapt to every configuration. I deliberately did not, for two reasons.\n\n-\nIt is not the 1970s anymore. Shipping eight\n\n`.po`\n\nfiles costs nothing, whereas an extra physical floppy in that era would have been costly. The old reason to force everything into one disk is gone. -\n**I do not own most of these machines**. So an adaptive runtime path would be code I could never fully verify.\n\n## Testing too many machines\n\nEverything above explains why testing became its own substantial project. Each of the eight images targets a slightly different machine.\n\n- Original Apple II (Plus) with language card\n- II Plus with Saturn\n- II Plus with Videx 80-column\n- //e\n- //e with auxiliary RAM\n\nA single `make ci`\n\nruns the plain C unit tests, builds all eight disk images, and checks every binary against its size budget.\n\nThe plain C unit tests are logic tests that run on my modern host Mac, but we still need to run the software on actual or emulated 6502 systems with ProDOS for verification.\n\nInstead of launching the emulator one by one with each hardware and disk configuration and checking it by hand, I decided an automated acceptance harness is necessary. It drives the entire UI flow under a headless build of [izapple2](https://github.com/ivanizag/izapple2), a portable Apple II Plus///e emulator written in Go.\n\nOne command sweeps the hardware matrix, boots the right emulated machine, injects keystrokes, scrapes the screen and reports pass/fail.\n\nThe acceptance harness drives the full suite across emulated Apple II configurations in about half an hour.\n\n## How it was built with AI\n\nSwiftII was built with heavy AI assistance, mostly Claude Code (running Opus 4.8) and Codex (GPT 5.5).\n\nFor context, I did the project in my spare time over a 2-month period, using the entry-level Claude Pro and ChatGPT Plus plans rather than the more expensive tiers.\n\n**Without AI, this project would not have been feasible for me as a hobby project.**\n\nBy hand at a similar standard, I think this would have taken well over 2-3 years work on the side or more.\n\n### The ground rules\n\nI set up some initial rules with AI’s help and fine-tuned them.\n\n**The budget is real.** Code that works but is twice as large as it needs to be has still failed. The binding target is always the 64 KB II Plus, and every feature is costed in bytes.**Write the design doc first.** Any non-obvious choice, zero-page allocation, value representation, bytecode format, banking scheme, had to be written up with alternatives and costs before code.**Bring me a decision with options, not an open question.**“How should I do this?” is not.** Try a feature, measure its real memory cost, and drop it if it isn’t worth it.**\n\n### The calls that were mine\n\nMy highest-leverage moments were usually architectural or about scope:\n\n**Ship one disk image per machine, not a runtime probe.** The AI had built a machine-detection scheme. I killed the approach because I cannot verify a probe on hardware I do not own. I’m not sure how accurate emulators are to this. In 2026 shipping more`.po`\n\nfiles costs nothing.**Floating point is not worth it.** The AI got actually got floating point working. But I cut it because it halved my already very limited heap for a feature very few demos needed.**Hot code in main RAM, cold code in the bank.** I controlled what stays resident in main RAM and what gets banked.**Raise the heap on the Saturn and aux tiers.** Those machines have spare RAM, so spend some of it giving real programs more room to allocate rather than leaving it idle.**Move features to the right family.** Whenever the AI started contorting the lite REPL to do something the on-disk compiler does better, the answer was to move the feature, not to cram it in.**Apple Pascal key bindings.** I pulled the editor back to the familiar UCSD conventions after the AI invented its own cleverer scheme nobody would recognise.**Testing on more than one emulator**: I tested on both Mariani and izApple2 emulators to not overfit my program’s behaviour to one emulator in case there are individual emulator bugs or quirks.\n\n### Claude and Codex did different jobs\n\nI leaned on two AI tools, and they were useful in different ways.\n\n**Claude Code** was stronger at greenfield work, initial architecture and scaffolding. The flip side is that it ran out of usage faster. It sometimes slipped during implementation and did not always keep every relevant design document in sync. I find that Claude is willing to push back on decisions or design choices that it deems to not be optimal.\n\n**Codex** was faster, more literal in following my instructions and useful as a checker over work already done. It was also better and more succinct at documentation, so it helped keep the written record tidy.\n\nThe rough division that emerged was Claude for initial structure and hard greenfield pieces, Codex for fast execution, verification and documentation cleanup.\n\n### Why autonomous mode kept tripping here\n\nFully auto or trying to single shot the project did not work well here. I suspect it’s because the architecture decisions are highly unusual. I’m sure development information for vintage computers especially one for an almost half-century Apple II is relatively thinner in the training data than modern web or app development.\n\nSo I managed the AI carefully. Plan first, approve or redirect, then edit. The AI could move fast, but I had to keep pointing it back at the limitations and what I really wanted.\n\n### The documentation as the AI’s memory\n\nOne more workflow detail mattered a lot, **limited AI context windows and session boundaries**.\n\nA coding AI can only hold so much in its context window at once, and when a session ends, it forgets everything. The next session starts cold, with no memory of why a decision was made previously.\n\nThe solution was to make the project’s documentation the durable memory that the AI itself lacks. SwiftII was built as **18 numbered phases**, 0 through 17, each a self-contained goal with a written record of what it was for and what shipped. The key milestones:\n\n**Phases 0–1**— scaffolding, then the real lexer, single-pass compiler and bytecode VM, with`1 + 2`\n\nprinting`3`\n\nend to end on both the host and the Apple II.**Phases 2–3**— the interactive REPL, then strings and control flow, which also settled the pre-IIe keyboard and display model.** Phase 4**— functions, optionals and arrays: the point where it starts to feel like Swift.** Phase 8**— hardware-capability detection and the multi-binary build matrix, the root of the lite-versus-extras split.** Phases 11–13**— the //e aux extras binary, 80-column text, and running`.swift`\n\nprograms straight off a disk.**Phases 14–15**— the in-launcher editor and the data disk, then the Family B on-disk compiler and runner for programs too big for the REPL.**Phases 16–17**— stretch features (`switch`\n\n,`random`\n\n,`tone`\n\n, paged bytecode, Videx 80-column) and the polish-and-ship pass for v1.0.\n\nAround the phases sit roughly **twenty numbered design documents**, each capturing one non-obvious decision. What was chosen, what the alternatives were and how it was done. The most useful ones you can considering reading are:\n\n**003**— the Apple II Plus typing model (auto-lowercase, case markers, digraphs).** 008**— the Phase 6 optimisation playbook, the standing list of where to reclaim bytes.** 011**— banking the extras into a Saturn card or //e aux RAM (the XLC trampoline and copy-down).** 013**— 80-column text on the //e firmware and the II Plus Videx.** 015 and 016**— the Family B compiler/runner toolchain and the streaming source window.** 018**— the on-target, self-advancing test harness.** 020**— the Saturn paged runner and a hard-won slot-clobber paging bug.\n\nI treated those as **handoff documents**. When a fresh session starts, or when I switch tools, it reads the roadmap, only the relevant design docs, and the lessons. That is how a brand-new context window inherits accumulated judgement instead of re-making old mistakes. The phased structure also keeps each unit of work small enough to fit in one session.\n\nAs the codebase and documents became bigger, each session had to load more context just to get oriented, so token budget became a real workflow constraint. The documentation helped both me and the AI to keep only the most important information “in context” for the task at hand.\n\n## Try it\n\nThe easiest way to try SwiftII is in an emulator, no toolchain required. Download a disk image (`.po`\n\n) from the [GitHub Releases](https://github.com/yeokm1/swiftii/releases) and open it in an Apple II emulator: [Mariani](https://github.com/sh95014/Mariani) on macOS, [AppleWin](https://github.com/AppleWin/AppleWin) on Windows, or [izapple2](https://github.com/ivanizag/izapple2/releases) which is cross-platform. Start with `swiftii-iip-lite-repl-vX.X.X.po`\n\nfor the broadest compatibility.\n\nIf you have real hardware, each disk is a standard 140 KB ProDOS 5.25\" image. Write the appropriate disk for your system to a floppy with [ADTPro](https://adtpro.com/), or run it from a floppy emulator like a [BMOW Floppy Emu](https://www.bigmessowires.com/floppy-emu/). The original Apple II from 1977 runs it too, as long as it has been upgraded to 48 KB RAM and a 16 KB Language Card.\n\nIf you want the toolchain, the repo builds the whole disk set from source with cc65. You can also cross-compile a `.swift`\n\nto a `.swb`\n\non your Mac and run it on the Apple II.\n\n## Conclusion\n\nIn the end, this was a labour of love and a nod to the legacy of the Apple II and Apple Pascal. Almost 50 years ago, Apple Pascal proved you could run a high-level language on tiny hardware by compiling down to bytecode. SwiftII is just my attempt to apply that exact same philosophy to a Swift-like language.\n\nA huge portion of the architectural credit goes to Bob Nystrom, the VM implementation is heavily adapted from his phenomenal Crafting Interpreters book. Equally important is the cc65 project, whose 6502 C cross-compiler made this entire endeavour technically viable.\n\nMassive thanks to Steve Wozniak for the Apple II. By designing it with an well-documented, open expandable architecture, he didn’t just contribute immensely to the home computer revolution, he built a timeless machine that continues to teach us about fundamental, bare-metal computing almost half a century later.\n\nThere is something deeply poetic about taking a language created by modern Apple and bringing it full circle back to the 8-bit hardware where the company began. The Swift team really did well by designing a language elegant enough to make this experiment even remotely feasible on such old hardware.\n\nIt’s a nice little perk that ProDOS allows 15-character filenames. You don’t have to squash everything into an archaic DOS 8.3 format for example, meaning you can actually use the real `.swift`\n\nextension. It’s a small detail but something worth pointing out.\n\nI’m amazed at what can still be accomplished for vintage systems with AI tools and a persistent human. A one-person hobby project of this size simply was not feasible a couple of years or even months ago.\n\nI hope you enjoyed reading this as much as I enjoyed building it. I dedicate this project to Steve Wozniak and the Swift team at Apple.\n\nThere is only so much technical detail of this project that I can cover in this blog post without making it too long. If you are keen to know more, the full documentation can be found at the [SwiftII repository’s docs directory](https://github.com/yeokm1/swiftii/tree/main/docs).", "url": "https://wpnews.pro/news/bringing-swift-to-the-apple", "canonical_source": "https://yeokhengmeng.com/2026/06/swift-on-apple-ii/", "published_at": "2026-06-25 08:26:54+00:00", "updated_at": "2026-06-25 08:43:47.561354+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence"], "entities": ["Apple II", "Swift", "SwiftII", "MOS 6502", "Embedded Swift", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/bringing-swift-to-the-apple", "markdown": "https://wpnews.pro/news/bringing-swift-to-the-apple.md", "text": "https://wpnews.pro/news/bringing-swift-to-the-apple.txt", "jsonld": "https://wpnews.pro/news/bringing-swift-to-the-apple.jsonld"}}