Skip to main content

ternaria_os/
cli.rs

1//! The command-line front end: boots the operating system against a real
2//! terminal.
3//!
4//! This lives in the library rather than in a binary so that more than one
5//! entry point can reach it. There is one inside this workspace and one in the
6//! launcher package alongside the repositories, and neither should hold a
7//! second copy of the logic.
8//!
9//! ```text
10//!   cargo run                  a fresh disk with a few files on it
11//!   cargo run -- disk.tfs      that image, created if it does not exist
12//!   cargo run -- --format d    a fresh image written to d, overwriting it
13//! ```
14//!
15//! The image is written back when the shell exits, so anything created inside
16//! the machine is still there next time. Leave with `exit` or Ctrl-D; Ctrl-C
17//! kills the host before it can write.
18
19use std::io::{BufRead, Write};
20
21use crate::source;
22use ternaria_arith::{Tryte, Word};
23use ternaria_dev::{BlockDevice, Bus, Console, Timer};
24use ternaria_fs::Builder;
25use ternaria_mem::Addressable;
26use ternaria_vm::assemble_and_load;
27
28/// Blocks in a freshly formatted disk.
29const DISK_BLOCKS: usize = 243;
30
31/// Instructions the machine may execute before the host gives up.
32///
33/// Generous: a session is a few thousand instructions per command, and the
34/// limit exists to stop a runaway rather than to bound normal use.
35const STEP_LIMIT: u64 = 2_000_000_000;
36
37/// Runs the front end. Exits the process on failure.
38pub fn main() {
39    let mut args = std::env::args().skip(1);
40    let mut format = false;
41    let mut path: Option<String> = None;
42    for arg in args.by_ref() {
43        match arg.as_str() {
44            "--format" | "-f" => format = true,
45            "--help" | "-h" => {
46                print_help();
47                return;
48            }
49            other => path = Some(other.to_string()),
50        }
51    }
52
53    let image = match load_image(path.as_deref(), format) {
54        Ok(image) => image,
55        Err(e) => {
56            eprintln!("ternaria: {e}");
57            std::process::exit(1);
58        }
59    };
60
61    match run(image, path.as_deref()) {
62        Ok(()) => {}
63        Err(e) => {
64            eprintln!("ternaria: {e}");
65            std::process::exit(1);
66        }
67    }
68}
69
70/// Prints the usage text.
71fn print_help() {
72    println!("ternaria - a balanced ternary machine running a small operating system");
73    println!();
74    println!("  cargo run                 boot a fresh disk held in memory");
75    println!("  cargo run -- <image>      boot <image>, creating it if absent");
76    println!("  cargo run -- --format <image>");
77    println!("                            overwrite <image> with a fresh disk");
78    println!();
79    println!("Commands inside the machine:");
80    println!("  ls              list the working directory");
81    println!("  cd <name>       enter a directory");
82    println!("  cat <name>      print a file");
83    println!("  mkdir <name>    create a directory");
84    println!("  touch <name>    create an empty file");
85    println!("  write <name>    create a file from the lines that follow,");
86    println!("                  ending with a line holding only a full stop");
87    println!("  rm <name>       remove a file or an empty directory");
88    println!();
89    println!("  run <name>      load and run a program, at user level");
90    println!("  exit, logout    leave the machine");
91    println!();
92    println!("An image given on the command line is written back when the shell");
93    println!("exits, whether through `exit` or Ctrl-D. Interrupting with Ctrl-C");
94    println!("kills the host before it can write, and loses the session.");
95}
96
97/// Reads the image named, or builds a fresh one.
98fn load_image(path: Option<&str>, format: bool) -> std::io::Result<Vec<u8>> {
99    match path {
100        Some(p) if !format && std::path::Path::new(p).exists() => std::fs::read(p),
101        _ => Ok(fresh_disk()),
102    }
103}
104
105/// A formatted disk with enough on it to have something to look at.
106fn fresh_disk() -> Vec<u8> {
107    let mut b = Builder::new(DISK_BLOCKS);
108    b.write_file(
109        "/readme",
110        b"This machine computes in base three.\n\
111          Digits are -1, 0 and +1.\n\
112          \n\
113          Try: ls, cat readme, mkdir work, cd work, write notes\n",
114    )
115    .expect("the fresh disk fits");
116    b.mkdir("/docs").expect("mkdir");
117    b.write_file(
118        "/docs/trits",
119        b"A trit holds -1, 0 or +1.\nNine trits are a tryte.\n",
120    )
121    .expect("write");
122    b.finish()
123}
124
125/// Boots the machine and hands it the terminal.
126fn run(image: Vec<u8>, save_to: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
127    let mut bus = Bus::new(Console::new(), Timer::new(), BlockDevice::new(image));
128
129    // The data segment holds the shell's strings. The guest reads them as
130    // ordinary memory, so the host writes them before the machine starts.
131    place_data(&mut bus)?;
132    let mut cpu = assemble_and_load(&mut bus, Word::ZERO, &source())?;
133
134    let stdin = std::io::stdin();
135    let mut lines = stdin.lock().lines();
136    let mut emitted = 0usize;
137    let mut stopped: Option<String> = None;
138
139    loop {
140        // A trap ends the session, and the image must still be written: work
141        // done before the fault is no less real for the fault happening.
142        match cpu.step(&mut bus) {
143            Ok(true) => {}
144            Ok(false) => break,
145            Err(trap) => {
146                stopped = Some(trap.to_string());
147                break;
148            }
149        }
150        bus.tick();
151
152        // Everything the guest has printed since the last look. Doing this
153        // every step keeps the prompt appearing before the read that follows
154        // it, which is what makes the session feel like a terminal.
155        let out = bus.console.output();
156        if out.len() > emitted {
157            let mut stdout = std::io::stdout();
158            stdout.write_all(&out[emitted..])?;
159            stdout.flush()?;
160            emitted = out.len();
161        }
162
163        // The guest looked for input and found none, so it is waiting on a
164        // person. This is the console's third status value earning its keep:
165        // without it the guest could not tell waiting from ended.
166        if bus.console.take_starved() {
167            match lines.next() {
168                Some(line) => {
169                    let mut line = line?;
170                    line.push('\n');
171                    bus.console.push_input(line.as_bytes());
172                }
173                None => {
174                    // Ctrl-D. Closing the console makes the status read
175                    // negative, and the guest stops on its own.
176                    println!();
177                    bus.console.close();
178                }
179            }
180        }
181
182        if cpu.steps() > STEP_LIMIT {
183            stopped = Some("step limit reached".to_string());
184            break;
185        }
186    }
187
188    // Flush anything printed after the last check.
189    let out = bus.console.output();
190    if out.len() > emitted {
191        let mut stdout = std::io::stdout();
192        stdout.write_all(&out[emitted..])?;
193        stdout.flush()?;
194    }
195
196    if let Some(p) = save_to {
197        std::fs::write(p, bus.block.image())?;
198        eprintln!("ternaria: wrote {p}");
199    }
200    if let Some(why) = stopped {
201        eprintln!("ternaria: {why}");
202    }
203    Ok(())
204}
205
206/// Writes the shell's strings into guest memory.
207fn place_data(bus: &mut Bus) -> Result<(), ternaria_mem::MemoryError> {
208    for (at, text) in crate::data_placement() {
209        for (k, byte) in text.bytes().enumerate() {
210            bus.write_tryte(
211                Word::from_value(at + k as i64),
212                Tryte::from_value(byte as i32),
213            )?;
214        }
215        bus.write_tryte(Word::from_value(at + text.len() as i64), Tryte::ZERO)?;
216    }
217    Ok(())
218}