Yul Explained: Ethereum's Intermediate Language for the EVM
Yul is an intermediate language that sits between Solidity and Ethereum Virtual Machine (EVM) bytecode: a lower-level, functional language the Solidity compiler can translate almost directly into opcodes. Developers write it directly inside assembly { ... } blocks in a Solidity contract, or as a standalone .yul file compiled on its own. Either way, the appeal is the same: more control over exactly which EVM operations run, at a real cost in readability and safety.
This guide starts with the plain definition, then works through why Yul exists, who actually writes it, and where it fits next to Solidity and Huff, the other end of the low-level spectrum.
What Is Yul?
Yul is "an intermediate language that can be compiled to bytecode for different backends," according to the official Solidity documentation. In practice, that means one thing on Ethereum today: it compiles to EVM bytecode, using EVM opcodes as built-in functions.
Yul shows up in Solidity in two separate ways. The first is inline assembly: a block of Yul code embedded directly inside an otherwise normal Solidity function, written when a specific operation needs more control than plain Solidity gives you. The second is structural: since Solidity 0.8.13, the compiler can translate an entire contract into Yul first and optimize that intermediate form before generating bytecode, an approach called the IR-based, or via-IR, pipeline.
An intermediate language is positioned between human-readable source code and machine code. It is designed to be easy for a compiler to analyze and optimize before the final translation step. Yul plays this role for Solidity the way LLVM IR does for C and Rust.
The EVM dialect of Yul supports exactly one data type: u256, a 256-bit unsigned integer matching the EVM's native word size. Control flow is limited to if, switch, for loops, and function calls. There is no while or do-while, and no direct stack manipulation like SWAP or DUP. That restriction is not an accident; it is what keeps Yul readable enough to analyze automatically.
What Yul Code Looks Like
Yul reads closer to a stripped-down C or JavaScript than to Solidity, since it drops types, inheritance, and most syntactic sugar in favor of direct function calls to EVM opcodes. A short example makes the shape clear: a loop that sums the elements of a fixed-length array using only Yul's for construct.
function sumArray(offset, length) -> total {
for { let i := 0 } lt(i, length) { i := add(i, 1) } {
total := add(total, mload(add(offset, mul(i, 0x20))))
}
}Every piece of that loop is an explicit opcode or opcode-backed built-in: add, mul, and lt map straight to EVM arithmetic and comparison instructions, and mload reads a 32-byte word from memory at a manually computed offset. Solidity would let you write total += arr[i] and handle the bounds check, the offset math, and the type behind the scenes. Yul makes you write all of that out by hand: the same memory-versus-storage distinction covered in more depth in our guide to Solidity's data locations.
A standalone .yul file wraps this kind of code in an object block instead of a contract, separating deployment code from runtime code explicitly. Inline assembly inside a Solidity function skips the object wrapper entirely and just uses assembly { ... } around a block like the one above.
Why Yul Exists
Solidity's own compiler is the reason. Before Yul, Solidity generated EVM bytecode straight from its abstract syntax tree, which made whole-program optimization difficult and produced a compiler internals problem now known as "stack too deep": a function with too many local variables for the EVM's 16-slot-deep stack to hold at once. That used to fail to compile with no good workaround beyond splitting the function.
Yul was designed to fix that at the architecture level. Solidity's documentation states its four goals directly: readability, clear control flow "to help in manual inspection, formal verification and optimization," a straightforward translation to bytecode, and suitability for "whole-program optimization." Routing compilation through an intermediate language that is easier to reason about than raw bytecode, but more disciplined than full Solidity, gives the optimizer a much better target.
That payoff is now the default reason most developers encounter Yul without ever writing a line of it themselves. Solidity's --via-ir flag compiles a contract to Yul first, runs the Yul optimizer over the whole program, and only then emits bytecode. Because the optimizer can relocate stack variables into memory, it resolves most stack-too-deep errors that block the legacy pipeline. The Solidity team's write-up on via-IR describes it as producing "better gas-optimized code than the default pipeline," and the team has said it plans to make via-IR the default codegen path once it ships alongside the Ethereum Object Format (EOF) upgrade.
The second reason developers reach for Yul directly is more basic: sometimes plain Solidity simply cannot express what you need. Multiple return values packed into one memory slot, a custom revert-reason encoding, or a CREATE2 deployment with hand-controlled salt and init code are all things inline assembly handles more directly than Solidity's higher-level syntax allows.
Who Writes Yul?
Four groups write or touch Yul in practice, usually for different reasons.
- Gas-optimization engineers hand-write small Yul blocks inside otherwise normal Solidity contracts: a storage read here, a calldata decode there, where the compiler's default codegen leaves gas on the table.
- MEV bot developers and searchers have the clearest incentive: every millisecond and every unit of gas shaved off an arbitrage or liquidation transaction is money, and their contracts run thousands of times a day.
- Protocol and library authors use Yul inside audited building blocks. OpenZeppelin's Clones library, for instance, hand-writes the EIP-1167 minimal-proxy bytecode in assembly because the pattern is fixed, tiny, and worth optimizing once for everyone downstream.
- Compiler and tooling engineers work with Yul as Solidity's own IR, whether they are building static analyzers, formal-verification tools, or the compiler itself.
The numbers make the incentive concrete. A gas comparison of a real MEV bundle-execution contract, run across the same logic in plain Solidity, hand-optimized Solidity, Yul, and Huff, found the unoptimized version cost 156,749 gas per bundle; hand-tuned Solidity cut that to 137,472 gas, about 12% less; and moving the hot path into Yul brought it down to 131,390 gas, according to the published benchmark. At the MEV margins in that test, the combined optimizations nearly doubled the bot's profit per bundle, which explains why performance-sensitive teams bother with a harder language at all.
Yul vs. Solidity vs. Huff
Yul sits in the middle of a spectrum, not at either end of it. Solidity is the highest-level option; Huff, a separate language that maps far more directly onto raw EVM opcodes, sits below Yul; and pure inline assembly inside Solidity is somewhere between the two, depending on how much of the function it touches.
The benchmark above is instructive here too: pure Huff shaved only about 20 gas off the equivalent Yul version in that test, which is essentially noise. Huff's real advantage is not raw speed over well-written Yul; it is that Yul still goes through Solidity's own optimizer and safety checks, while Huff hands the developer the stack directly, with none of the built-in guardrails.
That trade-off is worth taking seriously before reaching for either one. Michael Amadi and Jesse Raymond, the RareSkills researchers behind The RareSkills Book of Solidity Gas Optimization, put it plainly: "You should not assume that writing assembly will automatically lead to more efficient code." Their guidance is to benchmark the Solidity and Yul versions of a function side by side rather than assume assembly wins by default. The same advice applies directly to choosing between Yul and Huff.
The Risks of Writing Yul
Yul removes Solidity's memory-safety checks, its overflow protection outside of what unchecked already strips away, and most of the compiler's ability to catch a mistake before deployment. A bounds check Solidity inserts automatically has to be written by hand in Yul, and skipping it does not throw a compile error. It produces a contract that reads or writes the wrong memory slot at runtime.
For that reason, Yul is rarely the right choice for an entire application contract, and it is a poor fit for a team without deep EVM experience auditing every line. It earns its place in small, isolated, heavily tested sections where the gas savings are measured and worth the added review burden, not as a default coding style.
How to Try Yul
The fastest way to see Yul in context is inline, inside a contract you already understand.
function getBalance(address account) external view returns (uint256 bal) {
assembly {
bal := balance(account)
}
}That single balance opcode call replaces what Solidity would otherwise wrap in extra safety and calling-convention overhead: a realistic, if small, example of the kind of savings developers are chasing. From there, standalone .yul files compile with solc --strict-assembly, and the Remix IDE supports Yul directly if you want to experiment without a local toolchain.
Testing matters more here than anywhere else in Solidity development. Since Yul skips so many of the compiler's default checks, verifying behavior against live chain state before deployment is not optional. Simulating a transaction with eth_call or a full debug_traceCall shows exactly what a Yul function actually did, including any storage slot it touched, rather than relying on local test coverage alone. NOWNodes exposes both methods on its Ethereum RPC endpoint, including Debug and Trace access, so a hand-optimized function can be checked against real network state without running a self-hosted debug-enabled node.
Conclusion
Yul is the layer Solidity itself increasingly runs through, not just a curiosity for gas-obsessed developers. Most people benefit from it invisibly through the via-IR pipeline and the standard optimizer; a smaller group writes it directly, usually in short, isolated blocks where the gas savings are measurable and the extra review is worth it.
The practical rule holds for anyone considering it: reach for Yul when profiling shows a real cost worth cutting, benchmark the result against plain Solidity rather than assuming assembly wins, and treat every hand-written block as needing the scrutiny Solidity's compiler would otherwise provide for free.
FAQ
Can Yul be compiled without Solidity?
Yes. Yul compiles on its own through solc --strict-assembly, independent of any Solidity contract, though it is used far more often either embedded as inline assembly or as Solidity's internal IR.
Does Yul work only on Ethereum?
Yul targets any EVM-compatible chain, since its only current dialect compiles to EVM bytecode. That includes BNB Smart Chain, Polygon, and Arbitrum, alongside Ethereum mainnet and its testnets.
Can inline Yul call Solidity functions?
Inline Yul inside a Solidity contract can access that contract's local variables and call external functions through EVM opcodes like call or staticcall, but it does not call internal Solidity functions directly the way Solidity code calls itself.
Is Yul more dangerous than Solidity?
Yes, meaningfully. Yul skips Solidity's automatic overflow checks, memory-safety guarantees, and most compile-time sanity checks, so a mistake that Solidity would catch at compile time can become a live bug in Yul.