GitHub - falkosch/ai-experiments-cv-app-in-assembler: An LLM writing x86-64 by hand: my CV as a 31 KB Windows exe. 2,945 hand-encoded instructions, no compiler, no external assembler, no C runtime ... just a little bit of cheating by using Python and 787539 tokens in one session. · GitHub
Skip to content

About

An LLM writing x86-64 by hand: my CV as a 31 KB Windows exe. 2,945 hand-encoded instructions, no compiler, no external assembler, no C runtime ... just a little bit of cheating by using Python and 787539 tokens in one session.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

my-cv

A CV viewer for Windows, written in machine code.

The point of the project was to find out how far an AI model can get generating a complete application directly in machine code. It was written by Claude (Claude Code, Anthropic) on instruction, one instruction and one byte at a time.

my-cv.exe is a PE32+ (x64) image whose every instruction was encoded from the opcode tables, and whose every header — MZ header, 16-bit DOS stub, COFF header, optional header, section table, import directory — was emitted byte by byte. There is no C runtime, no framework, and no dependency beyond five Microsoft DLLs.

Where the line actually falls. No compiler translated a source language into instructions, and nothing of Python survives into the executable. But Python is doing real work at build time: asm.py is an assembler and pe.py is a linker, both small and both written as part of this project rather than taken off the shelf. The model chose the opcodes, addressing modes, encodings, register allocation, calling convention and image layout; Python appended bytes, resolved addresses and unrolled repetition. Calling that "no assembler" would be overclaiming, so: no external toolchain. The strict version — byte literals, no label resolution — is doable, but the interesting question was whether the model could do the encoding, not whether it could type numbers.

my-cv.exe   30,720 bytes
  code      12,775 bytes  = 2,915 hand-encoded instructions
  data      13,680 bytes  (strings, tables, sine table, embedded cv.txt)
  bss    1,303,200 bytes  (zero-filled by the loader)

It renders cv.txt as a printed-resume page: photo header, blue serif section rules, timeline entries with right-aligned dates, chips, word-wrapped body text — over a drifting flow-field line pattern that parts around the pointer.

Running it

Double-click my-cv.exe, or:

.\my-cv.exe

It reads cv.txt and the portrait from the folder the exe lives in (not the working directory), so it can be launched from anywhere. If cv.txt is missing, it falls back to a copy embedded in the image — press E to write that copy back out as a starting template.

key action
1–9 scroll to a section (eased)
↑ ↓, PgUp PgDn, Home End, wheel scroll (eased)
R reload cv.txt from disk, replaying the intro
E write the built-in template to cv.txt (never overwrites)
O open cv.txt in your editor
A replay the intro animation
move the pointer the background lines part around it
Esc quit

Edit cv.txt, save, press R, and the window re-parses, re-lays out and re-animates the document.

The background

The pattern behind the page is a flow field, traced live every frame.

Each line is a streamline: a point is dropped off the left edge and walked across the window, and at every step its direction is read from a field

dir(x, y, t) = Σ  ampᵢ · sin((x·fxᵢ + y·fyᵢ)/256 + t >> shiftᵢ)

Three waves are summed. One is not enough: a single plane wave has straight-line attractors — the direction along which its phase never changes — and every streamline collapses onto one, which is exactly what the first version did. Three waves at different angles, wavelengths and speeds keep it organic. A line stops as soon as it leaves the frame, and the whole field drifts slowly, so the pattern is never quite the same.

The points are drawn with GDI+: GdipDrawCurveI fits a cardinal spline through them and SmoothingModeAntiAlias renders it soft, so the integration can take long strides and still read as a smooth curve. GDI+ was already linked (it decodes the portrait), GpPoint is {INT, INT} so the existing point buffer is passed straight in, and the only float in the whole program is the pen width — a build-time constant loaded into xmm1 by the one SSE instruction the encoder needs for it.

Compositing

The window is built in three passes, bottom to top:

  1. the flat background — grey margins, the white sheet, its two border rules
  2. the line pattern, drawn over it
  3. the document — text, photo, rules, chips, timeline — on top

The third pass is the interesting one, because GDI has no alpha to composite with. The document is rendered into its own layer on white, and that layer is ANDed into the composite: white leaves the destination alone, so the lines show through wherever there is no content, and anything darker wins where there is. Antialiased glyph edges land between the two, which is close enough that it is invisible at these line intensities.

Two things are opaque rather than blended — the photo and the status strip — so after the AND their rectangles are copied straight over from the content layer. The photo's rect is recorded while the layer is drawn and clamped to the viewport, since it scrolls.

The AND is done by hand, sixteen bytes at a time with SSE2. GDI will do it with a SRCAND BitBlt, but that measured four times slower per byte than SRCCOPY: with identical passes, swapping only the raster op moved the frame from 21.5 % of a core to 53.2 %. A movdqu/pand/movdqu loop does the same work at 17.6 %, and SSE2 is baseline on every x86-64 CPU, so unlike a wider vectorisation it needs no feature test and no fallback path.

Animation

A 15 ms WM_TIMER drives everything:

  • Eased scrolling. Keys and the wheel move a target; each tick the current scroll closes a quarter of the remaining distance, so every jump and every wheel notch glides instead of snapping.
  • A staggered intro. Each row — the header, every section heading, every paragraph, bullet and chip — has an index. A rising intro counter minus index × stagger, clamped to 0…256, gives that row its alpha; text colour is interpolated toward the page white and the row is drawn a few pixels low, so the page fades and slides in from the top down. Section rules and the header rule grow out to full width on the same curve.
  • The drifting field, above.

Because the background always has something new to show, the window cannot simply idle. The clock runs at 60 Hz and that is the frame ceiling: every tick while the pointer is moving or anything else is animating, every fourth tick otherwise.

The layer split is what makes 60 fps affordable. The document is rendered into its own layer and only re-rendered when something about it actually changed: a scroll, a resize, a reload, or a tick of the intro. The background and the compositing run every frame against that cached layer, so animating the pattern costs nothing in text layout, wrapping or glyph rasterisation.

Every layer is a DIB section rather than a compatible bitmap. That is not a detail: GDI+ rasterising into a device-dependent bitmap goes through a read-back path that measured nine times slower — 45 % of a core at 10 fps, against 5 % once the same drawing went into a DIB. The SSE2 composite needs the raw pixel pointer anyway.

Measured, at 60 fps with the pointer in motion: 17.6 % of one core. Idle at 15 fps with the pointer away from the window: 6.2 %.

Writing cv.txt

UTF-8, markdown-ish, line based:

@name Falko Schwabe            header fields (name and role are upper-cased)
@role Senior Developer
@photo photo.jpg               jpg/png/bmp, loaded via GDI+, centre-cropped
@contact you@example.com       up to eight, flowed with separators
@disclaimer A note.            fine print under the contact row
@title my-cv — Falko           the window title

# Erfahrung                    a section: blue serif heading + rule
## Senior Developer            a role — opens a timeline entry (dot + rule)
### Firma, Berlin              the line under it
> 03/2020 – heute              a date: lifted up, right-aligned beside the role
- A bullet.                    long ones wrap with a hanging indent
  a continuation line          two leading spaces, hangs under the bullet
+ A chip                       rounded outline pill
`monospaced line
---                            horizontal rule

Blank lines are vertical space. Anything else is body text.

Building it

python app.py

Requires only Python 3 (as the tool that emits the bytes — nothing from Python ends up in the executable).

file what it is
asm.py an x86-64 instruction encoder: ModRM/SIB/REX construction, labels, and RIP-relative / absolute fixups
pe.py the PE32+ container: DOS stub, COFF and optional headers, section table, and a from-scratch import directory (descriptors, ILT, IAT, hint/name table)
app.py the program itself, written as a sequence of encoder calls, plus its data: palette, font table, style table, layout constants, and the embedded cv.txt

app.py emits one contiguous virtual blob, splits it at a page boundary into .text (r-x) and .data (rw-), resolves every label, and writes the image. Relocations are stripped and the image base is fixed at 0x140000000, so data pointers are baked in as absolute addresses.

How the program works

Startup. SetProcessDPIAware, read LOGPIXELSX from a screen DC, then scale every layout constant and style-table field in place by dpi/96. Create ten fonts, nine brushes, two GDI pens and two GDI+ pens, start GDI+. Build <exe dir>\cv.txt, read it, MultiByteToWideChar it from UTF-8, parse it, and load the portrait. Size the window from the DPI, clamp it to the screen, centre it, start the animation timer, and run a plain GetMessage/DispatchMessage loop.

Parsing. A single pass over the wide buffer. Each line is NUL-terminated in place, classified by its first character, and appended to a flat item array ({pointer, length, style, measured height, width it was measured at}, 32 bytes) grouped into sections ({name, length, first item, count, document offset}, 32 bytes). Bullet markers are rewritten in place from - to •; headings are upper-cased with CharUpperBuffW.

Layout and painting. WM_PAINT renders into a cached memory DC and blits it in one go, so there is no flicker. The body loop walks every section and item, indexes a 64-byte-per-style table (font, colour, indent, line height, gaps, flags, hanging indent, letter tracking), opens a timeline entry on each ## (drawing the dot with Ellipse and closing the entry's vertical rule when the next one starts), looks ahead for the entry's date line to set it right-aligned beside the heading, and hands every text run to the word wrapper, which measures with GetTextExtentExPointW, walks back to the last space, draws the line, and applies the hanging indent to what follows. The page is clipped with IntersectClipRect; the scrollbar thumb is sized and positioned from the measured content height.

Calling convention. Microsoft x64 throughout. Every function uses one uniform frame — eight pushes plus sub rsp, 0x88 — which keeps the stack 16-byte aligned at every call and leaves room for shadow space and outgoing arguments, so incoming stack arguments sit at fixed offsets [rsp+0xF0] and [rsp+0xF8].

Notes

The binary is unsigned, so SmartScreen may warn the first time it runs. It touches exactly two things on disk: it reads cv.txt and photo.jpg, and on E it creates cv.txt with CREATE_NEW — it will never overwrite an existing one.

About

An LLM writing x86-64 by hand: my CV as a 31 KB Windows exe. 2,945 hand-encoded instructions, no compiler, no external assembler, no C runtime ... just a little bit of cheating by using Python and 787539 tokens in one session.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages