For developers

Version 0.39

1. Introduction

This guide shall help you, the developer, to write your own virtual computer for emuStudio. From now on, every time a 'virtual computer' will be mentioned, it should be always understood as a computer emulator written for emuStudio, if it won’t be said otherwise.

1.1. What is emuStudio

From user’s point of view, emuStudio can be understood as an emulation platform, because it can run various emulators in the same environment. It has a simple IDE, debugger and other features like automatic emulation, which makes emuStudio quite powerful.

From developer’s point of view, it is also a framework, because it provides API and lifecycle management for virtual computers. There are some contracts how things work, what developer can count with, and what is not defined. This tutorial shall guide the developer through these waters, hopefully at the end there won’t be any serious problems with writing custom virtual computers.

1.1.1. Core technologies

During programming, there are some commonly used technologies, which are mandatory when relevant. These are the "core technologies", which are listed in the following table:

Table 1. Core technologies used in emuStudio
Technology Description Relevance Link

Asciidoctor

Documentation generator

Documentation module

http://asciidoctor.org/

Git

Version control system

All

https://git-scm.com/

Java 8 SE

Main programming language

All

http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html

Maven 3

Build lifecycle management

All

https://maven.apache.org/download.cgi

SLF4J

Facade for logger implementation

All

http://www.slf4j.org/

JFlex

Lexical analyzer generator

Compilers

http://jflex.de/

Java CUP

LR parser generator

Compilers

http://www2.cs.tum.edu/projects/cup/

Edigen

Disassembler generator

CPU

https://github.com/sulir/edigen

emuLib

Plugin API and runtime library

All

https://github.com/vbmacher/emuLib

JUnit

Java unit testing framework

All

http://junit.org/

EasyMock

Mock library

All

http://easymock.org/

emuStudio uses Maven for the build lifecycle management. All dependencies and their versions are defined in the root pom.xml file. When you use a library or some technology which is listed there, please use the same version as it is used by emuStudio. You will avoid unexpected conflicts or behavior of your plug-ins.

1.1.2. License

emuStudio with all its modules, including emuLib, is a free software. It is released with GNU GPL 2 license.

1.2. Contributing

Anyone can contribute to emuStudio. The source code is available on GitHub, at https://github.com/vbmacher/emuStudio. emuStudio repository contains the main module, plug-ins (predefined set of virtual computers), testing tools, and CI (continuous integration) tools.

There are basically two options how you can contribute. Either you fix or enhance emuStudio itself (or plug-ins), or you implement completely new computer which can be used with emuStudio. The latter does not really mean to contribute, unless it is included in the original emuStudio repository.

However, adding new computers to standard emuStudio must be consulted with the author in advance. While I encourage people writing their own computers, I assume they will be mostly students having only little experience and/or fixed-time projects which will end when the student passes. This is perfectly fine, but those projects should be rather kept in other places.

Also, in the future there might be something like a plug-in repository, and a user would download what he wants manually. So far, it is not like that and it is too soon to consider this.

1.2.1. Definition of DONE

There are some requirements which need to be fulfilled before we can say that the contribution is "done" and can be accepted or released. The list is very simple:

  • Code should be clean, conforming to the code style

  • Code must have proper unit tests, if applicable or possible

  • Documentation should be updated

1.2.2. Git workflow

Since the whole project is using git as the version control system (VCS), it has many benefits used for specification of a way how people can actually contribute.

emuStudio uses a much simplified version of the Git Flow model. Releasing of older versions due to "hot fixes" or maintenance is not supported. Fixing bugs and development of new features are both done in the single branch, called develop. On GitHub the branch is marked as default.

When a new release is out, the last commit must be tagged with a new tag named RELEASE-X, where X is the released version.

When contributing (fixing, new development), always derive new branch from the develop branch. In your branch, you can do any number of well-formed commits. When you are ready, raise new pull-request back to the develop branch.

Example of the git workflow is as follows:

At first, fork emuStudio on GitHub. Then:

git clone https://github.com/your_name/emuStudio.git
git checkout -b fix-issue-143 origin/develop

.. fixing/implementing ..

git commit -a -m '[#143] 88-disk: implement interrupts'
git push

You can repeat this process several times. When you are finished with all commits, create a pull request to original emuStudio repository, back to the develop branch.

The pull request will be seen by the author, which will make a review and either approve (and merge), comment or rejects the pull request (with explanation).

As you could notice, commits should be well-formed - named with the issue number before the commit title, in square brackets. Also, if it is related to specific plug-in or module, it should be written in the message, e.g.:

[#143] 88-disk: implement interrupts

GitHub then automatically links the commit with the issue (a comment appears). For more information, see https://help.github.com/articles/using-pull-requests/.

1.3. What is a virtual computer

Generally, a real computer can be decomposed into some cooperating components (still high-level), like CPU, bus, memory, or devices. It is not far different from how it is in emuStudio. The core concept of a virtual computer is inspired by the von Neumann model. The model defines three types of core components: CPU (control unit and arithmetic-logic unit), memory, and input/output devices. In emuStudio, the virtual computer includes also these components, but the possibilities of interconnection and cooperation are not bound to hardware limits or philosophy.

Each component of a virtual computer is a separate plug-in written in Java. A virtual computer is then just a set of cooperating plug-ins which are loaded and initialized by emuStudio. The selection of plugins is handled externally, by the user of emuStudio. The plugins list is extended with information about plug-in interconnection, which is specific for each computer. Then we have something which is called abstract schema. But as was said, abstract schemas are prepared by user, not plug-in developer.

For more information about how to create such a schema, please read the user manual. The whole process of loading and initializing the plug-ins into working emulator is completely handled by emuStudio. Developer must hold to some contracts, and principles of good object-oriented design, which are enough for ensuring that everything will work as expected.

The following schema defines all plug-in types and their possible interconnections, as it is currently in emuStudio.

emustudio plugins

As you can see, there are no restrictions about which plug-in can "see" or cooperate with another plug-in. For example, a compiler can access all computer components, including CPU, devices and memory.

Most probably a compiler would want to access memory, in which case it would be able to load a compiled program directly there. But the reason why the compiler is allowed to access also other components is that the compiled program can contain either some information about initial states, or initial data which are needed to be preloaded into other components before program can be started (for example, content of abstract tapes in the case of RAM machine).

1.4. Plug-in basics

Each plug-in is a separate Java module, usually single jar file, placed in the proper directory. As it is necessary to place the plug-in to proper location (compilers/, cpu/, mem/, and devices/), dependencies of both emuStudio and all plug-ins should be included in lib/ directory. The reason is to help ensuring that versions of shared dependencies across plug-ins themselves and across emuStudio must be the same within single emuStudio distribution.

In emuStudio, plug-in source codes are located in plugins/ subdirectory, then separated by plug-in type. For example:

In order to contribute to an existing plug-in, you can find the plug-in in some subdirectory. If you want to add a new plug-in which should exist in the default emuStudio distribution, you would create new plug-in in that place as well.

Standard or "default" plug-ins force to use Maven and you must follow the standard which will be defined later. Also, before making any design changes or new plug-in development, please contact the emuStudio author.

Usually, your plug-ins will not be the standard part of default emuStudio distribution. In that case, you are not forced to use Maven or any other technology, except of emuStudio API, contracts and the limits which might exist when involving unknown third party dependencies. Also, you can use your own code style if you like.

1.4.1. Plug-in API

The basic idea of the development of the plug-in is to implement an API of that specific plug-in. This is actually only thing which is required.

Plug-in API is stored in emuLib library (see Core technologies), so each plug-in must have emuLib as dependency. This and following guides will show you some examples of how to implement a plug-in. For deeper details of all available API, it is recommended to check the Javadoc.

1.4.2. Third-party dependencies

Each plug-in can depend on third-party libraries. It is recommended way how to avoid code duplication and reinventing a wheel. If a plug-in depend on some third-party library, it is required to put the class path to the Manifest file of the plug-in.

What is not required, however, is to define some default dependencies (listed below). emuStudio uses custom class-loader for loading plug-ins, which handles the default dependencies automatically.

emuLib

Plugin API and runtime library

slf4J

Facade for logger implementation

logback

Logger implementation, successor of log4j

These dependencies should not be present in plug-in manifest files, they will be automatically loaded with emuStudio. Please see emuStudio main POM file to determine the library versions.

In order to use other third-party dependencies, they must be mentioned in Manifest. The recommended way is to put the dependencies in /lib subdirectory, and define relative path in Manifest from the root directory of where the emuStudio is installed. For example, here is a Manifest file for RAM compiler plug-in:

Manifest-Version: 1.0
Implementation-Title: RAM Compiler
Implementation-Version: 0.39-SNAPSHOT
Archiver-Version: Plexus Archiver
Built-By: vbmacher
Specification-Title: RAM Compiler
Implementation-Vendor-Id: net.sf.emustudio
Class-Path: mem/ram-mem.jar lib/java-cup-runtime-11b.jar
Created-By: Apache Maven 3.3.3
Build-Jdk: 1.8.0_65
Specification-Version: 0.39-SNAPSHOT

The plug-in uses two non-default dependencies: RAM memory plug-in, and java-cup library. The first one is a memory plug-in for emuStudio, so it is placed in mem/ subdirectory, but java-cup library is completely third-party, and non-default. The recommended place for storing these kind of libraries is lib/ subdirectory.

Cyclic dependencies are also supported.

1.5. Emulation lifecycle

emuStudio is also a framework, which not only defines the API, but also the whole life cycle of plug-ins. It has the control of all emulation processes, including CPU and all virtual devices. It proactively loads, instantiates and initializes plug-ins. That way a plug-in developer can safely focus only on what the plug-in should do in the first place.

Behavior contracts define rules and assumptions which plug-in developer must hold to. emuStudio is assuming that plug-ins "behave good", and if it is true, everything should work as expected. By ignoring the behavioral contracts the emuStudio behavior is undefined; it can possibly corrupt the emulation process or crash whole emuStudio.

The list of some categories of behavioral contracts include:

  • Order of operations being called by emuStudio (e.g. order of loading / initialization of plug-ins)

  • Rules of allowed / not allowed method calls in particular contexts

  • Specification of signature of constructors

  • Threading concerns

  • Other

The behavioral contracts are described in particular Javadoc for emuLib and all modules to which it may concern. The Javadoc contains special note which starts with capital CONTRACT:. The contract is mainly in the form of explanation which other methods should not be called, or how particular thing should be implemented.

1.5.1. Main class

Each plug-in must have exactly one "main class" in Java, which will be annotated with emulib.annotations.PluginType annotation. This annotation provides several information, like:

  • Title of the plug-in

  • Copyright notice and description of the plug-in

  • What type of the plug-in is (compiler, CPU, memory, device),

  • What version of emuLib it supports

The class must also inherit from emulib.plugins.Plugin interface (not necessarily directly).

1.5.2. Loading and initialization

Setting up plug-ins is a two-phase process and it is done solely in emuStudio. emuStudio has custom class loader, into which it loads all plug-ins (classes and resources) and "registers" them in JVM.

Phase 1 - Loading

The plug-ins are loaded as a one bunch of extracted JARs mixed together, in a newly created class loader. The class loader is immutable so further modification of plug-in loading (e.g. adding another component at run-time) is not possible.

Dependencies explicitly specified in manifest files are recognized and loaded as well. With this, plug-ins can depend on each other. However, in case of circular dependency, plug-ins loadin will fail.

The result of this phase is that all plug-in classes are loaded in memory and all main-classes instantiated. Each plug-in main class must have a constructor with exactly two arguments:

SamplePlugin(Long pluginId, ContextPool contextPool) {
    ...
}

The ContextPool can be used (in this phase) only for registering custom plug-in contexts, but not for their obtaining. More information can be found in emuLib’s Javadoc.

Phase 2 - Initialization

The initialization of plug-ins follows as the second phase. In this phase, plug-ins should ask from given ContextPool in the previous phase of context(s) of other, already registered plug-ins.

The order in which plug-ins are initialized is:

  1. Compiler

  2. CPU

  3. Memory

  4. Devices in the order as they are defined in the virtual computer configuration file

When following this contract, it cannot happen that a plug-in will ask for context which is not registered.

1.6. Naming conventions

Plug-in names (jar file names) follow naming conventions. The names differ based on plug-in types. From the jar file name it should be clear what plug-in we are talking about. Generally, the jar file should begin with some custom abbreviation of the real world "model" optionally preceded with the manufacturer (e.g. intel-8080, lsi-adm-3A, etc.). Then plug-in type follows, as it is shown in the following table:

Table 2. Naming conventions for plug-in jar files
Plug-in type Naming convention Example

Device

<optional_manufacturer>-<model_abbreviation>-<device_type>

88-disk, adm3a-terminal

Compiler

<language_abbreviation>-compiler, or as-<language_abbreviation> for assemblers

as-8080, brainc-compiler

CPU

<optional_manufacturer>-<model_abbreviation>-cpu

8080-cpu, z80-cpu

Memory

<model_or_main_features_abbreviation>-mem

standard-mem, ram-mem

Plug-in names can contain digits, small and capital letters (regex: [a-zA-Z0-9]+). Capital letters shall be used only for the following reasons:

  • Word separation (e.g. zilogZ80),

  • Acronyms (e.g. RAM, standing for "Random Access Machine")

Using naming conventions for development of official plug-ins is a must; for custom projects it is highly recommended. emuStudio does not use the naming convention for searching for plugins.

1.7. Coding Style

Unified coding style is as important as being a team player. It is the commonly-accepted order, which puts the code readability at the same level everywhere. It is as in a classical book - you don’t usually see multiple writing styles or text organizations throughout the book. It is written as by only one author, even if it has more. The same purpose has the code style, because the reader is always just one.

I encourage you to read a book called Clean Code from Robert Martin. You can find there many inspiring thoughts and ideas how to write the code in a clean way.

1.7.1. License information

Each file must start with a comment with the license information. Please read part "How to Apply These Terms to Your New Programs" at link http://www.gnu.org/licenses/gpl.html.

1.7.2. Indentation

I consider this section as very important, so as there is lots of time consuming debates about the "indentation problem". Therefore I "codify" this to 4 spaces.

1.7.3. Logging

emuStudio is bundled with SLF4J logger API which is bound with logback logger. In code, it is possible to use the logger, like in this example:

Example of using logger
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SomeClass {
    private static final Logger LOGGER = LoggerFactory.getLogger(SomeClass.class);


    public void someMethod() {
        LOGGER.info("Information message...");
    }

    ...
}

Logging can be very important for analysis of a problem some other user had. emuStudio is supposed for many users so it’s reasonable to include logging.

It is not recommended to log information during running emulation. Logging significantly lowers the performance down.

1.8. Use Maven if you can

Maven is a standard for Java projects today. It helps with the build process and manages dependencies in satisfying and reusable way.

Each official emuStudio module (artifact) is available in custom Maven repository, including emuLib. In order to be able to use them from Maven, put the following code into your pom.xml file:

<distributionManagement>
  <repository>
    <id>emustudio-repository</id>
    <name>emuStudio Repository</name>
    <url>sftp://web.sourceforge.net:/home/project-web/emustudio/htdocs/repository</url>
  </repository>
</distributionManagement>
Development of official standard plug-ins require using Maven.

1.9. Documenting plug-ins

There are two types of documentation:

  • user documentation

  • developer’s documentation (not javadoc)

1.9.1. User documentation

User documentation is located at doc/src/user-manual.

Description of a plug-in usually should not be standalone, but put in a bigger document describing the whole computer. Description of each computer should be put in a separate directory, e.g. altair8800/, brainduck/, etc. The description should focus on the interactive part of the emulation, and do not describe what’s going under the hood in much detail.

The description should start with some introduction:

  • How the computer is related to the computer history?

  • Is it abstract or real?

  • The purpose of the computer

  • Comparison of features which it has as the emulator for emuStudio with the features of real computer

Then, every plug-in should be described, starting from compiler - in the form of the "programming language" tutorial.

It is important - keep the information useful. Do not try hard to put any information if you think it is too small. Some plug-ins are quite clear and don’t seem to interact much with user, which is OK. For example usually it’s the CPU plug-in.

Programming examples should follow, if the plug-in allows programming. For example, both MITS 88-DISK and MITS 88-SIO are programmable devices.

Then a very important section should be devoted to automatic emulation. More specifically:

  • How the plug-in will behave if emuStudio will run in automatic emulation mode?

  • Where can user find output files if the output is redirected to a file?

  • What is the behavior if the automatic emulation is run more times in a row? Will the files be overridden or appended?

  • Can be output file names changed?

The last section should talk about debugging of the plug-in. For example:

  • List of known bugs

  • How to report a bug

  • How to do some analysis when something does not work

1.9.2. Developer’s documentation

Developer documentation is optional, but suggested. It should be written in the form of Maven sites. The preferred formatter is Markdown.

In this type of documentation, only technical details should be explained. Majority of them should be the "why"s instead of "how"s.

1.10. Incorporating a plug-in to emuStudio

The philosophy about releasing is to keep everything as automatic as possible. The main reason is that if it was manual, it would take some time which can be spent on something better. Of course there will be always some manual steps, but it is better to keep them minimal.

The submodule release/ is used now to create emuStudio releases. It expects that emuStudio artifacts are either installed in local Maven repository, or they will be downloaded from emuStudio repository.

The submodule uses maven-assembly-plugin is used, and assembly.xml file exists which describes which artifacts and files should be placed in which directories.

The following artifacts can be included in the release:

  • Plug-in artifact (JAR file)

  • Plug-in examples

  • New computer configuration (if applicable)

1.10.1. Plug-in artifact

The condition is ofcourse that the plug-in must be a submodule in the main emuStudio repository. As an example, let’s use plug-in plugins/compilers/as-ssem. The point is to edit release/assembly.xml file, find the dependency set for compilers (look for the line <outputDirectory>/compilers</outputDirectory>) and add the plug-in in that set:

    <dependencySet>
      <includes>
        ...
        <include>net.sf.emustudio:as-ssem</include>
      </includes>
      ...
      <outputDirectory>/compilers</outputDirectory>
    </dependencySet>

Similarly, for other types of plug-ins there exist corresponding sections which should be used.

1.10.2. Plug-in examples

Similarly as was said in the previous subsection, the file which should be edited is release/assembly.xml. Examples section is located in the bottom part, in a fileSet section. Examples are usually bound with specific compiler - and they are also physically placed.

Compilation of compiler plug-ins does not create examples artifacts (maybe it should in the future). The assembly therefore points to relative path of the example files.

For example, example files for plug-in as-8080 are stored in the following section:

    <fileSet>
      <directory>../plugins/compilers/as-8080/examples</directory>
      <directoryMode></directoryMode>
      <includes>
        <include>**/*.asm</include>
        <include>**/*.inc</include>
      </includes>
      <outputDirectory>/examples/as-8080</outputDirectory>
    </fileSet>

The subdirectories in target examples/ directory are organized by compiler plug-in names, or machine names if the examples are rather bound to the whole virtual computer (e.g. disk images, etc.). Examples for whole virtual computers are usually not bound with specific plug-ins and should be placed directly in the release/files/examples/ directory.

All files in the release/files are automatically included in the release.

1.10.3. New computer configuration

All predefined computer configurations are placed in directory release/files/config. The only step needed to be done here is to create a computer configuration file and place it there. The maven-assembly-plugin will take care of it and the configuration will be included in the release automatically.

1.11. What to do next

What follows are tutorials for developing specific emuStudio plug-ins - compiler, CPU, memory or a device. Prepare your fingers, you’ll write some code. Let’s start.

2. Writing a compiler

This tutorial will guide you to how to create a compiler for emuStudio. It focuses on things around emuStudio, emuLib and some common practices. It will not dig deep into the compiler theory, only what will be required on the way.

In present days, general scheme of compiler’s work is parsing, semantic analysis, optimalization and code generation. Quite tedious and error-prone work of parsing can be generated automatically with parser generators. All emuStudio compilers use such tools; and they will be also used in this tutorial:

  • JFlex for generating the lexical analyzer, or lexer for short,

  • Java cup for generating LR(k) parsers.

Before reading on, please read the Introduction chapter. It provides the information needed for setting up the development environment and explains how emuStudio plug-ins' lifecycle work.

2.1. Getting started

In the context of the whole emulator, the compiler is usually an assembler which produces binary instructions for the emulated CPU. However, it does not have to be assembler; it really can be any language compiler. The output of the compiler is often loaded directly into emulated operating memory, so the program can be run in emuStudio in a single click. This is one of the great features of emuStudio.

A compiler is a plug-in, which means that it requires to implement an API. The API for compilers is defined in the form of Java interfaces and some classes in emuLib, in package emulib.plugins.compiler. This tutorial will guide you with implementing the API and the whole compiler.

2.2. Assembler for SSEM

In this tutorial, we will implement an assembler for the world’s very first stored-program computer, SSEM, nicknamed "Baby". It was a predecessor of Manchester Mark 1 which led to Ferranti Mark 1, the world’s first commercially available general-purpose computer.

It it very simple computer, which can run only 7 instructions. The instructions table follows (modified from Wikipedia):

Binary code Mnemonic Action Operation

111

STP / HLT

Stop

000

JMP S

S(L) → CI

Jump to the instruction at the address obtained from the specified memory address S(L) (absolute unconditional jump)

100

JRP / JPR / JMR S

CI + S(L) → CI

Jump to the instruction at the program counter (CI) plus the relative value obtained from the specified memory address S(L) (relative unconditional jump)

010

LDN S

-S(L) → A

Take the number from the specified memory address S(L), negate it, and load it into the accumulator

110

STO S

A → S(L)

Store the number in the accumulator to the specified memory address S(L)

001 or 101

SUB S

A - S(L) → A

Subtract the number at the specified memory address S(L) from the value in accumulator, and store the result in the accumulator

011

CMP / SKN

if A<0 then CI+1→CI

Skip next instruction if the accumulator contains a negative value

The instructions are stored in a memory, which had 32 cells. Each cell was 32 bits long, and each instruction fit into exactly one cell. So each instruction has 32 bits. The bit representation was reversed, so the most and the least significant bits were put on opposite sides. For example, value 3, in common personal computers represented as 011, was in SSEM represented as 110.

The instruction format is as follows:

Table 3. SSEM Instruction Format

Value:

2^0

2^31

Bit:

00

01

02

03

04

…​

13

14

15

…​

31

Use:

L

L

L

L

L

0

I

I

I

0

0

where bits LLLLL denote a "line", which is basically the memory address - index of a memory cell. It can be understood as instruction operand. Bits III specify the instruction opcode (3 bits are enough for 7 instructions).

2.3. Language specification

Each compiler is just a program which translates an input source code into output code. Usually, the input language is more high level than the output language, and often offers some features, which are not supported by the output language. For example, assembler source codes often support comments, various formats of number literals, labels, or macros. These are features, which are not supported by the instruction set itself, and they must be translated to it by compiler.

SSEM assembler specification can be started with informal expressing, what it will know:

2.3.1. Token categories

Tokens, parsed by lexical analyzer have assigned a so-called category. The category is useful mainly for syntax highlighter in emuStudio editor. For example, reserved words are colored differently than e.g. numeric constants, or comments.

The list of all categories can be seen in the following class: Token.java.

2.3.2. New-lines

New-line character (LF, CR, or CRLF) will be required as a delimiter of instructions, and as the last character. Successive empty new-line characters will be ignored.

2.3.3. Instructions

Assembler will support all forms of instructions shown in the table in section Assembler for SSEM. All instructions must start with a line number. For example:

01 LDN 20

Instructions belong to token category called "reserved words".

2.3.4. Literals / constants

Raw number constants can be defined in separate lines using special preprocessor keywords. The first one is NUM xxx, where xxx is a number in either decimal or hexadecimal form. Hexadecimal format must start with prefix 0x. For example:

00 NUM 0x20
01 NUM 1207943145

Another keyword is BNUM xxx, where xxx can be only a binary number. For example:

01 BNUM 10011011111000101111110000111111

It means that the number will be stored untouched to the memory in the format as it appears in the binary form.

There exists also a third keyword, BINS xxx, with the exact meaning as BNUM. The reason for its presence is to be compatible with most of the programs found on internet.

For all constants, the following rules hold. Only integral constants are supported, and the allowed range is from 0 - 31 (maximum is 2^5).

Word NUM, BNUM and BINS keywords belong to "preprocessor" category, but number constants to the category called "literals".

2.3.5. Comments

Only one-line comments will be supported, but of various forms. Generally, comment will be everything starting with some prefix until the end of the line. Comment prefixes are:

  • Double-slash (//)

  • Semi-colon (;)

  • Double-dash (--)

The token category of comments is "comments".

2.3.6. Full example

For example, simple 5+3 addition can be implemented as follows:

0 LDN 7 // load negative X into the accumulator
1 SUB 8 // subtract Y from the value in the accumulator
2 STO 9 // store the sum at address 7
3 LDN 9 // A = -(-Sum)
4 STO 9 // store sum
5 HLT
7 NUM 3 // X
8 NUM 5 // Y
9       // here will be the result

The accumulator should now contain value 8, as well as memory cell at index 9.

2.4. Preparing the environment

In order to start developing the compiler, create new Java project in your favorite IDE. In emuStudio, Maven is used for dependencies management. If you’re not familiar with Maven, you can start here.

The compiler will be implemented as another standard emuStudio plug-in in standard path plugins/compilers/as-ssem. It will inherit all Maven plug-in dependencies from the main POM file.

The directory structure is "dictated" by Maven, so it should look as follows:

src/
  main/
    java/
    resources/
test/
  java/
pom.xml
Note the naming of the plug-in. We are following the naming convention as described in the Naming conventions guide.

The POM file of the project looks as follows:

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <artifactId>emustudio-parent</artifactId>
    <groupId>net.sf.emustudio</groupId>
    <version>0.39</version>
    <relativePath>../../../pom.xml</relativePath>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <artifactId>as-ssem</artifactId>

  <name>SSEM Assembler</name>
  <description>Assembler of SSEM processor language</description>

  <build>
    <finalName>as-ssem</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <configuration>
          <archive>
            <manifest>
              <addClasspath>false</addClasspath>
              <mainClass>net.sf.emustudio.ssem.assembler.Main</mainClass>
              <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
              <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
            </manifest>
            <manifestEntries>
              <!-- DO NOT REMOVE THESE DEPENDENCIES; COMMAND LINE THEN WON'T WORK -->
              <Class-Path>lib/java-cup-runtime-${javacup.version}.jar lib/emuLib-${emulib.version}.jar lib/slf4j-api-${slf4j.version}.jar</Class-Path>
            </manifestEntries>
          </archive>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-dependency-plugin</artifactId>
      </plugin>
      <plugin>
        <groupId>de.jflex</groupId>
        <artifactId>jflex-maven-plugin</artifactId>
        <executions>
          <execution>
            <goals>
              <goal>generate</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>com.github.vbmacher</groupId>
        <artifactId>cup-maven-plugin</artifactId>
        <executions>
          <execution>
            <goals>
              <goal>generate</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <className>ParserImpl</className>
          <symbolsName>Symbols</symbolsName>
        </configuration>
      </plugin>
    </plugins>
  </build>

  <dependencies>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
    </dependency>
    <dependency>
      <groupId>net.sf.emustudio</groupId>
      <artifactId>emuLib</artifactId>
    </dependency>
    <dependency>
      <groupId>com.github.vbmacher</groupId>
      <artifactId>java-cup-runtime</artifactId>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
    </dependency>
    <dependency>
      <groupId>org.easymock</groupId>
      <artifactId>easymock</artifactId>
    </dependency>
    <dependency>
      <groupId>net.sf.emustudio</groupId>
      <artifactId>cpu-testsuite</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

2.5. Lexical analyzer (lexer)

We will start with definition of the lexer specfile. It is a special file, which will be given to JFlex during project compilation. Jflex will generate a Java class - the lexer - which will be used by the parser later, and by emuStudio editor, too. The specification file has special place in the directory structure:

src/
  main/
    jflex/
      lexer.jflex
Note that the specfile is not put into resources directory. If it was so, then it would be included in the final JAR file.

JFlex will be called during compilation of the assembler by the JFlex Maven plugin (see the POM file above). The content of the specfile is as follows:

src/main/jflex/ssem.jflex
package net.sf.emustudio.ssem.assembler;

import emulib.plugins.compiler.LexicalAnalyzer;
import emulib.plugins.compiler.Token;
import emulib.runtime.NumberUtils;
import emulib.runtime.RadixUtils;

import java.io.IOException;
import java.io.Reader;
import java.util.Arrays;

%%

/* options */
%class LexerImpl
%cup
%public
%implements LexicalAnalyzer, Symbols
%line
%column
%char
%caseless
%unicode
%type TokenImpl

%{
    @Override
    public Token getSymbol() throws IOException {
        return next_token();
    }

    @Override
    public void reset(Reader in, int yyline, int yychar, int yycolumn) {
        yyreset(in);
        this.yyline = yyline;
        this.yychar = yychar;
        this.yycolumn = yycolumn;
    }

    @Override
    public void reset() {
        this.yyline = 0;
        this.yychar = 0;
        this.yycolumn = 0;
    }

    private TokenImpl token(int type, int category) {
        return new TokenImpl(type, category, yytext(), yyline, yycolumn, yychar);
    }

    private TokenImpl token(int type, int category, Object value) {
        return new TokenImpl(type, category, yytext(), yyline, yycolumn, yychar, value);
    }
%}

%eofval{
    return token(EOF, Token.TEOF);
%eofval}

comment = "//"[^\r\n]*
comment2 = "--"[^\r\n]*
comment3 = ";"[^\r\n]*
eol = \r|\n|\r\n
space = [ \t\f]+
number = \-?[0-9]+
hexnumber = \-?0x[0-9a-fA-F]+
binnumber = [01]+

%xstate BIN

%%

<YYINITIAL> {
    /* reserved words */
    "jmp" {
        return token(JMP, Token.RESERVED);
    }
    "jrp" {
        return token(JPR, Token.RESERVED);
    }
    "jpr" {
        return token(JPR, Token.RESERVED);
    }
    "jmr" {
        return token(JPR, Token.RESERVED);
    }
    "ldn" {
        return token(LDN, Token.RESERVED);
    }
    "sto" {
        return token(STO, Token.RESERVED);
    }
    "sub" {
        return token(SUB, Token.RESERVED);
    }
    "cmp" {
        return token(CMP, Token.RESERVED);
    }
    "skn" {
        return token(CMP, Token.RESERVED);
    }
    "stp" {
        return token(STP, Token.RESERVED);
    }
    "hlt" {
        return token(STP, Token.RESERVED);
    }

    /* special */
    "start:" {
        return token(START, Token.PREPROCESSOR);
    }
    "num" {
        return token(NUM, Token.PREPROCESSOR);
    }
    "bnum" {
        yybegin(BIN);
        return token(BNUM, Token.PREPROCESSOR);
    }
    "bins" {
        yybegin(BIN);
        return token(BNUM, Token.PREPROCESSOR);
    }

    /* comment */
    {comment} {
        return token(TCOMMENT, Token.COMMENT);
    }
    {comment2} {
        return token(TCOMMENT, Token.COMMENT);
    }
    {comment3} {
        return token(TCOMMENT, Token.COMMENT);
    }

    /* literals */
    {number} {
        int num = Integer.parseInt(yytext(), 10);
        return token(NUMBER, Token.LITERAL, num);
    }

    {hexnumber} {
        int num = Integer.decode(yytext());
        return token(NUMBER, Token.LITERAL, num);
    }
}

/* separators */
<YYINITIAL, BIN> {eol} {
    return token(SEPARATOR_EOL, Token.SEPARATOR);
}
<YYINITIAL, BIN> {space} { /* ignore white spaces */ }

<BIN> {

    {binnumber} {
        yybegin(YYINITIAL);

        byte[] numberArray = RadixUtils.convertToNumber(yytext(), 2, 4);
        int num = NumberUtils.reverseBits(
            NumberUtils.readInt(
                NumberUtils.toObjectArray(numberArray), NumberUtils.Strategy.LITTLE_ENDIAN
            ), 32
        );

        return token(NUMBER, Token.LITERAL, num);
    }

    [^] {
        yybegin(YYINITIAL);
    }

}

/* error fallback */
[^] {
    return token(ERROR_UNKNOWN_TOKEN, Token.ERROR);
}

As you can notice, the specfile uses special class named TokenImpl. We must implement this class by ourselves. It holds the basic information about the parsed token, like offset, length, type, etc. There are several requirements when implementing the class:

  • It must extend java_cup.runtime.Symbol class, for JFlex - cup interoperability.

  • It must implement emulib.plugins.compiler.Token interface, for being able to use this class in emuStudio syntax highlighter

  • It’s now a secret, but it would have to implement also special Symbols interface, which will be generated by parser, described in section below.

Syntax highlighter in emuStudio represents the source code in a dynamic "lexical tree". It scans regularly required text blocks in the editor and translates them into the symbolic representation - into tokens, which are arranged in a tree structure. Tokens are parsed by the lexer, provided by us. And Token interface is the shared API known by our specific lexer and general syntax highlighter in emuStudio.

Tokens are assigned into categories, as was already mentioned in section Token categories. Token categories have assigned their specific editor style, like color or font.

The content of the net.sf.emustudio.ssem.assembler.TokenImpl class is as follows:

src/main/java/net/sf/emustudio/ssem/assembler/TokenImpl.java
package net.sf.emustudio.ssem.assembler;

import emulib.plugins.compiler.Token;
import java_cup.runtime.ComplexSymbolFactory;

public class TokenImpl extends ComplexSymbolFactory.ComplexSymbol implements Token, Symbols {
    private final int category;
    private final int cchar;

    public TokenImpl(int id, int category, String text, int line, int column, int cchar) {
        super(
            text, id, new ComplexSymbolFactory.Location(line, column), new ComplexSymbolFactory.Location(line, column)
        );
        this.category = category;
        this.cchar = cchar;
    }

    public TokenImpl(int id, int category, String text, int line, int column, int cchar, Object value) {
        super(
            text, id, new ComplexSymbolFactory.Location(line, column), new ComplexSymbolFactory.Location(line, column), value
        );
        this.category = category;
        this.cchar = cchar;
    }

    @Override
    public int getID() {
        return super.sym;
    }

    @Override
    public int getType() {
        return category;
    }

    @Override
    public int getLine() {
        return super.getLeft().getLine();
    }

    @Override
    public int getColumn() {
        return super.getLeft().getColumn();
    }

    @Override
    public int getOffset() {
        return cchar;
    }

    @Override
    public int getLength() {
        return getName().length();
    }

    @Override
    public String getErrorString() {
        return "Unknown token";
    }

    @Override
    public String getText() {
        return getName();
    }

    @Override
    public boolean isInitialLexicalState() {
        return super.sym != BNUM;
    }
}

2.6. Syntax analyzer (parser)

Next, we define the grammar file. It is also a special file, which will be given to cup during project compilation. Cup will generate Java classes - the parser - which we will use in our code. The specfile has special place in the directory structure:

src/
  main/
    cup/
      parser.cup

Grammar type and form we use depends on the parsing algorithm we choose. In emuStudio, all compilers use Java cup parser generator, which does bottom-up parsing, and supported grammars are of type LALR.

The main difference between LL and LALR grammars is that in LALR you can freely use left-recursion, but not right recursion. Otherwise you would get shift/reduce conflicts. For more information, see for example this site.

The grammar specfile of SSEM assembler parser follows:

src/main/cup/parser.cup
package net.sf.emustudio.ssem.assembler;

import emulib.plugins.compiler.Message;
import emulib.plugins.compiler.Token;
import java_cup.runtime.ComplexSymbolFactory;
import java_cup.runtime.Symbol;
import net.sf.emustudio.ssem.assembler.tree.ASTnode;
import net.sf.emustudio.ssem.assembler.tree.Constant;
import net.sf.emustudio.ssem.assembler.tree.Instruction;
import net.sf.emustudio.ssem.assembler.tree.Program;

import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

parser code {:
    private LexerImpl lexer;
    private boolean syntaxErrors;
    private CompilerImpl compiler;

    public ParserImpl(LexerImpl lex, ComplexSymbolFactory csf, CompilerImpl compiler) {
        super(lex, csf);
        lexer = Objects.requireNonNull(lex);
        this.compiler = Objects.requireNonNull(compiler);
    }

    @Override
    public void report_fatal_error(String message, Object info) throws Exception {
        done_parsing();
        report_error(message, info);
        throw new Exception("Can\'t recover from previous error(s)");
    }

    @Override
    public void report_error(String messageText, Object current) {
        syntaxErrors = true;

        Token token = (Token)current;

        messageText += ":" + token.getErrorString() + " ('" + token.getText() + "')";

        List expectedTokenIds = expected_token_ids()
            .stream()
            .map(id -> symbl_name_from_id(id.intValue()))
            .collect(Collectors.toList());

        if (!expectedTokenIds.isEmpty()) {
            messageText += "\nExpected tokens: " + expectedTokenIds;
        }

        Message message = new Message(
            Message.MessageType.TYPE_ERROR, messageText, token.getLine()+1, token.getColumn(), null, 0
        );

        if (compiler != null) {
            compiler.notifyOnMessage(message);
        } else {
            System.err.println(message.getFormattedMessage());
        }
    }

    public boolean hasSyntaxErrors() {
        return syntaxErrors;
    }

:};

terminal JMP, JPR, LDN, STO, SUB, CMP, STP, NUM, BNUM;
terminal SEPARATOR_EOL, TCOMMENT, ERROR_UNKNOWN_TOKEN;
terminal Integer NUMBER;
terminal START;

non terminal Program Program;
non terminal ASTnode Statement;
non terminal Instruction Instruction;
non terminal Constant Constant;
non terminal Comment;

start with Program;

Program ::= NUMBER:c Statement:s Program:p              {: if (s != null) p.statement(c, s); RESULT = p;  :}
    | NUMBER:c Comment SEPARATOR_EOL Program:p          {: RESULT = p; :}
    | Comment SEPARATOR_EOL Program:p                   {: RESULT = p; :}
    | START SEPARATOR_EOL Program:p                     {: p.nextLineStarts(); RESULT = p; :}
    | /* empty program */                               {: RESULT = new Program(); :}
    ;

Statement ::= Instruction:i Comment SEPARATOR_EOL       {: RESULT = i; :}
    | Constant:c Comment SEPARATOR_EOL                  {: RESULT = c; :}
    ;

Instruction ::= JMP NUMBER:line             {: RESULT = Instruction.jmp(line); :}
    | JPR NUMBER:line                       {: RESULT = Instruction.jrp(line); :}
    | LDN NUMBER:line                       {: RESULT = Instruction.ldn(line); :}
    | STO NUMBER:line                       {: RESULT = Instruction.sto(line); :}
    | SUB NUMBER:line                       {: RESULT = Instruction.sub(line); :}
    | CMP                                   {: RESULT = Instruction.cmp(); :}
    | STP                                   {: RESULT = Instruction.stp(); :}
    | error
    ;

Constant ::= NUM NUMBER:raw                 {: RESULT = new Constant(raw); :}
    | BNUM NUMBER:raw                       {: RESULT = new Constant(raw); :}
    ;

Comment ::= TCOMMENT
    | /* no comment*/
    ;

The right sides - code snippets wrapped between {: and :} - is Java code which will be executed when particular rule of the grammar is applied. Remember, that they will be applied in reverse - first will be applied the right-most rules.

There exist a special variable RESULT, which should return some Java object of type which the non-terminal defines for it [1].

For more information, especially about the error symbol, please read cup documentation.

2.7. Introducing AST

The code won’t compile so far. The reason is that the parser strangely uses some undefined classes, such as Program, ASTnode, Instruction and Constant. They are defined in the grammar file as follows (see above):

non terminal Program Program;
non terminal ASTnode Statement;
non terminal Instruction Instruction;
non terminal Constant Constant;

These classes are part of so-called abstract syntax tree, and they wait for our implementation. Abstract Syntax Tree (or AST) is a "symbolic" representation of the parsed program source code. The parser creates one as a side-effect of parsing. It is different from Parse Syntax Tree (PST), which represents a tree of true grammar derivations which were "detected" by the parser for given source code of a program.

AST is something more artificial, ie. not all grammar rules need to be taken into account when representing the program. For this reason, we define only some "nodes" of the derivation tree. In our case, it is Program, representing the "root" of the tree, which has children - `Statement`s. Statements have `Instruction`s or `Constant`s as its children.

Do you remember those code snippets in the grammar specfile wrapped in {: …​ :} ? This code snippets create the AST, just follow them.

It’s now time to implement them. Since we know all nodes are just nodes of our AST, we should define common ASTnode interface first:

src/main/java/net/sf/emustudio/ssem/assembler/tree/ASTnode.java
package net.sf.emustudio.ssem.assembler.tree;

public interface ASTnode {

    void accept(ASTvisitor visitor) throws Exception;

}

This interface will be useful when we will traverse the tree. For tree traversal it is very well-suited the Visitor pattern. The idea of traversing a tree using visitor pattern is to have the "visitor" object - which represents an object which wants to go through all nodes of the tree and do something. The algorithm of visiting is based on a premise that each node of the AST implements the accept() method. That way, each node is responsible for calling the visitor for each its children and itself. So the effect is that the "visitor" will "get" the all tree node objects, when the accept() method is called on the root of the tree.

We can now define the visitor interface as follows:

src/main/java/net/sf/emustudio/ssem/assembler/tree/ASTvisitor.java
package net.sf.emustudio.ssem.assembler.tree;

public interface ASTvisitor {

    void setCurrentLine(int line);

    void visit(Instruction instruction) throws Exception;

    void visit(Constant constant) throws Exception;

}

The methods of the visitor will be implemented by some visitor, for example a code generator. However, we need to finish implementation of the AST first.

2.7.1. 'Program' node

The root node of the AST is the Program class. According to the grammar, it contains all the statements, which are either Instruction or Constant. Notice how we implemented traversing of the node:

src/main/java/net/sf/emustudio/ssem/assembler/tree/Program.java
package net.sf.emustudio.ssem.assembler.tree;

import java.util.HashMap;
import java.util.Map;

public class Program implements ASTnode {
    private final Map<Integer, ASTnode> nodes = new HashMap<>();
    private int startLine = 0;
    private int previousLine = 0;

    public void statement(int line, ASTnode node) {
        previousLine = line;
        nodes.put(line, node);
    }

    public void nextLineStarts() {
        this.startLine = previousLine;
    }

    public int getStartLine() {
        return startLine;
    }

    @Override
    public void accept(ASTvisitor visitor) throws Exception {
        for (Map.Entry<Integer, ASTnode> node : nodes.entrySet()) {
            visitor.setCurrentLine(node.getKey());
            node.getValue().accept(visitor);
        }
    }
}

The important note is that how the statements are stored. They are in fact the children of the program node. For this purpose a key-value map is used. Key has type Integer and it represents the line - or memory cell index, or address - on which the statement will be located. That way we can write several instructions which lie on the same line, e.g.:

01 LDN 15
01 STO 06

which will be translated into two statements, but the program node will contain just the last one. The reason is that they share the line - 01 - which is the key in the map of statements, so the first statement will be "overwritten" by the second one.

It is for a debate if we want this behavior to happen. For simplicity, we allow it. Otherwise we would throw some compiler exception.

2.7.2. 'Instruction' node

Instruction node represents the instruction. If you remember, each instruction except STP and CMP has a parameter, or better - operand - which is a "line" - index of a memory cell. It would be possible to represent specific instructions by separate classes, but since the required operations would be shared, it would be much easier to have just one class for all the instructions. Generally, instructions with same number and type of parameters are usually implemented in one AST node.

Here’s the source code:

src/main/java/net/sf/emustudio/ssem/assembler/tree/Instruction.java
package net.sf.emustudio.ssem.assembler.tree;

import net.sf.emustudio.ssem.assembler.CompileException;

import java.util.Optional;

public class Instruction implements ASTnode {
    public final static byte JMP = 0; // 000
    public final static byte JRP = 4; // 100
    public final static byte LDN = 2; // 010
    public final static byte STO = 6; // 110
    public final static byte SUB = 1; // 001
    public final static byte CMP = 3; // 011
    public final static byte STP = 7; // 111
    private final static String[] INSTRUCTION_STRING = new String[] {
        "JMP", "SUB", "LDN", "CMP", "JRP", null, "STO", "STP"
    };

    private final int opcode;
    private final Optional<Byte> operand;

    private Instruction(int opcode, int operand) throws CompileException {
        if (operand > 31 || operand < 0) {
            throw new CompileException("Instruction operand must be in range <0,31>!");
        }
        this.operand = Optional.of((byte)(operand & 0xFF));
        this.opcode = opcode;
    }

    private Instruction(int opcode) {
        this.operand = Optional.empty();
        this.opcode = opcode;
    }

    public int getOpcode() {
        return opcode;
    }

    public Optional<Byte> getOperand() {
        return operand;
    }

    public static Instruction jmp(int address) throws CompileException {
        return new Instruction(JMP, address);
    }

    public static Instruction jrp(int address) throws CompileException {
        return new Instruction(JRP, address);
    }

    public static Instruction ldn(int address) throws CompileException {
        return new Instruction(LDN, address);
    }

    public static Instruction sto(int address) throws CompileException {
        return new Instruction(STO, address);
    }

    public static Instruction sub(int address) throws CompileException {
        return new Instruction(SUB, address);
    }

    public static Instruction cmp() {
        return new Instruction(CMP);
    }

    public static Instruction stp() {
        return new Instruction(STP);
    }

    @Override
    public void accept(ASTvisitor visitor) throws Exception {
         visitor.visit(this);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Instruction that = (Instruction) o;
        return opcode == that.opcode && operand.equals(that.operand);
    }

    @Override
    public int hashCode() {
        int result = opcode;
        result = 31 * result + operand.hashCode();
        return result;
    }

    @Override
    public String toString() {
        return INSTRUCTION_STRING[opcode] + " " + operand;
    }
}

Note that the constructor is private. The implication is that it is just impossible to create some invalid Instruction object. The only possible way how to define it is using static factory methods, which represent the instructions themselves. These are called from the parser - check the grammar specfile in the section Syntax analyzer (parser).

Also, note that we can compare instructions based on opcode and operand. This is allowed by custom implementations of methods hashCode() and equals().

2.7.3. 'Constant' node

Another kind of statement is a constant. The constant is just a number, and the node class is very simple:

package net.sf.emustudio.ssem.assembler.tree;

public class Constant implements ASTnode {
    private final int number;

    public Constant(int number) {
        this.number = number;
    }

    @Override
    public void accept(ASTvisitor visitor) throws Exception {
        visitor.visit(this);
    }

    public int getNumber() {
        return number;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Constant constant = (Constant) o;

        return number == constant.number;
    }

    @Override
    public int hashCode() {
        return number;
    }
}

Comparing Constant instances is based on comparing the numbers they represent.

2.8. Testing

It is very good practice to write automated tests. These will give us some level of confidence that what we did so far is actually working. It is the earliest feedback we can get on our work, which consequently improves the speed of creating sofware which actually works.

A unit test is just a normal class which contains test methods. A test method generally creates the testing object, does the testing operation and finally check if the operation did what it should. Each test method should test just one thing and should be short and clear. It is good practice to name test method according to the test case, possibly resulting in a whole sentence, in camel case.

Java projects use some unit testing framework for that, e.g. JUnit or TestNG, which recognizes those classes automatically and runs the test methods during the compilation of the project. If a test fails, the whole compilation is stopped as failed.

For lexer and parser we create unit test classes, which will be placed here (following to Maven directory structure):

src/
  test/
    java/
      net/
        sf/
          emustudio/
            ssem/
              assembler/
                LexerTest.java
                ParserTest.java

The content of the test classes are as follows:

src/test/java/net/sf/emustudio/ssem/assembler/LexerTest.java
package net.sf.emustudio.ssem.assembler;

import emulib.plugins.compiler.Token;
import org.junit.Test;

import java.io.IOException;
import java.io.StringReader;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;

public class LexerTest {

    LexerImpl lexer(String tokens) {
        return new LexerImpl(new StringReader(tokens));
    }

    @Test
    public void testNumberUpperBoundary() throws Exception {
        LexerImpl lexer = lexer("31");

        TokenImpl token = lexer.next_token();
        assertEquals(Token.LITERAL, token.getType());
        assertEquals(TokenImpl.NUMBER, token.getID());
        assertEquals(31, token.value);
    }

    @Test
    public void testNumberLowerBoundary() throws Exception {
        LexerImpl lexer = lexer("0");

        TokenImpl token = lexer.next_token();
        assertEquals(Token.LITERAL, token.getType());
        assertEquals(TokenImpl.NUMBER, token.getID());
        assertEquals(0, token.value);
    }

    @Test
    public void testNumber() throws Exception {
        LexerImpl lexer = lexer("22");

        TokenImpl token = lexer.next_token();
        assertEquals(Token.LITERAL, token.getType());
        assertEquals(TokenImpl.NUMBER, token.getID());
        assertEquals(22, token.value);
    }

    private void checkInstruction(int id, LexerImpl lexer) throws IOException {
        TokenImpl token = lexer.next_token();
        assertEquals(Token.RESERVED, token.getType());
        assertEquals(id, token.getID());
    }

    private void checkInstructionWithOperand(int id, LexerImpl lexer) throws IOException {
        checkInstruction(id, lexer);

        TokenImpl token = lexer.next_token();
        assertEquals(Token.LITERAL, token.getType());
        assertEquals(TokenImpl.NUMBER, token.getID());
    }

    @Test
    public void testInstructionsWithOperand() throws Exception {
        checkInstructionWithOperand(TokenImpl.JMP, lexer("jmp 12"));
        checkInstructionWithOperand(TokenImpl.JPR, lexer("jrp 12"));
        checkInstructionWithOperand(TokenImpl.JPR, lexer("jpr 12"));
        checkInstructionWithOperand(TokenImpl.JPR, lexer("jmr 12"));
        checkInstructionWithOperand(TokenImpl.LDN, lexer("ldn 12"));
        checkInstructionWithOperand(TokenImpl.STO, lexer("sto 12"));
        checkInstructionWithOperand(TokenImpl.SUB, lexer("sub 12"));
    }

    @Test
    public void testInstructionsWithoutOperand() throws Exception {
        checkInstruction(TokenImpl.CMP, lexer("cmp"));
        checkInstruction(TokenImpl.CMP, lexer("skn"));
        checkInstruction(TokenImpl.STP, lexer("stp"));
    }

    @Test
    public void testInstructionInComment() throws Exception {
        LexerImpl lexer = lexer("// cmp");
        TokenImpl token = lexer.next_token();

        assertEquals(TokenImpl.TCOMMENT, token.getID());
        assertEquals(Token.COMMENT, token.getType());

        token = lexer.next_token();
        assertEquals(Token.TEOF, token.getType());
        assertEquals(TokenImpl.EOF, token.getID());
    }

    @Test
    public void testBinaryNumber() throws Exception {
        LexerImpl lexer = lexer("BNUM 10011011111000101111110000111111\n");

        TokenImpl token = lexer.next_token();
        assertEquals(Token.PREPROCESSOR, token.getType());
        assertEquals(TokenImpl.BNUM, token.getID());
        assertFalse(token.isInitialLexicalState());

        token = lexer.next_token();
        assertEquals(Token.LITERAL, token.getType());
        assertEquals(TokenImpl.NUMBER, token.getID());
    }
}
src/test/java/net/sf/emustudio/ssem/assembler/ParserTest.java
package net.sf.emustudio.ssem.assembler;

import java_cup.runtime.ComplexSymbolFactory;
import net.sf.emustudio.ssem.assembler.tree.ASTvisitor;
import net.sf.emustudio.ssem.assembler.tree.Constant;
import net.sf.emustudio.ssem.assembler.tree.Instruction;
import net.sf.emustudio.ssem.assembler.tree.Program;
import org.junit.Test;

import java.io.StringReader;
import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedList;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

public class ParserTest {

    private ParserImpl program(String program) {
        return new ParserImpl(new LexerImpl(new StringReader(program)), new ComplexSymbolFactory());
    }

    @Test
    public void testInstructions() throws Exception {
        ParserImpl parser = program(
            "0 cmp // comment\n" +
            "1 stp\n" +
            "2 jmp 22\n" +
            "3 jrp 0\n" +
            "4 ldn 31\n" +
            "5 sto 10\n" +
            "6 sub 15\n"
        );

        Program program = (Program) parser.parse().value;
        assertFalse(parser.hasSyntaxErrors());

        Deque<Instruction> expectedInstructions = new LinkedList<>(Arrays.asList(
            Instruction.cmp(),
            Instruction.stp(),
            Instruction.jmp((byte)22),
            Instruction.jrp((byte)0),
            Instruction.ldn((byte)31),
            Instruction.sto((byte)10),
            Instruction.sub((byte)15)
        ));
        program.accept(new ASTvisitor() {

            @Override
            public void setCurrentLine(int line) {

            }

            @Override
            public void visit(Instruction instruction) throws Exception {
                assertEquals(expectedInstructions.removeFirst(), instruction);
            }

            @Override
            public void visit(Constant constant) throws Exception {
                fail("Didn't expect a constant");
            }
        });
    }


    @Test(expected = Exception.class)
    public void testInstructionWithoutEOL() throws Exception {
        ParserImpl parser = program("0 jmp 1");

        parser.parse();
    }

    @Test
    public void testInstructionWithoutProperArgument() throws Exception {
        ParserImpl parser = program("0 jmp ffff\n");

        parser.parse();
        assertTrue(parser.hasSyntaxErrors());
    }

    @Test
    public void testConstantIsTranslatedCorrectly() throws Exception {
        ParserImpl parser = program(
            "0 NUM 5\n"
        );

        Program program = (Program) parser.parse().value;

        assertFalse(parser.hasSyntaxErrors());
        assertConstant(program, 5);
    }

    @Test
    public void testHexadecimalConstant() throws Exception {
        ParserImpl parser = program(
            "0 NUM -0x20\n"
        );

        Program program = (Program) parser.parse().value;
        assertFalse(parser.hasSyntaxErrors());

        assertConstant(program, -32);
    }

    @Test
    public void testStartingPointIsAccepted() throws Exception {
        ParserImpl parser = program("0 jmp 1\nstart:\n3 cmp\n");

        Program program = (Program) parser.parse().value;
        assertFalse(parser.hasSyntaxErrors());
        assertEquals(3, program.getStartLine());
    }

    @Test
    public void testIndexOfLineThenCommentWorks() throws Exception {
        ParserImpl parser = program("0 --comment\n");

        Program program = (Program) parser.parse().value;
        assertFalse(parser.hasSyntaxErrors());
    }

    private void assertConstant(Program program, int value) throws Exception {
        program.accept(new ASTvisitor() {

            @Override
            public void setCurrentLine(int line) {

            }

            @Override
            public void visit(Instruction instruction) throws Exception {
                fail("Didn't expect an instruction");
            }

            @Override
            public void visit(Constant constant) throws Exception {
                assertEquals(new Constant(value), constant);
            }
        });
    }
}
The tests of parser are based on comparing Instruction and Constant instances with JUnit’s assertEquals() method. This is possible only because of overriden equals() and hashCode() methods in the classes, since they are used directly by Java when it is comparing the instances.

2.9. The main class

The time has come for implementing the main plug-in class. It will be placed in a package net.sf.emustudio.ssem.assembler, and the class will be named CompilerImpl.

There are several requirements (behavioral contracts) put on the compiler main class:

  • It must implement emulib.plugins.compiler.Compiler interface. There already exists helper abstract class called emulib.plugins.compiler.AbstractCompiler, which implements some fundamental and general methods. We will use that class.

  • It must be annotated with emulib.annotations.PluginType annotation.

  • The constructor gets two arguments: unique plugin ID (Long) and emulib.runtime.ContextPool object. Both values are created by emuStudio, and we will talk about them later.

Now, the "skeleton" of the class follows:

src/main/java/net/sf/emustudio/ssem/assembler/CompilerImpl.java
package net.sf.emustudio.ssem.assembler;

import emulib.annotations.PLUGIN_TYPE;
import emulib.annotations.PluginType;
import emulib.plugins.compiler.AbstractCompiler;
import emulib.plugins.compiler.LexicalAnalyzer;
import emulib.plugins.compiler.SourceFileExtension;
import emulib.runtime.ContextPool;

import java.io.IOException;
import java.io.Reader;
import java.util.Objects;

@PluginType(
    type = PLUGIN_TYPE.COMPILER,
    title = "SSEM Assembler",
    copyright = "\u00A9 Copyright 2016, YourName",
    description = "Assembler of SSEM processor language"
)
public class CompilerImpl extends AbstractCompiler {
    private static final SourceFileExtension[] SOURCE_FILE_EXTENSIONS = new SourceFileExtension[]{
        new SourceFileExtension("ssem", "SSEM source file")
    };
    private final ContextPool contextPool;

    public CompilerImpl(Long pluginID, ContextPool contextPool) {
        super(pluginID);
        this.contextPool = Objects.requireNonNull(contextPool);
    }

    @Override
    public boolean compile(String inputFileName, String outputFileName) {
        // TODO !!
        return false;
    }

    @Override
    public boolean compile(String inputFileName) {
        String outputFileName = Objects.requireNonNull(inputFileName);
        SourceFileExtension srcExtension = SOURCE_FILE_EXTENSIONS[0];

        int i = inputFileName.lastIndexOf("." + srcExtension.getExtension());
        if (i >= 0) {
            outputFileName = outputFileName.substring(0, i);
        }
        return compile(inputFileName, outputFileName + ".bin");
    }

    @Override
    public LexicalAnalyzer getLexer(Reader reader) {
        return new LexerImpl(reader);
    }

    @Override
    public SourceFileExtension[] getSourceSuffixList() {
        return SOURCE_FILE_EXTENSIONS;
    }

    @Override
    public void destroy() {

    }

    @Override
    public void showSettings() {

    }

    @Override
    public boolean isShowSettingsSupported() {
        return false;
    }

    @Override
    public String getVersion() {
        return "1.0";
    }
}

Some things are obvious, some maybe not. For example, method getLexer() is called by emuStudio for the syntax highlighter. It is very straightforward - just return new LexerImpl() which was generated by JFlex from our specfile.

Method compile(String) might seem complex at first look. It is "ugly" Java code which tries to check if the given file name ends with our supported file suffix, which is ".ssem". We chose it as the suffix of SSEM source code file. We could chose ".asm" or other extension as well. Then, the "real" compile() method is called with the input file and the output file name, which has suffix ".bin".

Method getSourceSuffixList() returns all supported extensions, and will be used in the file filter in the open file dialog shown in emuStudio.

Compiler can have it’s own settings dialog (GUI window) which can be shown. This is reflected by the methods isShowSettingsSupported() and showSettings(). Our assembler will not support the dialog.

The "real" compile() method is left to be done in the last section.

2.10. Generating code

So far, we have implemented a parser which creates our AST. Next operations which compilers do are semantic analysis, optimization and code generation. These three phases are performed on the AST. The algorithms traverse the tree and update some own internal state, or state of the AST based on visited nodes. Code generator at the end take the results, and again - by traversing AST - generates the code.

In our case, we don’t need any semantic analysis, like type-checks or so, because we have simple machine instructions, which do not require it. We could optimize them, but for the simplicity we will omit this step as well. We will rather focus on the final step - code generation.

You might remember the section Introducing AST, which talked about AST traversal. We already have ASTnode and ASTvisitor interfaces. The traversal is already implemented, according to the Visitor pattern, by the nodes themselves. One thing which is left to do is to implement the visitor itself - the code generator class.

For each encountered instruction, the code generator will generate a binary code. Our code generator will write the binary representation into a file. However, it is generally better if I/O classes work with I/O abstractions (streams, channels, etc.) rather than specific objects, e.g. files. Out code generator will be implemented in a similar fashion. The code is as follows:

src/main/java/net/sf/emustudio/ssem/assembler/CodeGenerator.java
package net.sf.emustudio.ssem.assembler;

import emulib.runtime.NumberUtils;
import emulib.runtime.NumberUtils.Strategy;
import java.io.IOException;
import java.util.Objects;
import net.sf.emustudio.ssem.assembler.tree.ASTvisitor;
import net.sf.emustudio.ssem.assembler.tree.Constant;
import net.sf.emustudio.ssem.assembler.tree.Instruction;

public class CodeGenerator implements ASTvisitor, AutoCloseable {
    private final SeekableOutputStream writer;
    private int currentLine;

    public CodeGenerator(SeekableOutputStream writer) {
        this.writer= Objects.requireNonNull(writer);
    }

    @Override
    public void setCurrentLine(int line) {
        this.currentLine = line;
    }

    @Override
    public void visit(Instruction instruction) throws CompileException, IOException {
        byte address = instruction.getOperand().orElse((byte)0);

        if (address < 0 || address > 31) {
            throw new CompileException("Operand must be between <0, 31>; it was " + address);
        }

        // Instruction has 32 bits, i.e. 4 bytes
        int addressSSEM = NumberUtils.reverseBits(address, 8) & 0xF8;
        writer.seek(4 * currentLine);

        writer.write(addressSSEM); // address + 3 empty bits

        // next: 5 empty bits + 3 bit instruction
        int opcode = instruction.getOpcode() & 7;
        writer.write(opcode);

        // 16 empty bits
        writer.write(new byte[2]);
    }

    @Override
    public void visit(Constant constant) throws Exception {
        int number = constant.getNumber();

        writer.seek(4 * currentLine);
        writeInt(number);
    }

    private void writeInt(int value) throws IOException {
        Byte[] word = new Byte[4];
        NumberUtils.writeInt(value, word, Strategy.REVERSE_BITS);

        writer.write(word[0]);
        writer.write(word[1]);
        writer.write(word[2]);
        writer.write(word[3]);
    }

    @Override
    public void close() throws Exception {
        writer.close();
    }
}

There are several things to notice:

  • Output code is written into class SeekableOutputStream. Wi will define this class later.

  • CodeGenerator is a visitor, which has separate methods for code generation of Constant and Instruction.

  • CodeGenerator can be "closed" - so it handles closing of the writer (our SeekableOutputStream).

  • Generated code is binary

The code generation is not that complex. There is some complexity caused by a fact, that the binary representations are reversed, when comparing to our present-time PCs/laptops.

The idea of generating code for instruction is to prepare 4 bytes, 32 bits, which are then written into the "writer" as one Integer, which has also 32 bits. The instruction format is explained in section Assembler for SSEM. First 5 bits from the left represent the "line" - instruction operand. It must be reversed. Then, bits 13,14,15 represent the instruction opcode. It does not have to be reversed here, since the instructions are already encoded properly in the Instruction class. Last two bytes are just empty.

Code generation for constant is much easier - it’s just retrieving the value and writing it in the reversed fashion as Integer.

Code generator uses a "seekable" output stream, which allows to seek in the output. It is a separate class, which is actually an abstract class, extending OutputStream. The reason is easier testing:

src/main/java/net/sf/emustudio/ssem/assembler/SeekableOutputStream.java
package net.sf.emustudio.ssem.assembler;

import java.io.IOException;
import java.io.OutputStream;

public abstract class SeekableOutputStream extends OutputStream {

    public abstract void seek(int position) throws IOException;

}

and here’s the implementation:

src/main/java/net/sf/emustudio/ssem/assembler/MemoryAndFileOutput.java
package net.sf.emustudio.ssem.assembler;

import emulib.plugins.memory.MemoryContext;
import net.jcip.annotations.NotThreadSafe;

import java.io.IOException;
import java.io.RandomAccessFile;

@NotThreadSafe
public class MemoryAndFileOutput extends SeekableOutputStream {
    private final RandomAccessFile file;
    private final MemoryContext<Byte> memoryContext;
    private int position = 0;

    public MemoryAndFileOutput(String filename, MemoryContext<Byte> memoryContext) throws IOException {
        this.file = new RandomAccessFile(filename, "rw");
        this.memoryContext = memoryContext;
    }

    @Override
    public void write(int b) throws IOException {
        if (memoryContext != null) {
            memoryContext.write(position, (byte) (b & 0xFF));
        }
        file.write(b);
        position++;
    }

    @Override
    public void seek(int position) throws IOException {
        this.position = position;
        file.seek(position);
    }

    @Override
    public void close() throws IOException {
        try {
            file.close();
        } finally {
            super.close();
        }
    }
}

The "seeking" capability is required, because, as you remember, it’s possible to write something like this:

06 STO 05
05 LDN 06
07 STP

It’s not quite common, but it’s possible.

2.10.1. Testing

As being our practice, we must test it.

src/test/java/net/sf/emustudio/ssem/assembler/CodeGeneratorTest.java
package net.sf.emustudio.ssem.assembler;

import net.sf.emustudio.ssem.assembler.tree.Instruction;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;

import static org.junit.Assert.assertArrayEquals;

public class CodeGeneratorTest {

    private SeekableByteArrayOutputStream out;
    private CodeGenerator codeGenerator;

    @Before
    public void setUp() throws Exception {
        out = new SeekableByteArrayOutputStream(32);
        codeGenerator = new CodeGenerator(out);
    }

    @After
    public void tearDown() throws Exception {
        codeGenerator.close();
    }

    @Test
    public void testCMP() throws Exception {
        codeGenerator.visit(Instruction.cmp());

        assertArrayEquals(new byte[] {0,Instruction.CMP,0,0}, out.toArray());
    }

    @Test
    public void testSTP() throws Exception {
        codeGenerator.visit(Instruction.stp());

        assertArrayEquals(new byte[] {0,Instruction.STP,0,0}, out.toArray());
    }

    @Test
    public void testJMP() throws Exception {
        codeGenerator.visit(Instruction.jmp((byte)6));

        assertArrayEquals(new byte[] {0x60,Instruction.JMP,0,0}, out.toArray());
    }

    @Test
    public void testJRP() throws Exception {
        codeGenerator.visit(Instruction.jrp((byte)23));

        assertArrayEquals(new byte[] {(byte)0xE8,Instruction.JRP,0,0}, out.toArray());
    }

    @Test
    public void testLDN() throws Exception {
        codeGenerator.visit(Instruction.ldn((byte)12));

        assertArrayEquals(new byte[] {(byte)0x30,Instruction.LDN,0,0}, out.toArray());
    }

    @Test
    public void testSTO() throws Exception {
        codeGenerator.visit(Instruction.sto((byte)30));

        assertArrayEquals(new byte[] {(byte)0x78,Instruction.STO,0,0}, out.toArray());
    }

    @Test
    public void testSUB() throws Exception {
        codeGenerator.visit(Instruction.sub((byte)18));

        assertArrayEquals(new byte[] {(byte)0x48,Instruction.SUB,0,0}, out.toArray());
    }

    private static class SeekableByteArrayOutputStream extends SeekableOutputStream {
        private final byte[] array;
        private int pos;
        private int length;

        public SeekableByteArrayOutputStream(int count) {
            this.array = new byte[count];
        }

        @Override
        public void seek(int position) throws IOException {
            length = Math.max(position, pos);
            pos = position;
        }

        @Override
        public void write(int b) throws IOException {
            array[pos] = (byte)b;
            pos++;
            length = Math.max(pos, length);
        }

        public byte[] toArray() {
            byte[] tmp = new byte[length];
            System.arraycopy(array, 0, tmp, 0, length);
            return tmp;
        }
    }
}

2.11. Finalizing

We’re almost done now! What is missing so far is to finish implementation of the main CompilerImpl.compile() method. Let’s begin with it first.

src/main/java/net/sf/emustudio/ssem/assembler/CompilerImpl.java
public class CompilerImpl extends AbstractCompiler {

    ...

    @Override
    public boolean compile(String inputFileName, String outputFileName) {
        notifyCompileStart();

        int errorCode = 0;
        try (Reader reader = new FileReader(inputFileName)) {
            MemoryContext<Byte> memory = contextPool.getMemoryContext(pluginID, MemoryContext.class);

            try (CodeGenerator codeGenerator = new CodeGenerator(new MemoryAndFileOutput(outputFileName, memory))) {
                LexerImpl lexer = new LexerImpl(reader);
                ParserImpl parser = new ParserImpl(lexer, new ComplexSymbolFactory(), this);

                Program program = (Program) parser.parse().value;
                if (program == null) {
                    throw new Exception("Unexpected end of file");
                }
                if (parser.hasSyntaxErrors()) {
                    throw new Exception("One or more errors has been found, cannot continue.");
                }

                program.accept(codeGenerator);
                programStart = program.getStartLine() * 4;
                notifyInfo("Compile was successful. Output: " + outputFileName);
            }
        } catch (Exception e) {
            errorCode = 1;
            LOGGER.error("Compilation error.", e);
            notifyError("Compilation error.");

            return false;
        } finally {
            notifyCompileFinish(errorCode);
        }

        return true;
    }

    ...
}

As input, we have full path to the input file, and to the output file. It is good to use Java try-with-resources statement for opening the files. The same approach can be used for the code generator, because the class implements AutoCloseable interface.

We want to notify emuStudio about compilation progress, as we have already done in the parser, when dealing with parsing errors. For this purpose, emulib.plugins.compiler.AbstractCompiler class offers several methods:

  • notifyCompileStart(), which will inform emuStudio that the compilation has started,

  • notifyCompileFinish(errorCode) will inform emuStudio that the compilation has finished, with given error code. [2]

  • notifyOnMessage() - notifies emuStudio about some general message, it can be either error, info, warning.

  • notifyWarning() - compiler warning

  • notifyError() - compilation error

  • notifyInfo() - informational message

2.12. Command-line interface (CLI)

It might be sometimes useful to being able to run the compiler from the command line. We can add the implementation in the Main class, as follows:

src/main/java/net/sf/emustudio/ssem/assembler/Main.java
package net.sf.emustudio.ssem.assembler;

import emulib.plugins.compiler.Compiler;
import emulib.plugins.compiler.Message;
import emulib.runtime.ContextPool;

public class Main {

    public static void main(String... args) {
        String inputFile;
        String outputFile = null;

        int i;
        for (i = 0; i < args.length; i++) {
            String arg = args[i].toLowerCase();
            if ((arg.equals("--output") || arg.equals("-o")) && ((i + 1) < args.length)) {
                outputFile = args[++i];
            } else if (arg.equals("--help") || arg.equals("-h")) {
                printHelp();
                return;
            } else if (arg.equals("--version") || arg.equals("-v")) {
                System.out.println(new CompilerImpl(0L, new ContextPool()).getVersion());
                return;
            } else {
                break;
            }
        }
        if (i >= args.length) {
            System.err.println("Error: expected input file name");
            return;
        }
        inputFile = args[i];
        if (outputFile == null) {
            int index = inputFile.lastIndexOf('.');
            if (index != -1) {
                outputFile = inputFile.substring(0, index);
            } else {
                outputFile = inputFile;
            }
            outputFile += ".bin";
        }

        CompilerImpl compiler = new CompilerImpl(0L, new ContextPool());
        compiler.addCompilerListener(new Compiler.CompilerListener() {
            @Override
            public void onStart() {
            }

            @Override
            public void onMessage(Message message) {
                System.err.println(message.toString());
            }

            @Override
            public void onFinish(int errorCode) {
                System.err.println("Compilation finished (error code: " + errorCode + ")");

            }
        });
        try {
            compiler.compile(inputFile, outputFile);
        } catch (Exception e) {
            System.err.println(e.getMessage());
        }
    }

    private static void printHelp() {
        System.out.println("Syntax: java -jar as-ssem.jar [-o outputFile] inputFile\nOptions:");
        System.out.println("\t--output, -o\tfile: name of the output file");
        System.out.println("\t--version, -v\t: print version");
        System.out.println("\t--help, -h\t: this help");
    }

}

And that was the very last thing, now you have SSEM compiler ready :)

3. Writing a memory

This tutorial will describe some basic knowledge about how to create an operating memory to be used in emuStudio. In emuStudio, virtual computers usually conform to von-Neumann architecture. In this architecture, the memory is a separate component. The tutorial focuses on how to use emuLib API in a memory for SSEM computer.

3.1. Getting started

Before reading on, please read the Introduction chapter. It gives the information needed for setting up the development environment and for basic understanding how the emuStudio/plug-ins lifecycle work.

Within this tutorial, we will implement a main store of SSEM "Baby" machine, to conform with other tutorials for other plug-ins. Before we start, here’s a few words about what purpose and capabilities memories in emuStudio can have.

In first stored-program computers, operating memory was accessible only by CPU. It means that all data between the memory and other devices had to be transferred through CPU. When data from memory was required, CPU "paused" the execution of instructions and accessed the memory in the meantime. This slowed down CPU and consequently the whole program. Later, DMA (direct memory access) capability was introduced, which allowed devices to directly access the memory, or be notified when the memory changes.

In emuStudio, you can connect the memory with devices, or even with a compiler. This is one example of the versatility and power of emuStudio.

There are three main behavioral contracts which need to be taken into account, when creating memory plug-in:

  • Memory can be connected with none, one or more plug-ins of any type

  • Memory plug-in is necessary for creating a virtual machine (emuStudio requires it)

  • Operations of memory can be extended using special class called "context"

The implication is that the memory can be shared across CPU and devices, and the communication can be optimized with custom operations. The context class is a customization which extends default context behavior. This tutorial will cover some basics in this topic.

3.1.1. SSEM memory

SSEM used the world’s first random-access memory called Williams or Williams-Kilburn tube [3]. The used principle was the same as in standard Cathode-Ray-Tubes (CRTs). Original EDSAC computer (which introduced the von Neumann architecture) did not have random-access memory.

The memory had 32 memory cells (called words), each had size of 32 bits. The memory could contain instructions and data. So, one SSEM instruction perfectly fits in the single memory word.

Within this tutorial, we will hardcode the word size and memory size. But we’ll also implement a GUI for the memory, which is often far more complicated than the emulated memory itself [4]

3.2. Preparing the environment

In order to start developing the memory, create new Java project. Here, Maven will be used for dependencies management. The plug-in will be implemented as another standard emuStudio plug-in, so it will inherit Maven plug-in dependencies from the main POM file.

The project should be located at emuStudio/plugins/mem/ssem-mem, and should contain the following structure:

src/
  main/
    java/
    resources/
test/
  java/
pom.xml
Note the naming of the plug-in. It follows the naming convention as described in the Naming conventions guide.

The POM file of the project might look as follows:

ssem-mem/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>net.sf.emustudio</groupId>
    <artifactId>emustudio-parent</artifactId>
    <version>0.39</version>
    <relativePath>../../..</relativePath>
  </parent>

  <artifactId>ssem-mem</artifactId>
  <packaging>jar</packaging>

  <name>SSEM Memory</name>
  <description>Operating memory (main store) for the SSEM computer</description>

  <build>
    <finalName>ssem-mem</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <configuration>
          <archive>
            <manifest>
              <addClasspath>false</addClasspath>
              <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
              <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
            </manifest>
          </archive>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-dependency-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

  <dependencies>
    <dependency>
      <groupId>net.sf.emustudio</groupId>
      <artifactId>emuLib</artifactId>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
    </dependency>
    <dependency>
      <groupId>org.easymock</groupId>
      <artifactId>easymock</artifactId>
    </dependency>
  </dependencies>
</project>

And let’s start with the first Java class, the main plug-in class. Let’s put it to package net.sf.emustudio.ssem.memory, and call it MemoryImpl.

3.3. The main class

Go to the MemoryImpl class source. Extend the class from emulib.plugins.memory.AbstractMemory class. The class extends from Memory interface and implements the most common methods, usable by all memories.

It is also necessary to annotate the class with emulib.annotations.PluginType annotation, which is required for every main class of any emuStudio plug-in. The code snippet looks as follows:

src/main/java/net/sf/emustudio/ssem/memory/MemoryImpl.java
package net.sf.emustudio.ssem.memory;

import emulib.annotations.PLUGIN_TYPE;
import emulib.annotations.PluginType;
import emulib.plugins.memory.AbstractMemory;
import emulib.runtime.ContextPool;

@PluginType(
        type = PLUGIN_TYPE.MEMORY,
        title = "SSEM memory",
        copyright = "\u00A9 Copyright 2016, Peter Jakubčo",
        description = "Main store for SSEM machine"
)
public class MemoryImpl extends AbstractMemory {
    private final static Logger LOGGER = LoggerFactory.getLogger(MemoryImpl.class);

    public MemoryImpl(Long pluginID, ContextPool contextPool) {
        super(pluginID);
    }

    // ... other methods ...
}
The constructor presented here is mandatory. This is one of the behavioral contracts, emuStudio expects that a plug-in will have a constructor with two arguments: pluginID (assigned by emuStudio), and a context pool, which is a storage or registrar of all plug-ins contexts.

3.4. Few notes on SSEM memory

When thinking about computer memory - what is it? During time, memory abstraction has evolved to an idea of memory as a collection of cells, equally-sized, which are ordered sequentially. This is the simplest description, and I would say that more-less all memories look like this. When emulating a computer memory in a programming language like Java, it would be then just a plain array.

SSEM had 32 so-called "lines", which represented cells in memory. Each line, or a cell, was 4 bytes long. It is therefore tempting to implement SSEM memory as array of integers, because int type has 4 bytes.

It is indeed possible, but we will face to an unsolvable problem when implementing CPU, if we want to use Edigen [5]. Unfortunately, Edigen is so-far designed in a way that it expects the memory having cell size of a byte, not more. Therefore, we will keep that also for SSEM memory emulation, but visually try to present it as having 4-byte cells. It would require additional mathematics when working with memory (in the CPU tutorial you would find it as well), but not having impact on performance.

3.5. Memory context

Abstracts are helping us to understand ideas and see how they can be composed into a whole. In this fashion, memory main interface, presented in section The main class, is the communication point with emuStudio. The main module is using methods in this interface. The communication with CPU and devices is done through another concept, which is called a context. The contexts differ for various plug-ins; and they can be customized.

Plug-ins which provide a context, must register it to the ContextPool, given in a constructor in the main plugin class. This registration should be performed as early as possible - in the constructor itself. After all plug-ins are instantiated, emuStudio expects that all contexts are already registered.

The next step, generally true for all plug-ins, is calling of Plugin.initialize() method, in this case - Memory.initialize(). The initialization can now use the context pool for different purpose - for obtaining contexts, if it requires some. More on this topic can be found in the Emulation lifecycle section.

So, in our case - we must create a memory context, which will be used by SSEM CPU and SSEM CRT display. We don’t need special customized context, so we can comfortably extend from class AbstractMemoryContext, which will implement some methods of the MemoryContext interface for us. We will call the class MemoryContextImpl:

src/main/java/net/sf/emustudio/ssem/memory/impl/MemoryContextImpl.java
package net.sf.emustudio.ssem.memory.impl;

import emulib.plugins.memory.AbstractMemoryContext;
import net.jcip.annotations.ThreadSafe;

import java.util.Arrays;

@ThreadSafe
public class MemoryContextImpl extends AbstractMemoryContext<Byte> {
    static final int NUMBER_OF_CELLS = 32 * 4;

    // byte type is atomic in JVM memory model
    private final byte[]memory = new byte[NUMBER_OF_CELLS];

    @Override
    public void clear() {
        Arrays.fill(memory, (byte)0);
        notifyMemoryChanged(-1); // notify that all memory has changed
    }

    @Override
    public Class<?> getDataType() {
        return Byte.class;
    }

    @Override
    public Byte read(int from) {
        return memory[from];
    }

    @Override
    public Byte[] readWord(int from) {
        return new Byte[] { memory[from], memory[from+1], memory[from+2], memory[from+3] };
    }

    @Override
    public void write(int to, Byte val) {
        memory[to] = val;
        notifyMemoryChanged(to);
    }

    @Override
    public void writeWord(int to, Byte[] cells) {
        int i = 0;
        for (byte cell : cells) {
            memory[to + i] = cell;
            notifyMemoryChanged(to+i);
            i++;
        }
    }

    @Override
    public int getSize() {
        return memory.length;
    }
}

As you can see, SSEM memory is indeed an array of bytes. This is the "core" idea of a memory. Ofcourse, it is possible to use a java.util.List or another collection, but we should keep eye on performance. Array is the simplest structure with access time O(1). Therefore, we chose array.

Also notice that the class AbstractMemoryContext, and also interface MemoryContext take a generic parameter T extends Number. This parameter is saying of what type cell is. In our case, T is a Byte. Due unhappy Java limitation, primitive types cannot be used in generics, so we can’t have something like MemoryContext<byte>.

3.6. GUI

Since emuStudio is interactive application, GUIs are a natural thing. Each memory plug-in should have its own GUI. The supported features can be any, but keep in mind, that GUI control in Swing is done in separate thread, often called "UI Thread". On the other hand, emulation itself is running in different, dedicated, thread, created in emuLib.

This means that the access to memory context should be synchronized. However, synchronization is very slow. Much better approach is to use a non-blocking algorithm for locking, if we really require absolutely reliable manipulation of memory cells in between threads. However, non-blocking algorithms are harder to implement good. In our tutorial we will do a trade-off, which we can afford - since we have final array of bytes, we have the following guarantees:

  1. the array itself will be always valid and visible to all accessing threads. Meaning - reading is always safe.

  2. we expect our host CPU can write a byte at once; therefore it is atomic. This does not have to hold for all CPUs - don’t worry, all x86 CPUs have it.

  3. According to Java Language Specification, Chapter 17.6:

    two threads that update adjacent elements of a byte array separately must not interfere or interact and do not need
    synchronization to ensure sequential consistency.

I consider these guarantees as good enough to leave the synchronization be. I guess the probability of modifying the same memory cells from both running emulation and by the user in GUI, is very small. What’s more, you shouldn’t modify memory cells at all when the emulation is running.

Now back to our GUI. It would be good if the GUI is looking good, so it’s up to you how you’ll draw the main form. It can be a class extending from a javax.swing.JFrame or javax.swing.JDialog. The look might be as follows:

SSEM Memory GUI sample look

So, we will need a custom memory model, a custom memory table - which will have a row header (the very first, gray-colored column) and column header (the very first, gray-colored row).

As you can see in the picture, a row represents single SSEM memory cell - 32 scattered bits, and the last few columns show both the number the bits represent, and a raw ASCII value of the 4-byte sequence of data.

Also, we would like to let user edit the cells manually - just by pointing to a bit, and pressing either 1 or 0 - possibly a DELETE key, committing the change immediately. We want to allow editing also for the value itself, and for the data column as well.

In addition, movement around cells should be possible with arrow keys.

For those "specifications", we will need to customize standard javax.swing.JTable, create custom memory model, cell editor, cell renderer and introduce row header renderer.

The source code of the main GUI class is here:

src/main/java/net/sf/emustudio/ssem/memory/gui/MemoryGUI.java
package net.sf.emustudio.ssem.memory.gui;

import emulib.plugins.memory.Memory;
import emulib.plugins.memory.MemoryContext;
import javax.swing.JDialog;

public class MemoryGUI extends JDialog {
    private final MemoryTableModel tableModel;

    private class MemoryListenerImpl implements Memory.MemoryListener {

        @Override
        public void memoryChanged(int memoryPosition) {
            tableModel.dataChangedAt(memoryPosition);
        }

        @Override
        public void memorySizeChanged() {
            tableModel.fireTableDataChanged();
        }
    }

    public MemoryGUI(MemoryContext<Byte> memory) {
        initComponents();
        super.setLocationRelativeTo(null);

        this.tableModel = new MemoryTableModel(memory);
        MemoryTable memoryTable = new MemoryTable(tableModel, scrollPane);
        memoryTable.setup();
        scrollPane.setViewportView(memoryTable);

        memory.addMemoryListener(new MemoryListenerImpl());
    }

    /**
     * This method is called from within the constructor to initialize the form. WARNING: Do NOT modify this code. The
     * content of this method is always regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        scrollPane = new javax.swing.JScrollPane();
        javax.swing.JToolBar jToolBar1 = new javax.swing.JToolBar();
        javax.swing.JButton btnClear = new javax.swing.JButton();

        setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
        setTitle("SSEM Memory (Williams-Killburn Tube)");

        jToolBar1.setFloatable(false);
        jToolBar1.setRollover(true);

        btnClear.setIcon(new javax.swing.ImageIcon(getClass().getResource("/net/sf/emustudio/ssem/memory/gui/clear.png"))); // NOI18N
        btnClear.setFocusable(false);
        btnClear.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
        btnClear.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
        btnClear.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                btnClearActionPerformed(evt);
            }
        });
        jToolBar1.add(btnClear);

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addComponent(scrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 965, Short.MAX_VALUE)
            .addComponent(jToolBar1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                .addComponent(jToolBar1, javax.swing.GroupLayout.PREFERRED_SIZE, 32, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(scrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 455, Short.MAX_VALUE))
        );

        pack();
    }// </editor-fold>//GEN-END:initComponents

    private void btnClearActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnClearActionPerformed
        tableModel.clear();
    }//GEN-LAST:event_btnClearActionPerformed

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JScrollPane scrollPane;
    // End of variables declaration//GEN-END:variables
}

Note that in the constructor we create a memory listener, which implements Memory.MemoryListener interface. The listener will receive events about external memory changes - in our case, from CPU emulator. Our reaction is - as supposed to be - reflect the change in the memory GUI.

The caller thread of listener methods is the one who calls Memory.write() on the other end. In our case, it can be either the emulator dedicated thread, as described above, or the user itself, doing changes in the UI thread.

Also, we use custom memory table, which extends from javax.swing.table.JTable. We will describe it now.

3.6.1. Memory table

Since we want "special" behavior, like custom cell renderer, custom row header, custom cell editor and custom widths of some columns, it will be good to wrap it up in a custom JTable. The code looks as follows:

src/main/java/net/sf/emustudio/ssem/memory/gui/MemoryTable.java
package net.sf.emustudio.ssem.memory.gui;

import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.Objects;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.table.TableColumn;
import static net.sf.emustudio.ssem.memory.gui.Constants.CHAR_HEIGHT;
import static net.sf.emustudio.ssem.memory.gui.Constants.COLUMN_WIDTH;
import static net.sf.emustudio.ssem.memory.gui.Constants.DEFAULT_FONT;

class MemoryTable extends JTable {
    private final MemoryTableModel model;
    private final CellRenderer cellRenderer;
    private final JScrollPane scrollPane;

    MemoryTable(MemoryTableModel model, JScrollPane scrollPane) {
        this.scrollPane = Objects.requireNonNull(scrollPane);
        this.model = Objects.requireNonNull(model);
        this.cellRenderer = new CellRenderer(model);

        super.setModel(this.model);
        super.setFont(DEFAULT_FONT);
        super.setCellSelectionEnabled(true);
        super.setFocusCycleRoot(true);
        super.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        super.getTableHeader().setFont(DEFAULT_FONT);
    }

    public void setup() {
        cellRenderer.setup(this);
        setDefaultRenderer(Object.class, cellRenderer);
        scrollPane.setRowHeaderView(cellRenderer.getRowHeader());

        CellEditor editor = new CellEditor();
        editor.setup(this);

        for (int i = 0; i < columnModel.getColumnCount(); i++) {
            TableColumn col = columnModel.getColumn(i);
            col.setPreferredWidth(COLUMN_WIDTH[i]);
            col.setCellEditor(editor);
        }
        setRowHeight(getRowHeight() + CHAR_HEIGHT);

        InputMap im = getInputMap(JTable.WHEN_FOCUSED);
        ActionMap am = getActionMap();
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete");
        am.put("delete", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent listener) {
                int row = getSelectedRow();
                int col = getSelectedColumn();

                if (row != -1 && col != -1) {
                    model.setValueAt("0", row, col);
                }
            }
        });

    }

}

Now, particular sub-components of the table will be implemented and described in more detail.

3.6.2. Memory table model

Memory model is the "back-end" of the memory GUI. It means, the model provide data which should be shown in the GUI. You can think of any Swing component as a combination of a "view" and "model" subcomponents. The "view" subcomponent "asks" the model about which data should be shown. By default, almost all Swing components use some default models, accessible directly from the Swing component. Consequently, the components allow to set custom models as well, which is our case.

For a table, the model must implement javax.swing.table.TableModel interface. As it is often a custom, Java implements some general methods in an abstract class called javax.swing.table.AbstractTableModel we can extend from.

The description of the table model can be found at this link.

The model must provide data for every row and column. So here, we must do some math to compute bits and also transform the data to hex value, and raw ASCII value. The class is called MemoryTableModel and it’s source code is here:

src/main/java/net/sf/emustudio/ssem/memory/gui/MemoryTableModel.java
package net.sf.emustudio.ssem.memory.gui;

import emulib.plugins.memory.MemoryContext;
import emulib.runtime.NumberUtils;
import emulib.runtime.NumberUtils.Strategy;
import emulib.runtime.RadixUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.table.AbstractTableModel;
import java.util.Objects;

public class MemoryTableModel extends AbstractTableModel {
    private final static Logger LOGGER = LoggerFactory.getLogger(MemoryTableModel.class);

    final static int COLUMN_HEX_VALUE = 32;
    final static int COLUMN_RAW_VALUE = 33;

    private final static int ROW_COUNT = 32;
    private final static int COLUMN_COUNT = 2 + 32;

    private final MemoryContext<Byte> memory;

    MemoryTableModel(MemoryContext<Byte> memory) {
        this.memory = Objects.requireNonNull(memory);
    }

    @Override
    public int getRowCount() {
        return ROW_COUNT;
    }

    @Override
    public int getColumnCount() {
        return COLUMN_COUNT;
    }

    /**
     * Determine if a column index points at a bit which is part of the 3 bits in a memory cell representing a SSEM
     * instruction.
     *
     * @param column column in the memory table
     * @return true if the column represents a SSEM instruction bit
     */
    static boolean isBitInstruction(int column) {
        return column >= 13 && column <= 15;
    }

    /**
     * Determine if a column index points at a bit which is part of the 5 bits in a memory cell representing a "line",
     * or address part of the memory cell.
     *
     * @param column column in the memory table
     * @return true if the column represents a line bit
     */
    static boolean isBitLine(int column) {
        return column >= 0 && column < 5;
    }

    @Override
    public String getColumnName(int columnIndex) {
        switch (columnIndex) {
            case COLUMN_HEX_VALUE:
                return "Value (hex)";
            case COLUMN_RAW_VALUE:
                return "Raw";
        }
        if (isBitLine(columnIndex)) {
            return "L";
        }
        if (isBitInstruction(columnIndex)) {
            return "I";
        }
        return "";
    }

    private byte[] readLineBits(Byte[] line) {
        byte[] lineBits = new byte[32];

        int j = 0;
        for (byte b : line) {
            for (int i = 7; i >= 0; i--) {
                lineBits[j++] = (byte)((b >>> i) & 1);
            }
        }
        return lineBits;
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        try {
            Byte[] row = memory.readWord(rowIndex * 4);
            int value = NumberUtils.readInt(row, Strategy.REVERSE_BITS);

            switch (columnIndex) {
                case COLUMN_HEX_VALUE:
                    return RadixUtils.formatDwordHexString(value).toUpperCase();
                case COLUMN_RAW_VALUE:
                    return "" + (char)((value >>> 24) & 0xFF) + (char)((value >>> 16) & 0xFF)
                            + (char)((value >>> 8) & 0xFF) + (char)(value & 0xFF);
                default:
                    byte[] lineBits = readLineBits(row);
                    return lineBits[columnIndex];
            }
        } catch (Exception e) {
            LOGGER.debug("[line={}] Could not read value from memory", rowIndex, e);
        }
        return "";
    }

    @Override
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
        if (isCellEditable(rowIndex, columnIndex)) {
            try {
                Byte[] row = memory.readWord(rowIndex * 4);
                String str = String.valueOf(aValue);

                if (columnIndex == COLUMN_HEX_VALUE) {
                    writeHex(str, row);
                } else if (columnIndex == COLUMN_RAW_VALUE) {
                    writeChar((String)aValue, row);
                } else if (columnIndex >= 0 && columnIndex < 33) {
                    writeBit(str, columnIndex, row);
                }
                memory.writeWord(rowIndex * 4, row);

                fireTableCellUpdated(rowIndex, columnIndex);
            } catch (Exception e) {
                LOGGER.debug("[line={}, value={}] Could not set value to memory", rowIndex, aValue, e);
            }
        }
    }

    private void writeHex(String aValue, Byte[] row) {
        int value = Integer.decode(aValue);
        NumberUtils.writeInt(value, row, Strategy.REVERSE_BITS);
    }

    private void writeChar(String aValue, Byte[] row) {
        int i = 3;
        int value = 0;

        for (char c : aValue.toCharArray()) {
            value |= ((c & 0xFF) << (i*8));
            i -= 1;
            if (i < 0) {
                break;
            }
        }
        NumberUtils.writeInt(value, row, Strategy.REVERSE_BITS);
    }

    private void writeBit(String aValue, int columnIndex, Byte[] row) {
        int value;
        value = Integer.parseInt("0" + aValue, 2);

        int byteIndex = columnIndex / 8;
        int bitIndex = 7 - columnIndex % 8;

        int bitMask = ~(1 << bitIndex);
        int bitValue = (value << bitIndex);

        if ((value & 1) != value) {
            LOGGER.error("[line={}, bit={}, value={}] Could not set bit value. Expected 0 or 1", byteIndex, bitIndex, value);
        } else {
            row[byteIndex] = (byte)(row[byteIndex] & bitMask | bitValue);
        }
    }


    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        return columnIndex >= 0 && columnIndex <= 33;
    }

    void dataChangedAt(int address) {
        for (int i = 0; i < COLUMN_COUNT; i++) {
            fireTableCellUpdated(address, i);
        }
    }

    void clear() {
        memory.clear();
        fireTableDataChanged();
    }

}

3.6.3. Row header renderer

Programming a GUI in Java Swing can be tricky. If we want something non-standard, the way of customization can be very non-obvious = painful. This is the case when we want to do a "header"-like column in a JTable. Long story short - this trick lies in a use of JScrollPane. This component implements a viewport, something as a virtual screen, allowing to put there some other components, and make visible only a part of this screen. Besides, it has a feature which is called a "row header". It has a dedicated method for setting up a custom row header:

JScrollPane.setRowHeaderView(Component view)

The trick is to set a javax.swing.JList as a component for the row header. So we end up with the following class:

src/main/java/net/sf/emustudio/ssem/memory/gui/RowHeaderRenderer.java
package net.sf.emustudio.ssem.memory.gui;

import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JTable;
import javax.swing.ListCellRenderer;
import javax.swing.UIManager;
import javax.swing.table.JTableHeader;
import java.awt.Component;
import java.awt.Dimension;

import static net.sf.emustudio.ssem.memory.gui.Constants.CHAR_HEIGHT;
import static net.sf.emustudio.ssem.memory.gui.Constants.CHAR_WIDTH;
import static net.sf.emustudio.ssem.memory.gui.Constants.DEFAULT_FONT;

class RowHeaderRenderer extends JLabel implements ListCellRenderer {
    private final static int NO_COLUMN_WIDTH = CHAR_WIDTH * 4;

    private int height;

    RowHeaderRenderer() {
        super.setOpaque(true);
        super.setBorder(UIManager.getBorder("TableHeader.cellBorder"));
        super.setHorizontalAlignment(CENTER);
        super.setFont(DEFAULT_FONT);
    }

    public void setup(JTable table) {
        JTableHeader header = table.getTableHeader();
        this.height = header.getPreferredSize().height + CHAR_HEIGHT;
        setForeground(header.getForeground());
        setBackground(header.getBackground());
    }

    @Override
    public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
        setPreferredSize(new Dimension(NO_COLUMN_WIDTH, height));
        setText((value == null) ? "" : value.toString());
        return this;
    }

}

3.6.4. Cell renderer + editor

The last two things we need is to display text using different fonts on certain cells. For example, we want that line and instruction bits have bold font, and others plain one. But - it is generally more readable if the data are shown in some monospaced font variant. These customizations are "big enough" to require custom class - a cell renderer.

Official tutorial and description of custom renderers can be found at this link.

The cell renderer code looks as follows:

src/main/java/net/sf/emustudio/ssem/memory/gui/CellRenderer.java
package net.sf.emustudio.ssem.memory.gui;

import java.awt.Color;
import java.awt.Component;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JTable;
import javax.swing.table.TableCellRenderer;
import static net.sf.emustudio.ssem.memory.gui.Constants.BOLD_FONT;
import static net.sf.emustudio.ssem.memory.gui.Constants.CHAR_HEIGHT;
import static net.sf.emustudio.ssem.memory.gui.Constants.COLOR_CELL_BACK;
import static net.sf.emustudio.ssem.memory.gui.Constants.COLOR_CELL_BACK_MOD2;
import static net.sf.emustudio.ssem.memory.gui.Constants.COLOR_FORE;
import static net.sf.emustudio.ssem.memory.gui.Constants.COLOR_FORE_UNIMPORTANT;
import static net.sf.emustudio.ssem.memory.gui.Constants.DEFAULT_FONT;

class CellRenderer extends JLabel implements TableCellRenderer {
    private final JList rowHeader;
    private final String[] rowNames;
    private final RowHeaderRenderer rowHeaderRenderer;

    private Color selectionForeground;
    private Color selectionBackground;

    CellRenderer(final MemoryTableModel model) {
        this.rowHeaderRenderer = new RowHeaderRenderer();

        rowNames = new String[model.getColumnCount()];
        for (int i = 0; i < rowNames.length; i++) {
            rowNames[i] = String.format("%02X", i);
        }
        rowHeader = new JList(rowNames);
        rowHeader.setCellRenderer(rowHeaderRenderer);

        super.setOpaque(true);
        super.setFont(DEFAULT_FONT);
        super.setHorizontalAlignment(CENTER);
    }

    public void setup(JTable table) {
        rowHeader.setFixedCellHeight(table.getRowHeight() + CHAR_HEIGHT);
        rowHeaderRenderer.setup(table);

        selectionBackground = table.getSelectionBackground();
        selectionForeground = table.getSelectionForeground();
    }

    public JList getRowHeader() {
        return rowHeader;
    }

    @Override
    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
        if (isSelected) {
            setBackground(selectionBackground);
            setForeground(selectionForeground);
        } else {
            Color back = ((row % 2) == 0) ? COLOR_CELL_BACK : COLOR_CELL_BACK_MOD2;
            Color front = COLOR_FORE_UNIMPORTANT;

            if (MemoryTableModel.isBitInstruction(column) || MemoryTableModel.isBitLine(column)) {
                setFont(BOLD_FONT);
                front = COLOR_FORE;
            } else {
                setFont(DEFAULT_FONT);
            }

            setBackground(back);
            setForeground(front);
        }
        setText(value.toString());
        return this;
    }

}

Similarly, if we want to let user edit a value in a table, we must provide the editor as a Swing component. The "cell editor" is in fact plain javax.swing.JTextField with some customizations. However if we want to use it, we must wrap it in a separate class, which needs to extend AbstractCellEditor and implement TableCellEditor.

Our customizations involve accommodating the width of the text field, and preparing the initial value when the editor shows up. User can activate the editor by double-clicking on a cell. The source code of the cell editor is as follows:

src/main/java/net/sf/emustudio/ssem/memory/gui/CellEditor.java
package net.sf.emustudio.ssem.memory.gui;

import java.awt.Component;
import java.awt.FontMetrics;
import javax.swing.AbstractCellEditor;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.table.TableCellEditor;
import static net.sf.emustudio.ssem.memory.gui.Constants.CHAR_HEIGHT;
import static net.sf.emustudio.ssem.memory.gui.Constants.COLUMN_WIDTH;
import static net.sf.emustudio.ssem.memory.gui.Constants.DEFAULT_FONT;
import static net.sf.emustudio.ssem.memory.gui.MemoryTableModel.COLUMN_HEX_VALUE;
import static net.sf.emustudio.ssem.memory.gui.MemoryTableModel.COLUMN_RAW_VALUE;

class CellEditor extends AbstractCellEditor implements TableCellEditor {
    private final JTextField editComponent = new JTextField();
    private FontMetrics fontMetrics;

    private void setComponentSize(int columnIndex) {
        if (fontMetrics != null) {
            editComponent.setSize(COLUMN_WIDTH[columnIndex], fontMetrics.getHeight() + CHAR_HEIGHT);
            editComponent.setBorder(null);
        }
    }

    public void setup(JTable table) {
        fontMetrics = table.getFontMetrics(DEFAULT_FONT);
    }

    @Override
    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int rowIndex, int columnIndex) {
        if (!isSelected) {
            return null;
        }
        setComponentSize(columnIndex);
        switch (columnIndex) {
            case COLUMN_RAW_VALUE:
                editComponent.setText("");
                break;
            case COLUMN_HEX_VALUE:
                editComponent.setText("0x"+ String.valueOf(value));
                break;
            default:
                editComponent.setText(String.valueOf(value));
                break;
        }
        return editComponent;
    }

    @Override
    public Object getCellEditorValue() {
        return editComponent.getText();
    }

}

3.7. Wrapping it up

The final step is to finish the class MemoryImpl. We need to enable the use our GUI in emuStudio. The emulib.plugins.Memory interface has a method named showSettings which is called when user clicks on the memory icon in the debug tool bar in emuStudio emulation panel. This method is responsible for showing the GUI of memory, and will be called repeatedly, always when a user clicks on the mentioned icon.

src/main/java/net/sf/emustudio/ssem/memory/MemoryImpl.java
...

public class MemoryImpl extends AbstractMemory {
    private final static Logger LOGGER = LoggerFactory.getLogger(MemoryImpl.class);

    private final MemoryContextImpl memContext = new MemoryContextImpl();
    private MemoryGUI memoryGUI;

    public MemoryImpl(Long pluginID, ContextPool contextPool) {
        super(pluginID);
        try {
            contextPool.register(pluginID, memContext, MemoryContext.class);
        } catch (AlreadyRegisteredException | InvalidContextException e) {
            StaticDialogs.showErrorMessage("Could not register SSEM memory", getTitle());
        }
    }

    @Override
    public String getVersion() {
        return "1.0.0";
    }

    @Override
    public void initialize(SettingsManager settings) throws PluginInitializationException {
        if (!Boolean.parseBoolean(settings.readSetting(pluginID, SettingsManager.NO_GUI))) {
            memoryGUI = new MemoryGUI(memContext);
        }
    }

    @Override
    public void destroy() {
    }

    @Override
    public int getSize() {
        return MemoryContextImpl.NUMBER_OF_CELLS;
    }

    @Override
    public boolean isShowSettingsSupported() {
        return true;
    }

    public void showSettings() {
        if (memoryGUI != null) {
            memoryGUI.setVisible(true);
        }
    }
}

As you can see from the code, there are several things to notice:

  • In constructor, we will register new instance of our memory context. Since we do not have custom context, the interface, passed as the second argument to call contextPool.register() will be plain MemoryContext.class. If we had custom context, it would require to be defined in separate interface which will extend the standard MemoryContext, and annotated with @emulib.annotations.ContextType annotation.

  • In the initialize() method, we determine if we run in a "non-GUI" mode. We use a SettingsManager object for this purpose, which is an API for reading/writing plugin settings - key/value pairs from the computer configuration file. The "non-GUI", also called "headless" mode, means that the user who run emuStudio did not want GUI to be available. This is often useful when performing automatic emulation. See the user manual for more details. For us, developers, it means that we need to ignore all requests for showing GUI. Therefore, we create the GUI only if we are NOT in the "non-GUI" mode.

3.8. Testing

It is not only a good practice, but often a safety net to perform automatic tests. They can save a lot of debugging time when something just does not work. Usually, tests should test the most important things - we usually don’t test setters or getters. In case of GUI, it also does not matter much for our case.

What should be tested is the context itself - since it’s the core part of the memory, and also some interaction with the main plugin class. For example, the automated unit test of the memory context can look as follows:

src/test/java/net/sf/emustudio/ssem/memory/impl/MemoryContextImplTest.java
package net.sf.emustudio.ssem.memory.impl;

import emulib.plugins.memory.Memory;
import org.junit.Test;

import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expectLastCall;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;

public class MemoryContextImplTest {

    @Test
    public void testAfterClearObserversAreNotified() throws Exception {
        MemoryContextImpl context = new MemoryContextImpl();

        Memory.MemoryListener listener = createMock(Memory.MemoryListener.class);
        listener.memoryChanged(eq(-1));
        expectLastCall().once();
        replay(listener);

        context.addMemoryListener(listener);
        context.clear();

        verify(listener);
    }

    @Test
    public void testReadWithoutWritReturnsZero() throws Exception {
        MemoryContextImpl context = new MemoryContextImpl();

        assertEquals(0L, (long)context.read(10));
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testReadAtInvalidLocationThrows() throws Exception {
        MemoryContextImpl context = new MemoryContextImpl();

        context.read(-1);
    }

    @Test
    public void testAfterReadNoObserversAreNotified() throws Exception {
        MemoryContextImpl context = new MemoryContextImpl();

        Memory.MemoryListener listener = createMock(Memory.MemoryListener.class);
        replay(listener);

        context.addMemoryListener(listener);
        context.read(10);

        verify(listener);
    }

    @Test
    public void testAfterWriteObserversAreNotified() throws Exception {
        MemoryContextImpl context = new MemoryContextImpl();

        Memory.MemoryListener listener = createMock(Memory.MemoryListener.class);
        listener.memoryChanged(eq(10));
        expectLastCall().once();
        replay(listener);

        context.addMemoryListener(listener);
        context.write(10, (byte)134);

        verify(listener);
    }

    @Test
    public void testWriteReallyWritesCorrectValueAtCorrectLocation() throws Exception {
        MemoryContextImpl context = new MemoryContextImpl();

        context.write(10, (byte)134);
        assertEquals((byte)134, (byte)context.read(10));
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testWriteAtInvalidLocationThrows() throws Exception {
        MemoryContextImpl context = new MemoryContextImpl();

        context.write(-1, (byte)134);
    }

    @Test
    public void testGetSizeReturnsNumberOfCells() throws Exception {
        MemoryContextImpl context = new MemoryContextImpl();

        assertEquals(MemoryContextImpl.NUMBER_OF_CELLS, context.getSize());
    }

    @Test
    public void testClassTypeIsByte() throws Exception {
        assertEquals(Byte.class, new MemoryContextImpl().getDataType());
    }

    @Test
    public void testReadWordIsSupported() throws Exception {
        assertArrayEquals(new Byte[] {0,0,0,0}, new MemoryContextImpl().readWord(0));
    }

    @Test
    public void testWriteWordIsSupported() throws Exception {
        MemoryContextImpl mem = new MemoryContextImpl();

        Byte[] row = new Byte[] {1,2,3,4};
        mem.writeWord(0, row);

        assertArrayEquals(row, mem.readWord(0));
    }
}

4. Writing a CPU

This tutorial will describe some basic knowledge about how to create a CPU to be used in emuStudio. The tutorial does not aim to explain emulation techniques in much detail, nor it is exhaustive. This is left for the programmer. The tutorial focuses on how to use emuLib API in a CPU project so it can be used in emuStudio.

4.1. Getting started

Before reading on, please read the Introduction chapter. It gives the information needed for setting up the development environment and for basic understanding how the emuStudio/plug-ins lifecycle work.

A CPU is just another plug-in, which means that it is "enough" to implement some API. The CPU API can be found in emulib.plugins.cpu project package.

CPU is supposed to be the core of the emulator in the similar way as the real CPU is core to the computer. The term "emulation technique" is usually used when we talk about CPU emulation. This tutorial will not cover the techniques in much detail. Furthermore, only instruction interpretation will be used for its simplicity.

More information about emulation techniques can be found for example at this link.

In this tutorial, a simple CPU will be implemented for the world’s very first stored-program computer, SSEM, nicknamed "Baby". It was a predecessor of Manchester Mark 1 which led to Ferranti Mark 1, the world’s first commercially available general-purpose computer.

4.2. What emuStudio’s CPU can do

CPU emulators in emuStudio are not just plain emulators. Besides, they must cooperate with emuStudio and provide capabilities allowing debugging and some visualization.

The CPU must, besides the emulation "engine", implement:

  • Disassembler

  • Java Swing GUI panel

Disassembler will be used by emuStudio for creating the list of instructions in the debugger panel. The visualization of CPU registers, possibly current frequency and run state must be implemented by the CPU plug-in. For that purpose the plug-in must provide the GUI panel to CPU.

Both the disassembler and GUI panel should be instantiated only once. emuStudio will ask for them also only once, during the process of loading of the virtual computer.

For developing disassembler, there is good news. There exist a tool which can generate emuStudio disassembler from a specification file. It is called Edigen, a short for "emulator disassembler generator". We will use this tool also in this tutorial. For more information, see the projects at:

4.3. Preparing the environment

We will use Maven for managing the source code and dependencies.

If you are new to Maven, please read Maven in 5 minutes tutorial.

The project should be located in emuStudio/plugins/cpu/ssem-cpu. In order to create the initial project structure, run mvn archetype:generate in that directory.

The following structure should now exist:

src/
  main/
    java/
    resources/
test/
  java/
pom.xml

We will start with the pom.xml file, which follows.

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>net.sf.emustudio</groupId>
    <artifactId>emustudio-parent</artifactId>
    <version>0.39</version>
    <relativePath>../../..</relativePath>
  </parent>

  <artifactId>ssem-cpu</artifactId>
  <packaging>jar</packaging>

  <name>SSEM CPU emulator</name>
  <description>Java-based SSEM CPU emulator</description>

  <build>
    <finalName>ssem-cpu</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <configuration>
          <archive>
            <manifest>
              <addClasspath>false</addClasspath>
              <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
              <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
            </manifest>
          </archive>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-dependency-plugin</artifactId>
      </plugin>
      <plugin>
        <groupId>com.github.sulir</groupId>
        <artifactId>edigen-maven-plugin</artifactId>
        <configuration>
          <decoderName>net.sf.emustudio.ssem.DecoderImpl</decoderName>
          <disassemblerName>net.sf.emustudio.ssem.DisassemblerImpl</disassemblerName>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>generate</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

  <dependencies>
    <dependency>
      <groupId>net.sf.emustudio</groupId>
      <artifactId>cpu-testsuite</artifactId>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-nop</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>net.sf.emustudio</groupId>
      <artifactId>emuLib</artifactId>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
    </dependency>
    <dependency>
      <groupId>org.easymock</groupId>
      <artifactId>easymock</artifactId>
    </dependency>
    <dependency>
      <groupId>net.sf.emustudio</groupId>
      <artifactId>cpu-testsuite</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

4.4. The main class

We will start with implementing the main class of the CPU. It provides the main communication point - the API - used by emuStudio main module. The main module can pass requests for starting or stopping the emulation, or it can request for the disassembler or the GUI panel.

We will start with small snippet of code which will be extended throughout the tutorial. The first snippet looks as follows:

src/main/java/net/sf/emustudio/ssem/cpu/CpuImpl.java
@PluginType(
    type = PLUGIN_TYPE.CPU,
    title = "SSEM CPU",
    copyright = "\u00A9 Copyright 2017, Your Name",
    description = "Emulator of SSEM CPU"
)
public class CpuImpl extends AbstractCPU {

    public CpuImpl(Long pluginID, ContextPool contextPool) {
        super(pluginID);
    }

    @Override
    protected void destroyInternal() {

    }

    @Override
    protected RunState stepInternal() throws Exception {
        return null;
    }

    @Override
    public JPanel getStatusPanel() {
        return null;
    }

    @Override
    public int getInstructionPosition() {
        return 0;
    }

    @Override
    public boolean setInstructionPosition(int i) {
        return false;
    }

    @Override
    public Disassembler getDisassembler() {
        return null;
    }

    @Override
    public void initialize(SettingsManager settingsManager) throws PluginInitializationException {

    }

    @Override
    public String getVersion() {
        return "1.0.0";
    }

    @Override
    public RunState call() throws Exception {
        return null;
    }
}

As you can see, there is a lot of methods which needs attention. Note two methods especially - getStatusPanel() and getDisassembler(). Those two methods will return the components, mentioned first in section What emuStudio’s CPU can do.

Also note that the class extends from AbstractCPU. The AbstractCPU class lies in emuLib library. It implements some fundamental methods required by CPU interface. For example, managing breakpoints and controlling the high-level emulation lifecycle in a thread-safe way. Generally speaking, the class eliminates lots of repeated boiler-plate code needed to be done in every CPU plug-in.

4.5. Behavioral contracts

4.5.1. Load and initialization

Loading the virtual computer starts with creating separate class loader derived from the one emuStudio is using, so each plug-in can see everything what emuStudio can see, roughly speaking. There can be loaded only one computer in emuStudio.

The CPU plug-in JAR file is loaded using the class loader as a second in order.

The loading process follows:

  1. JAR content is searched for a main class. Main class must be annotated with @PluginType(type = PLUGIN_TYPE.CPU) annotation and it does not matter in which package it resists.

  2. There must be just one main class in the JAR. If there are more classes annotated with mentioned annotation, the first found will be used; however the search order is non-deterministic.

  3. The main class must implement emulib.plugins.cpu.CPU interface (in any depth of inheritance).

  4. Plug-in is instantiated by calling the main class constructor. The constructor must be public and must have two parameters of type: java.lang.Long (as the first) and emulib.runtime.ContextPool (as the second). The first parameter represents a unique plug-in ID assigned by emuLib. This ID can then be used to access configuration of the emulated computer. The second parameter is a context pool object. It is a pool of plugin contexts, runtime entities intended for the plug-in intercommunication. In the constructor, the CPU should initialize its context to the context pool. However; it must not retrieve the contexts of other plugins now, because they are not present at this point, except compiler.

  5. Plug-in is initialized by calling method emulib.plugins.Plugin.initialize(SettingsManager settingsManager) from the main thread of emuStudio (not UI thread). This method is intended for all the initialization which could not be performed in the constructor, such as reading plug-in settings, or retrieving contexts of other plug-in(s) from the context pool.

  6. Specifically for CPU, emuStudio calls getDisassembler() and getStatusPanel() methods in unspecified order.

After those steps, the CPU plug-in is ready. Further work of the CPU is event-based. emuStudio will handle UI events and control the plug-in by calling appropriate methods of the main class instance. CPU emulators are run in different thread than UI thread, so all method calls come from the same "controller" thread.

In case of automatic emulation, the emulation control is performed only in the main thread.

4.5.2. Emulation lifecycle

As it was described in section "Emulation lifecycle" in the user manual of the main module, the emulation "life" is a state machine. This state machine manages a state, called "current state". It then reacts on events from the outside world and transitions the current state to another state, following the rules. The state transition can, and in this case does - cause side-effects. It means that except the simple changing the state, it performs some actions.

For example, you know that there is a button above the debug window, a green-filled arrow, when clicked on it the emulation will be executed. Besides, there are more buttons, for example "step emulation", which will do execute just one CPU instruction. The clicks on the buttons are the "outside world" events, which will be propagated to the state machine of emuStudio.

The state machine can be seen in the following diagram:

run states

The states of the state machine are encoded into an enum in emuLib:

emulib.plugins.cpu.CPU.RunState
public static enum RunState {
    STATE_STOPPED_NORMAL("stopped"),
    STATE_STOPPED_BREAK("breakpoint"),
    STATE_STOPPED_ADDR_FALLOUT("stopped (address fallout)"),
    STATE_STOPPED_BAD_INSTR("stopped (instruction fallout)"),
    STATE_RUNNING("running");

    ...
}

The initial state is breakpoint. This is a behavioral contract which all CPUs must fulfil.

The AbstractCPU class implements the state machine by implementing fundamental methods of the CPU interface:

emulib.plugins.cpu.CPU
public interface CPU extends Plugin {

    void step();

    void execute();

    void pause();

    void stop();

    void reset(int memoryLocation);

    ...
}

The CPU plug-in developer can benefit from using AbstractCPU which implements most of the methods in a thread-safe way. It is the required to implement only the following methods, which do not have to be synchronized:

emulib.plugins.cpu.AbstractCPU
@ThreadSafe
public abstract class AbstractCPU implements CPU, Callable<RunState> {
    protected abstract RunState stepInternal() throws Exception;

    protected abstract void resetInternal(int var1);

    protected abstract void destroyInternal();

    ...
}
CPU emulation modes

The CPU can work in two modes while performing the emulation: "step" mode or "run" mode. The modes are disjunct - only one of them can be active in time.

"Step" mode

In the "step" mode, the CPU emulates instructions in "steps", one-by-one. One instruction should be emulated by calling step() method. After the emulation of the instruction is finished, the CPU run state should be returned back to STATE_STOPPED_BREAK.

In case of error, the run state should change to STATE_STOPPED_(how), where (how) is the general root cause of the error (e.g. BAD_INSTR or ADDR_FALLOUT).

In this mode, it is not required to emulate the instruction in a performance-optimized manner.

"Run" mode

In the "run" mode, the CPU should emulate instructions infinitely until either some CPU-halt instruction is encountered or user stops the emulation by external GUI event. Within this mode the developer is encouraged to use some good emulation technique, which can focus on performance. The code paths which will be run by JVM in this mode should be optimized for performance.

Furthermore, emuStudio will stop disassembling instructions and also other performance-consuming tasks to unburden the CPU and other virtual components from various requests causing slow down of the general emulation performance.

When the emulation is finished, either by the external event (clicking on the "stop" button) or by some instruction, the run state should be set accordingly:

  • if the stop is "normal" or "expected", the run state should be STATE_STOPPED_NORMAL

  • if the stop is caused by trying to read/write from nonexistant location in memory, the run state should be STOPPED_ADDR_FALLOUT

  • if the stop is caused by trying to execute unknown instruction, the run state should be STOPPED_BAD_INSTR

Final notes

The described modes are reflected in methods of AbstractCPU class. The call() method represents the "run" mode, and stepInternal() method, represents the "step" mode.

The contract which needs attention is threading. Execution of mentioned methods is done always by emuStudio. It has dedicated one thread for this purpose. The methods are never executed from the UI thread, but from the dedicated thread, using a work-queue for the upcoming events.

This means that CPU emulation control will not block UI, even if the execution takes longer time. However, all the other methods from the CPU interface are (possibly) executed from the UI thread, so they should be implemented in a responsive manner; they can block.

4.6. Architecture

SSEM is one of the first implementations of the von-Neumann design of a computer. It contained control unit, arithmetic-logic unit and I/O subsystem (CRT display). More information about the real architecture can be found at this link.

The architecture of our SSEM CPU emulator will look as follows (below is Display and Memory just to show how it is connected overally):

ssem scheme

4.7. The main class

The fundamental steps when building a CPU involves the initialization and destruction code. After reading the Behavioral contracts, you should be aware of how the code should look like.

The initialization code is represented by the constructor and the initialize() method.

src/main/java/net/sf/emustudio/ssem/cpu/CpuImpl.java
public class CpuImpl extends AbstractCPU {
    private final ContextPool contextPool;

    public CpuImpl(Long pluginID, ContextPool contextPool) {
        super(pluginID);
        this.contextPool = Objects.requireNonNull(contextPool);
    }

    @Override
    public void initialize(SettingsManager settingsManager) throws PluginInitializationException {
        // TODO
    }

    ...
}

We will leave the other methods unimplemented for now.

While getting to the initialization part, what the CPU needs in order to operate? Especially, our SSEM "CPU". It requires memory. The I/O subsystem, as can be seen at the picture under Architecture section, will not be implemented in this tutorial. There is dedicated separate tutorial for the CRT display.

The first step of the initialization is getting the memory from the context pool:

src/main/java/net/sf/emustudio/ssem/cpu/CpuImpl.java
public class CpuImpl extends AbstractCPU {
    private MemoryContext<Integer> memory;

    @Override
    public void initialize(SettingsManager settingsManager) throws PluginInitializationException {
        memory = contextPool.getMemoryContext(getPluginID(), MemoryContext.class);
    }
}

Now we see what the context pool is used for. It is a "storage" of communication objects which plug-ins provide (contexts). Other plug-ins, which are connected with the one they want to communicate with, ask for the context. There exist many specific contexts - for CPU, for compilers, memories or devices.

What’s more, the context can be extended with another, custom methods. In this case, the context class should be passed as the second argument when calling get…​Context(). In our case, we expect the standard MemoryContext interface, so we pass MemoryContext.class as the second argument.

The question you might have is why not to get the memory in the constructor? To answer this question, please read the document "Introduction for writing virtual computers", section "Loading and initialization".

What now? We need to implement three fundamental components - GUI panel, disassembler and the emulator engine itself. We can start with the interesting stuff right away.

4.8. Emulator engine

Emulator engine is the core of the emulator. As we all probably know, the CPU interprets some binary-encoded "commands" - instructions - which are stored in memory. Basic von-Neumann CPUs work sequentially. Execution of one instruction involves four basic steps: fetch, decode and execute, and store, executed in order.

Implementation of these steps in a programming language like Java does not have to be so explicit. It is often true that the steps will overlap and mix up in the emulation algorithm; they really don’t have to be explicitly distinguished. The aim of the emulator is to preserve external behavior (output or the effect), not the internal behavior. This is different for the case of a simulator, which tries to mimic both internal and external behavior.

Emulator "looks" like real computer, "behaves" like the real one, but inside it is normal program which was written using any programming style; it can use various variables, methods and other language features.

The pseudo-algorithm for executing one instruction can look as follows:

step() {
  // fetch phase
  instruction = memory.read(current_instruction);
  current_instruction = current_instruction + 1;

  // decode phase
  line = parseLine(instruction);
  opcode = parseOpcode(instruction);

  // execute phase
  switch (opcode) {
    case 0: // JMP
      ...
    case 4: // JPR
      ...
    ...
  }
}

And what CPU does when it runs? It executes these steps in the infinite loop, until it is stopped either internally or by the external event. The main CPU emulation algorithm just described is called "interpretation", and it can look as follows:

run() {
  while (!stopped) {
    step();
  }
}
In Java, besides interpretation it is possible to write also a threaded dispatch algorithm, which requires Java relection or lambdas. Threaded dispatch stores the execution implementation of each instruction in a separate method. Then, there is a dispatch table (array of method references), which maps the methods by opcode. Then, after the decoding of the opcode, the instruction is executed just by indexing that table and executing the method it references to. This algorithm is generally faster than interpretation, and it is still simple enough to be implemented.

Our emulator engine will be constructed as a separate class. Besides the emulation methods it will contain the variables representing CPU registers - CI (current instruction) and Acc (accumulator). In SSEM, both are 32-bit values.

The class looks as follows:

src/main/java/net/sf/emustudio/ssem/cpu/EmulatorEngine.java
public class EmulatorEngine {

    private final MemoryContext<Byte> memory;
    private volatile CPU.RunState currentRunState;

    volatile int Acc;
    volatile int CI;

    EmulatorEngine(MemoryContext<Byte> memory) {
        this.memory = Objects.requireNonNull(memory);
    }

    void reset(int startingPos) {
        Acc = 0;
        CI = startingPos;
    }

    CPU.RunState step() {
        Byte[] instruction = memory.readWord(CI);
        CI += 4;

        int line = NumberUtils.reverseBits(instruction[0], 8) * 4;
        int opcode = instruction[1] & 7;

        switch (opcode) {
            case 0: // JMP
                int oldCi = CI - 4;
                CI = 4 * readInt(line);
                if (CI == oldCi) {
                    // endless loop detected;
                    return CPU.RunState.STATE_STOPPED_NORMAL;
                }
                break;
            case 4: // JPR
                CI = CI + 4 * readInt(line);
                break;
            case 2: // LDN
                Acc = -readInt(line);
                break;
            case 6: // STO
                writeInt(line, Acc);
                break;
            case 1: // SUB
                Acc = Acc - readInt(line);
                break;
            case 3: // CMP / SKN
                if (Acc < 0) {
                    CI += 4;
                }
                break;
            case 7: // STP / HLT
                return CPU.RunState.STATE_STOPPED_NORMAL;
            default:
                return CPU.RunState.STATE_STOPPED_BAD_INSTR;
        }
        return CPU.RunState.STATE_STOPPED_BREAK;
    }

    private int readInt(int line) {
        Byte[] word = memory.readWord(line);
        return NumberUtils.readInt(word, Strategy.REVERSE_BITS);
    }

    private void writeInt(int line, int value) {
        Byte[] word = new Byte[4];
        NumberUtils.writeInt(value, word, Strategy.REVERSE_BITS);
        memory.writeWord(line, word);
    }

    CPU.RunState run() {
        CPU.RunState currentRunState = CPU.RunState.STATE_STOPPED_BREAK;

        while (!Thread.currentThread().isInterrupted() && currentRunState == CPU.RunState.STATE_STOPPED_BREAK) {
            try {
                currentRunState = step();
            } catch (IllegalArgumentException e) {
                if (e.getCause() != null && e.getCause() instanceof IndexOutOfBoundsException) {
                    return CPU.RunState.STATE_STOPPED_ADDR_FALLOUT;
                }
                return CPU.RunState.STATE_STOPPED_BAD_INSTR;
            } catch (IndexOutOfBoundsException e) {
                return CPU.RunState.STATE_STOPPED_ADDR_FALLOUT;
            }
        }
        return currentRunState;
    }

}

Pretty short, huh? Method step() and run() return CPU.RunState enum, which is used by emuStudio to determine if the emulator is still running or in what state it is. The step() method is the most fundamental regarding the instruction emulation, but it is so easy that we’ll rather talk about the run() method.

The run() method begins with the already described cycle. However, the conditions of determining if the CPU should be running can look complex at the first sight. However, we are checking just two conditions - if the current run state has changed (look at the step() method - it can change there), or if the current thread is interrupted. It can interrupt by external condition, e.g. when somebody quits the emulator during CPU emulation.

Then, there are many catches. They are quite required because of many possible situations which can happen - when the CPU gets to the end of the memory, what it should do? It does nothing, so the memory will throw some variant of IndexOutOfBoundsException. For this purpose, CPU state contains one which is called STATE_STOPPED_ADDR_FALLOUT, meaning "address fallout", like if the address "fell out" of allowed range.

And the last bad thing which can happen is when the memory at the current instruction position contains some unknown data, not recognized by CPU. For this situation, we have STATE_STOPPED_BAD_INSTR state.

That’s pretty it. We will now extend the engine to support breakpoints and controlling the speed.

4.8.1. Breakpoints support

Since emuStudio is mainly intended for students, as they should get in touch with emulated computers and how they work, it should allow sometimes to pause the emulation at a point she wants. This capability is also useful when our program written for the emulated computer does not work and we want to know what happens after executing specific instruction. We can set a "breakpoint" to that instruction, a flag saying that CPU should pause itself when it encounters the instruction.

Breakpoint is in fact an address - memory location, at which the CPU should pause its execution. It is used only when CPU is running. Breakpoints are usually stored in a set. The class emulib.plugins.cpu.AbstractCPU has already this set as a protected member (called breakpoints) and implements all the breakpoints enabling/disabling. What is still left to do for us is to check if at specific address (current instruction position) the breakpoint is set, and if yes, somehow "pause" the CPU.

We implement this in the run() method, right before instruction execution:

src/main/java/net/sf/emustudio/ssem/cpu/EmulatorEngine.java
public class EmulatorEngine {
    ...
    private final CPU cpu;

    EmulatorEngine(MemoryContext<Byte> memory, CPU cpu) {
        this.memory = Objects.requireNonNull(memory);
        this.cpu = Objects.requireNonNull(cpu);
    }

    CPU.RunState run() {
        while (...) {
            try {
                if (cpu.isBreakpointSet(CI)) {
                    return CPU.RunState.STATE_STOPPED_BREAK;
                }
                currentRunState = step();
            } catch (...) {
              ...
            }
        }
        return currentRunState;
    }

    ...
}

Now the engine requires also the CPU main object, needed for checking if the breakpoint is set at current instruction location, denoted by the CI register. If the breakpoint is set, the resulting state is STATE_STOPPED_BREAK, and emuStudio will take care about the pausing and updating the GUI.

4.8.2. Preserving the speed

Every real computer runs at some speed, usually talking just about only CPU speed. Baby "CPU" could perform about 700 instructions per second. How we should achieve that? The simplest method would be something like that:

run() {
    while (!stopped) {
        start = measureTime();
        ... perform 700 instructions ...
        end = measureTime();

        to_wait = 1.second - (end - start);
        if (to_wait > 0) {
            wait_time(to_wait);
        }
    }
}

So perform 700 instructions, then wait until one second elapses, and go again. What’s wrong about this solution? That the algorithm is not "smooth". 700 instructions will be performed at full blast, and then there will be something like a "break", and the situation will repeat. Real CPU certainly didn’t work like that and we can do better.

If we know how long it takes to execute each instruction, if our host CPU is faster than CPU of SSEM (which is I suppose :), we can "wait" after each instruction the time difference, so we will artificially slow down to SSEM speed.

In reality, every instruction is performed in some number of machine "cycles". We can imagine the machine cycle as a time of elementary phase when performing the instruction. Based on this information which is usually available, can be build a technique for preserving speed even better.

Description of the speed-preservation technique can be seen e.g. at this link.

I don’t quite know the speed of particular SSEM instructions, and besides the algorithm is quite complex. More achievable is a bit different approach, but still quite interesting.

Waiting after each instruction requires computing the time difference, checking it and if it is > 0, wait the amount of time, by calling some Java method. However, we don’t know how long each instruction will take, but we can estimate it by measuring.

We will execute as many instructions as we can in a second, and by simple math we can then compute how long we should "wait", in average, so at the end we will execute 700 instructions in a second, in average. Once again, the steps are as follows:

  1. Measure how many instructions will be executed in 1 second, we will label the number as N.

  2. The goal is to achieve 700 instructions per second. It is assumed N > 700. In the first step, we need 1 / N * 700 seconds to pass and we know that 700 instructions will be then executed. We will label this as M = 1 / N * 700. It will be a constant, after the measurement.

  3. Then, we need to wait 1 / M seconds after each instruction, and 700 instructions per second is achieved.

The measurement will not be very accurate, since perfect or almost perfect measuring of method execution in Java has some rules, like warming up JVM before measurement, etc.

For time measurement it is necessary to use System.nanoTime() method instead of System.currentTimeMillis(). The reason is that the latter is corrected time-to-time by operating system because of errors caused by not really accurate timer in your computer. Then, the time difference can give invalid values, sometimes even negative ones. The System.nanoTime() is not corrected, so time difference works well.

The algorithm will work in the following steps:

1. Measure average instruction time
2. Compute how much CPU should wait after executing each instruction
3. Wait after each instruction for the computed time

The third step will be performed only if the time we should wait is greater than zero. It means that the host computer is faster than Baby computer (which is expected).

The algorithm can be implemented as follows:

public class EmulatorEngine {
    ...
    private volatile long averageInstructionNanos;

    CPU.RunState run() {
        if (averageInstructionNanos == 0) {
            measureAverageInstructionNanos();
        }
        long waitNanos = TimeUnit.SECONDS.toNanos(1) / averageInstructionNanos;
        while (...) {
            ...
            if (waitNanos > 0) {
                LockSupport.parkNanos(waitNanos);
            }
        }
        return currentRunState;
    }

    ...
}

Quite simple, so far. We will measure the instruction speed just once, on the first call of the run() method. The measured value will be reused for later executions and will not slow down the whole emulator.

However, how we should measure the average time which is taken by the instruction execution? Well, if we want to be at least somehow accurate, we should emulate the step() method several times, and then compute the average. However, we can’t. The reason is that step() method uses real memory and CPU registers. We should use kind of "fake" the step() method which will not change the emulator state or memory. But the fake step should implement instruction with the average "complexity", which we will do just with some estimation or better - feeling. The algorithm can look as follows (there’s lot to improve ofcourse):

src/main/java/net/sf/emustudio/ssem/cpu/EmulatorEngine.java
public class EmulatorEngine {
    final static int INSTRUCTIONS_PER_SECOND = 700;
    private final static int MEMORY_CELLS = 32 * 4;
    ...

    private void fakeStep() {
        Byte[] instruction = memory.readWord(CI);

        int line = NumberUtils.reverseBits(instruction[0], 8);
        int opcode = instruction[1] & 3;
        CI = (CI + 4) % MEMORY_CELLS;


        switch (opcode) {
            case 0: break;
            case 1: break;
            case 2: break;
            case 3: break;
            case 4: break;
            case 6: break;
            case 7: break;
        }

        Acc -= memory.read(line % MEMORY_CELLS);
    }


    private void measureAverageInstructionNanos() {
        int oldCI = CI;
        int oldAcc = Acc;

        long start = System.nanoTime();
        for (int i = 0; i < INSTRUCTIONS_PER_SECOND; i++) {
            fakeStep();
        }
        long elapsed = System.nanoTime() - start;

        averageInstructionNanos = elapsed / INSTRUCTIONS_PER_SECOND;

        CI = oldCI;
        Acc = oldAcc;
    }

    ...
}

At first, we will save the registers (emulator state). Then, we will execute the fake step for 700 times and then compute the average time. At the end we restore the state, and that’s it. As you might notice, we tried to use real things in this "fake" step method like real memory (but just for reading), and emulator registers, which we backed up and then restored.

That’s about it! If we had disassembler and GUI, the emulator is now ready - we have just implemented the core of the CPU.

4.9. Disassembler

Disassembler is not needed for the emulation itself. It is needed for emuStudio to be able to visually show the instructions. Instructions are encoded in a binary form and reside in memory. Disassembler "disassembles" - decodes the instructions and transforms them into a string representation which can be easily shown on screen.

Decoding binary instructions for disassembler can be a bit different from decoding used in the emulator. For example, instructions binary code can use constants which can be used directly in the emulator, but which must be translated in the disassembler. Also, decoding code is usually mixed up with emulator code for performance reasons, so it’s hard to reuse it. For these reasons, the programmer often need to implement the decoding part again and duplicate the work a bit. But not in emuStudio.

Fortunately, there exist a project called Edigen (https://github.com/sulir/edigen), a disassembler generator. It works similarly as a parser generator: developer writes a specification file with all the instructions of the CPU. Then, Edigen (either from the command line or from Maven) generates disassembler and decoder source code, using predefined templates. These generally do not need any further attention from the developer and can be used right away.

SSEM CPU specification file should be put in ssem-cpu/src/main/edigen/cpu.eds, and it looks as follows:

ssem-cpu/src/main/edigen/cpu.eds
instruction = "JMP": line(5)     ignore8(8) 000 ignore16(16) |
              "JPR": line(5)     ignore8(8) 100 ignore16(16) |
              "LDN": line(5)     ignore8(8) 010 ignore16(16) |
              "STO": line(5)     ignore8(8) 110 ignore16(16) |
              "SUB": line(5)     ignore8(8) 001 ignore16(16) |
              "CMP": 00000       ignore8(8) 011 ignore16(16) |
              "STP": 00000       ignore8(8) 111 ignore16(16);

line = arg: arg(8);

ignore5 = arg: arg(5);

ignore8 = arg: arg(8);

ignore16 = arg: arg(16);

%%

"%s %X" = instruction line(bit_reverse) ignore8 ignore16;
"%s" = instruction ignore8 ignore16;

The specification file might look a bit cryptic at first sight, but it’s quite easy. The content is divided into two sections, separated with two %% chars on a separate line. The first section contains rules which are used for parsing the instruction binary codes and assign labels to the codes. The second section specifies the disassembled string formats for particular rules.

There can exist multiple rules, and rules can include another rules. If the rule includes the same rule recursively, it means it’s a constant. In that case, in the parenthesis after the rule inclusion must be a number of bits which the constant takes.

4.9.1. Using generated disassembler

When you look into our pom.xml file, you can find a section:

...
      <plugin>
        <groupId>edigen</groupId>
        <artifactId>edigen-maven-plugin</artifactId>
        <configuration>
          <decoderName>net.sf.emustudio.ssem.DecoderImpl</decoderName>
          <disassemblerName>net.sf.emustudio.ssem.DisassemblerImpl</disassemblerName>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>generate</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
...

The disassembler will be generated in the class net.sf.emustudio.ssem.DisassemblerImpl. The class already implements the interface emulib.plugins.cpu.Disassembler, which is exactly what method CPU.getDisassembler() returns. Disassembler is an independent component so it also uses the memory from where it reads the instructions. Therefore, disassembler can be initialized after the memory. Now we are ready to do full initialization of the emulator, with the engine as well as disassembler. The code looks as follows:

src/main/java/net/sf/emustudio/ssem/cpu/CpuImpl.java
public class CpuImpl extends AbstractCPU {
    ...
    private EmulatorEngine engine;
    private Disassembler disasm;

    ...

    @Override
    public void initialize(SettingsManager settingsManager) throws PluginInitializationException {
        memory = contextPool.getMemoryContext(getPluginID(), MemoryContext.class);
        Decoder decoder = new DecoderImpl(memory);
        disasm = new DisassemblerImpl(memory, decoder);
        engine = new EmulatorEngine(memory, this);
    }

    @Override
    public Disassembler getDisassembler() {
        return disasm;
    }

    @Override
    public RunState call() throws Exception {
        return engine.run();
    }

    @Override
    protected RunState stepInternal() throws Exception {
        return engine.step();
    }

    @Override
    protected void resetInternal(int startPos) {
        engine.reset(startPos);
    }

    @Override
    public int getInstructionPosition() {
        return engine.CI;
    }

    @Override
    public boolean setInstructionPosition(int i) {
        int memSize = memory.getSize();
        if (i < 0 || i >= memSize) {
            throw new IllegalArgumentException("Instruction position can be in <0," + memSize/4 +">, but was: " + i);
        }
        engine.CI = i;
        return true;
    }

}

We are approaching the end of our road. The last thing to do is to implement a status panel GUI of the CPU.

4.10. Status panel

The status panel is a Java Swing panel (class extending java.swing.JPanel). The GUI can be "drawn" in any favorite IDE, like NetBeans or IntelliJ IDEA. The status panel should show the following:

  • CPU run state

  • Internal state: registers or possibly portion of memory

  • Optionally, speed (running frequency)

The status panel is the interaction point between CPU and the user. With it, the user can be allowed to modify or view the internal status of the CPU emulator. This is very handy when learning or checking how it works, what the registers' values really are (and compare them with those shown on a display), etc.

SSEM CPU status panel will look as follows:

SSEM CPU Status panel GUI

The class code is:

ssem-cpu/src/main/java/net/sf/emustudio/ssem/cpu/CpuPanel.java
package net.sf.emustudio.ssem.cpu;

import emulib.plugins.cpu.CPU;
import emulib.plugins.memory.MemoryContext;
import emulib.runtime.NumberUtils;
import java.util.Objects;

import static emulib.runtime.RadixUtils.formatBinaryString;

public class CpuPanel extends javax.swing.JPanel {
    private final EmulatorEngine engine;
    private final Updater updater;
    private final MemoryContext<Byte> memory;

    CpuPanel(CPU cpu, EmulatorEngine engine, MemoryContext<Byte> memory) {
        this.engine = Objects.requireNonNull(engine);
        this.memory = Objects.requireNonNull(memory);
        this.updater = new Updater();

        initComponents();
        cpu.addCPUListener(updater);
        lblSpeed.setText(String.valueOf(EmulatorEngine.INSTRUCTIONS_PER_SECOND));
    }

    private final class Updater implements CPU.CPUListener {

        @Override
        public void runStateChanged(CPU.RunState rs) {
            lblRunState.setText(rs.toString().toUpperCase());
        }

        @Override
        public void internalStateChanged() {
            int acc = engine.Acc;
            int ci = engine.CI;

            Byte[] mCI = memory.readWord(ci);
            int line = NumberUtils.reverseBits(mCI[0], 8);
            Byte[] mLine = memory.readWord(line * 4);

            txtA.setText(String.format("%08x", acc));
            txtCI.setText(String.format("%08x", ci / 4));
            txtMCI.setText(String.format("%08x", NumberUtils.readInt(mCI, NumberUtils.Strategy.REVERSE_BITS)));
            txtLine.setText(String.format("%02x", line));
            txtMLine.setText(String.format("%08x", NumberUtils.readInt(mLine, NumberUtils.Strategy.REVERSE_BITS)));

            txtBinA.setText(formatBinary(acc));
            txtBinCI.setText(formatBinary(ci));
            txtBinMCI.setText(formatBinary(NumberUtils.readInt(mCI, NumberUtils.Strategy.BIG_ENDIAN)));
            txtBinLine.setText(formatBinary(line, 8));
            txtBinMLine.setText(formatBinary(NumberUtils.readInt(mLine, NumberUtils.Strategy.BIG_ENDIAN)));
        }

        private String formatBinary(int number) {
            return formatBinary(number, 32);
        }

        private String formatBinary(int number, int length) {
            return formatBinaryString(number, length, 4, true);
        }

    }

    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        javax.swing.JPanel jPanel1 = new javax.swing.JPanel();
        lblRunState = new javax.swing.JLabel();
        javax.swing.JLabel jLabel7 = new javax.swing.JLabel();
        lblSpeed = new javax.swing.JLabel();
        javax.swing.JPanel jPanel2 = new javax.swing.JPanel();
        javax.swing.JLabel jLabel2 = new javax.swing.JLabel();
        javax.swing.JLabel jLabel3 = new javax.swing.JLabel();
        txtCI = new javax.swing.JTextField();
        txtA = new javax.swing.JTextField();
        txtBinA = new javax.swing.JTextField();
        txtBinCI = new javax.swing.JTextField();
        javax.swing.JPanel jPanel3 = new javax.swing.JPanel();
        javax.swing.JLabel jLabel4 = new javax.swing.JLabel();
        javax.swing.JLabel jLabel5 = new javax.swing.JLabel();
        txtMLine = new javax.swing.JTextField();
        txtMCI = new javax.swing.JTextField();
        txtBinMCI = new javax.swing.JTextField();
        txtBinMLine = new javax.swing.JTextField();
        javax.swing.JLabel jLabel6 = new javax.swing.JLabel();
        txtLine = new javax.swing.JTextField();
        txtBinLine = new javax.swing.JTextField();

        jPanel1.setBorder(javax.swing.BorderFactory.createTitledBorder("Run control"));

        lblRunState.setFont(new java.awt.Font("Monospaced", 0, 18)); // NOI18N
        lblRunState.setForeground(new java.awt.Color(0, 153, 0));
        lblRunState.setText("BREAKPOINT");

        jLabel7.setFont(jLabel7.getFont().deriveFont(jLabel7.getFont().getStyle() | java.awt.Font.BOLD));
        jLabel7.setText("ins/s");

        lblSpeed.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N
        lblSpeed.setText("0");
        lblSpeed.setToolTipText("Speed");

        javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1);
        jPanel1.setLayout(jPanel1Layout);
        jPanel1Layout.setHorizontalGroup(
            jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel1Layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(lblRunState)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                .addComponent(lblSpeed)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jLabel7)
                .addContainerGap())
        );
        jPanel1Layout.setVerticalGroup(
            jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel1Layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(lblRunState)
                    .addComponent(jLabel7)
                    .addComponent(lblSpeed))
                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
        );

        jPanel2.setBorder(javax.swing.BorderFactory.createTitledBorder("Registers"));

        jLabel2.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N
        jLabel2.setText("A");
        jLabel2.setToolTipText("Accumulator");

        jLabel3.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N
        jLabel3.setText("CI");
        jLabel3.setToolTipText("Control Instruction");

        txtCI.setEditable(false);
        txtCI.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N
        txtCI.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
        txtCI.setText("0");

        txtA.setEditable(false);
        txtA.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N
        txtA.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
        txtA.setText("0");

        txtBinA.setEditable(false);
        txtBinA.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N
        txtBinA.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
        txtBinA.setText("0000 0000  0000 0000  0000 0000  0000 0000");

        txtBinCI.setEditable(false);
        txtBinCI.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N
        txtBinCI.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
        txtBinCI.setText("0000 0000  0000 0000  0000 0000  0000 0000");

        javax.swing.GroupLayout jPanel2Layout = new javax.swing.GroupLayout(jPanel2);
        jPanel2.setLayout(jPanel2Layout);
        jPanel2Layout.setHorizontalGroup(
            jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel2Layout.createSequentialGroup()
                .addGap(47, 47, 47)
                .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jLabel3, javax.swing.GroupLayout.Alignment.TRAILING)
                    .addComponent(jLabel2))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
                    .addComponent(txtA, javax.swing.GroupLayout.DEFAULT_SIZE, 81, Short.MAX_VALUE)
                    .addComponent(txtCI))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(txtBinCI)
                    .addComponent(txtBinA))
                .addContainerGap())
        );
        jPanel2Layout.setVerticalGroup(
            jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel2Layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(jLabel2)
                    .addComponent(txtA, javax.swing.GroupLayout.PREFERRED_SIZE, 20, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(txtBinA, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(jLabel3)
                    .addComponent(txtCI, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(txtBinCI, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
        );

        jPanel3.setBorder(javax.swing.BorderFactory.createTitledBorder("Memory snippet"));

        jLabel4.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N
        jLabel4.setText("M[CI]");
        jLabel4.setToolTipText("Control Instruction");

        jLabel5.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N
        jLabel5.setText("M[line]");
        jLabel5.setToolTipText("Control Instruction");

        txtMLine.setEditable(false);
        txtMLine.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N
        txtMLine.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
        txtMLine.setText("0");

        txtMCI.setEditable(false);
        txtMCI.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N
        txtMCI.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
        txtMCI.setText("0");

        txtBinMCI.setEditable(false);
        txtBinMCI.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N
        txtBinMCI.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
        txtBinMCI.setText("0000 0000  0000 0000  0000 0000  0000 0000");

        txtBinMLine.setEditable(false);
        txtBinMLine.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N
        txtBinMLine.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
        txtBinMLine.setText("0000 0000  0000 0000  0000 0000  0000 0000");

        jLabel6.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N
        jLabel6.setText("line");
        jLabel6.setToolTipText("Control Instruction");

        txtLine.setEditable(false);
        txtLine.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N
        txtLine.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
        txtLine.setText("0");

        txtBinLine.setEditable(false);
        txtBinLine.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N
        txtBinLine.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
        txtBinLine.setText("0000 0000");

        javax.swing.GroupLayout jPanel3Layout = new javax.swing.GroupLayout(jPanel3);
        jPanel3.setLayout(jPanel3Layout);
        jPanel3Layout.setHorizontalGroup(
            jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel3Layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(jPanel3Layout.createSequentialGroup()
                        .addComponent(jLabel5)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(txtMLine, javax.swing.GroupLayout.PREFERRED_SIZE, 81, javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(txtBinMLine))
                    .addGroup(jPanel3Layout.createSequentialGroup()
                        .addGap(14, 14, 14)
                        .addGroup(jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
                            .addComponent(jLabel6)
                            .addComponent(jLabel4))
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addGroup(jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addGroup(jPanel3Layout.createSequentialGroup()
                                .addComponent(txtMCI, javax.swing.GroupLayout.PREFERRED_SIZE, 81, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                .addComponent(txtBinMCI))
                            .addGroup(jPanel3Layout.createSequentialGroup()
                                .addComponent(txtLine, javax.swing.GroupLayout.PREFERRED_SIZE, 81, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                .addComponent(txtBinLine)))))
                .addContainerGap())
        );
        jPanel3Layout.setVerticalGroup(
            jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel3Layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(jLabel4)
                    .addComponent(txtMCI, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(txtBinMCI, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(jLabel6)
                    .addComponent(txtLine, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(txtBinLine, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(jLabel5)
                    .addComponent(txtMLine, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(txtBinMLine, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
        );

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
        this.setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jPanel2, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(jPanel1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(jPanel3, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jPanel2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jPanel3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
        );
    }// </editor-fold>//GEN-END:initComponents


    private javax.swing.JLabel lblRunState;
    private javax.swing.JLabel lblSpeed;
    private javax.swing.JTextField txtA;
    private javax.swing.JTextField txtBinA;
    private javax.swing.JTextField txtBinCI;
    private javax.swing.JTextField txtBinLine;
    private javax.swing.JTextField txtBinMCI;
    private javax.swing.JTextField txtBinMLine;
    private javax.swing.JTextField txtCI;
    private javax.swing.JTextField txtLine;
    private javax.swing.JTextField txtMCI;
    private javax.swing.JTextField txtMLine;
    // End of variables declaration//GEN-END:variables
}

We don’t have to care about method initComponents() and the fields at the end of the class. Those are generated by the NetBeans by its GUI designer. I have included it just because the overall look and how the variables - text fields etc. are named.

The only thing which should grasp our attention is the nested Updater class. The class implements the mechanism of updating values of the GUI. The mechanism is the observer pattern, as you might have recognized. The updater implements CPU.CPUListener interface, with two methods. The runStateChanged() method is called by the CPU when the run state has changed. The argument is the new run state. The second method, internalStateChanged() is called by CPU always when the internal state of a CPU has changed - ie. values of registers. When the CPU is in running state, this method is not called for performance reasons.

Don’t forget to register the updater by calling cpu.addCPUListener(). The proper way upon shutting down should be to remove it, but the class AbstractCPU will take care about it.

Now we need to incorporate the panel into the main class. It’s easy:

src/main/java/net/sf/emustudio/ssem/cpu/CpuImpl.java
public class CpuImpl extends AbstractCPU {

    @Override
    public JPanel getStatusPanel() {
        return new CpuPanel(this, engine);
    }

}

4.11. Automatic emulation

The optional step is to change a behavior slightly when user runs the automatic emulation. Why here? Why not in e.g. CRT display or other plug-in? To answer this question, let’s think a bit.

Automatic emulation exists to suppress interaction with user and perform the whole emulation - from compilation to running the program - automatically. The important output is usually redirected to a file; so as the required input is read from file, instead asking the user for it. User then can check the output separately.

Usually some terminal input/output is redirected in case of automatic emulation. For example, LSI ADM-3A emulator redirects input from file adm3a-terminal.in and output to adm3a-terminal.out.

But for SSEM - what output is important enough to be put in a file in case of automatic emulation? Well, the answer is clear - content of the memory, which is not big - just 32 rows. In addition, it will be useful to see the content of the accumulator and CI register after the emulation finishes. Plug-in which has easy access to the memory, to the registers and knows the emulation state is CPU. Therefore, we implement the automation support here.

After each emulation "stop" - no matter the reason of stopping, if before the emulation was running, we want to perform a "snapshot" of the emulator state - registers Acc, CI and memory content. This snapshot will be then written to a file called ssem.out.

At first, let’s implement the class:

src/main/java/net/sf/emustudio/ssem/cpu/AutomaticEmulation.java
package net.sf.emustudio.ssem.cpu;

import emulib.plugins.cpu.CPU;
import emulib.plugins.memory.MemoryContext;
import emulib.runtime.NumberUtils;
import emulib.runtime.RadixUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Objects;

class AutomaticEmulation {
    private final static Logger LOGGER = LoggerFactory.getLogger(AutomaticEmulation.class);
    private final static String SSEM_FILE_NAME = "ssem.out";

    private final MemoryContext<Byte> memory;
    private final CPU cpu;
    private final EmulatorEngine engine;
    private final CPU.CPUListener listener;

    private volatile boolean waitingForStop = false;

    AutomaticEmulation(CPU cpu, EmulatorEngine engine, MemoryContext<Byte> memory) {
        this.memory = Objects.requireNonNull(memory);
        this.engine = Objects.requireNonNull(engine);
        this.cpu = Objects.requireNonNull(cpu);

        listener = new CPU.CPUListener() {
            @Override
            public void runStateChanged(CPU.RunState runState) {
                if (runState == CPU.RunState.STATE_RUNNING) {
                    waitingForStop = true;
                } else if (waitingForStop) { // runState != STATE_RUNNING
                    waitingForStop = false;
                    snapshot();
                }
            }

            @Override
            public void internalStateChanged() {

            }
        };

        cpu.addCPUListener(listener);
    }

    void destroy() {
        cpu.removeCPUListener(listener);
    }

    private void snapshot() {
        Byte[][] memorySnapshot = new Byte[memory.getSize() / 4][4];

        for (int i = 0; i < memorySnapshot.length; i++) {
            Byte[] word = memory.readWord(i * 4);
            System.arraycopy(word, 0, memorySnapshot[i], 0, 4);
        }

        int ciSnapshot = engine.CI;
        int accSnapshot = engine.Acc;

        saveSnapshot(ciSnapshot, accSnapshot, memorySnapshot);
    }

    private void saveSnapshot(int ciSnapshot, int accSnapshot, Byte[][] memorySnapshot) {
        try(OutputStream out = new FileOutputStream(SSEM_FILE_NAME)) {
            try(PrintWriter writer = new PrintWriter(out)) {

                writer.println("ACC=0x" + Integer.toHexString(accSnapshot));
                writer.println("CI=0x" + Integer.toHexString(ciSnapshot));
                writer.println();

                writer.println("   L L L L L 5 6 7 8 9 0 1 2 I I I 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1");
                for (int i = 0; i < memorySnapshot.length; i++) {
                    int number = NumberUtils.readInt(memorySnapshot[i], NumberUtils.Strategy.BIG_ENDIAN);
                    String binary = RadixUtils.formatBinaryString(number, 32, 0, true);
                    writer.println(String.format("%02d %s", i, binary.replaceAll("0","  ").replaceAll("1","* ")));
                }
            }
        } catch (IOException e) {
            LOGGER.error("Could not snapshot SSEM state", e);
        }
    }

}

Then, the class should be incorporated to the main class:

src/main/java/net/sf/emustudio/ssem/cpu/CpuImpl.java
public class CpuImpl extends AbstractCPU {
    ...

    private Optional<AutomaticEmulation> automaticEmulation = Optional.empty();

    ...

    public void initialize(SettingsManager settingsManager) throws PluginInitializationException {
        ...

        boolean auto = Boolean.parseBoolean(settingsManager.readSetting(getPluginID(), SettingsManager.AUTO));
        if (auto) {
            automaticEmulation = Optional.of(new AutomaticEmulation(this, engine, memory));
        }
    }

    ...
}

And that’s it. If we run the emulator with the command line:

java -jar emuStudio.jar --config SSEM --nogui --auto --input examples/as-ssem/the-fraj.ssem

the emulation will run without user interaction, and file ssem.out will be created with the following content:

ACC=0x0
CI=0x8

   L L L L L 5 6 7 8 9 0 1 2 I I I 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
00
01 * * *   *                   *                         * * *   *
02   *
03   * *   *                     *
04   *     *                 * *
05   *     *                   *
06   *     *                     *
07 * * *   *                 * *
08 *                           *
09   *     *                 * *
10   *     *                   *
11     *   *                     *
12                             * *
13 * *     *                   *
14 *                         * *
15 *   *   *                     *
16 * * *                     * *
17
18
19 *     *   * * * * * * * * *   * * * * * * * * * * * *       *
20 * * * * * * * * * * * * * * * * * * * * * * * * * * *   * * * *
21                           * * * * * * * * * * * * * * * * * * *
22 *
23   * * *   *   *   * * *   * * *   * * *       * *     * * * *
24     *     *   *   *       *       *     *   *     *       *
25     *     *   *   *       *       *     *   *     *       *
26     *     * * *   * *     * *     * * *     * * * *       *
27     *     * * *   * *     * *     * * *     * * * *       *
28     *     *   *   *       *       *   *     *     *       *
29     *     *   *   *       *       *     *   *     *   *   *
30     *     *   *   * * *   *       *     *   *     *     *
31

4.12. Testing

Now you have implemented complete CPU emulator. It should work. Should. But how do we now until we try? Every program can have bugs. And most likely it does. It is crucial for CPU emulator to work literally exactly as the real CPU. With little playing we can’t test all instructions, all their variants and check all possible inputs. This must be done systematically.

In languages which have mutable state ("inpure languages"), like Java, it is quite hard to reason about the correctness. There are some ways, but instead of formal reasoning became very popular a technique called automated testing. There exist several levels of automated tests. Those which are usually placed very close to the source code of the project, and which tests a single "unit" - the smallest entity - are called unit tests.

In object oriented languages, unit tests should test production classes and their behavior in the isolated environment. Each unit test is also a class. Maven uses standard path where the unit tests should be put.

Testing of SSEM CPU is left as an exercise for the plug-in developer.

5. Writing a Device

This tutorial will describe how to implement a virtual device to be used in emuStudio. The tutorial will focus on implementing simple CRT display for SSEM computer. The main focus is put on how to use emuLib API in a virtual device project so it can be used in emuStudio.

5.1. Getting started

Before reading on, please read the Introduction chapter. It gives the information needed for setting up the development environment and for basic understanding how the emuStudio/plug-ins lifecycle work.

In this tutorial we will implement a CRT display for our SSEM computer. It will not completely mimic the real "monitor" interface with switches and everything, but it will just display the content of the memory. There exist several good sources about how the real monitor looked like; and with many additional details - just to mention few:

As we know from Writing a memory, SSEM memory is in fact a 32x32 grid of bits. A memory cell has 4 bytes = 32 bits. So we have 32 rows or memory cells in the memory, each of size 4 bytes.

Next, the number stored in memory is "reversed" when compared to current x86 numbers representation in memory. It means, that LSB and MSB were switched. What’s more, SSEM used two’s complement to represent negative numbers.

With that information, we are able to create the display. For the simplicity, the display will be just a black canvas with the grid of squares - bits - filled with different color - based on whether the corresponding bit is 1 or 0.

The display should be able to "listen" for memory changes and re-paint itself to be up to date with the current state of the SSEM memory.

Besides displaying, there will be no other interaction from user.

5.2. Preparing the environment

In order to start developing the device, create new Java project. Here, Maven will be used for dependencies management. The plug-in will be implemented as another standard emuStudio plug-in, so it will inherit Maven plug-in dependencies from the main POM file.

The project should be located at emuStudio/plugins/devices/ssem-display, and should contain the following structure:

src/
  main/
    java/
    resources/
test/
  java/
pom.xml
Note the naming of the plug-in. It follows the naming convention as described in the Naming conventions guide.

The POM file of the project might look as follows:

ssem-display/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>net.sf.emustudio</groupId>
    <artifactId>emustudio-parent</artifactId>
    <version>0.39</version>
    <relativePath>../../..</relativePath>
  </parent>

  <artifactId>ssem-display</artifactId>
  <packaging>jar</packaging>

  <name>SSEM CRT Display</name>
  <description>CRT Display for SSEM computer</description>

  <build>
    <finalName>ssem-display</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <configuration>
          <archive>
            <manifest>
              <addClasspath>false</addClasspath>
              <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
              <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
            </manifest>
          </archive>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-dependency-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

  <dependencies>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
    </dependency>
    <dependency>
      <groupId>net.sf.emustudio</groupId>
      <artifactId>emuLib</artifactId>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
    </dependency>
    <dependency>
      <groupId>org.easymock</groupId>
      <artifactId>easymock</artifactId>
    </dependency>
  </dependencies>
</project>

And let’s start with the first Java class, the main plug-in class. Let’s put it to package net.sf.emustudio.ssem.display, and call it DisplaySSEM.

5.3. The main class

Go to the DeviceSSEM class source. Extend the class from emulib.plugins.devices.AbstractDevice class. The abstract class extends from emulib.plugins.devices.Device interface and implements the most common methods, usable by all devices.

It is also necessary to annotate the class with emulib.annotations.PluginType annotation, which is required for every main class of any emuStudio plug-in. The code snippet looks as follows:

src/main/java/net/sf/emustudio/ssem/display/DisplaySSEM.java
package net.sf.emustudio.ssem.display;

import emulib.annotations.PLUGIN_TYPE;
import emulib.annotations.PluginType;
import emulib.plugins.memory.AbstractDevice;
import emulib.runtime.ContextPool;

@PluginType(
        type = PLUGIN_TYPE.DEVICE,
        title = "SSEM CRT display",
        copyright = "\u00A9 Copyright 2006-2017, Peter Jakubčo",
        description = "CRT display for SSEM computer."
)
public class DisplaySSEM extends AbstractDevice {
    private final static Logger LOGGER = LoggerFactory.getLogger(DisplaySSEM.class);

    public DisplaySSEM(Long pluginID, ContextPool contextPool) {
        super(pluginID);
    }

    // ... other methods ...
}
The constructor presented here is mandatory. This is one of the behavioral contracts, emuStudio expects that a plug-in will have a constructor with two arguments: pluginID (assigned by emuStudio), and a context pool, which is a storage or registrar of all plug-ins contexts.

In the initialization phase (see Phase 2 - Initialization), we need to obtain SSEM memory, which will be used as the source of information which bits are "turned on":

src/main/java/net/sf/emustudio/ssem/display/DisplaySSEM.java
public class DisplaySSEM extends AbstractDevice {
    ...
    private final ContextPool contextPool;

    public DisplaySSEM(Long pluginID, ContextPool contextPool) {
        super(pluginID);
        this.contextPool = Objects.requireNonNull(contextPool);
    }

    @Override
    public void initialize(SettingsManager settings) throws PluginInitializationException {
        super.initialize(settings);
        MemoryContext<Byte> memory = contextPool.getMemoryContext(pluginID, MemoryContext.class);

        if (memory.getDataType() != Byte.class) {
            throw new PluginInitializationException(this, "Expected Byte memory cell type!");
        }
    }

    @Override
    public void showSettings() {
        // we don't have settings GUI
    }

    @Override
    public boolean isShowSettingsSupported() {
        return false;
    }

    @Override
    public void showGUI() {
        // TODO!
    }

    ...
}

At first - notice that in the constructor we are not registering any device context. It means that the device does not provide any interaction with other plug-ins. It is however possible to do it as for any other plug-in.

However, it is not the case the opposite way - the device can (and must) use memory to obtain its contents. It is very possible to get the memory context from the context pool. This is done in the initialization phase, so it is clear that contextPool is loaded with all available contexts from other plug-ins.

It is a good practice to check whether the data type of the memory cells is as we expect; unfortunately in Java the generics information does not differentiate a type so we need to do it manually.

Now notice there are two methods dealing with GUI. The first one is showGUI() and the second one is showSettings(), which creates a pair with isShowSettingsSupported(). In emuStudio, each device plug-in can have a "main GUI window", which is used primarily for the interaction with user. On the other hand, as it could be noticed in other plug-in types, each plug-in can have its own "settings" window, which shows specific settings of a plug-in. It is also the case for the device plug-ins.

5.4. The GUI

We are nowon the best way to implement the GUI of the dislpay. As it was the case for the SSEM memory GUI, the display will also use a javax.swing.JDialog window for displaying the GUI. Next, the window will contain the canvas - a javax.swing.JPanel - which will paint the grid with the squares. In order to do that, we need to create our own version of JPanel.

For better description about how painting of Swing components works, please see this tutorial.

Before that, let’s show how we want the result to look like:

SSEM Display GUI sample look

5.4.1. Display panel

And now we are ready for the source code of the DisplayPanel:

src/main/java/net/sf/emustudio/ssem/display/DisplayPanel.java
package net.sf.emustudio.ssem.display;

import emulib.runtime.NumberUtils;
import net.jcip.annotations.ThreadSafe;

import javax.swing.JPanel;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.util.stream.Collectors;

@ThreadSafe
public class DisplayPanel extends JPanel {
    private final static int PIXEL_SIZE = 10;
    private final static int PIXEL_SIZE_PLUS_GAP = PIXEL_SIZE + 2;
    private final static int CELL_SIZE = 32;
    private final static int ROWS = 32;

    private final boolean[][] memory = new boolean[CELL_SIZE][CELL_SIZE];

    DisplayPanel() {
        super.setBackground(Color.BLACK);
        super.setDoubleBuffered(true);
    }

    void writeRow(Byte[] value, int row) {
        int number = NumberUtils.readInt(value, NumberUtils.Strategy.BIG_ENDIAN);
        Boolean[] bits = String.format("%" + CELL_SIZE + "s", Integer.toBinaryString(number)).chars()
                .mapToObj(c -> c == '1')
                .collect(Collectors.toList())
                .toArray(new Boolean[0]);

        for (int i = 0; i < ROWS; i++) {
            memory[row][i] = bits[i];
        }
        repaint();
    }

    void clear() {
        for (boolean[] memoryRow : memory) {
            for (int j = 0; j < memoryRow.length; j++) {
                memoryRow[j] = false;
            }
        }
        repaint();
    }

     @Override
    public void paintComponent(Graphics g) {
        Dimension size = getSize();
        int startX = size.width / 2 - (CELL_SIZE / 2) * PIXEL_SIZE_PLUS_GAP - PIXEL_SIZE;
        int startY = size.height / 2 - (ROWS / 2) * PIXEL_SIZE_PLUS_GAP;

        g.setColor(Color.BLACK);
        g.fillRect(0, 0, size.width, size.height);

        for (int i = 0; i < memory.length; i++) {
            for (int j = 0; j < memory[i].length; j++) {
                if (memory[i][j]) {
                  g.setColor(Color.GREEN);
                  g.fillRect(
                          startX + j * PIXEL_SIZE_PLUS_GAP,
                          startY + i * PIXEL_SIZE_PLUS_GAP,
                          PIXEL_SIZE,
                          PIXEL_SIZE
                  );
                } else {
                  g.setColor(Color.DARK_GRAY);
                  g.fillRect(
                          startX + j * PIXEL_SIZE_PLUS_GAP,
                          startY + i * PIXEL_SIZE_PLUS_GAP,
                          PIXEL_SIZE,
                          PIXEL_SIZE
                  );
                }
            }
        }
    }

}

At first, notice that the display panel has its own memory - we can call it "video memory". It is absolutely not related to the real hardware, becasuse SSEM didn’t have this thing. I decided to introduce the video memory because when painting, which will occur in UI thread - and often!, don’t have to interact with the real SSEM memory, accessed also by the CPU, in emulation thread. So painting method - the paintComponent() - is using this vide memory to ask whether the bit - or the square - should be green or black, based on whether the memory bit is 1 or 0. Also this fact - bit representation in video memory - is a bit different. Instead of numbers 1 or 0 we store booleans, which better corresponds to a two-value options.

Except the paintComponent(), we can see there to be a writeRow() and clear() methods. The writeRow() method will be called by a memory listener, which is not now defined. The idea is that when a byte in the SSEM memory changes, the listener will be notified about the change, which will call the writeRow as the consequence.

It means that we will update the whole row - memory cell - even if only part of it had changed. The decision about this detail is simplicity, the performance can be improved if we update only specific bits.

Method clear() will erase the video memory.

5.4.2. GUI window

As it was said already, we need to implement a JDialog which will contain the display panel. The source code for the dialog is as follows:

src/main/java/net/sf/emustudio/ssem/display/DisplayDialog.java
package net.sf.emustudio.ssem.display;

import emulib.plugins.memory.Memory;
import emulib.plugins.memory.MemoryContext;

import javax.swing.JDialog;
import java.util.Objects;

class DisplayDialog extends JDialog {
    private final MemoryContext<Byte> memory;
    private final DisplayPanel displayPanel;

    DisplayDialog(MemoryContext<Byte> memory) {
        this.memory = Objects.requireNonNull(memory);
        this.displayPanel = new DisplayPanel();

        super.setLocationRelativeTo(null);
        initComponents();

        scrollPane.setViewportView(displayPanel);

        initListener();
    }

    private void initListener() {
        memory.addMemoryListener(new Memory.MemoryListener() {
            @Override
            public void memoryChanged(int bytePosition) {
                if (bytePosition == -1) {
                    reset();
                } else {
                    int row = bytePosition / 4;
                    displayPanel.writeRow(memory.readWord(row * 4), row);
                }
            }

            @Override
            public void memorySizeChanged() {
                // never happens
            }
        });
    }

    void reset() {
        displayPanel.clear();
        for (int i = 0; i < 4 * 32; i += 4) {
            displayPanel.writeRow(memory.readWord(i), i / 4);
        }
    }

    /**
     * This method is called from within the constructor to initialize the form. WARNING: Do NOT modify this code. The
     * content of this method is always regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        scrollPane = new javax.swing.JScrollPane();

        setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
        setTitle("SSEM CRT Display");

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(scrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 432, Short.MAX_VALUE)
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(scrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 416, Short.MAX_VALUE)
                .addContainerGap())
        );

        pack();
    }// </editor-fold>//GEN-END:initComponents


    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JScrollPane scrollPane;
    // End of variables declaration//GEN-END:variables
}

In the constructor you can notice that we add the memory listener to the memory which is responsible for updating the display, as was explained in the previous section.

Also, the interesting method is reset(), which causes to at first - clearing the display and then loading it with new content - by copying the whole memory into the video memory of the display.

5.5. Wrapping up

The last step is to finish the main class. We need to include and show the display when emuStudio asks for it:

src/main/java/net/sf/emustudio/ssem/display/DisplaySSEM.java
package net.sf.emustudio.ssem.display;

import emulib.annotations.PLUGIN_TYPE;
import emulib.annotations.PluginType;
import emulib.emustudio.SettingsManager;
import emulib.plugins.device.AbstractDevice;
import emulib.plugins.memory.MemoryContext;
import emulib.runtime.ContextPool;
import emulib.runtime.exceptions.PluginInitializationException;

import java.util.MissingResourceException;
import java.util.Objects;
import java.util.Optional;
import java.util.ResourceBundle;

@PluginType(
        type = PLUGIN_TYPE.DEVICE,
        title = "SSEM CRT display",
        copyright = "\u00A9 Copyright 2006-2017, Peter Jakubčo",
        description = "CRT display for SSEM computer."
)
@SuppressWarnings("unused")
public class DisplaySSEM extends AbstractDevice {
    private boolean nogui;
    private final ContextPool contextPool;
    private Optional<DisplayDialog> display = Optional.empty();

    public DisplaySSEM(Long pluginID, ContextPool contextPool) {
        super(pluginID);
        this.contextPool = Objects.requireNonNull(contextPool);
    }

    @Override
    public String getVersion() {
        try {
            ResourceBundle bundle = ResourceBundle.getBundle("net.sf.emustudio.ssem.display.version");
            return bundle.getString("version");
        } catch (MissingResourceException e) {
            return "(unknown)";
        }
    }

    @Override
    public void initialize(SettingsManager settings) throws PluginInitializationException {
        super.initialize(settings);
        MemoryContext<Byte> memory = contextPool.getMemoryContext(pluginID, MemoryContext.class);

        if (memory.getDataType() != Byte.class) {
            throw new PluginInitializationException(this, "Expected Byte memory cell type!");
        }

        String s = settings.readSetting(pluginID, SettingsManager.NO_GUI);
        nogui = (s != null) && s.toUpperCase().equals("TRUE");

        if (!nogui) {
            display = Optional.of(new DisplayDialog(memory));
        }
    }

    @Override
    public void reset() {
        display.ifPresent(DisplayDialog::reset);
    }

    @Override
    public void destroy() {
        display.ifPresent(DisplayDialog::dispose);
    }

    @Override
    public void showGUI() {
        display.ifPresent(displayDialog -> displayDialog.setVisible(true));
    }

    @Override
    public void showSettings() {
        // we don't have settings GUI
    }

    @Override
    public boolean isShowSettingsSupported() {
        return false;
    }
}

Notice the method initialize() - we added a check whether we are in a no-GUI mode. If yes, we should ignore all requests for showing the GUI. Otherwise, we will create the display GUI right away, and only once.

The method showGUI() will then make the GUI visible - show it.

Now we have finished the last piece of the SSEM computer emulator and it is ready for run.


1. For example, non terminal Instruction Statement; in the gramamr above defines a non-terminal Statement, which should return an instance of Instruction class. The class Instruction must be implemented manually - it is part of AST; there are no special requirements for the implementation.
2. The error code should be defined by you, developer, if you want. It is a convention used also in other compilers that specific error has assigned a unique number. In our compiler, we do not use it.
4. The code might seem generally complicated and bloated with a boiler-plate. I personally blame Java Swing or Java itself for it, since it is a "corporate" language, usually used for different purposes than writing an emulator :)
5. See Writing a CPU tutorial for more information about Edigen