Instruction Set Configuration File - michaelkamprath/bespokeasm GitHub Wiki
The instruction set configuration file defines the instruction set and assembly language features used by BespokeASM to assemble machine code. This file can be written in either JSON or YAML format.
The purpose of this configuration file is to control how machine code is generated for a given instruction set. BespokeASM uses a fixed method for compiling machine code for any instruction. The standard form of an instruction is:
MNEMONIC [OPERAND1[, OPERAND2[, ...]]]
Each instruction must have at least a mnemonic, and can optionally have one or more operands.
The machine code generated for an instruction consists of two parts: the instruction byte code and the argument values.
- Instruction Byte code: Indicates which instruction the CPU should execute. It is composed of values specific to the mnemonic and, optionally, each operand. The total size of the packed byte code (mnemonic plus operands) should match the instruction size of the target hardware.
- Argument values: These are parameters used by the instruction's microcode, such as immediate values or addresses. If multiple operands provide argument values, they are ordered according to the operand order in the instruction.
Both the instruction mnemonic and its operands can contribute to the byte code, but only operands can generate argument values.
For example, consider this assembly instruction:
mov a,[$8000] ; copy value at address $8000 into register AIn this case:
- The instgruction mnemonic
mov, the operanda(the register A), and the operand[...](an indirect value) each contribute to the instruction's overall byte code. - The numeric value
$8000is the argument for the[...]operand and is placed after the instruction byte code in the final machine code.
The diagram below illustrates this. Here the machine code is for a computer with an 8-bit data word using little-endian byte order:
Byte 0 Byte 1 Byte 2
========== ======== ========
01 001 110 00000000 10000000
-- --- --- -----------------
| | | |
| | | +-- The second operand's argument value ($8000) in little-endian byte order
| | +------------- The byte code 110 for the second operand ([...])
| +----------------- The byte code 001 for the first operand (register A)
+-------------------- The byte code 01 for the mov mnemonic
In summary, two types of machine code are generated for each instruction:
- Instruction Byte code: Specifies the instruction to execute (often used to select a microcode sequence).
- Argument values: Data that the instruction operates on (e.g., the address for a jump instruction).
Throughout this documentation, "instruction byte code" and "argument value" are used as defined above. In BespokeASM, argument values are always emitted after the instruction's byte code. Operands can affect both the byte code and the argument values.
In BespokeASM, a "word" is a unit of data whose size matches the native data bit size of the target CPU. A word is what a memory address points to. For example:
- An "8-bit computer" has an 8-bit word size (1 byte).
- A 32-bit CPU has a 32-bit word size (4 bytes).
Bytecode is represented as a sequence of words, and each word is addressable. For a 32-bit CPU, address 0 points to the first 4-byte word, address 1 to the next, and so on.
The word size of the target CPU is a fundamental configuration in BespokeASM. It is set in the general section of the configuration file. The default word size is 8 bits, but this can be changed as needed.
A word segment is a subdivision of a word, representing the physical ordering of bits within that word. For example, a 16-bit computer might store its 16-bit words so that the first 8 bits are the least significant (little-endian segment order), and the last 8 bits are the most significant. This is distinct from the endianness of multi-word values (e.g., a 32-bit value on an 8-bit CPU). Word segments are rarely used in most CPUs, but may be needed for special cases, such as the bit layout of an EPROM.
In most cases, the word segment size equals the word size. However, if a word needs to be subdivided and reordered, the segment size can be smaller. The segment size must be less than or equal to the word size, and the word size must be evenly divisible by the segment size.
The word segment size is set in the general section of the configuration file. By default, it matches the word size. This feature is rarely needed, so the default is usually sufficient.
BespokeASM supports two concepts of endianness:
- The order of words within a multi-word value (multi-word endianness)
- The order of word segments within a word (intra-word endianness)
Multi-Word Endianness refers to the order of words in memory that together represent a multi-word value. For example, a 32-bit value 0x12345678 on an 8-bit CPU could be stored as:
Address 0: 0x12
Address 1: 0x34
Address 2: 0x56
Address 3: 0x78
(big-endian: most significant byte at the lowest address)
Or:
Address 0: 0x78
Address 1: 0x56
Address 2: 0x34
Address 3: 0x12
(little-endian: least significant byte at the lowest address)
If the CPU has 16-bit words, the same 32-bit value in little-endian would be:
Address 0: 0x5678
Address 1: 0x1234
Multi-word endianness is set in the general section of the configuration file. The default is big.
Intra-Word Endianness refers to the order of segments within a word. For example, a 16-bit word with 4-bit segments and a value of 0x1234:
- Big-endian:
0x1234(most significant segment first) - Little-endian:
0x3412(least significant segment first)
The address and value of the word remain the same; only the order of segments within the word changes. Intra-word endianness is useful for matching the physical layout required by certain memory devices.
Intra-word endianness is set in the general section of the configuration file. The default is big. This feature is rarely needed, so the default is usually sufficient.
When string_byte_packing is enabled in the general section, quoted strings in .byte and .cstr data directives are packed tightly into words, rather than each character being placed in its own word. This feature is only available if word_size is a multiple of 8 and at least 16. If not set, the default behavior is to place each character in its own word.
For example, with word_size: 16 and string_byte_packing: true, the directive:
.cstr "Hello World"will produce the following 16-bit words (big-endian):
0x4865, 0x6c6c, 0x6f20, 0x576f, 0x726c, 0x6400
If the string does not fill the last word, the value of string_byte_packing_fill (default 0) is used to pad the remaining bytes. For example, with word_size: 32, string_byte_packing: true, and string_byte_packing_fill: 0xFF:
.byte "Hello World"will produce:
0x48656c6c, 0x6f20576f, 0x726c64FF
For .cstr, the configured cstr_terminator value is always appended to the string before packing and padding. If the terminator does not fill the last word, the remaining bytes are padded with string_byte_packing_fill.
For example, with word_size: 32, string_byte_packing: true, string_byte_packing_fill: 0xFF, and cstr_terminator: 0xAA:
.cstr "Hello World!"will produce:
0x48656c6c, 0x6f20576f, 0x726c6421, 0xAAFFFFFF
If string_byte_packing is not enabled, the default behavior is to emit each character (and the terminator for .cstr) in its own word, regardless of string_byte_packing_fill.
The configuration has the following main sections:
The general section defines the general configuration of BespokeASM and various assembly language features. The general section is required. The supported options are:
| Option Key | Value Type | Description |
|---|---|---|
address_size |
integer | The number of bits that is required to represent a memory address. |
page_size |
integer |
(Optional) The default memory page size in bytes to be used with the .page directive. Defaults to a value of 1. |
word_size |
integer |
(Optional) The default number of bits in a word. Defaults to a value of 8. |
word_segment_size |
integer |
(Optional) The default number of bits in a word segment. Defaults to the value of word_size. |
endian |
string |
deprecated (Optional) Defines the endianness of multi-word values. Allowed values are big and little. If not present, this option defaults to big. |
multi_word_endian |
string |
(Optional) Defines the endianness of multi-word values. Allowed values are big and little. If not present, this option defaults to big. |
intra_word_endian |
string |
(Optional) Defines the endianness of intra-word segments when converting to bytes. Allowed values are big and little. If not present, this option defaults to the value of big. |
default_numeric_base |
string |
(Optional) Controls how bare, unprefixed numeric tokens in assembly source are interpreted. Allowed values are decimal (default), hex, hexadecimal, base16, octal, base8, binary, and base2. Explicit numeric forms such as $7C, 0x7C, 7CH, b01111100, %01111100, and character ordinals are unaffected. When this is set to a non-decimal base, register names that would also be valid bare literals in that base are rejected when the ISA configuration loads. |
registers |
list[string] or dict[sting:dict] |
(Optional) A list of register labels strings that will be used in this instruction set. Anything that is declared as a register label cannot be used as a constant or address label, and anything not declared as a register label cannot be used an a register operand. If not present, no register labels are defined. Alternatively, this is a dictionary where the keys are the register label strings and the value is a dictionary containing the following elements:
|
documentation |
dictionary | (Optional) Documentation metadata for the instruction set. See General Documentation. |
min_version |
string | (Required) The minimum version of BespokeASM that this instruction set configuration file will work with. BespokeASM will also do a counter-minimum version check to make sure this instruction set configuration file has the schema it is expecting. |
identifier |
dictionary |
(Optional) Configures name and version information for the assembly language defined by this configuration file. This field is used both by language extension generation and source code language requirements. Contains the following key/value items:
|
origin |
integer |
(Optional) Defines the default starting origin address for byte code generated with this configuration file. This is an offset from the start of the GLOBAL memory zone. The starting origin defaults to an address of 0 if this option is not present. |
cstr_terminator |
integer |
(Optional) Defines the terminating character for byte sequences made with the .cstr data directive. Defaults to 0 if unset. |
allow_embedded_strings |
boolean |
(Optional) If set true, the compiler will allow the embedded string feature. Defaults to false. |
string_byte_packing |
boolean |
(Optional) If set to true, quoted strings in .byte and .cstr data directives will be packed tightly into words, rather than each character being placed in its own word. Only allowed to be true if word_size is a multiple of 8 and at least 16. Defaults to false. |
string_byte_packing_fill |
integer |
(Optional) The byte value (0-255) used to pad the last word when string byte packing is enabled and the string does not fill all bytes in the word. Defaults to 0 if not present. |
flags |
dictionary |
(Optional) Documents the flags operated on by various instructions (e.g., "carry", "overflow", etc). Each entry is keyed by the flag symbol and contains metadata such as description. Generally only used for documentation purposes. |
The addressing_modes entries may include title and description values. In legacy configurations, description maps to title and details maps to description when generating documentation.
default_numeric_base applies only to bare source-language numeric tokens for that ISA. It does not change explicit numeric forms, and it does not override defined labels or constants. In other words, if default_numeric_base: hex is set, f + 1 evaluates as 0x10, but a defined symbol named face still resolves as the symbol value rather than the numeric literal 0xFACE.
To avoid confusing source languages, BespokeASM rejects ISA configurations where a non-decimal default_numeric_base would make an ISA-defined source identifier ambiguous with a bare literal. This validation applies to register names, predefined constant names, predefined data labels, and predefined preprocessor symbols. For example, default_numeric_base: hex cannot be combined with a register named b or a predefined constant named face.
The general section may include a documentation dictionary to describe the ISA at a high level. All fields are optional; when omitted, documentation is skipped.
| Option Key | Value Type | Description |
|---|---|---|
description |
string | A short description of the instruction set architecture. |
addressing_modes |
dictionary |
(Optional) A dictionary of addressing modes. Keys are addressing mode names and values are dictionaries with title and description strings (Markdown allowed). These keys are referenced by operand documentation mode values. |
examples |
array |
(Optional) An array of example instructions. Each example is a dictionary with title, optional description (Markdown allowed), and code. |
Both compiler constants and memory blocks can be defined in the ISA configuration file, and the labels defined with these entities can be used in code compiled with the ISA configuration file. This section is identified with the predefined key and contains a dictionary with the following key/values.
Compiler constants for numerical values can be defined for use in the instruction set. This subsection, identified by the constants key, contains a list of dictionaries with these keys:
| Option Key | Value Type | Description |
|---|---|---|
name |
string | The label string assigned to this constant value. This case-sensitive label can be used at compile time to reference the assigned integer value. |
value |
integer | The integer value assigned to this constant. |
Each constant entry may include an optional documentation dictionary:
These documentation fields are also used by generated VS Code and Sublime Text hovers when referencing predefined constants.
| Option Key | Value Type | Description |
|---|---|---|
type |
string | The constant type. Allowed values are subroutine, variable, or address. |
size |
integer |
(Optional) Size in bytes for variable constants. Required when type is variable. |
description |
string | A short summary of the constant's meaning or usage. |
Predefined data blocks can be used to reserve sections of memory for hardware features or common uses (such as buffers). BespokeASM will generate an error if the addresses of compiled code or data should ever overlap with predefined memory blocks. These are defined under the data key as a list of dictionaries:
| Option Key | Value Type | Description |
|---|---|---|
name |
string | The label for the first address value in this data block. This label can be used at compile time to reference the assigned address value. |
address |
integer | The start address of the data block. |
size |
integer | The number of bytes associated with this data block (minimum 1). |
value |
integer |
(Optional) The byte value to fill this data block with when generating a binary image. Defaults to 0 if not present. |
Each data block entry may include an optional documentation dictionary:
These documentation fields are also used by generated VS Code and Sublime Text hovers when referencing predefined data labels.
| Option Key | Value Type | Description |
|---|---|---|
description |
string | A short summary of the data block's meaning or usage. |
A predefined memory zone can be defined in the predefined section under memory_zones, as a list of dictionaries:
| Option Key | Value Type | Description |
|---|---|---|
name |
string | The name of the memory zone. |
start |
integer | The start address of the memory zone. |
end |
integer | The end address of the memory zone. |
Each memory zone entry may include an optional documentation dictionary:
These documentation fields are also used by generated VS Code and Sublime Text hovers when referencing predefined memory zone labels.
| Option Key | Value Type | Description |
|---|---|---|
title |
string |
(Optional) A friendly display name for the memory zone. Falls back to the zone name when not provided. |
description |
string | A short summary of the memory zone's purpose or usage. |
The GLOBAL memory zone may be defined here by using the GLOBAL name. If defined, the origin value in the general settings is interpreted as an offset from the GLOBAL zone's start address. If not defined, a default GLOBAL zone is created.
Memory zones are where bytecode for code and data is assembled, while a data block is a preallocated block of bytecode.
Preprocessor macro symbols can be predefined in the predefined section under symbols, as a list of dictionaries:
| Option Key | Value Type | Description |
|---|---|---|
name |
string | The name of the preprocessor macro symbol. |
value |
string | (Optional) The string replacement value for the macro. If not provided, the empty string is assumed. |
The operand_sets section defines sets of operands for instructions. An operand set represents all possible operand values for a specific operand position and defines the byte code and argument values to be packed when forming machine code. Operand sets are defined separately from instructions to allow reuse. Each operand set consists of one or more distinct operands.
The operand_set section is a dictionary, where the dictionary key is the name for the operand set, and the value is the configuration of that operand set. The name of the operand set is only use internally within this configuration file and does not directly impact the assembly language that is derived from this configuration file.
Each item listed in the operand_sets consists of a single element titled operand_values, which contains a dictionary that configures each of the operand variants in this operand set.
Operand sets may include an optional documentation dictionary alongside operand_values:
| Option Key | Value Type | Description |
|---|---|---|
title |
string | (Optional) A friendly display name for the operand set. Falls back to the operand set key when not provided. |
category |
string | (Optional) A grouping label (for example "Registers", "Addressing", "Literals") used to cluster operand sets within generated documentation. |
description |
string | A short sentence summarizing the operand set's role. |
operand_order |
array[string] |
(Optional) Ordered list of operand keys from operand_values that dictates how operands are presented in documentation. Defaults to the configuration order if omitted. |
The operand configuration dictionary specifies the assembly behavior of a specific operand value. The key is the internal name of the operand value, and the value is a collection of configuration items:
| Option Key | Value Type | Description |
|---|---|---|
type |
string | Specifies one of the operand types and operand addressing modes supported by BespokeASM. The allowed values are:
Operand-label annotation in assembly source ( @name:) is supported only for numeric, indirect_numeric, deferred_numeric, address, and relative_address operand types. See Operand Labels for wrapper placement rules and constraints. Operand labels are not supported for instruction macros, either in macro definitions or macro invocations. |
bytecode |
dictionary |
(Optional) A dictionary that configures the byte code associated with this operand. If not present this operand will not generate any byte code. This dictionary contains the following keys:
|
argument |
dictionary | Configures how the operand argument will be emitted into the machine code. Must be present for the numeric, numeric_indirect, enumeration , numeric_enumeration, and address operand types. Ignored for all other types.The dictionary contains the following keys:
|
register |
string | The assembly code representation of the register value to be used for this operand. Must be one of the register values listed in the registers list of the general section. Must be present for the register, register_indirect, and indirect_indexed_register operand types, ignore for all other operand types. |
offset |
dictionary | Configures the offset value that is optional for the indirect_register operand type. Ignored for all other types. If not present, then no offset is enabled, and no argument value will be emitted in the machine code. If offset values are enabled, this operand will generate an argument value in the machine code equal to the offset value specified in the assembly code. The compiler will still permit not specifying an offset for a indirect_register instruction configured to enabled offsets. In this case, the offset of zero is implied and will be emitted as the argument value.The dictionary contains the following keys:
|
index_operands |
dictionary | Configures the allowed offset operands for the indexed_register and indirect_indexed_register operand types. Contains a dictionary, where the key is an internal name for each offset operand option, and the value is an operand configuration formatted the same as described in this table. When compiling, BespokeASM will attempt to match one operand listed in index_operands. Note that the byte code of the matched index operand will be appended to this operand's configured byte code to form the overall byte code for this operand. If the matched index operand generates an argument, that will be appended to this operand's arguments, if any. |
use_curly_braces |
boolean |
(Optional) Used only with the relative_address operand type. Determines whether the assembly notation for this operand should use curly braces {..} around the expression that indicates the target address. Defaults to FALSE if not present. |
offset_from_instruction_end |
boolean |
(Optional) Used only with the relative_address operand type. Indicates whether the relative offset to be calculated should be calculated from the program counter value at the ned of the instruction (TRUE) or the program counter value at the beginning of the instruction (FALSE). Defaults to FALSE (beginning of instruction) if not present. |
decorator |
dictionary |
(Optional) Indicates whether this operand requires a decorator in order to match. Only supported by the register, indirect_register, and indirect_indexed_register operand types. The decorator configuration dictionary requires two keys:
|
documentation |
dictionary |
(Optional) Provides documentation for this operand. Contains:
|
Note: This configuration dictionary is used both by Operand Set configuration and by specific operands in other sections.
Note
For sliced address operands, the comparison page used by match_address_msb normally comes from the instruction's start address. When match_on_argument_bytcode: true is set, BespokeASM instead uses the page containing the operand's first emitted argument word or byte in the final instruction layout. This matters when an instruction crosses a page boundary between its opcode and its address argument.
Example:
operand_sets:
fast_address:
operand_values:
page_offset:
type: address
argument:
size: 8
word_align: true
slice_lsb: true
match_address_msb: true
match_on_argument_bytcode: trueThe optional top-level flow_counters dictionary enables flow-counter analysis and declares the counter classes available to assembly source. An ISA with no flow_counters section has the feature disabled; ordinary source behaves as before, while flow-counter syntax is rejected under either flow-check setting. Flow-specific names are reserved only when this section is present, so a non-flow ISA may continue using track, endtrack, entry, set, suspend, resume, COUNTER, or COORDINATE as ordinary symbols or configured mnemonics. assert is always reserved because #assert is also a general preprocessor directive that does not require flow counters. Merely adding a valid flow_counters section or instruction metadata does not change emitted code for source that does not use the feature.
flow_counters:
stack:
documentation:
title: Data Stack Depth
description: Tracks routine-owned bytes on the data stack.
operation: add
min_value: 0
max_value: 16
coordinate_offsets: positive
allow_zero_offset: false
invalidate_on_write:
- 0xffff
default_init: 0
exit_policy: balanced
unknown_instructions: error
entry_modes:
called:
init: 0
exit: 0
description: Entered with a stack-saved return address.
jumped:
init: 0
exit: 0Flow-counter analysis supports concurrent named scalar counters, control-flow propagation across direct branches and loops, scoped counter-coordinate symbols, named entry modes and explicit #entry roots, compile-time operand-dependent effects, manual assertion and re-anchoring, indeterminate spans, conventional call summaries, and unconditional return terminals. The key beneath flow_counters is the class name used by #track; for example, #track stack mode=called opens the stack class above with its 0 -> 0 routine-owned contract.
Each class accepts:
| Option Key | Value Type | Description |
|---|---|---|
documentation |
dictionary |
(Optional) User-facing metadata for generated ISA documentation. title names the class (falling back to the class key when omitted), and optional description provides Markdown prose. The generated Flow Counters section combines this prose with properties derived from the validated class configuration. |
operation |
string |
(Optional) How an instruction effect is combined with the counter. add is the default and only currently valid value. |
source |
string |
(Optional) Non-empty dotted path in a selected instruction's effective configuration from which to read its integer or supported operand-dependent delta. Defaults to flow_effects.<class-name>. For example, source: documentation.cycles reuses an instruction's configured cycle count. |
min_value |
integer | (Optional) Inclusive lower bound, checked at region entry and after each instruction. |
max_value |
integer | (Optional) Inclusive upper bound, checked at region entry and after each instruction. |
coordinate_offsets |
string |
(Optional) Allowed nonzero signs for offsets in COORDINATE(counter, offset): positive permits positive offsets, negative permits negative offsets, and both permits either sign. Defaults to both. Use positive for a descending stack whose live values are addressed as sp+N; use negative for the opposite physical orientation. |
allow_zero_offset |
boolean |
(Optional) Whether a coordinate may be declared at offset zero and remain live when its later referenced offset reaches zero. Defaults to true. Set this to false when the counter points just outside the live region and the first valid position is, for example, sp+1. |
invalidate_on_write |
list[integer] |
(Optional) Memory-mapped state addresses whose modification makes the counter indeterminate. Each address must fit the ISA address width and may appear only once. A selected instruction identifies its write-target operands with flow_write_operands; a compile-time match to one of these addresses invalidates every active instance of this class. |
default_init |
integer |
(Optional) Initial value when #track provides neither init= nor mode=. Defaults to 0. |
exit_policy |
string |
(Optional) balanced (default) requires a plain #endtrack to restore the entry value; none closes without an implicit equality check. An explicit exit= is checked under either policy. |
entry_modes |
dictionary |
(Optional) Named entry conventions selected with #track ... mode=<name>. Each mode is a dictionary with an integer init, optional integer exit, and optional string description used in generated ISA documentation. Explicit init= / exit= directive values override the mode. If a mode omits exit, the class's exit_policy supplies the default. These values describe state owned by the region; an execution address saved on the stack for later return to the caller is not folded into init. |
unknown_instructions |
string |
(Optional) Controls a selected instruction that lacks this class's configured source field: ignore, warn (default), or error. This policy does not remove the separate requirement for explicit flow_transfer metadata. |
join |
string |
(Optional) Only the scalar require-equal policy can currently be tracked, and it is the default. interval is recognized as a reserved configuration value but is not yet supported by the analyzer. |
Instruction definitions and variants provide the semantics consumed by a tracked class:
instructions:
push4:
flow_effects:
stack: 4
flow_transfer: none
bytecode:
value: 0x14
size: 8
pop4:
flow_effects:
stack: -4
flow_transfer: none
bytecode:
value: 0x15
size: 8
addsp:
flow_effects:
stack: -ARG(0)
flow_transfer: none
# bytecode and one numeric operand omitted
lds:
flow_effects:
stack: 0
flow_transfer: none
# bytecode and operand configuration omitted
store_immediate:
flow_effects:
stack: 0
flow_transfer: none
flow_write_operands: [1]
# bytecode, immediate operand 0, and address operand 1 omitted
replace_stack_pointer:
flow_invalidates: [stack]
flow_transfer: none
# bytecode omitted
return_from_subroutine:
flow_effects:
stack: -2
flow_terminal:
stack: before_effect
flow_transfer: return
# bytecode omitted
jump_if_zero:
flow_effects:
stack: 0
flow_transfer: conditional
flow_target_operand: 0
# bytecode and one direct target operand omitted
jump:
flow_effects:
stack: 0
flow_transfer: unconditional
flow_target_operand: 0
# bytecode and one direct target operand omitted
call:
flow_effects:
stack: 2
flow_transfer: call
flow_target_operand: 0
flow_call_effects:
stack: 0
# bytecode and one direct target operand omittedflow_effects maps declared class names to integer deltas, supported operand expressions, or edge maps. ARG(n) is a zero-based index into the selected variant's actual source-written operands. It reads a compile-time semantic value such as 4 or FRAME_SIZE, not emitted selector bits; a register operand is runtime-valued and is rejected. Arithmetic such as -ARG(0) is supported. An out-of-range ARG() index is a configuration error. Structural effects such as COUNT() are not currently supported.
An edge map describes a physical effect that depends on which successor is taken. For a conditional transfer or conditional call, use the keys taken and fall_through; each value may be an integer or supported operand expression:
instructions:
call_if_zero:
flow_effects:
stack: {taken: 2, fall_through: 0}
flow_transfer: call
flow_target_operand: 0
flow_call_effects:
stack: 0Here, the instruction physically pushes a two-byte return address only on its taken edge. flow_call_effects is deliberately separate: it declares the caller-visible value after a conventional taken callee has returned, so both call outcomes have a net stack effect of zero. The current scalar analyzer uses that call summary rather than propagating into the callee. General propagation of edge maps on non-call conditional branches is reserved for the forthcoming edge-aware interval analysis; until then, use a scalar delta when such an instruction is reached inside a tracked region. A scalar delta applies the same effect to every successor.
The same edge-map form may appear at a custom class source, such as documentation.cycles, to express different taken and fall-through cycle costs. Generated documentation renders the example above as taken +2 / fall-through +0; configuration uses the underscore spelling fall_through.
For a class using the default source, an effect of zero must be written explicitly when the instruction is known not to change that counter and unknown_instructions: error is desired. A class with a custom source reads that dotted field instead:
flow_counters:
cycles:
source: documentation.cycles
exit_policy: none
instructions:
nop:
flow_transfer: none
documentation:
cycles: 1Root instruction metadata is inherited by effective variants; a selected variant may override it. Instruction aliases use the selected canonical instruction's metadata. Instruction macros may not declare flow_effects or other instruction flow metadata themselves—the analyzer consumes the metadata of the concrete instructions produced by macro expansion.
flow_write_operands identifies memory addresses written by an instruction. Its entries are zero-based indexes into the selected variant's source-written operands. When a target matches an active class's invalidate_on_write address, that counter becomes indeterminate after the instruction and any coordinates belonging to it are permanently invalidated. A run-time or otherwise unresolved target is treated conservatively as a possible match. The source must use #resume <counter> = <value> before it can read or assert the counter again. This models memory-mapped state such as a stack pointer: writing its backing address bypasses ordinary push/pop effects, so the analyzer must no longer claim to know the depth.
Macros do not need or accept their own flow_write_operands. A macro inherits the aggregate behavior of the concrete instructions in its expansion. For example, a stack-initialization macro that expands to a store at the configured stack-pointer address invalidates the stack counter at that store; the source can then re-anchor the initialized state with #resume stack = 0.
flow_invalidates handles an instruction that replaces a counter's physical anchor directly rather than through a memory write. It is a list of declared counter classes; every active instance of a listed class becomes indeterminate after the instruction, and its coordinates are permanently invalidated. For example, an instruction that loads the stack-pointer register wholesale from an immediate value or another register can declare flow_invalidates: [stack]. Source then uses #resume stack = <known-depth> when it knows the resulting logical depth. Like other instruction flow metadata, this key belongs to the real instruction or selected variant. A macro inherits it from its expanded instructions and cannot declare it itself.
Every selected instruction reached on a live tracked path must explicitly declare flow_transfer; the analyzer never infers "ordinary instruction" from missing metadata. Multiple named scalar instances may be active concurrently, including multiple instances of one class; each reads and applies its own class effect independently. The classification tells the analyzer where execution can continue after the instruction:
flow_transfer value |
Successors | Meaning |
|---|---|---|
none |
physical fall-through | An ordinary, non-branching instruction. Execution continues at the next physical address (address + word_count), which must be an unambiguous executable location. Declare this explicitly on every ordinary instruction. |
conditional |
direct target and physical fall-through | A branch that may or may not be taken (e.g. a jz or bne). The analyzer follows both edges, and counter values must reconcile wherever the paths rejoin. Requires flow_target_operand. |
unconditional |
direct target only | A jump that always transfers (e.g. jmp). The physically following instruction is not a successor of this one; code after it is reachable only through some other edge. Requires flow_target_operand. |
call |
physical fall-through, after the callee returns | A subroutine call to a direct target. The analyzer does not walk into the callee: it applies the declared flow_call_effects summary to each active counter and continues at the fall-through. Requires flow_target_operand; the target must be the callee region's initial entry or a label declared with #entry. |
return |
none | Execution leaves the routine; the path has no successor. Normally declared together with flow_terminal, which reconciles each mapped counter's exit contract; a live counter of an unmapped class must be closed with #endtrack before the return. |
indirect |
unknowable | A computed or register-indirect transfer (e.g. a jump through a register). Inside an active tracking region this is an error — suspending a counter does not make the target knowable — so close every region with #endtrack before it. Outside any region it is accepted. |
multiway |
reserved | Reserved for a future declared-target-set dispatch (jump tables). Not currently analyzable; inside a region it is rejected like indirect. |
flow_target_operand identifies which operand carries the branch target for the three direct-transfer kinds (conditional, unconditional, and call). Its value is the zero-based index into the instruction's source-written operand list for the selected variant — 0 is the first operand as written in assembly source. The indexed operand must resolve to a direct code target at compile time, so its operand type must be numeric, address, or relative_address; register-valued and indirect operand types cannot supply a target. The analyzer never guesses a target from an address-like operand that is not named here — a load whose operand happens to hold an address is not a branch. Declaring flow_target_operand for a transfer kind with no direct target (none, return, indirect, multiway), omitting it on a direct transfer, indexing outside the selected variant's operand list, or naming an operand of an incompatible type is a configuration error at load.
Direct targets and fall-through addresses must resolve to unambiguous executable locations and may not cross lexical tracking boundaries. A call into a tracked region must target that region's initial entry or a label declared with #entry, not an interior label.
flow_call_effects maps counter classes to caller-visible net deltas after a conventional call returns. This is distinct from the call instruction's own flow_effects, which describes its physical effect when analyzing a callee entry convention. A declared summary may be zero or nonzero, but it is a target-independent ISA calling-convention contract: the analyzer takes it on faith for every target of that instruction variant and does not infer or verify it from callee bodies. The callee need not be in the assembled program: a call target address holding no program content is treated as an external callee (for example, a ROM routine named by a predefined constant), while a target landing on emitted data remains an error. If an active class has no call summary, or its effect varies by target, its post-call value remains unresolved and cannot be used by COUNTER(), a coordinate reference, an exit check, or another exact assertion.
flow_terminal maps a counter class to before_effect or after_effect. It reconciles every live instance of each mapped class. before_effect checks the incoming value against the exit contract and ends that instance's path without applying the instruction's delta inside the region. For an instruction that returns control to its caller by restoring an execution address saved on the stack, this lets the callee restore its own stack movement to zero before the instruction consumes the caller-owned return address. after_effect applies the delta, bounds checks, and coordinate invalidation first, then checks the resulting exit value. A return has no fall-through successor, so every live counter must have a way across it: a live instance of an unmapped class reaching the terminal is an error naming the counter — either map that class on the return as well (after_effect when the return's own cost belongs in the counter, such as a cycle counter counting the return instruction) or close it with #endtrack before the return. A terminal closes mapped execution paths but not lexical extent; following unreachable #endtrack directives are legal lexical-only delimiters, and EOF is also valid after all paths have terminated.
Flow checks are enabled by default (--flow-checks / -a). --no-flow-checks / -A suppresses optional program verification and listing annotations, but flow-specific ISA configuration remains validated. If emitted bytecode depends on COUNTER() or a counter-coordinate reference, the assembler automatically performs the analysis required to resolve the value. No fallback value is guessed when a requested value is unsafe or ambiguous.
The instructions section defines supported instruction mnemonics. Each instruction definition consists of three parts: the mnemonic, the instruction arguments, and the instruction byte code. This section is a key/value dictionary where the keys are the mnemonic strings and the values are dictionaries defining the instruction's arguments and byte code.
| Option Key | Value Type | Description |
|---|---|---|
aliases |
list[string] | (Optional) A list of alternative mnemonics for this instruction. Each alias is accepted as a valid mnemonic in assembly source and language extensions, and generates the same code as the root mnemonic. All aliases must be globally unique across all mnemonics and aliases. |
bytecode |
dictionary | A dictionary that describes the base byte code for this instruction that should be emitted to indicate the instruction. The key and values that must be present are:
|
operands |
dictionary | A dictionary that configures the set of operands that are allowed for this instruction mnemonic. The key and values that are used in this dictionary are described in the table below. If not present, then the instruction mnemonic is assumed to have no operands. |
variants |
list |
(Optional) This options allows the specification of one or more alternative configurations for the mnemonic. This is useful when a different instruction byte code prefix should be emitted for a certain operand signature. The value of this key is a list, and each list element is another instruction configuration with bytecode and operands as specified above. Variant configurations are processed if the operands do not match the main configurations, and then each variant configuration is processed in order present in the list, using the first match found to generate the byte code. Variant dictionaries may also include mnemonic_decorator, which makes the decorator part of the mnemonic itself (for example m+ or ++inc). |
documentation |
dictionary |
(Optional) Provides documentation for the instruction. Contains:
|
flow_effects |
dictionary |
(Optional) Maps declared flow-counter class names to integer deltas, supported operand expressions such as -ARG(0), or edge maps such as {taken: 2, fall_through: 0}. Root metadata is inherited by variants unless a selected variant overrides it. A class with a custom source reads that field instead. Instruction macros cannot declare this metadata. |
flow_terminal |
dictionary |
(Optional) Maps declared flow-counter class names to before_effect or after_effect terminal reconciliation. The former checks and closes before the physical instruction delta; the latter applies the delta before checking and closing. |
flow_transfer |
string |
(Optional outside flow analysis) Assembly control-flow classification. Every selected instruction on a reachable tracked path must declare one of none, conditional, unconditional, call, return, indirect, or multiway. See the classification table in the Flow Counters section for the meaning and successor semantics of each value. |
flow_target_operand |
integer | Required for conditional, unconditional, and call; forbidden for transfers without a direct target. Zero-based index into the selected variant's source-written operand list identifying the compile-time direct target; the indexed operand's type must be numeric, address, or relative_address. |
flow_call_effects |
dictionary | Valid only with flow_transfer: call. Maps counter classes to target-independent caller-visible net deltas applied at the call's fall-through state. Values may be zero or nonzero and are ISA contracts taken on faith. A missing active-class summary leaves that value unresolved. |
flow_invalidates |
list[string] |
(Optional) Declared counter classes whose active instances become indeterminate after the instruction because it directly replaces their physical anchor. Existing coordinates are invalidated and precise use requires #resume. Instruction macros cannot declare this metadata; their expanded instructions supply it. |
flow_write_operands |
list[integer] |
(Optional) Zero-based indexes into the selected variant's source-written operands that identify memory addresses written by the instruction. A known match—or a run-time/unresolved target that might match—a class's invalidate_on_write address makes every active instance of that class indeterminate and invalidates its coordinates. Instruction macros cannot declare this metadata; their expanded instructions supply it. |
Instruction variants may also include their own documentation block. Variant-level documentation uses the same schema as the top-level instruction documentation and applies only to that operand signature.
When a variant uses mnemonic_decorator, the decorator must appear immediately adjacent to the mnemonic in source with no whitespace. The supported decorator types are the same as operand decorators: plus (+), minus (-), plus_plus (++), minus_minus (--), exclamation (!), and at (@). The is_prefix flag works the same way as operand decorators: true means the decorator is written before the mnemonic, and false means it is written after the mnemonic. If an instruction also defines aliases, the decorated forms apply to those aliases too.
Example:
instructions:
m:
variants:
- bytecode:
value: 0x6
size: 4
mnemonic_decorator:
type: plus
is_prefix: false
- bytecode:
value: 0x7
size: 4
mnemonic_decorator:
type: minus
is_prefix: falseThis configuration accepts m+ and m- as distinct source mnemonics even though they share the same root mnemonic m in the configuration file.
You can define alternative mnemonics (aliases) for an instruction using the aliases field in the instruction's configuration. Aliases are treated as first-class mnemonics: they are accepted in assembly source, generate the same code as the root mnemonic, and are included in language extension syntax highlighting. All aliases must be globally unique across all mnemonics and aliases in the configuration.
- The
aliasesfield is a list of one or more alternative names for the instruction mnemonic. - If
aliasesis not present, the instruction has no aliases. - Aliases are not supported for macros (only for native instructions).
Example:
instructions:
jsr:
aliases: [call, jump_to_subroutine]
bytecode:
value: 42
size: 8
nop:
bytecode:
value: 0
size: 8In this example, jsr, call, and jump_to_subroutine are all valid mnemonics for the same instruction. Any of these can be used in assembly code, and they will generate the same machine code.
The operands configuration for an instruction requires at least one of operand_sets or specific_operands, or both.
| Option Key | Value Type | Description |
|---|---|---|
count |
integer | The number of operands this mnemonic must have. |
operand_sets |
dictionary |
(Optional) Present if operand sets are used to configure the operands of the mnemonic. Contains the following keys and values:
|
specific_operands |
dictionary |
(Optional) A dictionary of specific operand combination configurations that are allowed when assembling this instruction. Takes precedence over the operand combinations allowed in the operand_sets configuration for this instruction when both configure the same operand combination. The keys of this dictionary are arbitrary strings used internally to identify a specific operand configuration, and the values are the keys' operand configuration. Each operand configuration is a dictionary that contains the following keys and values:
|
Instruction macros are a way to make configurable sequences of instructions and then just just use a single instruction (macro) to insert that instruction sequence into the byte code. For example, if the ISA of the computer only has a single byte move instruction named mov, a two byte move instruction (macro) named mov2 can be constructed from the following sequence of instructions:
mov [addr1],[addr2]
mov [addr1+1],[addr2+1]Then, the macro instruction mov2 [addr1],[addr2] can be defined such that it expands to this sequence.
BespokeASM enables the ability for instruction macros to be defined in the ISA configuration file. Once defined, the macro mnemonic can be used in the assembly code identically to native instruction mnemonics, with the only noticeable difference being that instruction macros generate more byte code than native instructions. What BespokeASM does here is essentially run a pre-assembler that expand a macro instruction into desired set of replacement instruction lines through a string parsing and replacement process. Then the constructed instruction lines are assembled with all the other instruction lines from the assembly code to generate the machine code.
Macros are defined in the macros section of the configuration file. The section is structured similar to the instructions section in that the section is a dictionary where the keys are the mnemonic of the macro. Any given macro is defined by a mnemonic, which is used as a key in this section, and a list of one or more configurations, with each configuration allowing for a varying set of operands and corresponding instructions. Each macro entry can be expressed in one of two ways:
-
Legacy (list) form - the macro mnemonic maps directly to a list of distinct configurations for that macro. This list is implied to be the
variantlist in the Dictionary Form below. All others keys described in the Dictionary Form will be set to tyhe default values. -
Dictionary form - the macro mnemonic maps to a dictionary that contains a
variantskey. The value ofvariantsis the same list of configurations used in the legacy form. This form allows room for additional macro metadata while keeping the variant definition unchanged.
Either form can be used when defining any given macro within a single configuration file. Regardless of the form used for a macro, the list of configurations for a macro is a list of dictionaries. Each dictionary has two elements, operands and instructions.
When using the dictionary form, a macro may include a documentation block alongside variants. The documentation schema matches instruction documentation (category, title, description, modifies, examples). Each entry in the variants list may also include its own documentation block with the same fields. The legacy list form does not support macro-level documentation because it has no place for a documentation key.
The operands section is configured the same as the operands section for instructions is configured, however it is worth noting that since no byte code is emitted directly from a macro, any configuration provided for a macro's operand's byte code is ignored. The goal of the operand section for a macro is simply to define what the allowed types of operands are for a specific macro configurations.
The instructions section of a macro definition lists in order the instruction templates that will be used to compile the instruction sequence that the macro will be expanded into. Each instruction is written as is to be assembled, the macro mechanism essentially replaces the macro instruction in the assembly code with the assembly code listed in instructions. However, before doing so, certain tokens that may be present in the instruction section get replaced with finalized values. The tokens are of the form @YYY(x), where YYY is the token label, and x is an integer indicating what macro operand will be the source of its value. The first macro operand is represent by x being zero to 0, the second is 1, and so on. The following macro tokens are supported:
-
@OP(x)- Generates a value based on the whole string of thexmacro operand. -
@ARG(x)- Generates a value based on the argument numeric expression of thexmacro operand -
@REG(x)- Generates a value based on the register label used in thexmacro operand.
The specific value emitted by each macro token depends on the operand type that the x macro operand is configured to be in the operands section of this macro configuration. The following table lists what each macro token will generate for all supported operand types.
| Operand Type |
operand argument
@ARG(x)
|
operand register
@REG(x)
|
entire operand
@OP(x)
|
|---|---|---|---|
numeric |
The original numeric expression | error | The original numeric expression |
indirect_numeric |
The numeric expression of the indirect address | error | The entire operand, including the [ ] brackets |
deferred_numeric |
The numeric expression of the indirect address | error | The entire operand, including the [[ ]] brackets |
register |
error | The register | The register |
indirect_register |
The offset expression applied to the register | The register | The entire operand, including the [ ] brackets |
indirect_indexed_register |
? | The base register | The entire operand, including the [ ] brackets |
enumeration |
The string of the enumeration value | error | The string of the enumeration value |
numeric_enumeration |
The numeric expression of the enumeration value | error | The numeric expression of the enumeration value |
numeric_bytecode |
The original numeric expression | error | The original numeric expression |
empty |
error | error | error |
See the table in the original documentation for details on what each token emits for each operand type.
To illustrate how to configure a macro, the following shows the dictionary form for the mov2 example discussed above (remove the variants wrapper to use the legacy list form):
macros:
mov2:
variants:
- operands:
count: 2
specific_operands:
indirect_indirect:
list:
iaddr1:
type: indirect_numeric
argument:
size: 16
byte_align: true
iaddr2:
type: indirect_numeric
argument:
size: 16
byte_align: true
instructions:
- "mov [@ARG(0)],[@ARG(1)]"
- "mov [@ARG(0)+1],[@ARG(1)+1]"- Macros cannot define labels or constants, nor use directives. However, they can use predefined labels and constants in expressions.
- Operand labels (
@name:) are not supported for macros, either in macro definition operands or in macro invocation operands. - The instructions listed in the
instructionsection of a given instance of a macro definition are tightly coupled to the operands types configured for the macro in theoperandssection. If the instructions do not match what the macro operands would provide, then errors would be generated during assembly. Whileoperand_setscan be used to configure a macro's operands, care should be taken to ensure all operands listed in the operand set are consistent with each other in terms of how the macro instructions will use it. If operands are inconsistent, a different configuration for the macro should be created in the list of configurations for a given macro.
Example configuration files can be found in the examples directory of the BespokeASM repository.