Phase 0: boot sector, build pipeline

human · Jul 17, 18:56:06
I want to ask Claude to write a tiny OS with a tiny file system, with a tiny compiler and scripting language, all to be used in qemu/kvm or equivalent. the goal is that 100% of lines of code come from Claude. Make a plan.
agent · Jul 17, 18:56:23
A few architecture decisions will shape the plan significantly — let me pin those down first.
AskUserQuestion · Jul 17, 18:56:33 · toolu_01RPMpMoUggkbKdae6xR4ACj
68 lines
{
 "questions": [
  {
   "question": "Target boot platform for QEMU/KVM?",
   "header": "Boot target",
   "options": [
    {
     "label": "x86 BIOS, custom boot sector (Recommended)",
     "description": "16-bit real-mode boot sector we write ourselves, jump to 32-bit protected mode. Simplest to reason about, no GRUB/Multiboot dependency, classic 'tiny OS' path."
    },
    {
     "label": "x86 Multiboot (GRUB)",
     "description": "Kernel loaded via GRUB/Multiboot spec. Less boot-sector code to write, but pulls in a GRUB dependency and Multiboot header contract."
    },
    {
     "label": "RISC-V (QEMU virt machine)",
     "description": "Cleaner/more modern ISA, fewer legacy quirks (no real-mode/A20/segmentation), but less common tutorial ground and needs a RISC-V cross toolchain."
    }
   ],
   "multiSelect": false
  },
  {
   "question": "Implementation language for the OS/kernel/compiler source?",
   "header": "Source language",
   "options": [
    {
     "label": "Assembly + freestanding C (Recommended)",
     "description": "Boot sector and lowest-level entry in asm, rest of kernel/compiler/scripting engine in freestanding C. Best productivity-to-'tiny' ratio."
    },
    {
     "label": "Pure assembly",
     "description": "Everything hand-written in x86/RISC-V asm, no C at all. Maximally 'tiny' and dependency-free, but far more code and slower to build out a compiler on top of it."
    }
   ],
   "multiSelect": false
  },
  {
   "question": "What counts as 'Claude writes 100% of the code' regarding the toolchain?",
   "header": "Toolchain scope",
   "options": [
    {
     "label": "Use existing assembler/linker/QEMU as tools (Recommended)",
     "description": "nasm/gcc/ld/qemu are treated like a compiler is for any project — infrastructure, not authored code. Claude authors every line of the OS, filesystem, compiler, and scripting language itself."
    },
    {
     "label": "Claude also writes the assembler/linker from scratch",
     "description": "Much larger scope: a self-authored assembler and linker too, not just OS-level code. Significantly increases project size before any OS work starts."
    }
   ],
   "multiSelect": false
  },
  {
   "question": "How minimal should the OS itself be (scheduling/memory/drivers)?",
   "header": "OS scope",
   "options": [
    {
     "label": "Single-tasking, polling drivers (Recommended)",
     "description": "One running program at a time, no preemptive scheduler, simple bump/free-list allocator, polled keyboard + serial + framebuffer/VGA text output. Keeps focus on filesystem + compiler + scripting language."
    },
    {
     "label": "Cooperative multitasking",
     "description": "Simple round-robin cooperative scheduler with yield(), multiple 'processes' resident in memory. More OS-y, more code, more failure surface before the language layer is reached."
    }
   ],
   "multiSelect": false
  }
 ]
}
result
Your questions have been answered: "Target boot platform for QEMU/KVM?"="x86 BIOS, custom boot sector (Recommended)", "Implementation language for the OS/kernel/compiler source?"="Assembly + freestanding C (Recommended)", "What counts as 'Claude writes 100% of the code' regarding the toolchain?"="Use existing assembler/linker/QEMU as tools (Recommended)", "How minimal should the OS itself be (scheduling/memory/drivers)?"="Single-tasking, polling drivers (Recommended)". You can now continue with these answers in mind.
ToolSearch · Jul 17, 18:59:50 · toolu_01DihLNTgPrMBDXvuYE7ksj4
{
 "query": "select:EnterPlanMode,ExitPlanMode",
 "max_results": 5
}
result
EnterPlanMode · Jul 17, 18:59:51 · toolu_01F7sSYR9mn6zTr9eWZVVfSX
{}
result
Entered plan mode. You should now focus on exploring the codebase and designing an implementation approach.

In plan mode, you should:
1. Thoroughly explore the codebase to understand existing patterns
2. Identify similar features and architectural approaches
3. Consider multiple approaches and their trade-offs
4. Use AskUserQuestion if you need to clarify the approach
5. Design a concrete implementation strategy
6. When ready, use ExitPlanMode to present your plan for approval

Remember: DO NOT write or edit any files yet. This is a read-only exploration and planning phase.
Bash · Jul 17, 18:59:59 · toolu_01RSR3ZaXVqtezcf1kN81Ja3
Inspect repo contents and history
$ ls -la /home/martin/workspace/prototypes/boot-to-claude/ && cat /home/martin/workspace/prototypes/boot-to-claude/README.md 2>/dev/null; git -C /home/martin/workspace/prototypes/boot-to-claude log --oneline -5
result
total 48
drwxr-x---   3 martin martin  4096 Jul 17 18:55 .
drwxr-x--- 220 martin martin 36864 Jul 17 18:54 ..
drwxr-x---   8 martin martin  4096 Jul 17 18:55 .git
17c6619 init
Bash · Jul 17, 19:00:58 · toolu_01NdrEAW58nVPjQLtttmktHn
Check available toolchain: nasm, qemu, gcc -m32 support, cross tools
$ echo "--- nasm ---"; which nasm && nasm -v
echo "--- qemu ---"; which qemu-system-i386 qemu-system-x86_64 2>/dev/null
echo "--- gcc ---"; which gcc && gcc --version | head -1
echo "--- gcc -m32 test ---"; echo 'int main(){return 0;}' > /tmp/t.c && gcc -m32 -ffreestanding -c /tmp/t.c -o /tmp/t.o 2>&1 | head -20 && echo OK32
echo "--- ld ---"; which ld && ld --version | head -1
echo "--- i686-elf cross ---"; which i686-elf-gcc i686-elf-ld 2>/dev/null
echo "--- mtools/dd ---"; which mtools dd 2>/dev/null
rm -f /tmp/t.c /tmp/t.o
result
--- nasm ---
/bin/nasm
NASM version 2.16.03
--- qemu ---
/bin/qemu-system-i386
/bin/qemu-system-x86_64
--- gcc ---
/bin/gcc
gcc (Ubuntu 15.2.0-4ubuntu4) 15.2.0
--- gcc -m32 test ---
OK32
--- ld ---
/bin/ld
GNU ld (GNU Binutils for Ubuntu) 2.45
--- i686-elf cross ---
--- mtools/dd ---
/bin/mtools
/bin/dd
Write · Jul 17, 19:01:38 · toolu_015VRP2RXcMNZVLKDS9pBvrM
/home/martin/.claude/plans/swirling-mixing-bee.md
163 lines
# Tiny OS + tinyfs + tiny compiler/scripting language for QEMU/KVM

## Context

Goal: a from-scratch, minimal x86 OS that boots in QEMU/KVM, with its own tiny
filesystem, a tiny interpreted scripting language, and a tiny compiler (source
→ bytecode) with a bytecode VM to run it — all authored by Claude. Repo is
currently empty (just an `init` commit), so this is fully greenfield.

Confirmed decisions (from user):
- Boot: x86 BIOS, custom MBR boot sector (no GRUB/Multiboot).
- Language: assembly (bootloader, entry stubs) + freestanding C (everything else).
- Toolchain: nasm, gcc, ld, qemu are used as build infrastructure (like any
  compiler) — not counted against "100% Claude-authored" code. Claude authors
  every line of OS/fs/compiler/scripting source itself.
- OS model: single-tasking, no preemption, all drivers polled — no PIC/IDT-driven
  interrupts, no scheduler, no process/syscall separation. Kernel is monolithic;
  shell and interpreters run directly in kernel context. This keeps the OS layer
  small so effort goes into the filesystem and language layers.

Toolchain verified present on this machine: `nasm 2.16`, `qemu-system-i386`,
`gcc 15.2` with working `-m32 -ffreestanding`, `ld 2.45`, `mtools`, `dd`. No
i686-elf cross-compiler needed — `gcc -m32 -ffreestanding -nostdlib` on the
host is sufficient, which avoids building/maintaining a cross toolchain.

Two distinct language layers, to make "compiler" and "scripting language"
genuinely separate deliverables rather than the same thing twice:
- **Scripting language**: dynamic, tree-walking interpreter, runs directly
  from source each time (`run script.tsh`). Shell-automation flavored.
- **Tiny compiler**: a small statically-structured language compiled to a
  custom stack-based bytecode, executed by an in-kernel bytecode VM
  (`build prog.tc out.tbc` then `exec out.tbc`). Native x86 codegen is called
  out as an optional stretch goal, not in the base plan — register allocation
  and instruction selection would balloon scope for little benefit over a
  bytecode VM given the "no process isolation" model.

## Repo layout

```
Makefile
README.md
.gitignore
boot/
  boot.S            stage1 MBR (512B): loads stage2 via BIOS INT13h, jumps to it
  stage2.S           stage2: enable A20, load kernel sectors, GDT, pmode switch, jump to kmain
kernel/
  entry.S            32-bit entry stub -> calls kmain
  kmain.c
  gdt.c/.h
  idt.c/.h           minimal fault handlers only (divide-by-zero/GPF/PF -> print+halt), no IRQs
  console.c/.h       VGA text mode (0xB8000) driver
  serial.c/.h        16550 UART driver (used for QEMU -serial stdio test output)
  memory.c/.h        free-list allocator over a fixed RAM region
  string.c/.h        freestanding libc subset (memcpy/memset/strlen/strcmp/itoa/...)
  keyboard.c/.h      polled PS/2 keyboard, scancode->ASCII (US subset)
  ata.c/.h           polled ATA PIO (LBA28) sector read/write
  fs/
    tinyfs.c/.h      superblock+bitmap+inode table+data blocks; flat or 1-level dirs
  shell/
    shell.c/.h       REPL: ls, cat, write, rm, run, build, exec, help, reboot, clear
  lang/
    lexer.c/.h       shared tokenizer for both languages
    script/
      parser.c/.h ast.c/.h eval.c/.h   scripting language front-end + tree-walk evaluator
    compiler/
      parser.c/.h codegen.c/.h          tiny compiler front-end + bytecode codegen
    vm.c/.h          bytecode VM (stack machine, executes compiled programs)
tools/
  mkfs_tinyfs.c      host-side tool: formats disk image + seeds initial files at build time
  run_tests.sh        host-side: boots QEMU headless with scripted input, checks serial output
tests/
  *.tsh                scripting-language fixtures
  *.tc                 compiler-language fixtures
  expected/*.txt        expected serial output per fixture
docs/
  tinyfs.md            on-disk format spec
  bytecode-isa.md       VM instruction set
  scripting-language.md  language reference
```

## Build phases (each independently buildable + testable in QEMU)

**Phase 0 — Scaffolding**
Makefile driving: nasm assembly, `gcc -m32 -ffreestanding -nostdlib -fno-pic`
compiles, `ld -m elf_i386` links with a custom linker script, `dd`/`mtools`
assembles the final disk image, `qemu-system-i386 -drive file=disk.img` boots
it. Minimal boot.S that prints one character via BIOS teletype and hangs.
Verify: boots in QEMU, prints the character, .gitignore covers build artifacts.

**Phase 1 — Bootloader**
Stage1 (512B, must end in 0x55AA): loads stage2 from disk via BIOS INT13h,
jumps to it. Stage2: enable A20 line, load kernel sectors into memory, build
a minimal GDT (flat code/data segments), switch to 32-bit protected mode,
far-jump into `kernel/entry.S` -> `kmain`.
Verify: kmain prints a banner string directly to VGA memory.

**Phase 2 — Kernel core**
Finalize GDT, add a minimal IDT with only exception handlers (no PIC
remapping, no IRQ handling — polling only) that print a diagnostic and halt.
console.c (VGA text), serial.c (UART, used as the machine-readable test
channel throughout), string.c, memory.c (free-list allocator over a fixed
known-size RAM region under QEMU's default map).
Verify: kmain runs a small self-test (alloc/free, string ops) and prints
pass/fail to serial; `run_tests.sh` checks for "PASS" on the serial line.

**Phase 3 — Disk + filesystem**
ata.c: polled PIO sector read/write. tinyfs: superblock + free-block bitmap +
fixed-size inode table + data blocks, flat namespace (or one level of
directories if it stays simple) — design documented in docs/tinyfs.md before
coding. fs/tinyfs.c: mount, list, create, read, write, delete.
tools/mkfs_tinyfs.c (host binary, built with the host's regular gcc, run at
image-build time) formats the disk image and seeds initial files/scripts.
Verify: kernel mounts the fs, lists seeded files, reads one back and compares
bytes against an expected constant, reports PASS/FAIL over serial.

**Phase 4 — Keyboard + shell**
keyboard.c: polled PS/2 driver + scancode table. Simple line editor
(printable chars, backspace, enter). shell/shell.c: `ls`, `cat`, `write`,
`rm`, `help`, `clear`, `reboot`.
Verify: `run_tests.sh` drives QEMU with scripted keystrokes (via QEMU's
`-serial stdio`, using stdin as a fake keyboard feed or the QEMU monitor's
`sendkey`) and asserts each shell command's expected serial output.

**Phase 5 — Scripting language**
lexer.c shared tokenizer. script/parser.c + ast.c build an AST for: ints,
strings, variables, if/while, function definitions/calls, `print`, and OS
builtins (open/read/write/list). script/eval.c tree-walks and executes it.
Shell gains `run <file>`.
Verify: mkfs seeds a `.tsh` fixture from tests/, `run_tests.sh` boots QEMU,
runs it via the shell, diffs serial output against tests/expected/.

**Phase 6 — Tiny compiler + bytecode VM**
docs/bytecode-isa.md defines a small stack-machine ISA (push/pop const,
arithmetic, jumps, calls, a handful of OS-facing "syscall" opcodes for I/O).
lang/compiler/{parser,codegen}.c compiles a minimal statically-structured
language (int vars, functions, if/while, arithmetic, builtin calls) to that
bytecode, reusing lexer.c. lang/vm.c executes bytecode. Shell gains
`build <src> <out>` and `exec <out>`.
Verify: mkfs seeds a `.tc` fixture, `run_tests.sh` boots QEMU, runs
`build`+`exec`, diffs serial output against expected.

**Phase 7 — Integration + docs**
One combined demo fixture exercising fs + scripting language + compiler
together. README.md with build/run instructions and an architecture
overview. Finish docs/*.md. Confirm `.gitignore` excludes all build
artifacts (*.o, *.bin, *.img, disk images) and history stays clean.

## Verification strategy

No hardware, no existing test framework — the serial port is the test
oracle throughout. `tools/run_tests.sh` boots
`qemu-system-i386 -display none -serial stdio -no-reboot -drive file=disk.img`,
feeds any needed scripted input, captures serial output, and diffs against
`tests/expected/*.txt`. This script is added in Phase 2 and reused/extended
by every later phase, giving each phase an automated pass/fail signal instead
of manual QEMU interaction.

## Sequencing note

Work proceeds phase by phase, each ending in a QEMU-verified checkpoint
before starting the next. This plan covers the full arc; implementation will
be done incrementally with a commit at the end of each phase.
result
File created successfully at: /home/martin/.claude/plans/swirling-mixing-bee.md (file state is current in your context — no need to Read it back)
ExitPlanMode · Jul 17, 19:01:40 · toolu_017z9LAq6WR2F7Vf2RJN25LS
{
 "plan": "# Tiny OS + tinyfs + tiny compiler/scripting language for QEMU/KVM\n\n## Context\n\nGoal: a from-scratch, minimal x86 OS that boots in QEMU/KVM, with its own tiny\nfilesystem, a tiny interpreted scripting language, and a tiny compiler (source\n→ bytecode) with a bytecode VM to run it — all authored by Claude. Repo is\ncurrently empty (just an `init` commit), so this is fully greenfield.\n\nConfirmed decisions (from user):\n- Boot: x86 BIOS, custom MBR boot sector (no GRUB/Multiboot).\n- Language: assembly (bootloader, entry stubs) + freestanding C (everything else).\n- Toolchain: nasm, gcc, ld, qemu are used as build infrastructure (like any\n  compiler) — not counted against \"100% Claude-authored\" code. Claude authors\n  every line of OS/fs/compiler/scripting source itself.\n- OS model: single-tasking, no preemption, all drivers polled — no PIC/IDT-driven\n  interrupts, no scheduler, no process/syscall separation. Kernel is monolithic;\n  shell and interpreters run directly in kernel context. This keeps the OS layer\n  small so effort goes into the filesystem and language layers.\n\nToolchain verified present on this machine: `nasm 2.16`, `qemu-system-i386`,\n`gcc 15.2` with working `-m32 -ffreestanding`, `ld 2.45`, `mtools`, `dd`. No\ni686-elf cross-compiler needed — `gcc -m32 -ffreestanding -nostdlib` on the\nhost is sufficient, which avoids building/maintaining a cross toolchain.\n\nTwo distinct language layers, to make \"compiler\" and \"scripting language\"\ngenuinely separate deliverables rather than the same thing twice:\n- **Scripting language**: dynamic, tree-walking interpreter, runs directly\n  from source each time (`run script.tsh`). Shell-automation flavored.\n- **Tiny compiler**: a small statically-structured language compiled to a\n  custom stack-based bytecode, executed by an in-kernel bytecode VM\n  (`build prog.tc out.tbc` then `exec out.tbc`). Native x86 codegen is called\n  out as an optional stretch goal, not in the base plan — register allocation\n  and instruction selection would balloon scope for little benefit over a\n  bytecode VM given the \"no process isolation\" model.\n\n## Repo layout\n\n```\nMakefile\nREADME.md\n.gitignore\nboot/\n  boot.S            stage1 MBR (512B): loads stage2 via BIOS INT13h, jumps to it\n  stage2.S           stage2: enable A20, load kernel sectors, GDT, pmode switch, jump to kmain\nkernel/\n  entry.S            32-bit entry stub -> calls kmain\n  kmain.c\n  gdt.c/.h\n  idt.c/.h           minimal fault handlers only (divide-by-zero/GPF/PF -> print+halt), no IRQs\n  console.c/.h       VGA text mode (0xB8000) driver\n  serial.c/.h        16550 UART driver (used for QEMU -serial stdio test output)\n  memory.c/.h        free-list allocator over a fixed RAM region\n  string.c/.h        freestanding libc subset (memcpy/memset/strlen/strcmp/itoa/...)\n  keyboard.c/.h      polled PS/2 keyboard, scancode->ASCII (US subset)\n  ata.c/.h           polled ATA PIO (LBA28) sector read/write\n  fs/\n    tinyfs.c/.h      superblock+bitmap+inode table+data blocks; flat or 1-level dirs\n  shell/\n    shell.c/.h       REPL: ls, cat, write, rm, run, build, exec, help, reboot, clear\n  lang/\n    lexer.c/.h       shared tokenizer for both languages\n    script/\n      parser.c/.h ast.c/.h eval.c/.h   scripting language front-end + tree-walk evaluator\n    compiler/\n      parser.c/.h codegen.c/.h          tiny compiler front-end + bytecode codegen\n    vm.c/.h          bytecode VM (stack machine, executes compiled programs)\ntools/\n  mkfs_tinyfs.c      host-side tool: formats disk image + seeds initial files at build time\n  run_tests.sh        host-side: boots QEMU headless with scripted input, checks serial output\ntests/\n  *.tsh                scripting-language fixtures\n  *.tc                 compiler-language fixtures\n  expected/*.txt        expected serial output per fixture\ndocs/\n  tinyfs.md            on-disk format spec\n  bytecode-isa.md       VM instruction set\n  scripting-language.md  language reference\n```\n\n## Build phases (each independently buildable + testable in QEMU)\n\n**Phase 0 — Scaffolding**\nMakefile driving: nasm assembly, `gcc -m32 -ffreestanding -nostdlib -fno-pic`\ncompiles, `ld -m elf_i386` links with a custom linker script, `dd`/`mtools`\nassembles the final disk image, `qemu-system-i386 -drive file=disk.img` boots\nit. Minimal boot.S that prints one character via BIOS teletype and hangs.\nVerify: boots in QEMU, prints the character, .gitignore covers build artifacts.\n\n**Phase 1 — Bootloader**\nStage1 (512B, must end in 0x55AA): loads stage2 from disk via BIOS INT13h,\njumps to it. Stage2: enable A20 line, load kernel sectors into memory, build\na minimal GDT (flat code/data segments), switch to 32-bit protected mode,\nfar-jump into `kernel/entry.S` -> `kmain`.\nVerify: kmain prints a banner string directly to VGA memory.\n\n**Phase 2 — Kernel core**\nFinalize GDT, add a minimal IDT with only exception handlers (no PIC\nremapping, no IRQ handling — polling only) that print a diagnostic and halt.\nconsole.c (VGA text), serial.c (UART, used as the machine-readable test\nchannel throughout), string.c, memory.c (free-list allocator over a fixed\nknown-size RAM region under QEMU's default map).\nVerify: kmain runs a small self-test (alloc/free, string ops) and prints\npass/fail to serial; `run_tests.sh` checks for \"PASS\" on the serial line.\n\n**Phase 3 — Disk + filesystem**\nata.c: polled PIO sector read/write. tinyfs: superblock + free-block bitmap +\nfixed-size inode table + data blocks, flat namespace (or one level of\ndirectories if it stays simple) — design documented in docs/tinyfs.md before\ncoding. fs/tinyfs.c: mount, list, create, read, write, delete.\ntools/mkfs_tinyfs.c (host binary, built with the host's regular gcc, run at\nimage-build time) formats the disk image and seeds initial files/scripts.\nVerify: kernel mounts the fs, lists seeded files, reads one back and compares\nbytes against an expected constant, reports PASS/FAIL over serial.\n\n**Phase 4 — Keyboard + shell**\nkeyboard.c: polled PS/2 driver + scancode table. Simple line editor\n(printable chars, backspace, enter). shell/shell.c: `ls`, `cat`, `write`,\n`rm`, `help`, `clear`, `reboot`.\nVerify: `run_tests.sh` drives QEMU with scripted keystrokes (via QEMU's\n`-serial stdio`, using stdin as a fake keyboard feed or the QEMU monitor's\n`sendkey`) and asserts each shell command's expected serial output.\n\n**Phase 5 — Scripting language**\nlexer.c shared tokenizer. script/parser.c + ast.c build an AST for: ints,\nstrings, variables, if/while, function definitions/calls, `print`, and OS\nbuiltins (open/read/write/list). script/eval.c tree-walks and executes it.\nShell gains `run <file>`.\nVerify: mkfs seeds a `.tsh` fixture from tests/, `run_tests.sh` boots QEMU,\nruns it via the shell, diffs serial output against tests/expected/.\n\n**Phase 6 — Tiny compiler + bytecode VM**\ndocs/bytecode-isa.md defines a small stack-machine ISA (push/pop const,\narithmetic, jumps, calls, a handful of OS-facing \"syscall\" opcodes for I/O).\nlang/compiler/{parser,codegen}.c compiles a minimal statically-structured\nlanguage (int vars, functions, if/while, arithmetic, builtin calls) to that\nbytecode, reusing lexer.c. lang/vm.c executes bytecode. Shell gains\n`build <src> <out>` and `exec <out>`.\nVerify: mkfs seeds a `.tc` fixture, `run_tests.sh` boots QEMU, runs\n`build`+`exec`, diffs serial output against expected.\n\n**Phase 7 — Integration + docs**\nOne combined demo fixture exercising fs + scripting language + compiler\ntogether. README.md with build/run instructions and an architecture\noverview. Finish docs/*.md. Confirm `.gitignore` excludes all build\nartifacts (*.o, *.bin, *.img, disk images) and history stays clean.\n\n## Verification strategy\n\nNo hardware, no existing test framework — the serial port is the test\noracle throughout. `tools/run_tests.sh` boots\n`qemu-system-i386 -display none -serial stdio -no-reboot -drive file=disk.img`,\nfeeds any needed scripted input, captures serial output, and diffs against\n`tests/expected/*.txt`. This script is added in Phase 2 and reused/extended\nby every later phase, giving each phase an automated pass/fail signal instead\nof manual QEMU interaction.\n\n## Sequencing note\n\nWork proceeds phase by phase, each ending in a QEMU-verified checkpoint\nbefore starting the next. This plan covers the full arc; implementation will\nbe done incrementally with a commit at the end of each phase.\n",
 "planFilePath": "/home/martin/.claude/plans/swirling-mixing-bee.md"
}
result
169 lines
User has approved your plan. You can now start coding. Start with updating your todo list if applicable

Your plan has been saved to: /home/martin/.claude/plans/swirling-mixing-bee.md
You can refer back to it if needed during implementation.

## Approved Plan:
# Tiny OS + tinyfs + tiny compiler/scripting language for QEMU/KVM

## Context

Goal: a from-scratch, minimal x86 OS that boots in QEMU/KVM, with its own tiny
filesystem, a tiny interpreted scripting language, and a tiny compiler (source
→ bytecode) with a bytecode VM to run it — all authored by Claude. Repo is
currently empty (just an `init` commit), so this is fully greenfield.

Confirmed decisions (from user):
- Boot: x86 BIOS, custom MBR boot sector (no GRUB/Multiboot).
- Language: assembly (bootloader, entry stubs) + freestanding C (everything else).
- Toolchain: nasm, gcc, ld, qemu are used as build infrastructure (like any
  compiler) — not counted against "100% Claude-authored" code. Claude authors
  every line of OS/fs/compiler/scripting source itself.
- OS model: single-tasking, no preemption, all drivers polled — no PIC/IDT-driven
  interrupts, no scheduler, no process/syscall separation. Kernel is monolithic;
  shell and interpreters run directly in kernel context. This keeps the OS layer
  small so effort goes into the filesystem and language layers.

Toolchain verified present on this machine: `nasm 2.16`, `qemu-system-i386`,
`gcc 15.2` with working `-m32 -ffreestanding`, `ld 2.45`, `mtools`, `dd`. No
i686-elf cross-compiler needed — `gcc -m32 -ffreestanding -nostdlib` on the
host is sufficient, which avoids building/maintaining a cross toolchain.

Two distinct language layers, to make "compiler" and "scripting language"
genuinely separate deliverables rather than the same thing twice:
- **Scripting language**: dynamic, tree-walking interpreter, runs directly
  from source each time (`run script.tsh`). Shell-automation flavored.
- **Tiny compiler**: a small statically-structured language compiled to a
  custom stack-based bytecode, executed by an in-kernel bytecode VM
  (`build prog.tc out.tbc` then `exec out.tbc`). Native x86 codegen is called
  out as an optional stretch goal, not in the base plan — register allocation
  and instruction selection would balloon scope for little benefit over a
  bytecode VM given the "no process isolation" model.

## Repo layout

```
Makefile
README.md
.gitignore
boot/
  boot.S            stage1 MBR (512B): loads stage2 via BIOS INT13h, jumps to it
  stage2.S           stage2: enable A20, load kernel sectors, GDT, pmode switch, jump to kmain
kernel/
  entry.S            32-bit entry stub -> calls kmain
  kmain.c
  gdt.c/.h
  idt.c/.h           minimal fault handlers only (divide-by-zero/GPF/PF -> print+halt), no IRQs
  console.c/.h       VGA text mode (0xB8000) driver
  serial.c/.h        16550 UART driver (used for QEMU -serial stdio test output)
  memory.c/.h        free-list allocator over a fixed RAM region
  string.c/.h        freestanding libc subset (memcpy/memset/strlen/strcmp/itoa/...)
  keyboard.c/.h      polled PS/2 keyboard, scancode->ASCII (US subset)
  ata.c/.h           polled ATA PIO (LBA28) sector read/write
  fs/
    tinyfs.c/.h      superblock+bitmap+inode table+data blocks; flat or 1-level dirs
  shell/
    shell.c/.h       REPL: ls, cat, write, rm, run, build, exec, help, reboot, clear
  lang/
    lexer.c/.h       shared tokenizer for both languages
    script/
      parser.c/.h ast.c/.h eval.c/.h   scripting language front-end + tree-walk evaluator
    compiler/
      parser.c/.h codegen.c/.h          tiny compiler front-end + bytecode codegen
    vm.c/.h          bytecode VM (stack machine, executes compiled programs)
tools/
  mkfs_tinyfs.c      host-side tool: formats disk image + seeds initial files at build time
  run_tests.sh        host-side: boots QEMU headless with scripted input, checks serial output
tests/
  *.tsh                scripting-language fixtures
  *.tc                 compiler-language fixtures
  expected/*.txt        expected serial output per fixture
docs/
  tinyfs.md            on-disk format spec
  bytecode-isa.md       VM instruction set
  scripting-language.md  language reference
```

## Build phases (each independently buildable + testable in QEMU)

**Phase 0 — Scaffolding**
Makefile driving: nasm assembly, `gcc -m32 -ffreestanding -nostdlib -fno-pic`
compiles, `ld -m elf_i386` links with a custom linker script, `dd`/`mtools`
assembles the final disk image, `qemu-system-i386 -drive file=disk.img` boots
it. Minimal boot.S that prints one character via BIOS teletype and hangs.
Verify: boots in QEMU, prints the character, .gitignore covers build artifacts.

**Phase 1 — Bootloader**
Stage1 (512B, must end in 0x55AA): loads stage2 from disk via BIOS INT13h,
jumps to it. Stage2: enable A20 line, load kernel sectors into memory, build
a minimal GDT (flat code/data segments), switch to 32-bit protected mode,
far-jump into `kernel/entry.S` -> `kmain`.
Verify: kmain prints a banner string directly to VGA memory.

**Phase 2 — Kernel core**
Finalize GDT, add a minimal IDT with only exception handlers (no PIC
remapping, no IRQ handling — polling only) that print a diagnostic and halt.
console.c (VGA text), serial.c (UART, used as the machine-readable test
channel throughout), string.c, memory.c (free-list allocator over a fixed
known-size RAM region under QEMU's default map).
Verify: kmain runs a small self-test (alloc/free, string ops) and prints
pass/fail to serial; `run_tests.sh` checks for "PASS" on the serial line.

**Phase 3 — Disk + filesystem**
ata.c: polled PIO sector read/write. tinyfs: superblock + free-block bitmap +
fixed-size inode table + data blocks, flat namespace (or one level of
directories if it stays simple) — design documented in docs/tinyfs.md before
coding. fs/tinyfs.c: mount, list, create, read, write, delete.
tools/mkfs_tinyfs.c (host binary, built with the host's regular gcc, run at
image-build time) formats the disk image and seeds initial files/scripts.
Verify: kernel mounts the fs, lists seeded files, reads one back and compares
bytes against an expected constant, reports PASS/FAIL over serial.

**Phase 4 — Keyboard + shell**
keyboard.c: polled PS/2 driver + scancode table. Simple line editor
(printable chars, backspace, enter). shell/shell.c: `ls`, `cat`, `write`,
`rm`, `help`, `clear`, `reboot`.
Verify: `run_tests.sh` drives QEMU with scripted keystrokes (via QEMU's
`-serial stdio`, using stdin as a fake keyboard feed or the QEMU monitor's
`sendkey`) and asserts each shell command's expected serial output.

**Phase 5 — Scripting language**
lexer.c shared tokenizer. script/parser.c + ast.c build an AST for: ints,
strings, variables, if/while, function definitions/calls, `print`, and OS
builtins (open/read/write/list). script/eval.c tree-walks and executes it.
Shell gains `run <file>`.
Verify: mkfs seeds a `.tsh` fixture from tests/, `run_tests.sh` boots QEMU,
runs it via the shell, diffs serial output against tests/expected/.

**Phase 6 — Tiny compiler + bytecode VM**
docs/bytecode-isa.md defines a small stack-machine ISA (push/pop const,
arithmetic, jumps, calls, a handful of OS-facing "syscall" opcodes for I/O).
lang/compiler/{parser,codegen}.c compiles a minimal statically-structured
language (int vars, functions, if/while, arithmetic, builtin calls) to that
bytecode, reusing lexer.c. lang/vm.c executes bytecode. Shell gains
`build <src> <out>` and `exec <out>`.
Verify: mkfs seeds a `.tc` fixture, `run_tests.sh` boots QEMU, runs
`build`+`exec`, diffs serial output against expected.

**Phase 7 — Integration + docs**
One combined demo fixture exercising fs + scripting language + compiler
together. README.md with build/run instructions and an architecture
overview. Finish docs/*.md. Confirm `.gitignore` excludes all build
artifacts (*.o, *.bin, *.img, disk images) and history stays clean.

## Verification strategy

No hardware, no existing test framework — the serial port is the test
oracle throughout. `tools/run_tests.sh` boots
`qemu-system-i386 -display none -serial stdio -no-reboot -drive file=disk.img`,
feeds any needed scripted input, captures serial output, and diffs against
`tests/expected/*.txt`. This script is added in Phase 2 and reused/extended
by every later phase, giving each phase an automated pass/fail signal instead
of manual QEMU interaction.

## Sequencing note

Work proceeds phase by phase, each ending in a QEMU-verified checkpoint
before starting the next. This plan covers the full arc; implementation will
be done incrementally with a commit at the end of each phase.
ToolSearch · Jul 17, 19:03:23 · toolu_01HhAACQytuHDz16eAnQWe1K
{
 "query": "select:TaskCreate",
 "max_results": 3
}
result
TaskCreate · Jul 17, 19:03:26 · toolu_01VVxhgwNDzLKQA4R9Kh9VQq
{
 "subject": "Phase 0 — Scaffolding",
 "description": "Makefile, dirs, minimal boot.S that prints a char and hangs, boots in QEMU, .gitignore",
 "activeForm": "Setting up scaffolding"
}
result
Task #1 created successfully: Phase 0 — Scaffolding
TaskCreate · Jul 17, 19:03:28 · toolu_01V6omoKuQfsnLx83mWmoFfr
{
 "subject": "Phase 1 — Bootloader",
 "description": "Stage1 MBR + stage2: A20, load kernel sectors, GDT, protected mode switch, jump to kmain; kmain prints banner via VGA",
 "activeForm": "Building bootloader"
}
result
Task #2 created successfully: Phase 1 — Bootloader
TaskCreate · Jul 17, 19:03:29 · toolu_0138igE1aqRvUm8jLytwFUvS
{
 "subject": "Phase 2 — Kernel core",
 "description": "GDT/IDT finalize (exceptions only), console.c, serial.c, string.c, memory.c, self-test over serial, run_tests.sh harness",
 "activeForm": "Building kernel core"
}
result
Task #3 created successfully: Phase 2 — Kernel core
TaskCreate · Jul 17, 19:03:30 · toolu_01CuWiKfdQLaApxZDvTVrQDA
{
 "subject": "Phase 3 — Disk + tinyfs",
 "description": "ata.c polled PIO driver, tinyfs design doc + implementation, mkfs_tinyfs host tool, mount/list/read verified over serial",
 "activeForm": "Building disk driver and filesystem"
}
result
Task #4 created successfully: Phase 3 — Disk + tinyfs
TaskCreate · Jul 17, 19:03:31 · toolu_01PabPGvL5WU38JwNHoD6adR
{
 "subject": "Phase 4 — Keyboard + shell",
 "description": "Polled PS/2 keyboard driver, line editor, shell REPL (ls/cat/write/rm/help/clear/reboot), scripted-input test",
 "activeForm": "Building keyboard driver and shell"
}
result
Task #5 created successfully: Phase 4 — Keyboard + shell
TaskCreate · Jul 17, 19:03:32 · toolu_01HHCb6YeGLc3nByd5VbQqoP
{
 "subject": "Phase 5 — Scripting language",
 "description": "Shared lexer, parser/AST/tree-walk evaluator, shell `run` command, .tsh fixture test",
 "activeForm": "Building scripting language"
}
result
Task #6 created successfully: Phase 5 — Scripting language
TaskCreate · Jul 17, 19:03:33 · toolu_011ugVBfkhxhp52hY7iLMc4B
{
 "subject": "Phase 6 — Tiny compiler + bytecode VM",
 "description": "Bytecode ISA doc, compiler parser/codegen, VM, shell `build`/`exec` commands, .tc fixture test",
 "activeForm": "Building compiler and bytecode VM"
}
result
Task #7 created successfully: Phase 6 — Tiny compiler + bytecode VM
TaskCreate · Jul 17, 19:03:33 · toolu_019LhR1xbtJ1rzVv6DVLHcxU
{
 "subject": "Phase 7 — Integration + docs",
 "description": "Combined demo fixture, README, finish docs, verify .gitignore/history clean",
 "activeForm": "Finishing integration and docs"
}
result
Task #8 created successfully: Phase 7 — Integration + docs
agent · Jul 17, 19:03:35
Plan approved and tasks tracked. Starting Phase 0 now.
ToolSearch · Jul 17, 19:03:35 · toolu_016MAUkJAgsaRLaaFVccoZWh
{
 "query": "select:TaskUpdate",
 "max_results": 3
}
result
TaskUpdate · Jul 17, 19:03:37 · toolu_01CpPmFhsVZqYLHbA3UJinkY
{
 "taskId": "1",
 "status": "in_progress"
}
result
Updated task #1 status
Bash · Jul 17, 19:03:47 · toolu_01784ijs22nhLLQsurxkPwdv
Create boot/ directory
$ mkdir -p /home/martin/workspace/prototypes/boot-to-claude/boot
result
(Bash completed with no output)
Write · Jul 17, 19:03:53 · toolu_01DcRFhthskgWvjgpdYss8uw
/home/martin/workspace/prototypes/boot-to-claude/boot/boot.S
; Stage 1 MBR boot sector.
; BIOS loads this at 0x7c00 and jumps to it in 16-bit real mode.
; For now: prove the boot path works by printing a character and halting.
; Stage 2 loading is added in Phase 1.

BITS 16
ORG 0x7c00

start:
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7c00

    mov al, 'K'
    mov ah, 0x0e        ; BIOS teletype output
    mov bh, 0
    mov bl, 0x07
    int 0x10

hang:
    hlt
    jmp hang

times 510-($-$$) db 0
dw 0xAA55
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/boot/boot.S (file state is current in your context — no need to Read it back)
Write · Jul 17, 19:03:57 · toolu_016BCfg9NmnsA713wYWhsEee
/home/martin/workspace/prototypes/boot-to-claude/Makefile
AS      := nasm
QEMU    := qemu-system-i386
BUILD   := build

BOOT_SRC := boot/boot.S
BOOT_BIN := $(BUILD)/boot.bin
DISK_IMG := $(BUILD)/disk.img

.PHONY: all run clean

all: $(DISK_IMG)

$(BUILD):
	mkdir -p $(BUILD)

$(BOOT_BIN): $(BOOT_SRC) | $(BUILD)
	$(AS) -f bin -o $@ $<

$(DISK_IMG): $(BOOT_BIN)
	dd if=/dev/zero of=$@ bs=512 count=2880 status=none
	dd if=$(BOOT_BIN) of=$@ conv=notrunc status=none

run: $(DISK_IMG)
	$(QEMU) -drive file=$(DISK_IMG),format=raw -display none -serial stdio -no-reboot

clean:
	rm -rf $(BUILD)
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/Makefile (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:04:09
I'll add a minimal serial-port write in the boot sector too, so Phase 0 is verifiable headlessly (VGA text mode isn't observable with `-display none`).
Edit · Jul 17, 19:04:12 · toolu_019Z9VvR9JVPbXkpeedCfdkd
/home/martin/workspace/prototypes/boot-to-claude/boot/boot.S
-     mov al, 'K'
-     mov ah, 0x0e        ; BIOS teletype output
-     mov bh, 0
-     mov bl, 0x07
-     int 0x10
- 
- hang:
+     mov al, 'K'
+     mov ah, 0x0e        ; BIOS teletype output
+     mov bh, 0
+     mov bl, 0x07
+     int 0x10
+ 
+     ; Also write to COM1 (0x3f8) so headless QEMU (-display none -serial stdio)
+     ; can observe this too. Raw port I/O here only; serial.c driver comes in Phase 2.
+     mov dx, 0x3f8 + 5   ; line status register
+ .wait_tx:
+     in al, dx
+     test al, 0x20       ; transmit holding register empty?
+     jz .wait_tx
+     mov dx, 0x3f8
+     mov al, 'K'
+     out dx, al
+ 
+ hang:
result
The file /home/martin/workspace/prototypes/boot-to-claude/boot/boot.S has been updated successfully. (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:04:14
Now the `.gitignore` and a build/run check.
Write · Jul 17, 19:04:15 · toolu_01Bnn47UQa4L4x7xsMHmFdVq
/home/martin/workspace/prototypes/boot-to-claude/.gitignore
build/
*.o
*.bin
*.img
*.tbc
*.elf
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/.gitignore (file state is current in your context — no need to Read it back)
Bash · Jul 17, 19:04:18 · toolu_012hBhx1hNuJfVEBG39ig2Ee
Build boot image and boot it headlessly in QEMU, capturing serial output
$ make clean >/dev/null && make 2>&1 && timeout 5 qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial stdio -no-reboot; echo "exit=$?"
result
mkdir -p build
nasm -f bin -o build/boot.bin boot/boot.S
dd if=/dev/zero of=build/disk.img bs=512 count=2880 status=none
dd if=build/boot.bin of=build/disk.img conv=notrunc status=none
Kqemu-system-i386: terminating on signal 15 from pid 1859591 (timeout)
exit=124
agent · Jul 17, 19:04:27
Phase 0 verified — QEMU boots the disk image and prints `K` over serial. Committing and moving to Phase 1.
Bash · Jul 17, 19:04:28 · toolu_01N3n2TtTD7sxRGXwmtxVNPv
Stage Phase 0 files
$ git add Makefile .gitignore boot/boot.S && git status --short
result
A  .gitignore
A  Makefile
A  boot/boot.S