Bringing Swift to the Apple ][ 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. 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. I built SwiftII , a Swift-flavored mini development environment for the original Apple II to a //e and up. To 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. One 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. My 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. Other 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. Contents This post is quite long, so here are the main sections for you to jump straight to: 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 The App Boot the appropriate disk and you land in a launcher. You can select an interactive REPL, a file browser that runs .swift programs straight from the disk, a full-screen editor, all self-contained on one bootable floppy. Here is a demo video of the app running on my original Apple II Plus. It 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. The original II Plus keyboard has no lowercase and no \ key, so the program is typed with digraphs https://en.wikipedia.org/wiki/Digraphs and trigraphs %28programming%29 . ??/ becomes \ which SwiftII interprets as canonical Swift. More on that quirk later. Here are some sample screenshots: If 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 . Motivation I recently restored an Apple II Plus that was generously donated to me. I wondered what modern abilities I can make it do. Years 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 The 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. I 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. I also wanted a REPL too if some users want to test their code in an interactive manner. Target Hardware The 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. The 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. My II Plus has the following high-level specifications: - MOS 6502 CPU 1 MHz - 48 KB main RAM - Clone Saturn 128K RAM card backward compatible as 16K Language Card - Clone Videx Videoterm 80-column card - Yellowstone universal disk controller card can emulate original Disk II mode - Floppy Emu to emulate Disk II drives More 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 . The 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 or PR 6 . 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. I 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. The 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. As for memory, 64 KB is tiny by today’s standards more akin to a microcontroller. The language Here is a taste of what SwiftII looks like. If you know Swift, you should find this very familiar. js let answer = 42 var count = 0 let maybe: Int? = 5 if let x = maybe { print x } let n = maybe ?? 0 while count < 10 { count += 1 } for i in 0..<5 { print i } func greet name: String - String { return "Hello, \ name " } var xs = 1, 2, 3 xs.append 4 print "\ xs.count items" SwiftII supported features The core reads exactly like Swift: let and var with type inference if / else if / else , while , and for-in over ranges- Top-level functions with return - Optionals with if let , ?? , and force-unwrap - Arrays with append , count , isEmpty , and subscripting - Strings with + concatenation, \ ... interpolation, and String n Beyond that, additional capabilities depend on which disk you boot: - the extras disks add integer parsing Int s , byte/string helpers asc , chr , more array methods removeLast , removeAll , contains , peek / poke , cursor/text control, and lo-res graphics gr , color , plot , hlin , vlin ; - the compiler disks go further with switch , for-in directly over arrays, random in: , speaker tone , and file I/O. So 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. Where the types differ from Swift Types behave differently from conventional Swift: - Int is 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 and no Float . I will cover more on this in the section below /2026/06/swift-on-apple-ii/ no-floating-point-at-all . - String is just bytes, not Unicode. Modern Swift treats a string as a collection of Character objects. SwiftII strings are ASCII byte sequences, closer to C-style string arrays. - Items in arrays have to be all the same type. - Identifiers 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. There are also no no closures, no dictionaries, no error-handling or throws , no concurrency async / await , no macros, and no call-site argument labels. Each would cost memory or single-pass-compiler complexity the machine obviously cannot spare. The main constraint: 40,704 bytes To 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 binary, which starts at $2000 . SwiftII binaries are SYS programs and do not leave room for BASIC.SYSTEM above it, so the practical load-image ceiling is the contiguous range from $2000 up to the ProDOS global page at $BF00 - \$2000-$BEFF . I got AI to help me to draw up this infographic. That region is exactly 40,704 bytes . It is the entire main-RAM my binary can use. Memory beyond 64K 64KB 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. Memory 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. On 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. I will come back to this topic later. Development setup The entire project is written largely in C90 and is compiled in two ways: 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. Why 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. C 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. How the language works A SwiftII program flows through a conventional-looking pipeline: php Swift source --- lexer --- compiler --- bytecode --- VM --- output tokens Pratt .swb Most 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. Instead 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/ . From source text to bytecode For 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. Given the complexity of this section and to ensure accuracy, I got AI to significantly draft and vet this part Take this line: js let x = 1 + 2 The lexer first turns the characters into a stream of tokens: LET IDENT x EQUAL INT 1 PLUS INT 2 EOF The statement parser sees LET , so it knows it is compiling a variable declaration. It records x in the globals table, then asks the expression parser to compile the right-hand side. The expression parser is where Pratt parsing helps. Each operator has a precedence. When it sees 1 + 2 , it emits “push 1”, sees + , notices that + binds to the next value, emits “push 2”, then emits “add”. There is no BinaryExpr left: 1, op: +, right: 2 object sitting in memory. The output bytes are produced as the parser goes: source fragment bytecode emitted --------------- ---------------- 1 OP INT U8 1 2 OP INT U8 2 + OP ADD let x = ... OP DEFINE GLOBAL 0 The 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. Top-level functions are also just bytecode ranges. When the compiler sees func greet ... , 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. Strings take a slightly different path. The compiler copies each literal into the constant heap and emits OP STR