{"article":{"slug":"simple-sudoku-solvers-siii-eii-assembly","title":"Simple Sudoku Solvers SIII, EII: Assembly","subtitle":null,"summary":"The latest entry in Veit Heller's Simple Sudoku Solvers series writes a backtracking Sudoku solver in AArch64 assembly for macOS, walking through registers, memory layout, system calls and the board-checking loops with no type system to help.","content_type":"tutorial","language":"en","canonical_url":"https://blog.veitheller.de/Simple_Sudoku_Solvers_SIII%2C_EII%3A_Assembly.html","author":{"name":"Veit Heller","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Veit's Blog","url":"https://blog.veitheller.de/","listing_slug":null,"listing":null},"topics":[{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Assembly","slug":"assembly","url":"https://listedarticles.com/topics/assembly"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":3021,"reading_minutes":13,"published_at":"2026-10-15T00:00:00.000Z","added_at":"2026-10-26T23:14:36.838Z","updated_at":"2026-10-26T23:14:36.838Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/simple-sudoku-solvers-siii-eii-assembly","markdown_url":"https://listedarticles.com/articles/simple-sudoku-solvers-siii-eii-assembly.md","example":false,"citation":"Veit Heller, Veit's Blog. \"Simple Sudoku Solvers SIII, EII: Assembly.\" 15 Oct 2026. https://blog.veitheller.de/Simple_Sudoku_Solvers_SIII%2C_EII%3A_Assembly.html (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://blog.veitheller.de/Simple_Sudoku_Solvers_SIII%2C_EII%3A_Assembly.html"},"body_markdown":"Welcome back to another round of [Simple Sudoku Solvers](https://blog.veitheller.de/sss/)! If you\nare unfamiliar with the series, I suggest you start with [the\nfirst post](https://blog.veitheller.de/Six_Simple_Sudoku_Solvers_I:_Python_(Reference).html) or by perusing [the backlog](https://blog.veitheller.de/sss/). Last time, in [Agda](https://blog.veitheller.de/Simple_Sudoku_Solvers_SIII,_EI:_Agda.html),\nthe solver wasn’t allowed to hand us a board without a correctness\nproof. Today we go in the opposite direction: assembly, where nothing is\nchecked at all, and the machine stares at us directly.\n\nTo be quite specific, today we’ll write AArch64 assembly for macOS,\nbecause that’s the machine I’m sitting at[1](#1).\nSurprisingly, the solver is just a little over 200 lines, and as always,\n[the code is on\nGitHub](https://github.com/hellerve/sudoku).\n\n## Why assembly?\n\nSeason three is about solvers, specifications, and constraints, and\nassembly might seem like the odd duck. But I think it fits!\n\nAfter a proof assistant that checks everything, it’s worth looking at\nwhat’s left when nothing is checked, and nothing is proven.\n\n- **Nothing is hidden.** There is no allocator, no\n  garbage collector, no runtime, and no compiler making choices for us.\n  The board gets copied because we wrote exactly the instructions that\n  copy it.\n- **The machine already knows our tricks!** We’ve been\n  doing bit-twiddling with candidate masks since [the\n  Common Lisp episode](https://blog.veitheller.de/Six_Simple_Sudoku_Solvers_II:_Common_Lisp.html), and in [Forth](https://blog.veitheller.de/Simple_Sudoku_Solvers_SII,_EIII:_Forth.html)\n  we had to roll our own popcount and lowest-bit search. The CPU has\n  instructions for most of that, and today we get to use them directly.\n  So, in many ways, we gain some abstractions!\n- **The stack is, uh, the stack.** In Forth we built our\n  own stack of boards for backtracking. Here, the machine stack is right\n  there, so we might as well use it.\n\nSince there are no types, everything is a number, and the only thing\nstanding between us and a wrong answer is our own diligence. Fun!\n\n## A quick primer on reading AArch64\n\nIf you’ve never read ARM assembly before, here is what you need for\nthis post.\n\nAArch64 has 31 general-purpose registers, `x0` to\n`x30`, each 64 bits wide. Writing `w0` instead of\n`x0` means “the lower 32 bits of register 0”, and since all\nof our numbers are small, we’ll use `w` registers most of the\ntime. `sp` is the stack pointer.\n\nInstructions read like `op destination, source, source`,\nso `add w0, w1, w2` means `w0 = w1 + w2`. Memory\nis only touched by loads and stores:\n`ldrb w9, [x19, w21, uxtw]` loads the byte at address\n`x19 + w21` into `w9`, and `strb`\nstores one. `[x7], #1` means “load from `x7`, then\nadd 1 to `x7`”, which is handy for walking through\nmemory.\n\nWe call functions with `bl`, which jumps and stores the\nreturn address in `x30`, and we return with `ret`,\nwhich jumps back to `x30`. Arguments go in `x0`\nand up, and the result comes back in `x0`. A function may\nfreely overwrite `x0` to `x17` (`x18`\nis reserved), but has to restore `x19` to `x28`\nbefore it returns, so those are where we keep anything we need to\nsurvive a call.\n\nLabels like `1:` are local, and `b.ne 1b` means\n“branch to the closest label `1` going\n**b**ackwards, if the last comparison was\n**n**ot **e**qual”. `1f` looks\n**f**orwards. Comments start with `//`.\n\nThat should be enough to get through it. Everything else I’ll explain\nas we go. I recommend keeping your favorite drawing tool (pen and paper\nwork well) handy to simulate the stack and register movement in your\nhead. I’ve always had trouble with the mnemonics, so I read assembly\ndifferently than most other programming languages, modelling stack and\nregister state as I go. Maybe that works for you as well!\n\n## The board\n\nThe board is the same flat array of 81 bytes that we’ve been using\nfor most of the series, with `0` for an empty cell. We put it\nin the data section (which means it’s baked into the executable quite\nliterally), with the puzzle already filled in:\n\n```\n        .data\n        .p2align 4\nboard:  .byte 8,0,0,0,0,0,0,0,0\n        .byte 0,0,3,6,0,0,0,0,0\n        .byte 0,7,0,0,9,0,2,0,0\n        .byte 0,5,0,0,0,7,0,0,0\n        .byte 0,0,0,0,4,5,7,0,0\n        .byte 0,0,0,1,0,0,0,3,0\n        .byte 0,0,1,0,0,0,0,6,8\n        .byte 0,0,8,5,0,0,0,1,0\n        .byte 0,9,0,0,0,0,4,0,0\n```\n\nThis is the same puzzle we used in the Agda episode, and it’s one of\nthe harder ones I use for checking. `.p2align 4` aligns the\nboard to 16 bytes. Strictly speaking we don’t need that, but we’re about\nto copy it around in 16-byte chunks, and it doesn’t cost us anything, so\nwe might as well do it.\n\nThere is one global board, and the solver mutates it in place.\nWhatever is in `board` when `solve` returns is our\nanswer.\n\n## The solver\n\nAs always, we’ll go outside-in. This is `solve` in its\nentirety, and it is the longest function in the file, so we’ll take it\nin pieces.\n\n```\nsolve:\n        stp     x29, x30, [sp, #-64]!\n        mov     x29, sp\n        stp     x19, x20, [sp, #16]\n        stp     x21, x22, [sp, #32]\n        str     x23, [sp, #48]\n        bl      propagate\n        cbz     w0, fail\n```\n\nThe first five lines are the prologue, which is the ceremony every\nfunction that calls other functions usually goes through (it’s also the\nmain thing that distinguishes functions and labels inside the block). We\nreserve 64 bytes on the stack, save our return address\n(`x30`) and the frame pointer (`x29`), and save\nthe callee-saved registers we’re about to use. `stp` stores a\npair of registers in one go, and the `!` means “update\n`sp` first”.\n\nThe rest is familiar. We propagate, and if propagation hits a dead\nend, we fail. `cbz` is “compare and branch if zero”, which\ncomes up a lot, because it’s basically an `if` statement.\n\nNext up is finding the cell with the fewest candidates:\n\n```\n        adrp    x19, board@PAGE\n        add     x19, x19, board@PAGEOFF\n        mov     w20, #-1                // best cell\n        mov     w21, #10                // its candidate count\n        mov     w22, #0                 // cell\nmrv:\n        ldrb    w9, [x19, w22, uxtw]\n        cbnz    w9, 1f\n        mov     w0, w22\n        bl      cands\n        fmov    s0, w0\n        cnt     v0.8b, v0.8b\n        addv    b0, v0.8b\n        fmov    w9, s0\n        cmp     w9, w21\n        b.hs    1f\n        mov     w21, w9\n        mov     w20, w22\n        mov     w23, w0                 // its candidates\n1:      add     w22, w22, #1\n        cmp     w22, #81\n        b.lo    mrv\n        tbnz    w20, #31, done          // no empty cell left\n```\n\nThe first two lines load the address of the board. On AArch64, an\ninstruction is always 32 bits wide, so you can’t fit a full address into\none. Instead, `adrp` gets the address of the 4KB page the\nboard is on, and `add` adds the offset within that page.\nYou’ll see this pair a lot. It’s a weird little ceremony if you ask me,\nbut nobody did.\n\nThen we loop over all 81 cells. Filled cells are skipped. For empty\ncells, we get the candidate mask from `cands` and count its\nbits, and if the count beats the best one so far, we remember the cell,\nthe count, and the mask.\n\nCounting bits is the first weird, delightful bit. Base AArch64 has no\ninstruction for counting the bits in a general-purpose\nregister[2](#2). It does have one for its vector\nregisters, though. `cnt` counts the bits in each of eight\nbytes, and `addv` adds those eight counts up. So we move our\nmask over to the vector unit with `fmov`, count there, and\nmove the result back. It’s four instructions instead of the\nnine-iteration loop we wrote in Forth where we implemented bit-counting\nby hand.\n\n`w20` starts out as `-1` (all ones in binary).\nIf it’s unchanged after the loop, there are no empty cells left, and\nwe’re done. `tbnz` tests a single bit and branches if it is\nset, and bit 31 is the sign bit, so this is a cheap “is it negative?”\ncheck, and another one of those idioms you just kind of have to\nknow.\n\nNote that we never check if the best cell has zero candidates.\nPropagation already returns a dead end for that, so by the time we\nselect one, every cell has at least one candidate.\n\nNow for the backtracking:\n\n```\n        sub     sp, sp, #96\n        copy81  sp, x19\ntry:\n        rbit    w9, w23\n        clz     w9, w9\n        strb    w9, [x19, w20, uxtw]\n        bl      solve\n        cbnz    w0, found\n        copy81  x19, sp\n        sub     w9, w23, #1\n        and     w23, w23, w9 // drop the digit we tried\n        cbnz    w23, try\n        add     sp, sp, #96\n```\n\nBefore we try anything, we make room on the stack and copy the board\nthere. The board is 81 bytes, but the stack pointer has to stay aligned\nto 16 bytes, so we take 96, more ceremony.\n\nThis is basically what we did in Forth with the board stack, except\nthat it lives on the “real” stack, grows by itself with recursion, and\ngets popped when we return. We don’t have to pick a maximum depth this\ntime, though the depth is bounded anyway, since every level fills at\nleast one cell.\n\nThen we try each candidate. `rbit` reverses the bits of\nthe mask and `clz` counts the leading zeros, which together\ngive us the position of the lowest set bit. That’s the trailing zero\ncount we also used before, in [Rust](https://blog.veitheller.de/Six_Simple_Sudoku_Solvers_V:_Rust.html),\nand as we’ll see in a minute, the position of the bit maps to the digit.\nFinally, we write the digit into the cell and recurse!\n\nIf the recursive call succeeds, we’re done. Otherwise we copy the\nsaved board back and drop the digit we just tried from the mask.\n`x & (x - 1)` clears the lowest set bit of `x`\n(still one of my favorite little tricks from the series), and when the\nmask is empty, we’ve run out of options.\n\nThe rest is bookkeeping:\n\n```\nfail:\n        mov     w0, #0\n        b       1f\nfound:\n        add     sp, sp, #96\ndone:\n        mov     w0, #1\n1:      ldr     x23, [sp, #48]\n        ldp     x21, x22, [sp, #32]\n        ldp     x19, x20, [sp, #16]\n        ldp     x29, x30, [sp], #64\n        ret\n```\n\nWe set the return value and undo the prologue, in reverse. The only\nsubtle part is the order of the labels. `found` has to give\nthe 96 bytes of board back before falling through into\n`done`. `fail` and `done` can be\nreached without them, so they have to navigate around\n`found`.\n\nYou won’t notice right away if you mess any of this up (ask me how I\nknow). If you’re lucky it’ll just crash, if you’re not, it’ll return\nsomewhere random. There are no types to catch you, and nothing checks\nthat you restore what you saved. Functions by hand.\n\n### Copying boards\n\n`copy81` is a macro. The assembler has a simple macro\nsystem, which we use to avoid writing the same eight lines twice (you\nknow that I’m a macro fiend):\n\n```\n.macro copy81 dst, src\n        ldp     q0, q1, [\\src]\n        stp     q0, q1, [\\dst]\n        ldp     q0, q1, [\\src, #32]\n        stp     q0, q1, [\\dst, #32]\n        ldr     q0, [\\src, #64]\n        str     q0, [\\dst, #64]\n        ldrb    w9, [\\src, #80]\n        strb    w9, [\\dst, #80]\n.endm\n```\n\n`q0` and `q1` are 128-bit vector registers, so\none `ldp` moves 32 bytes. Two pairs and a single give us 80\nbytes, and one more byte gets us to 81.\n\nIt’s basically `memcpy`, specialized to our board!\n\n## Propagation\n\nPropagation is the loop we’ve written way too often at this point. We\nfill every cell that has a single candidate, and keep going until\nnothing changes.\n\n```\npropagate:\n        stp     x29, x30, [sp, #-48]!\n        mov     x29, sp\n        stp     x19, x20, [sp, #16]\n        str     x21, [sp, #32]\n        adrp    x19, board@PAGE\n        add     x19, x19, board@PAGEOFF\nagain:\n        mov     w20, #0 // changed?\n        mov     w21, #0 // cell\ncell:\n        ldrb    w9, [x19, w21, uxtw]\n        cbnz    w9, next\n        mov     w0, w21\n        bl      cands\n        cbz     w0, dead\n        sub     w9, w0, #1\n        tst     w0, w9\n        b.ne    next // more than one candidate\n        rbit    w9, w0\n        clz     w9, w9\n        strb    w9, [x19, w21, uxtw]\n        mov     w20, #1\nnext:\n        add     w21, w21, #1\n        cmp     w21, #81\n        b.lo    cell\n        cbnz    w20, again\n        mov     w0, #1\n        b       1f\ndead:\n        mov     w0, #0\n1:      ldr     x21, [sp, #32]\n        ldp     x19, x20, [sp, #16]\n        ldp     x29, x30, [sp], #48\n        ret\n```\n\nThere are two small departures from earlier versions.\n\nFirst, we don’t need popcount here. We only care whether a mask has\nexactly one bit set, and our `x & (x - 1)` trick can\nanswer that. It’s zero exactly when there was at most one bit.\n`tst` computes the `and` without storing it, so we\ndon’t even need an extra register.\n\nSecond, the contradiction check is folded in. In Forth, we had a\nseparate `contradiction?` pass after propagation. Here, if\nany empty cell has no candidates, we nope out on the spot with\n`cbz w0, dead`. It means that an invalid input puzzle can\npass, but surely no one would hand us the wrong input?\n\n## Candidates\n\nThis is where the bit tricks pay their dues. We need the set of\ndigits that don’t appear in a cell’s row, column, or box. As in every\nother bitmask solver so far, we collect the digits we’ve seen in a mask\nand invert it at the end.\n\nThere is one change from before, though. Usually digit `d`\nlives in bit `d-1`, so we have to skip empty cells, because\nthere is no bit -1.\n\nHere, digit `d` lives in bit `d`. Empty cells\nare 0, so they set bit 0, and we throw that bit away at the end. That\nmeans we never have to check whether a cell is empty, and no branching\nis needed.\n\nIt also means that the position of a bit is the digit, which is what\nsaved us the `+ 1` in `solve` and\n`propagate`.\n\nSince we do this for 27 cells, it too gets its own macro:\n\n```\n.macro mark used, at\n        ldrb    w10, \\at\n        lsl     w10, w6, w10\n        orr     \\used, \\used, w10\n.endm\n```\n\n`w6` holds 1, so this loads a cell, shifts 1 left by its\nvalue, and `or`s it into the mask. With that, the candidate\nfunction becomes three loops:\n\n```\ncands:\n        adrp    x9, board@PAGE\n        add     x9, x9, board@PAGEOFF\n        mov     w2, #9\n        udiv    w3, w0, w2       // row\n        msub    w4, w3, w2, w0   // col\n        mov     w5, #0           // used digits\n        mov     w6, #1\n\n        mul     w7, w3, w2\n        add     x7, x9, w7, uxtw // first cell of the row\n        mov     w8, #9\n1:      mark    w5, \"[x7], #1\"\n        subs    w8, w8, #1\n        b.ne    1b\n\n        add     x7, x9, w4, uxtw // first cell of the column\n        mov     w8, #9\n2:      mark    w5, \"[x7], #9\"\n        subs    w8, w8, #1\n        b.ne    2b\n\n        mov     w2, #3\n        udiv    w3, w3, w2\n        mov     w8, #27\n        mul     w3, w3, w8       // box row offset\n        udiv    w4, w4, w2\n        mul     w4, w4, w2       // box column offset\n        add     w3, w3, w4\n        add     x7, x9, w3, uxtw // first cell of the box\n        mov     w8, #3\n3:      mark    w5, \"[x7]\"\n        mark    w5, \"[x7, #1]\"\n        mark    w5, \"[x7, #2]\"\n        add     x7, x7, #9\n        subs    w8, w8, #1\n        b.ne    3b\n\n        mvn     w0, w5\n        and     w0, w0, #0x3fe\n        ret\n```\n\nThere is no modulo instruction, so to get the column we divide\n(`udiv`) and then compute `cell - row * 9` with\n`msub`, which multiplies and subtracts in one go. This seems\nlike a clever trick at first, but again, this is one of those tricks\nyou’ll just find online and then try to remember every time until it’s\nhard-wired in your brain.\n\nThe row loop walks nine consecutive bytes, and the column loop walks\nwith a stride of nine. The box loop unrolls the three cells of each box\nrow and then skips ahead by nine. The box offset is the same arithmetic\nwe did in Forth, 27 for every block of rows, and 3 for every block of\ncolumns. `cands` doesn’t call anything, so it only uses\nregisters it may clobber, and we can skip the prologue entirely. This\nmight not be a good idea in big programs written by multiple people, but\nit’s fine between lazy friends.\n\nAt the end, `mvn` inverts the mask and the\n`and` keeps bits 1 to 9. That throws away bit 0, where the\nempty cells went, and all the bits above 9, which the inversion set (but\nwhich we don’t care about).\n\nAnd that’s about it!\n\n## How fast is it?\n\nOn my machine, the whole program takes about 4 milliseconds, and\nroughly 1.7 of those are the operating system starting a process. The\nPython reference takes about 70 milliseconds for the same puzzle, and\nAgda took 2.7 seconds. I didn’t do anything clever to make it fast,\neither. It recomputes candidates from scratch every time, just like the\nForth solver did. There just isn’t anything to slow us down except our\nown laziness.\n\n## Fin\n\nWe wrote a Sudoku solver in about 200 lines of assembly, and we used\nnative bit instructions to do what we’ve been emulating before. We kept\nour backtracking boards on the “real” stack, and we learned some cool\nnew tricks like `x & (x - 1)`.\n\nThis episode remains the surprise guest this season, but I did enjoy\nwriting it. If the Agda episode was about trusting nothing but our spec,\nthis was about trusting nothing, period. Nothing verified that we\nrestored our registers or that our stack stayed aligned.\n\nPersonally, I prefer the proof, but it was fun to find out how to do\nit with just our CPU.\n\nIn the next episode, we go even lower: Verilog, where there are no\ninstructions anymore, only wires and blinkenlights. See you there!\n\n#### Footnotes\n\n1. If you’re on Linux on an ARM machine, two\nthings change, according to untested claims by a language genie. System\ncalls take their number in `x8` and use `svc #0`,\nand `write` is number 64 there. Addresses are written as\n`adrp x9, board` and\n`add x9, x9, :lo12:board`.\n\nBy the way, Apple doesn’t promise that system call numbers stay\nstable, and officially you should go through the C library. For a blog\npost, I think we can live with that. Just harden your production version\na bit.\n\n2. Newer revisions of the architecture added one,\nbut I didn’t want to depend on that, and I also wanted to show what\nyou’ll see in a lot of other places.\n","body_html":"<p>Welcome back to another round of <a href=\"https://blog.veitheller.de/sss/\" rel=\"nofollow ugc noopener\">Simple Sudoku Solvers</a>! If you\nare unfamiliar with the series, I suggest you start with <a href=\"https://blog.veitheller.de/Six_Simple_Sudoku_Solvers_I:_Python_(Reference).html\" rel=\"nofollow ugc noopener\">the\nfirst post</a> or by perusing <a href=\"https://blog.veitheller.de/sss/\" rel=\"nofollow ugc noopener\">the backlog</a>. Last time, in <a href=\"https://blog.veitheller.de/Simple_Sudoku_Solvers_SIII,_EI:_Agda.html\" rel=\"nofollow ugc noopener\">Agda</a>,\nthe solver wasn’t allowed to hand us a board without a correctness\nproof. Today we go in the opposite direction: assembly, where nothing is\nchecked at all, and the machine stares at us directly.</p>\n<p>To be quite specific, today we’ll write AArch64 assembly for macOS,\nbecause that’s the machine I’m sitting at<a href=\"#1\">1</a>.\nSurprisingly, the solver is just a little over 200 lines, and as always,\n<a href=\"https://github.com/hellerve/sudoku\" rel=\"nofollow ugc noopener\">the code is on\nGitHub</a>.</p>\n<h2 id=\"why-assembly\">Why assembly?</h2>\n<p>Season three is about solvers, specifications, and constraints, and\nassembly might seem like the odd duck. But I think it fits!</p>\n<p>After a proof assistant that checks everything, it’s worth looking at\nwhat’s left when nothing is checked, and nothing is proven.</p>\n<ul><li><p><strong>Nothing is hidden.</strong> There is no allocator, no</p><p>garbage collector, no runtime, and no compiler making choices for us.\nThe board gets copied because we wrote exactly the instructions that\ncopy it.</p></li><li><p><strong>The machine already knows our tricks!</strong> We’ve been</p><p>doing bit-twiddling with candidate masks since <a href=\"https://blog.veitheller.de/Six_Simple_Sudoku_Solvers_II:_Common_Lisp.html\" rel=\"nofollow ugc noopener\">the\nCommon Lisp episode</a>, and in <a href=\"https://blog.veitheller.de/Simple_Sudoku_Solvers_SII,_EIII:_Forth.html\" rel=\"nofollow ugc noopener\">Forth</a>\nwe had to roll our own popcount and lowest-bit search. The CPU has\ninstructions for most of that, and today we get to use them directly.\nSo, in many ways, we gain some abstractions!</p></li><li><p><strong>The stack is, uh, the stack.</strong> In Forth we built our</p><p>own stack of boards for backtracking. Here, the machine stack is right\nthere, so we might as well use it.</p></li></ul>\n<p>Since there are no types, everything is a number, and the only thing\nstanding between us and a wrong answer is our own diligence. Fun!</p>\n<h2 id=\"a-quick-primer-on-reading-aarch64\">A quick primer on reading AArch64</h2>\n<p>If you’ve never read ARM assembly before, here is what you need for\nthis post.</p>\n<p>AArch64 has 31 general-purpose registers, <code>x0</code> to\n<code>x30</code>, each 64 bits wide. Writing <code>w0</code> instead of\n<code>x0</code> means “the lower 32 bits of register 0”, and since all\nof our numbers are small, we’ll use <code>w</code> registers most of the\ntime. <code>sp</code> is the stack pointer.</p>\n<p>Instructions read like <code>op destination, source, source</code>,\nso <code>add w0, w1, w2</code> means <code>w0 = w1 + w2</code>. Memory\nis only touched by loads and stores:\n<code>ldrb w9, [x19, w21, uxtw]</code> loads the byte at address\n<code>x19 + w21</code> into <code>w9</code>, and <code>strb</code>\nstores one. <code>[x7], #1</code> means “load from <code>x7</code>, then\nadd 1 to <code>x7</code>”, which is handy for walking through\nmemory.</p>\n<p>We call functions with <code>bl</code>, which jumps and stores the\nreturn address in <code>x30</code>, and we return with <code>ret</code>,\nwhich jumps back to <code>x30</code>. Arguments go in <code>x0</code>\nand up, and the result comes back in <code>x0</code>. A function may\nfreely overwrite <code>x0</code> to <code>x17</code> (<code>x18</code>\nis reserved), but has to restore <code>x19</code> to <code>x28</code>\nbefore it returns, so those are where we keep anything we need to\nsurvive a call.</p>\n<p>Labels like <code>1:</code> are local, and <code>b.ne 1b</code> means\n“branch to the closest label <code>1</code> going\n<strong>b**ackwards, if the last comparison was\n*<em>n</strong>ot <strong>e</em>*qual”. <code>1f</code> looks\n**f</strong>orwards. Comments start with <code>//</code>.</p>\n<p>That should be enough to get through it. Everything else I’ll explain\nas we go. I recommend keeping your favorite drawing tool (pen and paper\nwork well) handy to simulate the stack and register movement in your\nhead. I’ve always had trouble with the mnemonics, so I read assembly\ndifferently than most other programming languages, modelling stack and\nregister state as I go. Maybe that works for you as well!</p>\n<h2 id=\"the-board\">The board</h2>\n<p>The board is the same flat array of 81 bytes that we’ve been using\nfor most of the series, with <code>0</code> for an empty cell. We put it\nin the data section (which means it’s baked into the executable quite\nliterally), with the puzzle already filled in:</p>\n<pre><code>        .data\n        .p2align 4\nboard:  .byte 8,0,0,0,0,0,0,0,0\n        .byte 0,0,3,6,0,0,0,0,0\n        .byte 0,7,0,0,9,0,2,0,0\n        .byte 0,5,0,0,0,7,0,0,0\n        .byte 0,0,0,0,4,5,7,0,0\n        .byte 0,0,0,1,0,0,0,3,0\n        .byte 0,0,1,0,0,0,0,6,8\n        .byte 0,0,8,5,0,0,0,1,0\n        .byte 0,9,0,0,0,0,4,0,0</code></pre>\n<p>This is the same puzzle we used in the Agda episode, and it’s one of\nthe harder ones I use for checking. <code>.p2align 4</code> aligns the\nboard to 16 bytes. Strictly speaking we don’t need that, but we’re about\nto copy it around in 16-byte chunks, and it doesn’t cost us anything, so\nwe might as well do it.</p>\n<p>There is one global board, and the solver mutates it in place.\nWhatever is in <code>board</code> when <code>solve</code> returns is our\nanswer.</p>\n<h2 id=\"the-solver\">The solver</h2>\n<p>As always, we’ll go outside-in. This is <code>solve</code> in its\nentirety, and it is the longest function in the file, so we’ll take it\nin pieces.</p>\n<pre><code>solve:\n        stp     x29, x30, [sp, #-64]!\n        mov     x29, sp\n        stp     x19, x20, [sp, #16]\n        stp     x21, x22, [sp, #32]\n        str     x23, [sp, #48]\n        bl      propagate\n        cbz     w0, fail</code></pre>\n<p>The first five lines are the prologue, which is the ceremony every\nfunction that calls other functions usually goes through (it’s also the\nmain thing that distinguishes functions and labels inside the block). We\nreserve 64 bytes on the stack, save our return address\n(<code>x30</code>) and the frame pointer (<code>x29</code>), and save\nthe callee-saved registers we’re about to use. <code>stp</code> stores a\npair of registers in one go, and the <code>!</code> means “update\n<code>sp</code> first”.</p>\n<p>The rest is familiar. We propagate, and if propagation hits a dead\nend, we fail. <code>cbz</code> is “compare and branch if zero”, which\ncomes up a lot, because it’s basically an <code>if</code> statement.</p>\n<p>Next up is finding the cell with the fewest candidates:</p>\n<pre><code>        adrp    x19, board@PAGE\n        add     x19, x19, board@PAGEOFF\n        mov     w20, #-1                // best cell\n        mov     w21, #10                // its candidate count\n        mov     w22, #0                 // cell\nmrv:\n        ldrb    w9, [x19, w22, uxtw]\n        cbnz    w9, 1f\n        mov     w0, w22\n        bl      cands\n        fmov    s0, w0\n        cnt     v0.8b, v0.8b\n        addv    b0, v0.8b\n        fmov    w9, s0\n        cmp     w9, w21\n        b.hs    1f\n        mov     w21, w9\n        mov     w20, w22\n        mov     w23, w0                 // its candidates\n1:      add     w22, w22, #1\n        cmp     w22, #81\n        b.lo    mrv\n        tbnz    w20, #31, done          // no empty cell left</code></pre>\n<p>The first two lines load the address of the board. On AArch64, an\ninstruction is always 32 bits wide, so you can’t fit a full address into\none. Instead, <code>adrp</code> gets the address of the 4KB page the\nboard is on, and <code>add</code> adds the offset within that page.\nYou’ll see this pair a lot. It’s a weird little ceremony if you ask me,\nbut nobody did.</p>\n<p>Then we loop over all 81 cells. Filled cells are skipped. For empty\ncells, we get the candidate mask from <code>cands</code> and count its\nbits, and if the count beats the best one so far, we remember the cell,\nthe count, and the mask.</p>\n<p>Counting bits is the first weird, delightful bit. Base AArch64 has no\ninstruction for counting the bits in a general-purpose\nregister<a href=\"#2\">2</a>. It does have one for its vector\nregisters, though. <code>cnt</code> counts the bits in each of eight\nbytes, and <code>addv</code> adds those eight counts up. So we move our\nmask over to the vector unit with <code>fmov</code>, count there, and\nmove the result back. It’s four instructions instead of the\nnine-iteration loop we wrote in Forth where we implemented bit-counting\nby hand.</p>\n<p><code>w20</code> starts out as <code>-1</code> (all ones in binary).\nIf it’s unchanged after the loop, there are no empty cells left, and\nwe’re done. <code>tbnz</code> tests a single bit and branches if it is\nset, and bit 31 is the sign bit, so this is a cheap “is it negative?”\ncheck, and another one of those idioms you just kind of have to\nknow.</p>\n<p>Note that we never check if the best cell has zero candidates.\nPropagation already returns a dead end for that, so by the time we\nselect one, every cell has at least one candidate.</p>\n<p>Now for the backtracking:</p>\n<pre><code>        sub     sp, sp, #96\n        copy81  sp, x19\ntry:\n        rbit    w9, w23\n        clz     w9, w9\n        strb    w9, [x19, w20, uxtw]\n        bl      solve\n        cbnz    w0, found\n        copy81  x19, sp\n        sub     w9, w23, #1\n        and     w23, w23, w9 // drop the digit we tried\n        cbnz    w23, try\n        add     sp, sp, #96</code></pre>\n<p>Before we try anything, we make room on the stack and copy the board\nthere. The board is 81 bytes, but the stack pointer has to stay aligned\nto 16 bytes, so we take 96, more ceremony.</p>\n<p>This is basically what we did in Forth with the board stack, except\nthat it lives on the “real” stack, grows by itself with recursion, and\ngets popped when we return. We don’t have to pick a maximum depth this\ntime, though the depth is bounded anyway, since every level fills at\nleast one cell.</p>\n<p>Then we try each candidate. <code>rbit</code> reverses the bits of\nthe mask and <code>clz</code> counts the leading zeros, which together\ngive us the position of the lowest set bit. That’s the trailing zero\ncount we also used before, in <a href=\"https://blog.veitheller.de/Six_Simple_Sudoku_Solvers_V:_Rust.html\" rel=\"nofollow ugc noopener\">Rust</a>,\nand as we’ll see in a minute, the position of the bit maps to the digit.\nFinally, we write the digit into the cell and recurse!</p>\n<p>If the recursive call succeeds, we’re done. Otherwise we copy the\nsaved board back and drop the digit we just tried from the mask.\n<code>x &amp; (x - 1)</code> clears the lowest set bit of <code>x</code>\n(still one of my favorite little tricks from the series), and when the\nmask is empty, we’ve run out of options.</p>\n<p>The rest is bookkeeping:</p>\n<pre><code>fail:\n        mov     w0, #0\n        b       1f\nfound:\n        add     sp, sp, #96\ndone:\n        mov     w0, #1\n1:      ldr     x23, [sp, #48]\n        ldp     x21, x22, [sp, #32]\n        ldp     x19, x20, [sp, #16]\n        ldp     x29, x30, [sp], #64\n        ret</code></pre>\n<p>We set the return value and undo the prologue, in reverse. The only\nsubtle part is the order of the labels. <code>found</code> has to give\nthe 96 bytes of board back before falling through into\n<code>done</code>. <code>fail</code> and <code>done</code> can be\nreached without them, so they have to navigate around\n<code>found</code>.</p>\n<p>You won’t notice right away if you mess any of this up (ask me how I\nknow). If you’re lucky it’ll just crash, if you’re not, it’ll return\nsomewhere random. There are no types to catch you, and nothing checks\nthat you restore what you saved. Functions by hand.</p>\n<h3 id=\"copying-boards\">Copying boards</h3>\n<p><code>copy81</code> is a macro. The assembler has a simple macro\nsystem, which we use to avoid writing the same eight lines twice (you\nknow that I’m a macro fiend):</p>\n<pre><code>.macro copy81 dst, src\n        ldp     q0, q1, [\\src]\n        stp     q0, q1, [\\dst]\n        ldp     q0, q1, [\\src, #32]\n        stp     q0, q1, [\\dst, #32]\n        ldr     q0, [\\src, #64]\n        str     q0, [\\dst, #64]\n        ldrb    w9, [\\src, #80]\n        strb    w9, [\\dst, #80]\n.endm</code></pre>\n<p><code>q0</code> and <code>q1</code> are 128-bit vector registers, so\none <code>ldp</code> moves 32 bytes. Two pairs and a single give us 80\nbytes, and one more byte gets us to 81.</p>\n<p>It’s basically <code>memcpy</code>, specialized to our board!</p>\n<h2 id=\"propagation\">Propagation</h2>\n<p>Propagation is the loop we’ve written way too often at this point. We\nfill every cell that has a single candidate, and keep going until\nnothing changes.</p>\n<pre><code>propagate:\n        stp     x29, x30, [sp, #-48]!\n        mov     x29, sp\n        stp     x19, x20, [sp, #16]\n        str     x21, [sp, #32]\n        adrp    x19, board@PAGE\n        add     x19, x19, board@PAGEOFF\nagain:\n        mov     w20, #0 // changed?\n        mov     w21, #0 // cell\ncell:\n        ldrb    w9, [x19, w21, uxtw]\n        cbnz    w9, next\n        mov     w0, w21\n        bl      cands\n        cbz     w0, dead\n        sub     w9, w0, #1\n        tst     w0, w9\n        b.ne    next // more than one candidate\n        rbit    w9, w0\n        clz     w9, w9\n        strb    w9, [x19, w21, uxtw]\n        mov     w20, #1\nnext:\n        add     w21, w21, #1\n        cmp     w21, #81\n        b.lo    cell\n        cbnz    w20, again\n        mov     w0, #1\n        b       1f\ndead:\n        mov     w0, #0\n1:      ldr     x21, [sp, #32]\n        ldp     x19, x20, [sp, #16]\n        ldp     x29, x30, [sp], #48\n        ret</code></pre>\n<p>There are two small departures from earlier versions.</p>\n<p>First, we don’t need popcount here. We only care whether a mask has\nexactly one bit set, and our <code>x &amp; (x - 1)</code> trick can\nanswer that. It’s zero exactly when there was at most one bit.\n<code>tst</code> computes the <code>and</code> without storing it, so we\ndon’t even need an extra register.</p>\n<p>Second, the contradiction check is folded in. In Forth, we had a\nseparate <code>contradiction?</code> pass after propagation. Here, if\nany empty cell has no candidates, we nope out on the spot with\n<code>cbz w0, dead</code>. It means that an invalid input puzzle can\npass, but surely no one would hand us the wrong input?</p>\n<h2 id=\"candidates\">Candidates</h2>\n<p>This is where the bit tricks pay their dues. We need the set of\ndigits that don’t appear in a cell’s row, column, or box. As in every\nother bitmask solver so far, we collect the digits we’ve seen in a mask\nand invert it at the end.</p>\n<p>There is one change from before, though. Usually digit <code>d</code>\nlives in bit <code>d-1</code>, so we have to skip empty cells, because\nthere is no bit -1.</p>\n<p>Here, digit <code>d</code> lives in bit <code>d</code>. Empty cells\nare 0, so they set bit 0, and we throw that bit away at the end. That\nmeans we never have to check whether a cell is empty, and no branching\nis needed.</p>\n<p>It also means that the position of a bit is the digit, which is what\nsaved us the <code>+ 1</code> in <code>solve</code> and\n<code>propagate</code>.</p>\n<p>Since we do this for 27 cells, it too gets its own macro:</p>\n<pre><code>.macro mark used, at\n        ldrb    w10, \\at\n        lsl     w10, w6, w10\n        orr     \\used, \\used, w10\n.endm</code></pre>\n<p><code>w6</code> holds 1, so this loads a cell, shifts 1 left by its\nvalue, and <code>or</code>s it into the mask. With that, the candidate\nfunction becomes three loops:</p>\n<pre><code>cands:\n        adrp    x9, board@PAGE\n        add     x9, x9, board@PAGEOFF\n        mov     w2, #9\n        udiv    w3, w0, w2       // row\n        msub    w4, w3, w2, w0   // col\n        mov     w5, #0           // used digits\n        mov     w6, #1\n\n        mul     w7, w3, w2\n        add     x7, x9, w7, uxtw // first cell of the row\n        mov     w8, #9\n1:      mark    w5, &quot;[x7], #1&quot;\n        subs    w8, w8, #1\n        b.ne    1b\n\n        add     x7, x9, w4, uxtw // first cell of the column\n        mov     w8, #9\n2:      mark    w5, &quot;[x7], #9&quot;\n        subs    w8, w8, #1\n        b.ne    2b\n\n        mov     w2, #3\n        udiv    w3, w3, w2\n        mov     w8, #27\n        mul     w3, w3, w8       // box row offset\n        udiv    w4, w4, w2\n        mul     w4, w4, w2       // box column offset\n        add     w3, w3, w4\n        add     x7, x9, w3, uxtw // first cell of the box\n        mov     w8, #3\n3:      mark    w5, &quot;[x7]&quot;\n        mark    w5, &quot;[x7, #1]&quot;\n        mark    w5, &quot;[x7, #2]&quot;\n        add     x7, x7, #9\n        subs    w8, w8, #1\n        b.ne    3b\n\n        mvn     w0, w5\n        and     w0, w0, #0x3fe\n        ret</code></pre>\n<p>There is no modulo instruction, so to get the column we divide\n(<code>udiv</code>) and then compute <code>cell - row * 9</code> with\n<code>msub</code>, which multiplies and subtracts in one go. This seems\nlike a clever trick at first, but again, this is one of those tricks\nyou’ll just find online and then try to remember every time until it’s\nhard-wired in your brain.</p>\n<p>The row loop walks nine consecutive bytes, and the column loop walks\nwith a stride of nine. The box loop unrolls the three cells of each box\nrow and then skips ahead by nine. The box offset is the same arithmetic\nwe did in Forth, 27 for every block of rows, and 3 for every block of\ncolumns. <code>cands</code> doesn’t call anything, so it only uses\nregisters it may clobber, and we can skip the prologue entirely. This\nmight not be a good idea in big programs written by multiple people, but\nit’s fine between lazy friends.</p>\n<p>At the end, <code>mvn</code> inverts the mask and the\n<code>and</code> keeps bits 1 to 9. That throws away bit 0, where the\nempty cells went, and all the bits above 9, which the inversion set (but\nwhich we don’t care about).</p>\n<p>And that’s about it!</p>\n<h2 id=\"how-fast-is-it\">How fast is it?</h2>\n<p>On my machine, the whole program takes about 4 milliseconds, and\nroughly 1.7 of those are the operating system starting a process. The\nPython reference takes about 70 milliseconds for the same puzzle, and\nAgda took 2.7 seconds. I didn’t do anything clever to make it fast,\neither. It recomputes candidates from scratch every time, just like the\nForth solver did. There just isn’t anything to slow us down except our\nown laziness.</p>\n<h2 id=\"fin\">Fin</h2>\n<p>We wrote a Sudoku solver in about 200 lines of assembly, and we used\nnative bit instructions to do what we’ve been emulating before. We kept\nour backtracking boards on the “real” stack, and we learned some cool\nnew tricks like <code>x &amp; (x - 1)</code>.</p>\n<p>This episode remains the surprise guest this season, but I did enjoy\nwriting it. If the Agda episode was about trusting nothing but our spec,\nthis was about trusting nothing, period. Nothing verified that we\nrestored our registers or that our stack stayed aligned.</p>\n<p>Personally, I prefer the proof, but it was fun to find out how to do\nit with just our CPU.</p>\n<p>In the next episode, we go even lower: Verilog, where there are no\ninstructions anymore, only wires and blinkenlights. See you there!</p>\n<h4 id=\"footnotes\">Footnotes</h4>\n<ol><li><p>If you’re on Linux on an ARM machine, two</p><p>things change, according to untested claims by a language genie. System\ncalls take their number in <code>x8</code> and use <code>svc #0</code>,\nand <code>write</code> is number 64 there. Addresses are written as\n<code>adrp x9, board</code> and\n<code>add x9, x9, :lo12:board</code>.</p></li></ol>\n<p>By the way, Apple doesn’t promise that system call numbers stay\nstable, and officially you should go through the C library. For a blog\npost, I think we can live with that. Just harden your production version\na bit.</p>\n<ol start=\"2\"><li><p>Newer revisions of the architecture added one,</p><p>but I didn’t want to depend on that, and I also wanted to show what\nyou’ll see in a lot of other places.</p></li></ol>","headings":[{"level":2,"text":"Why assembly?","id":"why-assembly"},{"level":2,"text":"A quick primer on reading AArch64","id":"a-quick-primer-on-reading-aarch64"},{"level":2,"text":"The board","id":"the-board"},{"level":2,"text":"The solver","id":"the-solver"},{"level":3,"text":"Copying boards","id":"copying-boards"},{"level":2,"text":"Propagation","id":"propagation"},{"level":2,"text":"Candidates","id":"candidates"},{"level":2,"text":"How fast is it?","id":"how-fast-is-it"},{"level":2,"text":"Fin","id":"fin"}]}}