Simple Assembler and Simple CPUs First
2025-10-22
My Programming Journey: From Assembly to Modern Paradigms
My programming education began with CPUs and assembly language. This foundation proved invaluable—when I later explored higher-level languages, I immediately understood their purpose: to help developers generate assembly code faster and more effectively.
The Assembly Language Foundation
Fundamentally, there’s only one programming language: assembly. Every higher-level language ultimately compiles down to machine code. This perspective helps us evaluate programming languages by asking two key questions: “What benefits does this provide?” and “Are the trade-offs acceptable?”
My learning approach involved sampling multiple languages to expand my thinking and discover diverse problem-solving approaches. Reading and debugging others’ code forced me to develop deep comprehension of different programming styles.
The C-Centric Programming Model
Most popular languages resemble C, adopting a synchronous, sequential programming model—essentially a clockwork design that mirrors how CPUs and assembly language operate—along with a call stack and additional code that transfers data via the call stack (though this extra code has been whittled down over time by transferring work to the hardware).
CPUs were originally conceived as programmable integrated circuits that could be connected to memory chips, replacing hard-wired ICs that performed only single, fixed operations.
The trade-off was significant: ideally, each thread would have its own CPU, but the cost was prohibitive in the early days of computing. Today, the tables have turned—we can now afford to give each thread an isolated CPU with private memory.
The Evolution of Resource Sharing
To conserve resources, we developed time-slicing—splitting CPU cycles among multiple applications. With ten applications running, each operated at one-tenth speed. When an application waited for I/O operations, it would yield CPU time to others, improving overall system performance.
This common pattern evolved into shared code libraries we now call “operating systems.” The cost? All applications must share the same CPU and memory, creating complications like the need for thread safety. We wouldn’t need thread safety if we could isolate each application on its own CPU with private memory—something unaffordable in early computing.
We developed workarounds for these limitations and, crucially, applied them universally—even to applications that didn’t need the workarounds. We became so accustomed to these constraints that we forgot they weren’t inevitable.
The Modern Irony
Today, we can afford dedicated CPUs per thread, yet we continue programming as if we’re still resource-constrained. We’ve forgotten the original, simpler models and still assume shared memory is necessary for inter-application messaging.
Expanding Your Programming Horizons
If possible, programmers should explore languages fundamentally different from C: Prolog, Tiny Basic, Forth, and Smalltalk offer valuable perspectives.
I no longer consider C a “low-level language.” C established conventions like using the call stack to pass data between functions—a specific design choice, not an inevitability. Forth parameterizes code differently. Early Lisps, written in assembly, built compact engines for list handling. Lisps represent code as data structures (not text), enabling runtime code manipulation through list operations—the origin of macros.
Most compiled languages, including C, don’t allow runtime code manipulation. This capability is both powerful and dangerous, but understanding it opens new problem-solving pathways.
The REPL Advantage
Early languages often featured compact assembly-written kernels that interpreted code at runtime, providing REPLs (Read-Eval-Print Loops) that boosted productivity in ways largely lost to modern development environments.
State Machines and Statecharts
Coming from electronics, I learned that state machines enhance productivity, rigor, and correctness. Discovering Harel’s paper on Statecharts revealed tremendous potential in these concepts and demonstrated that parts of programs could be expressed in diagram form.
Choosing the Right Tool
No single approach—Statecharts, C, or functional programming—should be applied universally. We need workflows that combine solutions written in different paradigms. Current approaches fall short; code libraries seem to provide this flexibility, but functions introduce ad-hoc blocking where callers must wait for callees to return values.
UNIX pipelines offered a glimpse of better productivity, but their potential was limited by:
Text-based syntax in UNIX scripting languages, which severely limits how conveniently dataflow via pipelines can be expressed
Complex internal implementations (essentially Greenspunian closures that appear more complicated than necessary)
Narrow application to text processing
Confusion between nested functions (LIFO dataflow) and pipelines (FIFO principles), which produce vastly different results
Lessons from Electronics
Electronics already solved many of these problems, though we didn’t recognize the solutions. ICs aren’t limited to single input/output pins. Schematics use flexible diagrams rather than text. Feedback is common in audio electronics but rare in software—partly because text-based representations make expressing feedback unnecessarily difficult.
Operating System Bloat
“Time-sharing” evolved into “multi-tasking.” The first time-sharing kernel I encountered required about one page of code. Modern operating systems span hundreds of thousands or millions of lines. We’ve come to believe operating systems are necessary for all computing tasks, yet early cartridge-based gaming systems proved otherwise.
Understanding CPU Architecture
I think the MC6800 and MC6809 were simple, understandable 8-bit CPUs that provided excellent learning platforms. The more common 6502 made additional trade-offs for cost and efficiency, which tends to make it harder to understand the basics.
Resources
Dr. Dobb’s Tiny Basic
Dr. Dobb’s Small C
Dr. Dobb’s Potpourri of Lisp Functions
build a single stepper
build a simple hierarchical/tree editor
Forth-ish discord (basics of building simple Forth-like constructs in other languages)
Smalltalk Blue Book (contains implementation of Smalltalk in Smalltalk - Part 4)
Frits van der Wateren Lisp (early Lisp for MC6800)
Pong
First Principles of CPUs and Programming Playlist
Prolog in Scheme Transpiled to Javascript
space skipping problems could be handled better using OhmJS “#” lexical vs. syntactic ideas - please correct and post
at the time I made this video I hadn’t yet learned how to handle space skipping better
MOAD (Mother Of All Demos)
TXL (language for text transformation)
Alan Kay on Using Existing Languages As Assemblers for New Languages
Ohm In Small Steps (diary of learning OhmJS and converting Scheme to JS)
The Most Beautiful Program Ever Written
Building Software With Black Boxes
(WIP book) Programming Simplicity - Takeaways
(WIP book) Programming Simplicity - Broad Brush
(book) WASM From The Ground Up
Homebrew NMOS Transistor Step by Step - So Easy Even Jeri Can Do It
Evolution of Programming Functions
See Also
Email: ptcomputingsimplicity@gmail.com
Substack: paultarvydas.substack.com
Videos: https://www.youtube.com/@programmingsimplicity2980
Discord: https://discord.gg/65YZUh6Jpq
Leanpub: [WIP] https://leanpub.com/u/paul-tarvydas
Twitter: @paul_tarvydas
Bluesky: @paultarvydas.bsky.social
Mastodon: @paultarvydas
(earlier) Blog: guitarvydas.github.io
References: https://guitarvydas.github.io/2024/01/06/References.html

