Reading the Sail RISC-V specification

April 23, 2026 ยท View on GitHub

This RISC-V specification is written in the Sail language. Although specifications in Sail are quite readable as pseudocode, it would help to have the Sail manual handy.

Structure of the specification

The Sail files in the model subdirectory have a modular structure that is best understood by looking at the riscv.sail_project file. This file describes the various modules in the Sail model, the files that constitute them, and their inter-dependencies. More information on Sail modules is present in the Sail language manual.

A Sail module typically consists of a group of closely related Sail source files, and a declaration of their dependencies on other modules. The RISC-V specification consists of a few core modules and several extension modules. Within a module, the later files in the module usually depend on the earlier ones.

The core, sys and postlude modules are the primary modules, with most of the other modules being submodules of the extensions module.

The prelude module

prelude.sail contains key type definitions and useful Sail library functions. errors.sail defines model-internal errors and exceptions.

The core module

This module provide the types and functions that the rest of the modules depend on.

xlen.sail, flen.sail and vlen.sail define the types and widths used in the model for the base ISA (e.g. xlen, physaddr_bits), the floating point extensions (flen) and the vector extensions (vlen) respectively. These widths are specified as config values, which means their value is derived from the configuration file for the model.

The lowest level memory access primitives are defined in phys_mem_interface.sail and are implemented by the various Sail backends. mem_addrtype.sail and mem_metadata.sail contain other low-level definitions related to memory.

Functions that implement some more complex arithmetic operations used in several instructions are implemented in arithmetic.sail, and utilities to deal with address ranges are in range_util.sail. float_classify.sail contains a classifier for the floating-point format.

platform_config.sail contains various configuration parameters for the platform, some of which can affect which extensions can be supported on the platform.

extensions.sail sets up the basic infrastructure for the definition of modules implementing RISC-V extensions. The hartSupports function determines whether an extension is supported by the model configuration, while the currentlyEnabled function determines whether the extension is usable given the current dynamic state of the hart.

rvfi_dii*.sail implement functionality for RISC-V Formal Interface - Direct Instruction Injection (RVFI-DII), allowing the model to be used with testing tools such as TestRIG. These files can be ignored on a first reading.

types_*.sail and *_types.sail contain important types that are used in the rest of the specification. types.sail contains some basic RISC-V definitions. This file should be read early since these definitions are used throughout the specification for privilege levels, register indices, interrupt and exception definitions and enumerations, and types used to define memory accesses.

mem_type_utils.sail contains helper functions to map memory access types to the corresponding memory exceptions that can be generated from those accesses.

regs.sail contains the base register file, where each register is defined as having the regtype type defined in reg_type.sail and indexed by the indices defined in types.sail.

csr_begin.sail sets up the infrastructure for the scattered definitions of CSRs and their access for read and write operations.

callbacks.sail contains definitions for callbacks that inform an external harness (such as the C++ emulator) about state-changing events.

pc_access.sail defines functions to access and modify the program counter.

sys_regs.sail describes the privileged architectural state, viz. M-mode and S-mode CSRs, and contains helpers to interpret their content, such as WLRL and WARL fields. CSRs dealing with interrupts are in interrupt_regs.sail.

addr_checks_common.sail and addr_checks.sail contain extension hooks to support the checking and transformation of memory addresses during the execution of an instruction. The transformed addresses are used for any address translation; however, any memory access exceptions are reported in terms of the original memory address (i.e. the one generated by the instruction, not the hook).

The floating point arithmetic in the model is implemented by a wrapper around the Berkeley Softfloat library; this wrapper is implemented in softfloat_interface.sail.

The exceptions and pmp modules

The handling of the addresses involved in exception handling are specified by the functions in sys_exceptions.sail while sync_exception.sail defines a structure that is used to capture the architectural information for an exception. These files constitute the exceptions module.

The pmp module implements physical memory protection (PMP). pmp_regs.sail defines the PMP registers and their read and write accessors while pmp_control.sail implements the PMP permission checks and matching priority.

The sys module

This core module deals with the hart's reservation state, physical and virtual memory, the platform memory map, and interrupt and exception handling.

The reservation state is maintained external to the model and is accessed through the functions in sys_reservation.sail.

sys_control.sail describes interrupt and exception delegation and dispatch, the handling of privilege transitions and access control for CSRs.

platform.sail implements platform-specific functionality for the model. It contains the CLINT local interrupt controller, and the MMIO interfaces to the clock, timer and terminal devices.

A simple model of an MMIO-based external interrupt controller is implemented in simple_interrupt_generator.sail and simple_interrupt_generator_regs.sail. This enables testing the model with external interrupts, as described in its documentation.

pma.sail implements Physical Memory Attributes (PMAs), in terms of which the physical memory layout is configured in the configuration file.

mem.sail contains the functions that convert accesses to physical addresses into accesses to physical memory, or MMIO accesses to the devices provided by the platform, or into the appropriate access fault. This file also contains definitions that are used in the weak memory concurrency model.

The vmem_{types,pte,ptw,tlb}.sail and vmem.sail files describe the S-mode address translation. More details are in Virtual Memory Notes.

Callbacks to track page table walks are in callbacks.sail, while callbacks to track operations on the TLB are in vmem_tlb.sail.

vmem_utils.sail provides a higher level interface to virtual memory for load/store style instructions that handles address translation and accesses to misaligned addresses taking platform configuration options into account.

insts_begin.sail sets up the infrastructure for the definition of instructions in the rest of the model. Files matching <ext_name>_insts.sail capture the instruction definitions and their assembly language formats. Each file contains the instructions for an extension. Each instruction is represented as a variant clause of the instruction type, and its execution semantics are represented as a clause of the execute function. mapping clauses specify the encoding and decoding of each instruction to and from their binary representations and assembly language formats. Though the assembly mappings are defined bidirectionally, only the disassembler direction (i.e., binary encoding to assembler) is used.

Extensions

The extensions module contains a sequence of submodules, each typically implementing an ISA extension. In some cases, submodules implementing related extensions (e.g. Zaamo and Zalrsc) may be grouped together and nested within another submodule (e.g. A) under the extensions module. This nested structure helps to organize the files implementing large related extensions such as those in the Vector (V) and cryptography (Zk*) extensions.

The mops module

The mops module is a module containing the Zimop and Zcmop extensions. It is included after the extensions module containing all the other extensions, since those other extensions could potentially override the maybe-operations (MOPs) that are defined by Zimop and Zcmop.

The postlude module

This module essentially completes the specification by providing implementations of the instruction fetcher and the driving function for the fetch-decode-execute cycle.

insts_end.sail and csr_end.sail terminate the scattered definitions begun in the insts_begin.sail file in the sys module and the csr_begin.sail file in the core module respectively.

Definitions for the instruction stepper are in step_common.sail, while some hooks to customize the stepper and the instruction decode are in step_ext.sail and decode_ext.sail respectively. The instruction fetch is implemented in fetch.sail, where the fetch is done in 16-bit granules to handle RVC instructions.

The top-level fetch-decode-execute driver is in step.sail The try_step function performs the instruction fetch, handles any fetch errors, decodes the fetched value, dispatches the execution of the decoded instruction, checks for any pending interrupts that may need to be handled, and maintains the current state of the model. The try_step function is the primary interface to the external C++ simulator harness.

A loop function in step.sail implements the standalone version of the fetch-decode-execute loop, and uses the same HTIF (host-target interface) mechanism as the Spike emulator to detect termination of execution. This function can be used to drive the model without the use of the C++ simulator.

The configuration for the model is validated in validate_config.sail. A device tree and ISA string for the configuration is generated using functions in device_tree.sail. Model initialization and reset are implemented in model.sail.

fetch_rvfi.sail provides the fetch function when the model is used for RVFI, and complements the rvfi_dii*.sail files mentioned above.

Other modules

The unit_tests module collects Sail unit tests for the specification. The main module provides a main() function that is used in other Sail backends.

Miscellaneous notes

When implementing atomic accesses (e.g. for AMOs and instructions fetches in Ziccif), there are two approaches that can be taken in the Sail model. One approach is to implement an atomic memory access using a single memory operation at the lowest level (i.e. in phys_mem_interface.sail; this is the approach taken in this model. The second approach splits an atomic access into multiple smaller intra-instruction memory operations at the level of phys_mem_interface.sail; however, these would have to be accompanied by additional rules in the RVWMO memory model specifying that no other memory accesses can intervene in between these operations. Both approaches are observationally equivalent for the C++ emulator since it currently implements a single-hart system, but the first approach is preferred to simplify the rules in the memory model.

Structure of the C++ emulator

The diagram below illustrates how the C++ emulator is built from the Sail model.

The nodes that are not colored are the handwritten files for the C++ emulator. The black arrows indicate dependency relationships, while the blue arrow indicates a file generated by the Sail compiler from Sail source files. A box with a double border indicates a user-provided configuration file. A purple arrow indicates a validation step.

riscv_sim.cpp is the top level file for the emulator: it processes command line options, initializes the platform model with any ISA implementation choices if specified, loads the ELF program or OS image into raw memory, including any ROM firmware and DeviceTree binary blobs, and initializes the memory map.

The generated C++ model (build/sail_riscv_model.{cpp,h}) is built from the Sail sources by the Sail compiler. It contains a class called hart::Model which is derived from the PlatformInterface class in riscv_platform_if.h. PlatformInterface contains virtual methods that are called by hart::Model. These methods provide platform functions (e.g. load_reservation), as well as informative state change callbacks (e.g. freg_write_callback) for logging.

The actual implementation of the platform methods are in the ModelImpl class in riscv_model_impl.h, so the callback flow is like this:

The primary reason for this convoluted structure is to allow the callback implementations to access hart::Model.

All of these callbacks are declared in the Sail model in platform.sail, sys_reservation.sail and callbacks.sail.

The Sail run-time system provides a C library for use with its C backend, which provides the low-level details of the implementation of raw memory and bitvectors (typically optimized to use the native machine word representation).

The model is configured by a file in JSON format that specifies various ISA options (e.g. RV32 or RV64, number of PMP entries), the memory map and whether various extensions are enabled. Users of the model can provide their own configuration file based on the sample template or on the configurations generated during the build process under the config subdirectory of the build directory.

The Sail compiler generates a JSON schema for the configuration in sail_riscv_config_schema.json; this schema file is used to validate the user-provided configuration (as indicated by the purple arrow).