Skip to main content

ternaria_os/
lib.rs

1//! A command-line operating system running on tryte code.
2//!
3//! The operating system itself is [`SOURCE`], written in Ternaria assembly.
4//! This crate is the harness around it: it lays out the data segment, generates
5//! the constants the assembly needs, assembles it, and boots it against a bus.
6//!
7//! # Why the data lives here
8//!
9//! The assembler emits instructions, and an instruction stream is a poor place
10//! to hide strings: every word in it must decode. Rather than add a data
11//! directive that would then need its own alignment and addressing rules, the
12//! host writes the strings into guest memory before the machine starts and
13//! passes their addresses in as `.equ` names. The guest sees ordinary memory at
14//! ordinary addresses.
15//!
16//! # What it can do
17//!
18//! `ls`, `cd`, `cat` and `mkdir`, against a TFS image on the block device.
19
20#![forbid(unsafe_code)]
21#![warn(missing_docs)]
22
23pub mod cli;
24pub mod exec;
25
26use ternaria_arith::{Tryte, Word};
27use ternaria_dev::{BlockDevice, Bus, Console, Timer};
28use ternaria_mem::{Addressable, MemoryError};
29use ternaria_vm::{Cpu, Trap, assemble_and_load};
30
31/// The operating system, in assembly.
32pub const SOURCE: &str = include_str!("os.s");
33
34/// Where the data segment begins.
35///
36/// Above the buffers the assembly reserves and below the stack, so nothing
37/// overlaps. The addresses are deliberately far apart: allocation is sparse, so
38/// gaps cost nothing.
39pub const DATA_BASE: i64 = 50_000;
40
41/// A string the host places in guest memory before the machine starts.
42struct Datum {
43    name: &'static str,
44    text: &'static str,
45}
46
47/// Every string the operating system refers to.
48///
49/// The command names are here rather than in the assembly for the same reason
50/// the messages are, and it means the shell's vocabulary can be read in one
51/// place.
52const DATA: &[Datum] = &[
53    Datum {
54        name: "MSG_PROMPT",
55        text: "$ ",
56    },
57    Datum {
58        name: "MSG_NOSUCH",
59        text: "no such name\n",
60    },
61    Datum {
62        name: "MSG_NOTDIR",
63        text: "not a directory\n",
64    },
65    Datum {
66        name: "MSG_EXISTS",
67        text: "already exists\n",
68    },
69    Datum {
70        name: "MSG_FULL",
71        text: "no space\n",
72    },
73    Datum {
74        name: "MSG_UNKNOWN",
75        text: "unknown command\n",
76    },
77    Datum {
78        name: "MSG_NOTEMPTY",
79        text: "directory not empty\n",
80    },
81    Datum {
82        name: "MSG_CONT",
83        text: "> ",
84    },
85    Datum {
86        name: "MSG_FAULT",
87        text: "program faulted\n",
88    },
89    Datum {
90        name: "MSG_NOTEXE",
91        text: "not a program\n",
92    },
93    Datum {
94        name: "CMD_LS",
95        text: "ls",
96    },
97    Datum {
98        name: "CMD_CD",
99        text: "cd",
100    },
101    Datum {
102        name: "CMD_CAT",
103        text: "cat",
104    },
105    Datum {
106        name: "CMD_MKDIR",
107        text: "mkdir",
108    },
109    Datum {
110        name: "CMD_RM",
111        text: "rm",
112    },
113    Datum {
114        name: "CMD_TOUCH",
115        text: "touch",
116    },
117    Datum {
118        name: "CMD_WRITE",
119        text: "write",
120    },
121    Datum {
122        name: "CMD_EXIT",
123        text: "exit",
124    },
125    Datum {
126        name: "CMD_LOGOUT",
127        text: "logout",
128    },
129    Datum {
130        name: "CMD_RUN",
131        text: "run",
132    },
133];
134
135/// The address each datum is placed at, and the `.equ` line naming it.
136fn data_layout() -> (Vec<(i64, &'static str)>, String) {
137    let mut placed = Vec::new();
138    let mut equates = String::new();
139    let mut at = DATA_BASE;
140    for d in DATA {
141        placed.push((at, d.text));
142        equates.push_str(&format!(".equ {}, {}\n", d.name, at));
143        // One tryte per character plus a terminator, rounded up to a word so
144        // that nothing needs a misaligned access to reach the next string.
145        let span = (d.text.len() as i64 + 3) / 3 * 3;
146        at += span;
147    }
148    (placed, equates)
149}
150
151/// Where each string is placed in guest memory, and its text.
152///
153/// A host that drives the machine itself rather than calling [`boot`] needs
154/// this to lay the data segment out the same way.
155pub fn data_placement() -> Vec<(i64, &'static str)> {
156    data_layout().0
157}
158
159/// The complete assembler source: every prelude, then the operating system.
160pub fn source() -> String {
161    let (_, data_equates) = data_layout();
162    format!(
163        "{}{}{}{}{}{}",
164        ternaria_dev::prelude(),
165        ternaria_fs::prelude(),
166        ternaria_vm::csr::prelude(),
167        exec::prelude(),
168        data_equates,
169        SOURCE
170    )
171}
172
173/// Writes the data segment into guest memory.
174fn place_data<A: Addressable>(mem: &mut A) -> Result<(), MemoryError> {
175    let (placed, _) = data_layout();
176    for (at, text) in placed {
177        for (k, byte) in text.bytes().enumerate() {
178            mem.write_tryte(
179                Word::from_value(at + k as i64),
180                Tryte::from_value(byte as i32),
181            )?;
182        }
183        // Terminate. Every string routine stops here.
184        mem.write_tryte(Word::from_value(at + text.len() as i64), Tryte::ZERO)?;
185    }
186    Ok(())
187}
188
189/// Something that stopped the machine before the shell finished.
190#[derive(Debug)]
191pub enum BootError {
192    /// The operating system did not assemble.
193    Assembly(String),
194    /// The data segment could not be written.
195    Memory(MemoryError),
196    /// The machine trapped.
197    Trap(Trap),
198}
199
200impl std::fmt::Display for BootError {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        match self {
203            BootError::Assembly(e) => write!(f, "assembling the operating system: {e}"),
204            BootError::Memory(e) => write!(f, "writing the data segment: {e}"),
205            BootError::Trap(t) => write!(f, "the machine trapped: {t}"),
206        }
207    }
208}
209
210impl std::error::Error for BootError {}
211
212/// Boots the operating system against `image`, feeding it `input`.
213///
214/// Returns the console output and the image as it stands afterwards, so a
215/// caller can check both what was printed and what was written.
216pub fn boot(image: Vec<u8>, input: &[u8], max_steps: u64) -> Result<Session, BootError> {
217    let mut bus = Bus::new(
218        Console::with_input(input.to_vec()),
219        Timer::new(),
220        BlockDevice::new(image),
221    );
222    place_data(&mut bus).map_err(BootError::Memory)?;
223
224    let mut cpu = assemble_and_load(&mut bus, Word::ZERO, &source())
225        .map_err(|e| BootError::Assembly(e.to_string()))?;
226    cpu.run(&mut bus, max_steps).map_err(BootError::Trap)?;
227
228    Ok(Session {
229        output: bus.console.output().to_vec(),
230        image: bus.block.image().to_vec(),
231        steps: cpu.steps(),
232    })
233}
234
235/// What a completed run produced.
236pub struct Session {
237    /// Everything the shell printed.
238    pub output: Vec<u8>,
239    /// The image, including anything the shell wrote to it.
240    pub image: Vec<u8>,
241    /// How many instructions ran.
242    pub steps: u64,
243}
244
245impl Session {
246    /// The output as text, with invalid sequences replaced.
247    pub fn text(&self) -> String {
248        String::from_utf8_lossy(&self.output).into_owned()
249    }
250
251    /// The output with the prompts removed, which is usually what a test wants.
252    pub fn without_prompts(&self) -> String {
253        self.text().replace("$ ", "")
254    }
255}
256
257/// Assembles the operating system without running it.
258///
259/// A cheap check that the source is well-formed, and it reports the size.
260pub fn assemble() -> Result<usize, String> {
261    ternaria_isa::asm::assemble(&source())
262        .map(|p| p.instructions.len())
263        .map_err(|e| e.to_string())
264}
265
266/// A [`Cpu`] and [`Bus`] ready to step, for a caller that wants to drive the
267/// machine itself rather than run it to completion.
268pub fn prepare(image: Vec<u8>, input: &[u8]) -> Result<(Cpu, Bus), BootError> {
269    let mut bus = Bus::new(
270        Console::with_input(input.to_vec()),
271        Timer::new(),
272        BlockDevice::new(image),
273    );
274    place_data(&mut bus).map_err(BootError::Memory)?;
275    let cpu = assemble_and_load(&mut bus, Word::ZERO, &source())
276        .map_err(|e| BootError::Assembly(e.to_string()))?;
277    Ok((cpu, bus))
278}