Writing Instructions (Developers) - ezasm-org/ezasm GitHub Wiki
Creating and Updating Instructions
EzASM uses an annotation-based instruction discovery system that automatically registers methods as executable instructions. This makes adding new instructions straightforward and maintainable. Here's how to create or update instructions:
Architecture Overview
Instructions in EzASM follow a transformation-based model where each instruction returns a TransformationSequence that describes state changes to memory, registers, or other simulator components. This design enables features like undo/redo and maintains a clean separation between instruction logic and simulator state management. The InstructionDispatcher automatically discovers and registers any method annotated with @Instruction by scanning registered instruction handler classes at runtime.
Step-by-Step Guide
1. Create or Open an Instruction Handler Class
Instruction handler classes are located in src/main/java/com/ezasm/instructions/implementation/. Group related instructions together (e.g., ArithmeticInstructions, FileIOInstructions, MemoryInstructions). Each handler class must have a constructor that accepts a Simulator instance:
public class MyInstructions {
private final Simulator simulator;
public MyInstructions(Simulator simulator) {
this.simulator = simulator;
}
}
2. Implement Instruction Methods
Create a public method annotated with @Instruction that returns a TransformationSequence. The method name becomes the instruction name in EzASM (case-insensitive). Method parameters must use abstraction types like IAbstractInput or IAbstractInputOutput to support various operand types (registers, memory addresses, immediates):
@Instruction
public TransformationSequence myinstruction(IAbstractInput input) throws SimulationException {
// Get the value from the input
int value = (int) input.get(simulator).intValue();
// Perform operation
int result = value * 2;
// Create transformation to update a register
InputOutputTransformable io = new InputOutputTransformable(
simulator,
new RegisterInputOutput(Registers.R0)
);
Transformation t = io.transformation(new RawData(result));
return new TransformationSequence(t);
}
3. Register the Handler Class
Add your new instruction handler class to the static registration block in InstructionDispatcher.java:
static {
registerInstructions(ArithmeticInstructions.class);
registerInstructions(MyInstructions.class); // Add your class here
// ... other registrations
}
4. Handle Different Operand Types
Use IAbstractInput for read-only operands and IAbstractInputOutput for operands that can be both read and written. Common patterns include reading from memory using simulator.getMemory().read(address), accessing registers via simulator.getRegisters().getRegister(name), and creating transformations for state changes using InputOutputTransformable, MemoryTransformable, or RegisterInputOutput.
5. Working with Transformations
For immediate side effects (like file I/O), return an empty TransformationSequence. For state changes that should support undo/redo, create appropriate transformations. Multiple transformations can be combined: new TransformationSequence(t1, t2, t3). Common transformable types include InputOutputTransformable for register/memory updates, MemoryTransformable for direct memory manipulation, and HeapPointerTransformable for heap management.
6. Add Documentation and Examples
Write clear JavaDoc explaining what the instruction does, its parameters, return values, and potential exceptions. Create example EzASM programs in the examples/ directory demonstrating your instruction's usage. Update or create markdown documentation in examples/ to explain the instruction's purpose and usage patterns.
Best Practices
- Use descriptive method names that clearly indicate the instruction's purpose
- Validate inputs and throw
SimulationExceptionwith helpful error messages when operations fail - Follow existing patterns by examining similar instructions in other handler classes
- Test thoroughly with edge cases, invalid inputs, and typical use scenarios
- Handle memory carefully when working with null-terminated strings, use
Memory.getWordSize()to calculate proper offsets - Prefix Java keywords with underscore if you need to use a reserved word as an instruction name (e.g.,
_importbecomesimport)
Example: Complete Instruction Implementation
Here's a complete example showing how the open instruction was implemented:
@Instruction
public TransformationSequence open(IAbstractInput input) throws SimulationException {
// Read null-terminated string from memory
int address = (int) input.get(simulator).intValue();
StringBuilder sb = new StringBuilder();
int index = 0;
long current = simulator.getMemory().read(address).intValue();
while (current != 0) {
sb.append((char) current);
index++;
current = simulator.getMemory().read(address + index * Memory.getWordSize()).intValue();
}
// Call simulator method to perform file operation
int fd = simulator.openFile(sb.toString());
// Create transformation to set FD register
InputOutputTransformable fdio = new InputOutputTransformable(
simulator,
new RegisterInputOutput(Registers.FD)
);
Transformation t = fdio.transformation(new RawData(fd));
return new TransformationSequence(t);
}
This instruction reads a filename from memory, opens the file via the simulator, and stores the resulting file descriptor in the $fd register—all following EzASM's transformation-based architecture.