Skip to content
This repository was archived by the owner on Jan 10, 2026. It is now read-only.

Commit b88d7c6

Browse files
implement proper cli
1 parent 139ed17 commit b88d7c6

File tree

5 files changed

+188
-13
lines changed

5 files changed

+188
-13
lines changed

docs/src/Home.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This documentation details the Stack with Objects and Functions programming language, an experimental stack-based reverse-polish-notation functional programming language created by kleines Filmröllchen.
44

5-
- [Source code](https://github.com/kleinesfilmroellchen/sof-language)
5+
[Source code](https://github.com/kleinesfilmroellchen/sof-language)
66

77
While the README is comprehensive on basic concepts and a good starting point for interested people (like you), the docs shall provide the most thorough information on SOF, including a full language description/specification and a from-scratch tutorial (no programming knowledge required).
88

sof-rs/Cargo.lock

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sof-rs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ edition = "2024"
55

66
[dependencies]
77
ahash = { version = "0.8", default-features = false, features = ["std"] }
8+
argh = { version = "0.1", default-features = false }
89
env_logger = { version = "0.11", default-features = false, features = [
910
"auto-color",
1011
] }

sof-rs/src/cli.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
use std::path::{Path, PathBuf};
2+
use std::str::FromStr;
3+
4+
use argh::FromArgs;
5+
6+
#[derive(FromArgs, Debug, Clone)]
7+
/// sof-rs: interpreter for the Stack with Objects and Functions Language.
8+
pub struct Args {
9+
/// run sof-rs in interactive mode.
10+
#[argh(switch, short = 'i')]
11+
interactive: bool,
12+
13+
/// code to execute (before interactive mode or source files)
14+
#[argh(option, short = 'c')]
15+
command: Option<String>,
16+
17+
/// internal debugging options, see documentation for a full list.
18+
#[argh(option, short = 'D', long = "debug-opt")]
19+
pub(crate) debug_options: Vec<DebugOption>,
20+
21+
/// path to library
22+
#[argh(option, short = 'l', default = "Path::new(\"../lib\").into()")]
23+
pub(crate) library_path: PathBuf,
24+
25+
/// SOF source file to execute. (Root module.)
26+
#[argh(positional)]
27+
pub(crate) input: Option<PathBuf>,
28+
}
29+
30+
impl Args {
31+
/// Determines whether a REPL should be opened. The rules are:
32+
/// - if --interactive, always open REPL
33+
/// - otherwise, only open REPL if neither input file nor inline command was used.
34+
pub fn should_open_repl(&self) -> bool {
35+
self.interactive || (!self.input.is_some() && !self.command.is_some())
36+
}
37+
38+
/// Configure the logger with the options that control logging behavior.
39+
pub fn configure_env_logger(&self, builder: &mut env_logger::Builder) {
40+
let mut should_apply_default = true;
41+
for log_level in self.debug_options.iter().filter_map(|f| match f {
42+
DebugOption::LogLevel(log_level) => Some(log_level),
43+
_ => None,
44+
}) {
45+
log_level.configure_env_logger(builder);
46+
should_apply_default = false;
47+
}
48+
49+
if should_apply_default {
50+
builder.filter_level(log::LevelFilter::Info);
51+
}
52+
}
53+
}
54+
55+
#[derive(Clone, Debug)]
56+
pub enum DebugOption {
57+
/// Outputs serialized version of stack after execution, which can be used as a snapshot for faster module loading
58+
/// or integrating into the binary.
59+
ExportSnapshot { target_filename: PathBuf },
60+
/// Sets interpreter log level.
61+
LogLevel(LogLevel),
62+
}
63+
64+
#[derive(Clone, Copy, Debug)]
65+
pub enum LogLevel {
66+
/// All logging, even default logging, disabled.
67+
Off,
68+
/// Enable debug logging in sof-rs.
69+
SelfDebug,
70+
/// Enable debug logging everywhere.
71+
AllDebug,
72+
/// Enable tracing in sof-rs (no change to external debug logging).
73+
Trace,
74+
}
75+
76+
impl LogLevel {
77+
pub fn configure_env_logger(&self, builder: &mut env_logger::Builder) {
78+
match self {
79+
Self::Off => builder.filter_level(log::LevelFilter::Off),
80+
Self::SelfDebug => builder.filter_module("sof", log::LevelFilter::Debug),
81+
Self::AllDebug => builder.filter_level(log::LevelFilter::Debug),
82+
Self::Trace => builder.filter_module("sof", log::LevelFilter::Trace),
83+
};
84+
}
85+
}
86+
87+
impl FromStr for DebugOption {
88+
type Err = String;
89+
90+
fn from_str(s: &str) -> Result<Self, Self::Err> {
91+
let Some((option, value)) = s.split_once('=') else {
92+
return Err(format!("invalid debug option {s}, should have format `key=value`"));
93+
};
94+
match option {
95+
"export-snapshot" => Ok(Self::ExportSnapshot { target_filename: Path::new(value).to_owned() }),
96+
"log" => Ok(Self::LogLevel(value.parse()?)),
97+
_ => Err(format!("invalid debug option {option}")),
98+
}
99+
}
100+
}
101+
102+
impl FromStr for LogLevel {
103+
type Err = String;
104+
105+
fn from_str(s: &str) -> Result<Self, Self::Err> {
106+
Ok(match s {
107+
"off" => Self::Off,
108+
"self-debug" => Self::SelfDebug,
109+
"all-debug" => Self::AllDebug,
110+
"trace" => Self::Trace,
111+
_ => return Err(format!("invalid log configuration: {s}")),
112+
})
113+
}
114+
}

sof-rs/src/main.rs

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ use miette::{NamedSource, miette};
1212
use rustyline::Config;
1313
use rustyline::error::ReadlineError;
1414

15+
use crate::cli::{Args, DebugOption};
1516
use crate::runtime::StackArena;
1617
use crate::runtime::interpreter::{new_arena, run, run_on_arena};
1718

1819
mod arc_iter;
20+
mod cli;
1921
mod error;
2022
mod identifier;
2123
mod lib;
@@ -77,36 +79,42 @@ fn main() -> miette::Result<()> {
7779
.build(),
7880
)
7981
}))?;
80-
env_logger::Builder::from_default_env().filter_level(log::LevelFilter::Info).init();
82+
let args: Args = argh::from_env();
83+
84+
let mut builder = env_logger::Builder::new();
85+
builder.parse_default_env();
86+
args.configure_env_logger(&mut builder);
87+
builder.init();
8188

8289
let cwd = std::env::current_dir().map_err(|err| {
8390
miette! {
8491
code = "IOError",
8592
"could not determine current directory: {err}"
8693
}
8794
})?;
88-
let library_path = cwd.join("../lib").canonicalize().map_err(|err| {
95+
let library_path = cwd.join(&args.library_path).canonicalize().map_err(|err| {
8996
miette! {
9097
code = "IOError",
91-
"could not determine standard library path: {err}"
98+
"standard library path {} invalid: {err}", args.library_path.display()
9299
}
93100
})?;
94101

95-
if let Some(filename) = std::env::args().nth(1) {
96-
let readable_filename = file_name_for(Path::new(filename.as_str()));
102+
if let Some(filename) = &args.input {
103+
let readable_filename = file_name_for(filename);
97104
info!("sof version {} (sof-rs), main file is {readable_filename}", env!("CARGO_PKG_VERSION"),);
98105
let code = std::fs::read_to_string(&readable_filename).map_err(|err| {
99106
miette! {
100107
code = "IOError",
101108
"could not read source file {readable_filename}: {err}"
102109
}
103110
})?;
104-
let result = sof_main(&code, Path::new(&filename), library_path);
105-
match result {
106-
Ok(()) => Ok(()),
107-
Err(why) => Err(why.with_source_code(NamedSource::new(readable_filename, code))),
111+
let result = sof_main(&code, filename, &library_path);
112+
if let Err(why) = result {
113+
return Err(why.with_source_code(NamedSource::new(readable_filename, code)));
108114
}
109-
} else {
115+
}
116+
117+
if args.should_open_repl() {
110118
let fake_filename = cwd.join(".repl-input.sof");
111119

112120
let mut rl =
@@ -126,15 +134,31 @@ fn main() -> miette::Result<()> {
126134
}
127135
},
128136
Err(ReadlineError::Interrupted | ReadlineError::Eof) => {
129-
break Ok(());
137+
break;
130138
},
131139
Err(err) => {
132140
println!("error: {err:#?}");
133-
break Ok(());
141+
break;
134142
},
135143
}
136144
}
137145
}
146+
147+
// Finalize.
148+
149+
if let Some(snapshot_filename) = args
150+
.debug_options
151+
.iter()
152+
.filter_map(|d| match d {
153+
DebugOption::ExportSnapshot { target_filename } => Some(target_filename),
154+
_ => None,
155+
})
156+
.next()
157+
{
158+
info!("Output snapshot to {}", snapshot_filename.display());
159+
}
160+
161+
Ok(())
138162
}
139163

140164
fn run_code_on_arena(

0 commit comments

Comments
 (0)