<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alexander Batashev</title>
    <description>The latest articles on DEV Community by Alexander Batashev (@alexbatashev).</description>
    <link>https://dev.to/alexbatashev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F384920%2Fa2bf55f8-9b4f-471c-95f2-1082ef4d18e0.png</url>
      <title>DEV Community: Alexander Batashev</title>
      <link>https://dev.to/alexbatashev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alexbatashev"/>
    <language>en</language>
    <item>
      <title>What Is an Instruction?</title>
      <dc:creator>Alexander Batashev</dc:creator>
      <pubDate>Fri, 28 Aug 2026 14:47:54 +0000</pubDate>
      <link>https://dev.to/alexbatashev/what-is-an-instruction-44mk</link>
      <guid>https://dev.to/alexbatashev/what-is-an-instruction-44mk</guid>
      <description>&lt;p&gt;Did you ever think about how your CPU executes the code that you wrote? The compiler takes your code and turns it into an executable binary. This binary is loaded into memory. CPU reads instructions from the memory one-by-one and executes them. Let's look closely at the example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://godbolt.org/e#g:!((g:!((g:!((h:codeEditor,i:(filename:'1',fontScale:14,fontUsePx:'0',j:1,lang:___c,selection:(endColumn:1,endLineNumber:1,positionColumn:1,positionLineNumber:1,selectionStartColumn:1,selectionStartLineNumber:1,startColumn:1,startLineNumber:1),source:'int+square(int+num)+%7B%0A++++return+num+*+num%3B%0A%7D%0A'),l:'5',n:'0',o:'C+source+%231',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:cg161,filters:(b:'0',binary:'1',binaryObject:'1',commentOnly:'0',debugCalls:'1',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'0',trim:'1',verboseDemangling:'0'),flagsViewOpen:'1',fontScale:14,fontUsePx:'0',j:1,lang:___c,libs:!(),options:'-O3',overrides:!(),selection:(endColumn:12,endLineNumber:4,positionColumn:12,positionLineNumber:4,selectionStartColumn:12,selectionStartLineNumber:4,startColumn:12,startLineNumber:4),source:1),l:'5',n:'0',o:'+x86-64+gcc+16.1+(Editor+%231)',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4" rel="noopener noreferrer"&gt;Open this example in Compiler Explorer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With all optimizations enabled in GCC it's just three lines of assembly&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;imul edi, edi # multiply 1st argument by itself and store result back to edi
mov eax, edi  # move contents of edi to eax
ret           # return control back to caller
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first puzzling thing we meet in the piece above is &lt;code&gt;edi&lt;/code&gt;. What is that? CPU instructions operate on registers. x86_64 provides 16 general-purpose 64-bit registers, up to 32 vector registers (256 or 512 bits long depending on extension support) and recently even 8 tile registers of up to 1KB size. Registers hold state. They can either be explicit like in the example above or implicit. Consider this example.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://godbolt.org/e#g:!((g:!((g:!((h:codeEditor,i:(filename:'1',fontScale:14,fontUsePx:'0',j:1,lang:___c,selection:(endColumn:1,endLineNumber:6,positionColumn:1,positionLineNumber:6,selectionStartColumn:1,selectionStartLineNumber:6,startColumn:1,startLineNumber:6),source:'int+relu(int+num)+%7B%0A++++if+(num+%3C+0)%0A++++++++return+0%3B%0A++++return+num%3B%0A%7D%0A'),l:'5',n:'0',o:'C+source+%231',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:cg161,filters:(b:'0',binary:'1',binaryObject:'1',commentOnly:'0',debugCalls:'1',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'0',trim:'1',verboseDemangling:'0'),flagsViewOpen:'1',fontScale:14,fontUsePx:'0',j:1,lang:___c,libs:!(),options:'-O3',overrides:!(),selection:(endColumn:12,endLineNumber:5,positionColumn:12,positionLineNumber:5,selectionStartColumn:12,selectionStartLineNumber:5,startColumn:12,startLineNumber:5),source:1),l:'5',n:'0',o:'+x86-64+gcc+16.1+(Editor+%231)',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4" rel="noopener noreferrer"&gt;Open this example in Compiler Explorer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;cmovns&lt;/code&gt; instruction performs a &lt;em&gt;conditional move&lt;/em&gt;. Specifically, it copies contents of &lt;code&gt;edi&lt;/code&gt; into &lt;code&gt;eax&lt;/code&gt; if condition is met. But there's only two arguments. How does &lt;code&gt;cmov&lt;/code&gt; know when to actually perform the move? The &lt;code&gt;test&lt;/code&gt; instruction just above it writes to a special &lt;code&gt;EFLAGS&lt;/code&gt; register and &lt;code&gt;cmov&lt;/code&gt; reads from it. You never see the register name in assembly, but it's always there.&lt;/p&gt;

&lt;p&gt;Collectively we refer to a set of instructions, register files and other states as Instruction Set Architecture (ISA).&lt;/p&gt;

&lt;p&gt;Now, why do we read specifically from &lt;code&gt;edi&lt;/code&gt;? In most modern architectures registers a truly general purpose. You can use them however you want. But to make programs and libraries interact with each other platforms usually come up with conventions. For example, on x86_64 under Linux integer function arguments are placed into registers in the following order: &lt;code&gt;rdi&lt;/code&gt;, &lt;code&gt;rsi&lt;/code&gt;, &lt;code&gt;rdx&lt;/code&gt;, &lt;code&gt;rcx&lt;/code&gt;, &lt;code&gt;r8&lt;/code&gt;, and &lt;code&gt;r9&lt;/code&gt;. If there are more integer arguments, they will be spilled into stack. These rules are known as Application Binary Interface (ABI). Linux usually follows System V ABI rules: &lt;a href="https://wiki.osdev.org/System_V_ABI" rel="noopener noreferrer"&gt;https://wiki.osdev.org/System_V_ABI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;ABI and ISA are two of the most important hardware-software contracts when building a compiler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Formalizing ISA descriptions
&lt;/h2&gt;

&lt;p&gt;This was a short primer on assembly programming for those who are unfamiliar with it. This topic is usually referred to as extremely complicated but in practice it is not. It's more of a cognitive load issue when writing big applications rather than complexity of the concept. Unless you try to make sense of all of it.&lt;/p&gt;

&lt;p&gt;ISA usually comes in a form of a huge manual. For example, RISC-V is around 700 pages. How do you compress all that knowledge into a compiler? Usually, with a ton of sweat, blood, tears and some elbow grease. But a couple of years ago a colleague introduced me to the concept of Architecture Description Languages and a wonderful blog of &lt;a href="https://alastairreid.github.io/" rel="noopener noreferrer"&gt;Alastair Reid&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;ADLs try to express human wisdom about computers in a machine-readable format. They describe assembly syntax, encoding rules, and semantics of the instruction. &lt;a href="https://github.com/rems-project/sail" rel="noopener noreferrer"&gt;Sail&lt;/a&gt; is probably the biggest one of those. There's also Arm Architecture Specification Language that is specific to Arm and a few other commercial options.&lt;/p&gt;

&lt;p&gt;My biggest problems with them is usually these languages are developed by hardware people and the languages usually lack the structure required to actually build a compiler from them. Which is why for TIR I decided to create a new ADL tailored for its need.&lt;/p&gt;

&lt;p&gt;Requirements for this new language a roughly this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Have a rigid structure for compilers to rely on;&lt;/li&gt;
&lt;li&gt;Easy to understand, no steep learning curve;&lt;/li&gt;
&lt;li&gt;Semantics should be accurate (even precise) for formal verification of both compiler and hardware;&lt;/li&gt;
&lt;li&gt;Capture performance details of the target devices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To understand the requirements better we first must understand what we're dealing with.&lt;/p&gt;

&lt;h3&gt;
  
  
  RISC-V
&lt;/h3&gt;

&lt;p&gt;Let's start with the new kid on the block - RISC-V. Developed at the University of California in 2010, it quickly gained popularity with big firms like Google, IBM and Qualcomm being actively involved in its development. The specification is open source and royalty free. RISC-V promise is to liberate us from bloatware of past architecture and make both hardware and compiler development much easier.&lt;/p&gt;

&lt;p&gt;RISC-V is built around the idea of extensions. It's like walking into a restaurant and ordering off the menu. I'll have floats, doubles and vector instructions, please. Or you can just order &lt;del&gt;coffee&lt;/del&gt; basic integer extension. Each extension can define a set of instructions and architectural states (registers, etc). Base ISA defines 32 registers with one of them being a hardwired-zero. It also defines 40 instructions (for 32 bit variant) split into 7 instruction formats.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;
RISC-V instruction formats
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;31–27&lt;/th&gt;
&lt;th&gt;26–25&lt;/th&gt;
&lt;th&gt;24–20&lt;/th&gt;
&lt;th&gt;19–15&lt;/th&gt;
&lt;th&gt;14–12&lt;/th&gt;
&lt;th&gt;11–7&lt;/th&gt;
&lt;th&gt;6–0&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;R&lt;/th&gt;
&lt;td colspan="2"&gt;funct7&lt;/td&gt;
&lt;td&gt;rs2&lt;/td&gt;
&lt;td&gt;rs1&lt;/td&gt;
&lt;td&gt;funct3&lt;/td&gt;
&lt;td&gt;rd&lt;/td&gt;
&lt;td&gt;opcode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;I&lt;/th&gt;
&lt;td colspan="3"&gt;imm[11:0]&lt;/td&gt;
&lt;td&gt;rs1&lt;/td&gt;
&lt;td&gt;funct3&lt;/td&gt;
&lt;td&gt;rd&lt;/td&gt;
&lt;td&gt;opcode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;S&lt;/th&gt;
&lt;td colspan="2"&gt;imm[11:5]&lt;/td&gt;
&lt;td&gt;rs2&lt;/td&gt;
&lt;td&gt;rs1&lt;/td&gt;
&lt;td&gt;funct3&lt;/td&gt;
&lt;td&gt;imm[4:0]&lt;/td&gt;
&lt;td&gt;opcode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;B&lt;/th&gt;
&lt;td colspan="2"&gt;imm[12|10:5]&lt;/td&gt;
&lt;td&gt;rs2&lt;/td&gt;
&lt;td&gt;rs1&lt;/td&gt;
&lt;td&gt;funct3&lt;/td&gt;
&lt;td&gt;imm[4:1|11]&lt;/td&gt;
&lt;td&gt;opcode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;U&lt;/th&gt;
&lt;td colspan="5"&gt;imm[31:12]&lt;/td&gt;
&lt;td&gt;rd&lt;/td&gt;
&lt;td&gt;opcode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;J&lt;/th&gt;
&lt;td colspan="5"&gt;imm[20|10:1|11|19:12]&lt;/td&gt;
&lt;td&gt;rd&lt;/td&gt;
&lt;td&gt;opcode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;R4&lt;/th&gt;
&lt;td&gt;rs3&lt;/td&gt;
&lt;td&gt;fmt&lt;/td&gt;
&lt;td&gt;rs2&lt;/td&gt;
&lt;td&gt;rs1&lt;/td&gt;
&lt;td&gt;rm&lt;/td&gt;
&lt;td&gt;rd&lt;/td&gt;
&lt;td&gt;opcode&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sounds good, right? But let's look closer at some of the instructions, specifically &lt;code&gt;slli&lt;/code&gt; (shift left logical immediate):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;code&gt;slli&lt;/code&gt; encoding
&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;ISA&lt;/th&gt;
&lt;th&gt;31–26&lt;/th&gt;
&lt;th&gt;25&lt;/th&gt;
&lt;th&gt;24–20&lt;/th&gt;
&lt;th&gt;19–15&lt;/th&gt;
&lt;th&gt;14–12&lt;/th&gt;
&lt;th&gt;11–7&lt;/th&gt;
&lt;th&gt;6–0&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;RV32I&lt;/th&gt;
&lt;td colspan="2"&gt;0000000&lt;/td&gt;
&lt;td&gt;shamt[4:0]&lt;/td&gt;
&lt;td&gt;rs1&lt;/td&gt;
&lt;td&gt;001&lt;/td&gt;
&lt;td&gt;rd&lt;/td&gt;
&lt;td&gt;0010011&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;th&gt;RV64I&lt;/th&gt;
&lt;td&gt;000000&lt;/td&gt;
&lt;td&gt;shamt[5]&lt;/td&gt;
&lt;td&gt;shamt[4:0]&lt;/td&gt;
&lt;td&gt;rs1&lt;/td&gt;
&lt;td&gt;001&lt;/td&gt;
&lt;td&gt;rd&lt;/td&gt;
&lt;td&gt;0010011&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Technically, this instruction is supposed to be I-type. In practice it does not make sense to shift a value to more than XLEN bits - a value that only requires five or six bits out of the available twelve. The spec shows the other bits are used to distinguish between logical and arithmetic right shift. So, technically, we have at least one more instruction format, because the meaning of those bits is different from what's described by I-type. Just a few pages into the spec and we're already breaking rules and making exceptions.&lt;/p&gt;

&lt;p&gt;Here's another one. Look at the example below. Normally, this is a two-instruction function: we add two numbers and return. Maybe three if you need to move to another register. With RISC-V you don't really have instructions to operate on 16 bit (or 8 bit) values. So you have to do this shit trick to get an expected value. This really complicates our otherwise beautiful instruction selection logic (seriously, sometimes I think these architecture guys genuinely hate us).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://godbolt.org/e#g:!((g:!((g:!((h:codeEditor,i:(filename:'1',fontScale:14,fontUsePx:'0',j:1,lang:___c,selection:(endColumn:2,endLineNumber:3,positionColumn:1,positionLineNumber:1,selectionStartColumn:2,selectionStartLineNumber:3,startColumn:1,startLineNumber:1),source:'short+foo(short+a,+short+b)+%7B%0A++++return+a+%2B+b%3B%0A%7D'),l:'5',n:'0',o:'C+source+%231',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:rv32-cgcc1610,filters:(b:'0',binary:'1',binaryObject:'1',commentOnly:'0',debugCalls:'1',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'0',trim:'1',verboseDemangling:'0'),flagsViewOpen:'1',fontScale:14,fontUsePx:'0',j:1,lang:___c,libs:!(),options:'-O3',overrides:!(),selection:(endColumn:1,endLineNumber:1,positionColumn:1,positionLineNumber:1,selectionStartColumn:1,selectionStartLineNumber:1,startColumn:1,startLineNumber:1),source:1),l:'5',n:'0',o:'+RISC-V+(32-bits)+gcc+16.1.0+(Editor+%231)',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4" rel="noopener noreferrer"&gt;Open this example in Compiler Explorer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And don't get me started on RVV vector extension. It ruins all the claimed benefits entirely. I will do a separate piece on vector instruction sets but here's a short list of complaints. &lt;code&gt;vtype&lt;/code&gt; register is an implicit state we so desperately tried to escape so far. It complicates literally every piece of the stack: hardware, software, compilers. LMUL (grouping multiple registers into one big register) is marketed as free performance. Well, it's not. The hardware pipeline does not magically get wider. Instead, it would split your wide instruction into multiple micro operations and execute sequentially. The only worthy application here is hardware-assisted loop interleaving, but is it worth all the effort? The extension written in a way that assumes CPUs would grow wider indefinitely. Bloody physics constraints us though. Most modern processors have 64 byte cache lines. This perfectly translates to 512-bit registers. Growing beyond that requires changes down the entire stack: memory controller, cache port width, etc. This is too much. And the tasks that benefit from this kind of change are also perfect for specialized accelerators like GPUs or NPUs, which on top of better performance give you far better power efficiency.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F26so0uisalm6tgbj3mgt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F26so0uisalm6tgbj3mgt.png" alt="Two people extrapolate absurd conclusions from short-term trends." width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://xkcd.com/605/" rel="noopener noreferrer"&gt;“Extrapolating,” xkcd #605&lt;/a&gt;, licensed under &lt;a href="https://creativecommons.org/licenses/by-nc/2.5/" rel="noopener noreferrer"&gt;CC BY-NC 2.5&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  ARM
&lt;/h3&gt;

&lt;p&gt;ARM is arguably the most used ISA today. Your phone runs on ARM. In the recent years it got real traction in the data center space and on laptops. Apple's M series chips proved that powerful compute is not limited to x86 and ARM can be just as good and even better.&lt;/p&gt;

&lt;p&gt;But there are a lot of misconceptions about ARM. For starters, ARM is not a single ISA. Modern 64-bit ARM processors use A64 instruction set in the AArch64 execution state. Older ARM processors also used A32 and T32 instruction sets. They have different encodings and pursuite different goals. For the rest of this section I exclusively talk about A64.&lt;/p&gt;

&lt;p&gt;A64 has 32-bit instruction words, 31 general-purpose integer registers, and a load-store architecture. Arithmetic instructions operate on registers; memory is accessed through explicit load and store instructions. Very similar where RISC-V landed. But I'd say this is where similarities end.&lt;/p&gt;

&lt;p&gt;First, let's look at the register file map. Unlike RISC-V, AArch64 exposes multiple views of its integer and SIMD registers. The SIMD and floating-point views overlap, and SVE extends the same low 128 bits. There's a very good reason to do this. Register files can be huge - I've seen vector register files exceeding die area of the actual compute core. Saving that precious space makes your chips either cheaper or more powerful by fitting more cores into the same area. But it comes at the expense of compiler complexity - we now need to take into account multiple register classes and we overall have less architectural space to work with which forces more frequent spills. A quite reasonable trade off, if you ask me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AArch64 register aliases&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Register view&lt;/th&gt;
&lt;th&gt;Width&lt;/th&gt;
&lt;th&gt;Relationship&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;X0&lt;/td&gt;
&lt;td&gt;64 bits&lt;/td&gt;
&lt;td&gt;Full integer register&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;W0&lt;/td&gt;
&lt;td&gt;32 bits&lt;/td&gt;
&lt;td&gt;Low half of X0; a write clears X0's upper 32 bits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Z0&lt;/td&gt;
&lt;td&gt;128 to 2048 bits&lt;/td&gt;
&lt;td&gt;Scalable SVE register&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V0 / Q0&lt;/td&gt;
&lt;td&gt;128 bits&lt;/td&gt;
&lt;td&gt;Low 128 bits of Z0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D0&lt;/td&gt;
&lt;td&gt;64 bits&lt;/td&gt;
&lt;td&gt;Low scalar view of V0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S0&lt;/td&gt;
&lt;td&gt;32 bits&lt;/td&gt;
&lt;td&gt;Low scalar view of V0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H0&lt;/td&gt;
&lt;td&gt;16 bits&lt;/td&gt;
&lt;td&gt;Low scalar view of V0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B0&lt;/td&gt;
&lt;td&gt;8 bits&lt;/td&gt;
&lt;td&gt;Low scalar view of V0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Encoding 31 means &lt;code&gt;XZR&lt;/code&gt; or &lt;code&gt;SP&lt;/code&gt; depending on the operand position. The same five encoded bits therefore name either the zero register or stack pointer.&lt;/p&gt;

&lt;p&gt;For comparison, a RISC-V &lt;code&gt;xN&lt;/code&gt; register has XLEN bits and no narrower aliases.&lt;/p&gt;

&lt;p&gt;But shennanigans don't end with just aliasing. See in the map above that register number 31? Depending on the instruction and operand position, the five-bit value &lt;code&gt;11111&lt;/code&gt; can mean either the stack pointer or the hard-wired zero register. Reading &lt;code&gt;xzr&lt;/code&gt; produces zero and writing it discards the result. Reading or writing &lt;code&gt;sp&lt;/code&gt;, unsurprisingly, accesses the stack pointer. So, in practice we have two registers with overlapping encodings.&lt;/p&gt;

&lt;p&gt;Immediate values have their own surprises. An &lt;code&gt;add&lt;/code&gt; immediate contains a 12-bit value that can optionally be shifted left by twelve bits. Logical instructions use a stranger scheme: they encode repeated, rotated bit patterns rather than a conventional integer. Consequently, this is encodable in one instruction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;and x0, x1, #0x00ff00ff00ff00ff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while many seemingly simpler 64-bit constants are not.&lt;/p&gt;

&lt;p&gt;From a compiler's perspective, an immediate operand therefore cannot be described merely as “an integer between X and Y.” It may need an encodability predicate and transformations between the value seen by the programmer and the bits stored in the instruction.&lt;/p&gt;

&lt;p&gt;Now let's look at a piece of code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ldr w8, [x0, x1, lsl #2]
add w0, w8, w2
ret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ldr&lt;/code&gt; instruction takes the address in x0, adds x1 multiplied by four, and loads a 32-bit value from the resulting address. The &lt;code&gt;add&lt;/code&gt; then adds the third argument and places result into the return register. Quite a lot in just three instructions. And this is another major difference between ARM and RISC-V.&lt;/p&gt;

&lt;p&gt;RISC-V tried to be true to its &lt;strong&gt;reduced&lt;/strong&gt; instruction set motto. A similar load on RISC-V would require a shift and an add before we get to load. The benefit of this is that you can implement a fully working RISC-V chip RTL in just one weekend (probably faster if you know what you're doing). Downsides? You see, real programs have those nasty things called arrays. And the way arrays work is you have some base pointer and you add an index multiplied by element size to it to get the destination. Exactly the thing ldr computes for you. x86 similarly have multiple complex addressing modes for the same reason. RISC-V later responded with &lt;code&gt;Zba&lt;/code&gt; extension that adds &lt;code&gt;shNadd&lt;/code&gt; (N=1,2,3) instructions and they also propose macro-op fusion with &lt;code&gt;ld&lt;/code&gt; instruction to achieve parity with x86. Frankly, I think this came too late and the mechanism should have been opt-out style (similar to rv32e 16 register reduced ISA) for microcontrollers rather then coming as an optional extension.&lt;/p&gt;

&lt;p&gt;ARM also has a few flag registers (which RISC-V lacks completely in the base ISA). Unlike x86, most integer arithmetic instructions do not update condition flags unless you explicitly ask them to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;add  x0, x1, x2   // do not update flags
adds x0, x1, x2   // update N, Z, C and V
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids creating an implicit dependency on the flags register when the result is not going to be used by a conditional instruction. But it introduces aliases. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cmp x1, x2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is not really a separate operation. It is an alias for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;subs xzr, x1, x2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The subtraction is performed, the flags are retained, and the numeric result is thrown away by writing it to the zero register.&lt;/p&gt;

&lt;p&gt;Aliases matter when describing an ISA. Does &lt;code&gt;cmp&lt;/code&gt; exist as an instruction? To an assembly programmer, yes. To the decoder, no: it is one particular use of &lt;code&gt;subs&lt;/code&gt;. A machine description needs to represent both views without duplicating the instruction semantics.&lt;/p&gt;

&lt;p&gt;There are some nice things about recent ARM decisions, though. Thumb32 is mostly dead. All instructions are 32 bits length (unlike RISC-V where they can be 2-16 bytes and compressed 2-byte instructions are mandatory in RVA23 profile, or x86 where they are 1-15 bytes). Such uniformity means instruction decoder (hardware or software for that matter) is extremely simple and the code is always aligned at instruction size. This is exactly what allowed Apple introduce an 8-issue beast in 2020.&lt;/p&gt;

&lt;h3&gt;
  
  
  x86
&lt;/h3&gt;

&lt;p&gt;If RISC-V was designed and Arm was carefully engineered, x86 mostly happened.&lt;/p&gt;

&lt;p&gt;That is not an insult. The architecture survived because every generation preserved an enormous body of existing software while adding whatever the next generation needed. The result is extraordinarily capable, remarkably compatible, and deeply inconvenient to describe.&lt;/p&gt;

&lt;p&gt;Much like A64, registers in x86 can overlap. But they do so in a much more chaotic way. The original 8086 had fourteen 16-bit registers, but only four of them were general-purpose. You could also access low and high parts of those via AH/AL style aliases. 80386 upgraded registers to 32-bits. Old AX, BX, CX, DX are now the lower part of EAX, EBX, ECX, EDX, but you can't access high part. AMD Opteron introduced x86-64 architecture, which did another upgrade to 64-bit registers and added eight additional GPRs r8-r15.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;x86-64 register aliases&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Register view&lt;/th&gt;
&lt;th&gt;Width or bits&lt;/th&gt;
&lt;th&gt;Relationship&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RAX&lt;/td&gt;
&lt;td&gt;64 bits&lt;/td&gt;
&lt;td&gt;Full integer register&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EAX&lt;/td&gt;
&lt;td&gt;32 bits&lt;/td&gt;
&lt;td&gt;Low half of RAX; a write clears the upper 32 bits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AX&lt;/td&gt;
&lt;td&gt;16 bits&lt;/td&gt;
&lt;td&gt;Low 16 bits; a write preserves the rest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AH&lt;/td&gt;
&lt;td&gt;bits 8 to 15&lt;/td&gt;
&lt;td&gt;High byte; unavailable with a REX prefix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AL&lt;/td&gt;
&lt;td&gt;bits 0 to 7&lt;/td&gt;
&lt;td&gt;Low byte; a write preserves the rest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZMM0&lt;/td&gt;
&lt;td&gt;512 bits&lt;/td&gt;
&lt;td&gt;Full vector register&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YMM0&lt;/td&gt;
&lt;td&gt;256 bits&lt;/td&gt;
&lt;td&gt;Low 256 bits of ZMM0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XMM0&lt;/td&gt;
&lt;td&gt;128 bits&lt;/td&gt;
&lt;td&gt;Low 128 bits of ZMM0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Legacy, VEX, and EVEX encodings differ in what a write to YMM0 or XMM0 does to the upper bits.&lt;/p&gt;

&lt;p&gt;Unlike A64, x86 instructions have variable length. An instruction can contain legacy prefixes, a REX or vector prefix, one or more opcode bytes, a ModR/M byte, an optional SIB byte, a displacement, and an immediate. The complete instruction may be anywhere from one to fifteen bytes long.&lt;/p&gt;

&lt;p&gt;Consider the following instruction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;add eax, DWORD PTR [rbx + rcx*4 + 8]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads a 32-bit integer from memory, adds it to &lt;code&gt;eax&lt;/code&gt;, writes the result back to &lt;code&gt;eax&lt;/code&gt;, and updates the condition flags. The address is calculated from a base register, a scaled index register, and a displacement—all in one instruction.&lt;/p&gt;

&lt;p&gt;One possible encoding is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;03 44 8b 08
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first byte selects a form of &lt;code&gt;add&lt;/code&gt;. The next byte says that the source is a memory operand, the destination is &lt;code&gt;eax&lt;/code&gt;, and a SIB byte follows. The SIB byte chooses &lt;code&gt;rbx&lt;/code&gt; as the base, &lt;code&gt;rcx&lt;/code&gt; as the index, and four as its scale. The final byte is the displacement.&lt;/p&gt;

&lt;p&gt;This is not a row of independent fields like a RISC-V instruction format. One field determines whether another field exists, and that field can in turn change the interpretation of values elsewhere in the instruction. Prefixes may change operand size, address size, available registers, or even the instruction selected by an opcode.&lt;/p&gt;

&lt;p&gt;The same assembly instruction can also have more than one valid encoding. Conversely, almost identical encodings can acquire different meanings depending on the execution mode and prefixes preceding them. Decoding x86 is closer to walking through a decision tree than extracting fields from a bit structure.&lt;/p&gt;

&lt;p&gt;x86 also makes extensive use of implicit state. Ordinary integer arithmetic normally updates several flags whether the program needs them or not. Multiplication and division have forms with implicit input and output registers. String instructions implicitly use source and destination pointers, a counter, and the direction flag. An instruction that appears to have no operands in assembly may therefore read and modify half a dozen pieces of architectural state.&lt;/p&gt;

&lt;p&gt;There are no scalar floats in x86 instruction set. At all. The old way of doing things was something called x87. To process floats you had to acquire a separate co-processor, like 8087. Floats were 80 bit long. The architecture itself was stack-based as opposed to register based architecture we use and love today. Only with introduction of x86-64 we gained true floating point instructions. What we didn't get is separate float registers. Instead, SSE re-uses vector registers for float calculations. So, conceptually, each float operation just rides on a single lane of a wider SIMD engine. And I like that decision a lot. SIMD is a great way to extract more parallelism in the program and float subroutines tend to be compute heavy and parallelization friendly most of the time. I really wish RISC-V had adopted this decision (it still can for OoO cores with register renaming).&lt;/p&gt;

&lt;h2&gt;
  
  
  TMDL
&lt;/h2&gt;

&lt;p&gt;TIR Machine Description Language. One specification to generate selection rules, usable ISA simulators, assembly and object parsers/emitters and more.&lt;/p&gt;

&lt;p&gt;Let's start with registers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;isa RV32I {
    param XLEN: Integer = 32;
}

isa RV64I {
    param XLEN: Integer = 64;
}

register_class GPR for [RV32I, RV64I] {
    param ENCODING_LEN: Integer = 5;
    param WIDTH: Integer = self.XLEN;

    registers {
        x0("zero") =&amp;gt; { traits = [hardwired_zero] },
        x1("ra") =&amp;gt; {},
        x2("sp") =&amp;gt; {},
        x10..x17("a{}") =&amp;gt; {},
        x28..x31("t{}") =&amp;gt; {},
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;code&gt;isa&lt;/code&gt; is a feature and a bag of parameters. In this case both base ISAs define &lt;code&gt;XLEN&lt;/code&gt;, so the same register class can use &lt;code&gt;self.XLEN&lt;/code&gt; and become 32 or 64 bits wide depending on the selected target. Extensions are ISAs too. They can say &lt;code&gt;requires RV64I&lt;/code&gt;, or even &lt;code&gt;requires [RV32I | RV64I]&lt;/code&gt; when either base will do. This makes the dependency graph explicit instead of hiding it in a Rust &lt;code&gt;if&lt;/code&gt; that nobody remembers to update.&lt;/p&gt;

&lt;p&gt;Registers have an encoding width and a value width. Those are not the same thing. A RISC-V register holds 64 bits on RV64, but its name still occupies five bits in an instruction. Aliases go in parentheses. Ranges are expanded by the compiler, and &lt;code&gt;{}&lt;/code&gt; in &lt;code&gt;"a{}"&lt;/code&gt; is replaced with a number. Finally, &lt;code&gt;hardwired_zero&lt;/code&gt; is a trait rather than a special case in the simulator. Reading &lt;code&gt;x0&lt;/code&gt; produces zero. Writing it goes into the same black hole where my expectations for RVV went.&lt;/p&gt;

&lt;p&gt;The backend also describes the ABI separately. This one is used by the RV64 and vector definitions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;abi LP64("lp64") for [RV64I, RVV] {
    stack { align = 16; grows = down; red_zone = 0; slot_size = 8; }
    sp = GPR::x2;
    ra = GPR::x1;
    fp = GPR::x8;
    args int -&amp;gt; [GPR::x10..GPR::x17], then stack;
    args vector -&amp;gt; [VR::v8..VR::v23], then stack;
    rets int -&amp;gt; [GPR::x10, GPR::x11];
    rets vector -&amp;gt; [VR::v8, VR::v9];
    callee_saved = [GPR::x2, GPR::x8..GPR::x9, GPR::x18..GPR::x27];
    reserved = [GPR::x0, GPR::x3, GPR::x4];
    classifier = riscv;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember our &lt;code&gt;edi&lt;/code&gt; question at the beginning? This is where its answer belongs. An ISA says which registers exist. An ABI says that the first argument lives in one of them. Mixing the two is tempting and wrong. The same x86 instruction set is used by System V and Windows, yet they pass arguments in different registers.&lt;/p&gt;

&lt;p&gt;Register classes can inherit from one another and describe overlapping files. The x86 backend defines 64-bit &lt;code&gt;GPR&lt;/code&gt;, then derives &lt;code&gt;GPR32&lt;/code&gt;, &lt;code&gt;GPR16&lt;/code&gt; and &lt;code&gt;GPR8&lt;/code&gt; from it. &lt;code&gt;eax&lt;/code&gt; and &lt;code&gt;rax&lt;/code&gt; have the same index, therefore the allocator knows they are views of the same physical register. High-byte registers are even more entertaining:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;register_class GPR8H for [X86] {
    file = GPR;
    param ENCODING_LEN: Integer = 4;
    param WIDTH: Integer = 8;
    param WRITE_POLICY: String = "merge";
    param BIT_OFFSET: Integer = 8;

    registers {
        ah =&amp;gt; { index = 0 },
        ch =&amp;gt; { index = 1 },
        dh =&amp;gt; { index = 2 },
        bh =&amp;gt; { index = 3 },
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ah&lt;/code&gt; shares the &lt;code&gt;GPR&lt;/code&gt; file but starts at bit eight. It also cannot be used with a REX prefix, because x86 is a museum where exhibits are still load-bearing. AArch64's register 31 trick is described using the same mechanism. One class names slot 31 &lt;code&gt;xzr&lt;/code&gt;, another inherits the file and names that slot &lt;code&gt;sp&lt;/code&gt;. Operand position chooses the class and therefore the meaning. No handwritten exception in the assembly parser is required.&lt;/p&gt;

&lt;p&gt;Now let's define an instruction. TMDL splits the repetitive shape into a template and keeps the interesting part in the instruction itself. Here is the RISC-V R-type template, shortened only by removing comments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;template RType for [RV32I, RV64I] {
    param MNEMONIC: String;
    param FUNCT7: bits&amp;lt;7&amp;gt;;
    param FUNCT3: bits&amp;lt;3&amp;gt;;
    param OPCODE: bits&amp;lt;7&amp;gt;;

    operands {
        rd: GPR,
        rs1: GPR,
        rs2: GPR,
    }

    encoding {
        0..6 =&amp;gt; OPCODE,
        7..11 =&amp;gt; rd,
        12..14 =&amp;gt; FUNCT3,
        15..19 =&amp;gt; rs1,
        20..24 =&amp;gt; rs2,
        25..31 =&amp;gt; FUNCT7,
    }

    asm { "{self.MNEMONIC} {rd}, {rs1}, {rs2}" }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Integer&lt;/code&gt; is an unbounded value used while compiling the specification. &lt;code&gt;bits&amp;lt;N&amp;gt;&lt;/code&gt; is an N-bit value that exists in the described machine. This distinction catches a surprising number of mistakes. Accidentally stuffing eight bits into a seven-bit opcode should fail here, not become a broken instruction three code generators later.&lt;/p&gt;

&lt;p&gt;Bit zero in an &lt;code&gt;encoding&lt;/code&gt; block is the least significant bit. Ranges are inclusive, so &lt;code&gt;0..6&lt;/code&gt; really is seven bits. Operands may be sliced too. The RISC-V store immediate is split across two distant fields with &lt;code&gt;imm[0..4]&lt;/code&gt; and &lt;code&gt;imm[5..11]&lt;/code&gt;. In x86, &lt;code&gt;r8&lt;/code&gt; through &lt;code&gt;r15&lt;/code&gt; are even sillier. The low three bits go into ModR/M while the fourth bit goes into REX. The real &lt;code&gt;mov&lt;/code&gt; load definition says exactly that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;encoding {
    rex!({ base[3] }, { dst[3] }, 0b1)
    8..15 =&amp;gt; 0x8B,
    16..18 =&amp;gt; base[0..2],
    19..21 =&amp;gt; dst[0..2],
    22..23 =&amp;gt; 0b00,
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no requirement that an instruction looks like a neat table. TMDL describes the bits that exist, even when one of them has escaped into a prefix several bytes away.&lt;/p&gt;

&lt;p&gt;Concrete instructions inherit the structure and fill the holes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;instruction Add for [RV32I, RV64I] : ALUOp {
    param MNEMONIC: String = "add";
    param FUNCT3: bits&amp;lt;3&amp;gt; = 0b000;

    behavior {
        rd = rs1 + rs2;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ALUOp&lt;/code&gt; itself inherits &lt;code&gt;RType&lt;/code&gt; and supplies the common opcode, function bits and scheduling class. This is deliberately boring. Adding an instruction should mostly mean naming its odd bits and stating what it does.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;behavior&lt;/code&gt; block is executable semantics. It is used by the interpreter, but it is not merely simulator code. TIR can translate the expression into symbolic form and ask an SMT solver whether an instruction-selection rule preserves the same value. Arithmetic operates on fixed-width bitvectors, so overflow behaves like hardware overflow. Helpers such as &lt;code&gt;sext&lt;/code&gt;, &lt;code&gt;zext&lt;/code&gt;, &lt;code&gt;extract&lt;/code&gt;, &lt;code&gt;load&lt;/code&gt; and &lt;code&gt;store&lt;/code&gt; make width changes and memory effects explicit.&lt;/p&gt;

&lt;p&gt;Real instructions get less cute. This is &lt;code&gt;lw&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;instruction LoadWord for [RV32I, RV64I] : LoadInst {
    param MNEMONIC: String = "lw";
    param FUNCT3: bits&amp;lt;3&amp;gt; = 0b010;

    behavior {
        try {
            rd = sext(load(rs1 + sext(imm, self.XLEN), 4, 0b1), self.XLEN);
        } except misaligned_load(addr) {
            trap(4, addr);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It calculates the address, loads four bytes, sign-extends the result, and describes the misaligned-access trap. That last part matters. A simulator that quietly accepts every unaligned load is pleasant right until somebody uses it to validate a compiler.&lt;/p&gt;

&lt;p&gt;The earlier &lt;code&gt;slli&lt;/code&gt; exception also falls out naturally. Its operand is &lt;code&gt;bits&amp;lt;log2Ceil(self.XLEN)&amp;gt;&lt;/code&gt;, which is five bits on RV32 and six on RV64. The supposedly generic twelve-bit I-type immediate never enters the picture. We describe the instruction that exists, not the aesthetically pleasing table printed a few pages earlier in the manual.&lt;/p&gt;

&lt;p&gt;Implicit state is written by its qualified name. An x86 arithmetic instruction assigns &lt;code&gt;EFLAGS::zf&lt;/code&gt;, &lt;code&gt;EFLAGS::sf&lt;/code&gt; and friends. A conditional jump reads them in its guard. A call modifies &lt;code&gt;GPR::rsp&lt;/code&gt;, stores the return address, then changes &lt;code&gt;PC::pc&lt;/code&gt;. This is a small detail with a large consequence: an instruction with zero assembly operands can still expose every state dependency to the compiler and verifier.&lt;/p&gt;

&lt;p&gt;Assembly is just another view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;asm { "{self.MNEMONIC} {rd}, {imm}({rs1})" }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same template drives printing and parsing. Placeholders are typed operands, not a regular expression assembled with hope. Multiple instruction definitions may share a mnemonic when their operand classes or encodings differ. The AArch64 backend has several &lt;code&gt;ldr&lt;/code&gt; definitions for integer, float, vector and scaled-offset forms. This is correct. &lt;code&gt;ldr&lt;/code&gt; is a spelling, not an instruction identity.&lt;/p&gt;

&lt;p&gt;Templates stop being convenient on x86 because changing operand width may add a prefix, move every following field and switch the legal register class. TMDL therefore has Rust-like declarative macros. They rewrite tokens before parsing and can expand at the top level, inside a behavior or even inside an encoding block. The x86 backend uses a tiny &lt;code&gt;rex!&lt;/code&gt; macro above, and larger macros generate whole families of ALU and atomic instructions. I did not want to turn the language into a general-purpose metaprogramming swamp, so macro arguments are deliberately limited to identifiers, literals and token trees. Enough rope to remove boilerplate, not enough to establish a consultancy around it.&lt;/p&gt;

&lt;p&gt;Finally, an instruction has performance properties. The ISA assigns it to a machine-independent scheduling class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sched_class WriteIALU { latency = 1; }
sched_class WriteLoad { latency = 3; }

template LoadInst for [RV32I, RV64I] : IType {
    schedule { units = [WriteLoad]; }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A concrete machine then binds those classes to real resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;machine OutOfOrderCore ("rv64-ooo") for [RV64I] {
    issue_width = 4;

    buffers { rob = 128; lsq = 32; iq = 64; }
    unit ALU { count = 4; }
    unit LSU { count = 2; }

    bind WriteIALU { latency = 1; uses = [ALU]; }
    bind WriteLoad { latency = 4; uses = [LSU]; }
    override Add { latency = 2; uses = [ALU]; }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation is important. &lt;code&gt;add&lt;/code&gt; means the same thing on every compliant RISC-V core, but it certainly does not cost the same. A machine can define issue width, pipeline stages, functional units, buffers, forwarding paths, register-file sizes, micro-ops, macro-fusion and dependency-breaking idioms. The compiler cost model and the &lt;code&gt;tir sched&lt;/code&gt; analyzer consume the same description. At least when they disagree, they have to find a more interesting excuse.&lt;/p&gt;

&lt;p&gt;So an instruction in TMDL is not one record with an opcode glued to it. It is the intersection of several contracts: where it exists, which operands it accepts, how those operands become bits and text, what architectural state changes, and how a particular machine executes it. The syntax stays rigid because every consumer needs to agree on those facts. There is still plenty of room for architectural nonsense. It is simply written down once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outro
&lt;/h2&gt;

&lt;p&gt;I think we covered a lot today. Instruction sets appear to be easy. Untill they are not. These complications are a byproduct of their time, or rather constraints engineers faced at that time - expensive memory, power walls, process nodes, even programs people were writing and using. I think TMDL finds a good balance between capturing enough detail about the instructions and leaving old baggage behind. In the next post I will dive deeper into how the DSL compiler itself is built and what kinds of outputs it can produce.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://cache-miss.dev/posts/what-is-an-instruction.html" rel="noopener noreferrer"&gt;Originally published on cache-miss.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>compilers</category>
      <category>hardware</category>
      <category>programming</category>
      <category>riscv</category>
    </item>
    <item>
      <title>Building a Compiler from First Principles</title>
      <dc:creator>Alexander Batashev</dc:creator>
      <pubDate>Sun, 02 Aug 2026 13:12:10 +0000</pubDate>
      <link>https://dev.to/alexbatashev/building-a-compiler-from-first-principles-5b51</link>
      <guid>https://dev.to/alexbatashev/building-a-compiler-from-first-principles-5b51</guid>
      <description>&lt;p&gt;The moment I wrote my first program, I knew I wanted to build a programming language. But how are languages made? How did these carefully arranged magic spells become instructions a machine could execute?&lt;/p&gt;

&lt;p&gt;For a long time, compiler construction was my main technical obsession — and it still is. I devoured anything I could find about compilers, intermediate representations, parsers, and LLVM. I worked through the Kaleidoscope tutorials and spent hours refining the syntax of my soon-to-be language. At the start of my third year of college, that obsession led me to an internship with Intel's Compilers and Languages team. I was suddenly being paid to work on real programming languages used by real developers.&lt;/p&gt;

&lt;p&gt;Eight years later, I know much more about compilers, CPUs, GPUs, and the systems around them. I also have more questions.&lt;/p&gt;

&lt;p&gt;Why are production compilers so complicated? Which parts of that complexity are inherent to the problem, and which are consequences of accumulated design decisions? Is the architecture we use today the only reasonable one? Do I really understand how compilers work well enough to derive one from the machine upward?&lt;/p&gt;

&lt;p&gt;Puzzled by those questions, and deeply inspired by John Regehr's &lt;a href="https://arxiv.org/abs/1809.02161" rel="noopener noreferrer"&gt;&lt;em&gt;Future Directions for Optimizing Compilers&lt;/em&gt;&lt;/a&gt;, I decided to build a compiler from scratch. Not by pretending that the last fifty years of compiler research never happened, and not by reproducing an existing compiler feature for feature. Instead, I want to treat familiar design choices as hypotheses rather than axioms. I want to begin with the semantics of the machine, derive the requirements of each layer, and justify every abstraction as it appears.&lt;/p&gt;

&lt;p&gt;TIR is my attempt to do that. It is the target-independent intermediate representation at the center of an end-to-end compiler infrastructure: a C front end, optimization passes, and back ends for multiple processor architectures. This series will document how it is designed, where it succeeds, and where its assumptions break down.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Counts as a Compiler?
&lt;/h2&gt;

&lt;p&gt;Okay, so I've talked about my passion for compiler construction probably a bit too much. But what are compilers? If you took a foundations of programming class in college, the lecturer probably explained to you that a compiler is a tool that turns your program in some programming language (say, C or Rust) into assembly — another language that a CPU can understand. That definition is useful, but narrower than the one I will use in this series.&lt;/p&gt;

&lt;p&gt;Personally, I define compilers as tools that lower the level of abstractions. A script that reads JSON and generates pre-filled C structures is a compiler. A tool that translates a small domain-specific language into configuration or executable code is also a compiler. GCC and Clang are simply larger and more familiar examples.&lt;/p&gt;

&lt;p&gt;For the purposes of this series, by "compiler" I am going to refer to something LLVM-shaped. If you have never heard of LLVM, it is a framework for compiler construction. It provides building blocks for programming language authors: target-independent intermediate representation, a set of optimizations, target definitions for x86, ARM, RISC-V and other ISAs. The broader LLVM project includes C and C++ frontends, a Fortran frontend, as well as linkers, debuggers, runtimes, and related tooling.&lt;/p&gt;

&lt;p&gt;And that is roughly what I am building — an end-to-end modular infrastructure for constructing compilers. C frontend, target-independent IR, a set of optimizations, and multiple backends to run on a bunch of different CPUs. But I want to do it differently. Unlike a traditional C-compiler-in-a-weekend kind of tutorial, I want my thing be real enough to compile a wide range of real-world codebases. Competitive performance is a stretch goal, not an assumption here. The measurements (and the failures) are part of the experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "From First Principles" Means
&lt;/h2&gt;

&lt;p&gt;Just twenty years ago every major chip manufacturer would provide their users with a suite of compilers (typically C/C++ and Fortran). Microsoft, Intel, IBM, AMD — all of them. Often they would take money for you to actually use it and in exchange you would get excellent performance out of the box.&lt;/p&gt;

&lt;p&gt;Over time, much of the industry converged on LLVM-based toolchains. Intel, IBM, Arm, AMD, and many other vendors now build substantial parts of their compiler offerings on LLVM. This happened for good reasons: LLVM offers permissive licensing, reusable infrastructure, support for many languages and targets, and a mature surrounding ecosystem.&lt;/p&gt;

&lt;p&gt;Its success also means that many new compiler projects inherit LLVM's architectural assumptions before asking whether those assumptions fit the problem they are trying to solve. TIR will use existing compilers as evidence and reference points, but not as specifications.&lt;/p&gt;

&lt;p&gt;Building from first principles does not mean blindly ignoring established compiler theory. It means starting from the requirements imposed by programs and machines, then deriving the design from those requirements. Familiar techniques remain available, but they must earn their place.&lt;/p&gt;

&lt;p&gt;Compiler architecture is often divided into three broad layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend.&lt;/strong&gt; Parses source text, checks its meaning, and translates it into an intermediate representation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Middle-end.&lt;/strong&gt; Analyzes and transforms that representation, mostly independently of the eventual target architecture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend.&lt;/strong&gt; Lowers the representation to machine instructions for a particular target and emits assembly or object code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TIR will retain this broad decomposition where it proves useful, but the details inside each layer are open to question. What information should the IR preserve? Which transformations belong in the middle end, and which depend on the target? Which abstractions make optimization easier, and which merely move complexity somewhere else?&lt;/p&gt;

&lt;p&gt;Those are the questions this project is meant to explore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Plan
&lt;/h2&gt;

&lt;p&gt;We will build an intuition for what an instruction is, how hardware and compilers represent it differently, and whether a better abstraction can bridge that gap. From there, we will derive the requirements for an intermediate representation, build analyses and optimization passes, and add code generation for multiple targets.&lt;/p&gt;

&lt;p&gt;In parallel with building the core infrastructure for middle end and backend, we will work on a simple C frontend and experiment with various design choices. Compilers bridge software and hardware. One does not make sense without the other. The design therefore has to evolve in both directions: real source programs must shape the low-level abstractions just as real hardware constraints shape the language and optimization layers above them.&lt;/p&gt;

&lt;p&gt;The first substantial milestone here will be compiling and running SQLite (although, I'm sure there will be stop gaps where we play with simpler programs). Success will not be measured only by whether the program runs, but also by language coverage, correctness, compiler and generated code performance and quality, and the ease with which the system can be understood and extended.&lt;/p&gt;

&lt;p&gt;With each new post we will uncover a new layer of this mystery. It won't always be a big win. It won't always be a win at all. But failure is part of the process. Stay tuned.&lt;/p&gt;

</description>
      <category>compilers</category>
      <category>llvm</category>
      <category>programminglanguages</category>
      <category>computerarchitecture</category>
    </item>
  </channel>
</rss>
