Project 10: Simple Windowing Machine (SWIM)
Overview
The Simple Windowing Machine (SWIM) is, as its name implies, a simple but
functional operating system. It has the following features:
- Four distinct windows, each of which can run a program or edit a file.
- An additional window displays the process manager.
- An interpreter for a simple programming language.
- This will be provided for you to use.
- Dynamic memory allocation for user programs, with a copying garbage collector.
- A disk for persistent storage, simulated using a RAM disk
To begin this project, you need to have completed the preceding projects to at least Level 2:
Setup
Use your repository for the Bare Metal Editor
as the repository for this project. Add the following items to Cargo.toml under [dependencies]:
compiler_builtins = { version = "0.1", features = ["mem"] }
simple_interp = {git = "https://github.com/gjf2a/simple_interp"}
gc_headers = {git = "https://github.com/gjf2a/gc_headers" }
ramdisk = {git = "https://github.com/gjf2a/ramdisk"}
- Link to the
GitHub repository for your garbage collector. Use the format below, but
substitute your own repository name and URL:
gc_heap = {git = "https://github.com/gjf2a/gc_heap" }
- Link to the
GitHub repository for your file system. Again, use the format below, but
substitute your own repository name and URL:
file_system_solution = {git = "https://github.com/gjf2a/file_system_solution" }
Level 1: Incorporating the File System
- Add the following constants to
lib.rs:
const TASK_MANAGER_WIDTH: usize = 10;
const WIN_REGION_WIDTH: usize = BUFFER_WIDTH - TASK_MANAGER_WIDTH;
const MAX_OPEN: usize = 16;
const BLOCK_SIZE: usize = 256;
const NUM_BLOCKS: usize = 255;
const MAX_FILE_BLOCKS: usize = 64;
const MAX_FILE_BYTES: usize = MAX_FILE_BLOCKS * BLOCK_SIZE;
const MAX_FILES_STORED: usize = 30;
const MAX_FILENAME_BYTES: usize = 10;
- Modify the following constant from the original template:
const WIN_WIDTH: usize = (WIN_REGION_WIDTH - 3) / 2;
- Add a
FileSystem object to the SwimInterface.
- Use
open_create(), write(), and close() at startup (i.e., in the SwimInterface
default() method) to add the four files below to the file system:
- Note: I highly recommend using Rust’s raw strings to encode these.
- Each file is listed in each of the four main windows. Each file listing is
given in three columns, each of which may contain up to 10 rows.
- In each window, one of the files should be highlighted.
- If you use the left-arrow and right-arrow keys, it should cycle through the
files, highlighting the current file.
After completing Level 1, SWIM should look like this:

File Text
hello
nums
average
sum := 0
count := 0
averaging := true
while averaging {
num := input("Enter a number:")
if (num == "quit") {
averaging := false
} else {
sum := (sum + num)
count := (count + 1)
}
}
print((sum / count))
pi
sum := 0
i := 0
neg := false
terms := input("Num terms:")
while (i < terms) {
term := (1.0 / ((2.0 * i) + 1.0))
if neg {
term := -term
}
sum := (sum + term)
neg := not neg
i := (i + 1)
}
print((4 * sum))
Level 2: Program Execution
- Add another set of constants to
lib.rs:
const MAX_TOKENS: usize = 100;
const MAX_LITERAL_CHARS: usize = 15;
const STACK_DEPTH: usize = 20;
const MAX_LOCAL_VARS: usize = 10;
const HEAP_SIZE: usize = 256;
const MAX_HEAP_BLOCKS: usize = HEAP_SIZE;
- Add the following import to the top of
lib.rs:
use simple_interp::{Interpreter, InterpreterOutput, ArrayString};
- When the user hits
r when a file is highlighted, the file should run.
- To run a program, create an
Interpreter object for it using Interpreter::new().
The program text (as a &str) will be the parameter to new().
- The
Interpreter object’s type
will be Interpreter<MAX_TOKENS, MAX_LITERAL_CHARS, STACK_DEPTH, MAX_LOCAL_VARS, WIN_WIDTH, CopyingHeap<HEAP_SIZE, MAX_HEAP_BLOCKS>>
The Interpreter type is defined in the simple_interp crate.
- If you created a generational collector, use the following type instead:
Interpreter<MAX_TOKENS, MAX_LITERAL_CHARS, STACK_DEPTH, MAX_LOCAL_VARS, WIN_WIDTH, GenerationalHeap<HEAP_SIZE, MAX_HEAP_BLOCKS, 2>>
- You will need to create a data type that implements the
InterpreterOutput
trait in order to receive output from the interpreter.
- Suggestion: The data type representing each of your four windows would be a
good choice to implement this trait. Implementing a trait looks something like
this in Rust source code:
struct DataType {
/* Your data declarations go here */
}
impl InterpreterOutput for DataType {
fn print(&mut self, chars: &[u8]) {
/* Your code here */
}
}
- The only method you will need to call on an
Interpreter object is .tick(), which
expects as a parameter an object of that type that implements the InterpreterOutput
trait.
- Use the
tick() method to execute one instruction in one program.
- If any programs are running, exactly one of them should execute an instruction.
- If
tick() returns TickResult::AwaitInput, block the process
until input is available.
- Once input is available, use the
provide_input() method to send
the input to the program.
- While the file is running, if the user hits
F6, the program should immediately
stop and return to the file selector.
- Ensure that all processes have a fair opportunity to run on the CPU. Again,
only one process should run per
tick().
- The number of instructions executed by each process should be shown in the
right window, as seen below:

Additional suggestions:
- Get the programs that do not use input (
hello, nums) working before those that do use input (average, pi).
- To store the input buffer, use the
ArrayString struct from simple_interp. It won’t display anything, but it is a very convenient input buffer. Features include:
- Easy conversion to
&str.
- You can create formatted strings using
write!().
Level 3: File Editor
- The kernel should not panic under any circumstances.
- If an error occurs, the user should receive sufficient information
to understand the error and continue activity.
- When
F5 is selected, the user can enter a filename of up to 10 characters
in the top window.
- When the user types the backspace (Unicode
\u{8}), it erases the previous
character.
- When the user types the Enter key:
- An empty file with the given name is opened, created, and closed on the disk
- The filename is cleared from the top window
- The file listings in the four quadrant windows are updated to include the new file
- If you type the
e key, the highlighted file should be loaded into the editor. At this
point, we switch from file navigation to editing.
- The selected filename appears as part of the window header, after the function
key.
- SWIM opens the file and reads its contents so as to be visible. It then closes
the file.
- As with the filename editing, each keystroke appears in the appropriate window,
with the backspace operating properly as well.
- Entering
F6 uses open_create() to reopen the file. It writes the contents
of the editor’s buffer to disk, then closes the file, restoring the window to
the file selection screen.
- When the file is re-opened, from any of the windows, the changes saved earlier
should be reflected.
When creating a file named test, SWIM should look like this:

When navigating to edit the file test in the F1 buffer, it should look like this:

While editing the file in the F1 buffer, it should look like this:

After saving the file using F6 and reopening it in the F2 buffer, it should look
like this:

After closing the F2 buffer and running the program in the F3 buffer, it should look
like this:

Acknowledgement
This assignment was adapted from materials developed by
Philipp Oppermann.