Skip to content
5 changes: 5 additions & 0 deletions src/domain/checklist/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
pub mod adapter;
pub mod types;
mod typst;

/// The checklist domain: flattens procedures into printable checklists.
pub struct Checklist;

impl crate::domain::Domain for Checklist {}
31 changes: 31 additions & 0 deletions src/domain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,34 @@ pub(crate) mod serialize;
pub mod source;

pub use adapter::Adapter;
pub use checklist::Checklist;
pub use nasa_esa_iss::NasaEsaIss;
pub use procedure::Procedure;
pub use recipe::Recipe;
pub use source::Source;

use crate::runner::Builtin;

/// The runtime facet of a domain: the domain-specific host functions it
/// contributes to the interpreter's function table, on top of `Library::core`
/// and the `Library::system` layer. Orthogonal to a domain's rendering
/// projection (the `Template` trait).
pub trait Domain {
fn functions(&self) -> Vec<Builtin> {
Vec::new()
}
}

/// Select a domain by name, for both the renderer and the runtime.
/// `None` if the name matches no known domain — the caller reports that as an
/// error rather than silently substituting a default.
pub fn domain_for(name: &str) -> Option<&'static dyn Domain> {
match name {
"checklist" => Some(&Checklist),
"nasa-esa-iss" => Some(&NasaEsaIss),
"procedure" => Some(&Procedure),
"recipe" => Some(&Recipe),
"source" => Some(&Source),
_ => None,
}
}
5 changes: 5 additions & 0 deletions src/domain/nasa_esa_iss/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
pub mod adapter;

/// The NASA/ESA ISS domain.
pub struct NasaEsaIss;

impl crate::domain::Domain for NasaEsaIss {}
5 changes: 5 additions & 0 deletions src/domain/procedure/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
pub mod adapter;
pub mod types;
mod typst;

/// The procedure domain: preserves the full procedure hierarchy when rendering.
pub struct Procedure;

impl crate::domain::Domain for Procedure {}
5 changes: 5 additions & 0 deletions src/domain/recipe/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
pub mod adapter;
pub mod types;
mod typst;

/// The recipe domain.
pub struct Recipe;

impl crate::domain::Domain for Recipe {}
5 changes: 5 additions & 0 deletions src/domain/source/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
pub mod adapter;
pub mod types;
mod typst;

/// The source domain: renders Technique source with syntax highlighting.
pub struct Source;

impl crate::domain::Domain for Source {}
18 changes: 10 additions & 8 deletions src/linking/checks/linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ powerdown :
}

#[test]
fn unknown_function_left_unresolved() {
fn unknown_function_is_an_error() {
let source = r#"
% technique v1

Expand All @@ -67,13 +67,12 @@ probe :
let document = parsing::parse(path, source).expect("parse");
let mut program = translate(&document).expect("translate");

link(&mut program, &Library::stub()).expect("link");

let executable = first_execute(&program.subroutines[0].body).expect("an Execute in the body");
let ExecutableRef::Unresolved(target) = &executable.target else {
panic!("expected Unresolved, got {:?}", executable.target);
let errors = link(&mut program, &Library::stub()).expect_err("unknown function");
assert_eq!(errors.len(), 1);
let LinkingError::UnresolvedFunction { function } = &errors[0] else {
panic!("expected UnresolvedFunction, got {:?}", errors[0]);
};
assert_eq!(target.value, "mystery");
assert_eq!(function.value, "mystery");
}

#[test]
Expand All @@ -95,7 +94,10 @@ powerdown :
function,
expected,
actual,
} = &errors[0];
} = &errors[0]
else {
panic!("expected ArityMismatch, got {:?}", errors[0]);
};
assert_eq!(function.value, "cmd");
assert_eq!(*expected, 1);
assert_eq!(*actual, 2);
Expand Down
33 changes: 20 additions & 13 deletions src/linking/linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ pub enum LinkingError<'i> {
expected: usize,
actual: usize,
},
/// A function call naming nothing in the function table — neither a core
/// nor system builtin nor a function the selected domain provides.
UnresolvedFunction { function: language::Identifier<'i> },
}

impl<'i> LinkingError<'i> {
pub fn span(&self) -> Span {
match self {
LinkingError::ArityMismatch { function, .. } => function.span,
LinkingError::UnresolvedFunction { function } => function.span,
}
}
}
Expand Down Expand Up @@ -48,20 +52,23 @@ fn link_operation<'i>(
match op {
Operation::Execute(executable) => {
if let ExecutableRef::Unresolved(id) = &executable.target {
if let Some(exec_id) = library.resolve(id.value) {
let expected = library.arity(exec_id);
let actual = executable
.arguments
.len();
if actual == expected {
executable.target = ExecutableRef::Resolved(exec_id);
} else {
problems.push(LinkingError::ArityMismatch {
function: *id,
expected,
actual,
});
match library.resolve(id.value) {
Some(exec_id) => {
let expected = library.arity(exec_id);
let actual = executable
.arguments
.len();
if actual == expected {
executable.target = ExecutableRef::Resolved(exec_id);
} else {
problems.push(LinkingError::ArityMismatch {
function: *id,
expected,
actual,
});
}
}
None => problems.push(LinkingError::UnresolvedFunction { function: *id }),
}
}
for arg in &mut executable.arguments {
Expand Down
60 changes: 57 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::str::FromStr;
use tracing::debug;
use tracing_subscriber::{self, EnvFilter};

use technique::domain::{self, Domain};
use technique::formatting::{self, Identity};
use technique::highlighting::{self, Terminal};
use technique::linking;
Expand Down Expand Up @@ -118,6 +119,17 @@ impl TypedValueParser for PaperSizeParser {
}
}

/// Resolve a domain name to its handle.
fn select_domain(name: &str) -> &'static dyn Domain {
match domain::domain_for(name) {
Some(domain) => domain,
None => {
eprintln!("{}: unrecognized domain \"{}\"", "error".bright_red(), name);
std::process::exit(1);
}
}
}

fn main() {
const VERSION: &str = concat!("v", env!("CARGO_PKG_VERSION"));

Expand Down Expand Up @@ -281,6 +293,14 @@ fn main() {
.required(true)
.help("The file containing the Technique document to run."),
)
.arg(
Arg::new("domain")
.short('d')
.long("domain")
.value_parser(["checklist", "nasa-esa-iss", "procedure", "recipe", "source"])
.action(ArgAction::Set)
.help("The kind of procedure this Technique document represents. By default the value specified in the input document's metadata will be used, falling back to source if unspecified."),
)
.arg(
Arg::new("arguments")
.num_args(0..)
Expand Down Expand Up @@ -418,7 +438,17 @@ fn main() {
std::process::exit(0);
}

let library = Library::core();
// Check validates against the functions a run would resolve
// against: core and system, plus the document's declared domain.
let name = technique
.header
.as_ref()
.and_then(|m| m.domain)
.unwrap_or("source");
let domain = select_domain(name);
let mut library = Library::core();
library.extend(Library::system());
library.extend(domain.functions());
if let Err(errors) = linking::link(&mut program, &library) {
for (i, error) in errors
.iter()
Expand Down Expand Up @@ -713,7 +743,26 @@ fn main() {
}
};

let library = Library::core();
// Add domain-specific host functions to the Library based on
// whether the document or command-line indicate the domain being
// used. FUTURE the `core` and `system` functions are added here
// regardless, in time we should make that more configurable.

let name = submatches
.get_one::<String>("domain")
.map(String::as_str)
.or_else(|| {
technique
.header
.as_ref()
.and_then(|m| m.domain)
})
.unwrap_or("source");
let domain = select_domain(name);

let mut library = Library::core();
library.extend(Library::system());
library.extend(domain.functions());
if let Err(errors) = linking::link(&mut program, &library) {
for (i, error) in errors
.iter()
Expand Down Expand Up @@ -813,7 +862,12 @@ fn main() {
}
};

let library = Library::core();
// TODO it is slightly problematic that we have to reconstruct the
// Library here and at present are hard-coding the functions being
// brought into scope.

let mut library = Library::core();
library.extend(Library::system());
if let Err(errors) = linking::link(&mut program, &library) {
for (i, error) in errors
.iter()
Expand Down
31 changes: 26 additions & 5 deletions src/problem/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,15 @@ functions calls:
.trim_ascii()
.to_string(),
),
LinkingError::UnresolvedFunction {
function: Identifier { value: name, .. },
} => (
format!("Unknown function {}()", name),
format!(
"The function {}() is neither builtin nor provided by the selected domain.",
name
),
),
}
}

Expand Down Expand Up @@ -1199,14 +1208,14 @@ procedure at the top of the Technique document.
),
r#"
Arguments were supplied on the command-line but the entry procedure at the top
of the document doesn't take ant parameters.
of the document doesn't take any parameters.
"#.trim_ascii().to_string(),
),
RunnerError::NotIterable => (
"Iteration requires a list".to_string(),
r#"
The foreach loop control structure requires a list to iterate over, but the
value supplied isn't one. A tablet is a dictonary, not a sequence. If you want
value supplied isn't one. A tablet is a dictionary, not a sequence. If you want
to use the values from a tablet convert them into a list first with the
values() function. There is also a labels() function to get each of the
tablet's labels, and pairs() to get a sequence of tuples of labels and values
Expand All @@ -1217,9 +1226,21 @@ you can iterate over.
format!("Wrong argument type passed to {}()", function),
format!("The {}() function expected {} but was given something else.", function, expected),
),
RunnerError::UnresolvedFunction(function) => (
format!("Unresolved function {}()", function),
format!("The function {}() is not a builtin and is not provided by the domain.", function),
RunnerError::UnknownFunction(function) => (
format!("Unknown function {}()", function),
format!("The function {}() is undefined. This should have been caught during the linking phase!", function),
),
RunnerError::ExecError(error) => (
"Could not run external command".to_string(),
format!("Launching or reading from the external command failed: {}.", error),
),
RunnerError::CommandFailed(code) => (
format!("External command exited with status {}", code),
"The shell command run by exec() finished with a non-zero exit status.".to_string(),
),
RunnerError::IncompatibleCombination { left, right } => (
format!("Cannot combine {} with {}", left, right),
format!("Combining Values requires compatible kinds; a {} and a {} can't be added together.", left, right),
),
RunnerError::UserQuit => (
"Interrupted".to_string(),
Expand Down
Loading