diff --git a/src/lib.rs b/src/lib.rs index bdb74fb5..6094cfaf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod domain; pub mod formatting; pub mod highlighting; pub mod language; +pub mod linking; pub mod parsing; pub mod program; pub(crate) mod regex; diff --git a/src/linking/checks/linker.rs b/src/linking/checks/linker.rs new file mode 100644 index 00000000..d862a699 --- /dev/null +++ b/src/linking/checks/linker.rs @@ -0,0 +1,102 @@ +// Hand-written check suite for the linking phase. Source strings are parsed +// and translated through the real pipeline, then linked against a stub +// `Library`, matching what the runner sees in production. + +use std::path::Path; + +use crate::linking::{link, LinkingError}; +use crate::parsing; +use crate::program::{Executable, ExecutableRef, Operation}; +use crate::runner::Library; +use crate::translation::translate; + +fn first_execute<'a, 'i>(op: &'a Operation<'i>) -> Option<&'a Executable<'i>> { + match op { + Operation::Execute(executable) => Some(executable), + Operation::Sequence(ops) => ops + .iter() + .find_map(first_execute), + Operation::Section { body, .. } => first_execute(body), + Operation::Step { body, .. } => first_execute(body), + Operation::Loop { over, body, .. } => over + .as_deref() + .and_then(first_execute) + .or_else(|| first_execute(body)), + Operation::Bind { value, .. } => first_execute(value), + Operation::Tablet(entries) => entries + .iter() + .find_map(|entry| first_execute(&entry.value)), + Operation::List(items) => items + .iter() + .find_map(first_execute), + _ => None, + } +} + +#[test] +fn known_function_resolves_to_reference() { + let source = r#" +% technique v1 + +powerdown : + 1. Inhibit the node { cmd("Inhibit") } + "# + .trim_ascii(); + let path = Path::new("Test.tq"); + 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::Resolved(_) = executable.target else { + panic!("expected Resolved, got {:?}", executable.target); + }; +} + +#[test] +fn unknown_function_left_unresolved() { + let source = r#" +% technique v1 + +probe : + 1. Run a mystery { mystery("x") } + "# + .trim_ascii(); + let path = Path::new("Test.tq"); + 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); + }; + assert_eq!(target.value, "mystery"); +} + +#[test] +fn wrong_arity_is_an_error() { + let source = r#" +% technique v1 + +powerdown : + 1. Inhibit too much { cmd("Inhibit", "Extra") } + "# + .trim_ascii(); + let path = Path::new("Test.tq"); + let document = parsing::parse(path, source).expect("parse"); + let mut program = translate(&document).expect("translate"); + + let errors = link(&mut program, &Library::stub()).expect_err("arity error"); + assert_eq!(errors.len(), 1); + let LinkingError::ArityMismatch { + function, + expected, + actual, + } = &errors[0]; + assert_eq!(function.value, "cmd"); + assert_eq!(*expected, 1); + assert_eq!(*actual, 2); +} diff --git a/src/linking/linker.rs b/src/linking/linker.rs new file mode 100644 index 00000000..abb6496f --- /dev/null +++ b/src/linking/linker.rs @@ -0,0 +1,113 @@ +use crate::language::{self, Span}; +use crate::program::{ExecutableRef, Fragment, Operation, Program}; +use crate::runner::Library; + +#[derive(Debug)] +pub enum LinkingError<'i> { + /// A function called with a number of arguments that doesn't match its + /// declared arity in the function table. + ArityMismatch { + function: language::Identifier<'i>, + expected: usize, + actual: usize, + }, +} + +impl<'i> LinkingError<'i> { + pub fn span(&self) -> Span { + match self { + LinkingError::ArityMismatch { function, .. } => function.span, + } + } +} + +/// Resolve every `Execute` target in the program against `library`. A target +/// naming a table entry becomes `Resolved` once its argument count matches the +/// entry's arity; a target naming nothing stays `Unresolved`. Returns the +/// collected arity errors, if any. +pub fn link<'i>(program: &mut Program<'i>, library: &Library) -> Result<(), Vec>> { + let mut problems = Vec::new(); + for subroutine in &mut program.subroutines { + link_operation(&mut subroutine.body, library, &mut problems); + } + if problems.is_empty() { + Ok(()) + } else { + Err(problems) + } +} + +// Walks the same Operation arms as the translator's `resolve_operation`; +// executable content is hoisted into `body` during translation, so only +// bodies need walking. +fn link_operation<'i>( + op: &mut Operation<'i>, + library: &Library, + problems: &mut Vec>, +) { + 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, + }); + } + } + } + for arg in &mut executable.arguments { + link_operation(arg, library, problems); + } + } + Operation::Invoke(invocable) => { + for arg in &mut invocable.arguments { + link_operation(arg, library, problems); + } + } + Operation::Sequence(ops) => { + for op in ops { + link_operation(op, library, problems); + } + } + Operation::Section { body, .. } => link_operation(body, library, problems), + Operation::Step { body, .. } => link_operation(body, library, problems), + Operation::Loop { over, body, .. } => { + if let Some(over) = over { + link_operation(over, library, problems); + } + link_operation(body, library, problems); + } + Operation::Bind { value, .. } => link_operation(value, library, problems), + Operation::String(fragments) => { + for fragment in fragments { + if let Fragment::Interpolation(op) = fragment { + link_operation(op, library, problems); + } + } + } + Operation::Tablet(entries) => { + for entry in entries { + link_operation(&mut entry.value, library, problems); + } + } + Operation::List(items) => { + for item in items { + link_operation(item, library, problems); + } + } + Operation::Variable(_) | Operation::Number(_) | Operation::Multiline(_, _) => {} + } +} + +#[cfg(test)] +#[path = "checks/linker.rs"] +mod check; diff --git a/src/linking/mod.rs b/src/linking/mod.rs new file mode 100644 index 00000000..537b4ecf --- /dev/null +++ b/src/linking/mod.rs @@ -0,0 +1,6 @@ +//! Linking phase: resolve the function references against the function table +//! the program will run against. + +mod linker; + +pub use linker::{link, LinkingError}; diff --git a/src/main.rs b/src/main.rs index 2e73c126..d2e35048 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,8 +10,9 @@ use tracing_subscriber::{self, EnvFilter}; use technique::formatting::{self, Identity}; use technique::highlighting::{self, Terminal}; +use technique::linking; use technique::parsing; -use technique::runner::{self, Outcome, RunId}; +use technique::runner::{self, Library, Outcome, RunId}; use technique::templating::{self, Checklist, NasaEsaIss, Procedure, Recipe, Source}; use technique::translation; @@ -31,6 +32,7 @@ enum Output { enum Phase { Parsing, Translation, + Linking, } // Page dimensions in millimetres @@ -171,13 +173,14 @@ fn main() { Arg::new("until") .long("until") .value_name("phase") - .value_parser(["parsing", "translation"]) - .default_value("parsing") + .value_parser(["parsing", "translation", "linking"]) + .default_value("linking") .action(ArgAction::Set) - .help("Stop compilation after the given phase is complete so that the result can be inspected. \ - Use this in conjunction with the --output option. The phases are: \ - parsing, where the input is parsed from the surface language to an internal abstract syntax tree; then \ - translation, which resolves names, checks references, and ensures the input is valid Technique.") + .help("Stop compilation early, after the given phase is complete. \ + Use this in conjunction with the --output option so that the result can be inspected. The phases are: \ + parsing, where the input is parsed from the surface language to an internal abstract syntax tree; \ + translation, which resolves names, checks references, and ensures the input is valid Technique; then finally \ + linking, which ensures functions being called are available, checks parameters being passed, and provides the context to the execution environment.") ) .arg( Arg::new("filename") @@ -330,6 +333,7 @@ fn main() { let until = match until.as_str() { "parsing" => Phase::Parsing, "translation" => Phase::Translation, + "linking" => Phase::Linking, _ => panic!("Unrecognized --until value"), }; @@ -382,7 +386,7 @@ fn main() { std::process::exit(0); } - let program = match translation::translate(&technique) { + let mut program = match translation::translate(&technique) { Ok(program) => program, Err(errors) => { for (i, error) in errors @@ -394,9 +398,7 @@ fn main() { } eprintln!( "{}", - problem::concise_translation_error( - &error, &filename, &content, &Terminal - ) + problem::full_translation_error(&error, &filename, &content, &Terminal) ); } std::process::exit(1); @@ -415,6 +417,36 @@ fn main() { } std::process::exit(0); } + + let library = Library::core(); + if let Err(errors) = linking::link(&mut program, &library) { + for (i, error) in errors + .iter() + .enumerate() + { + if i > 0 { + eprintln!(); + } + eprintln!( + "{}", + problem::full_linking_error(&error, &filename, &content, &Terminal) + ); + } + std::process::exit(1); + } + + if let Phase::Linking = until { + match output { + Output::Terminal => { + eprintln!("{}", "ok".bright_green()); + } + Output::Native => { + println!("{:#?}", program); + } + Output::Silent => {} + } + std::process::exit(0); + } } Some(("format", submatches)) => { let raw_output = *submatches @@ -660,7 +692,7 @@ fn main() { } }; - let program = match translation::translate(&technique) { + let mut program = match translation::translate(&technique) { Ok(program) => program, Err(errors) => { for (i, error) in errors @@ -681,7 +713,24 @@ fn main() { } }; - match runner::start(filename, &program, &arguments) { + let library = Library::core(); + if let Err(errors) = linking::link(&mut program, &library) { + for (i, error) in errors + .iter() + .enumerate() + { + if i > 0 { + eprintln!(); + } + eprintln!( + "{}", + problem::concise_linking_error(&error, &filename, &content, &Terminal) + ); + } + std::process::exit(1); + } + + match runner::start(filename, &program, &arguments, library) { Ok((run_id, Outcome::Quit)) => { eprintln!("paused; resume with `technique resume {}`", run_id.render()); std::process::exit(0); @@ -743,7 +792,7 @@ fn main() { } }; - let program = match translation::translate(&technique) { + let mut program = match translation::translate(&technique) { Ok(program) => program, Err(errors) => { for (i, error) in errors @@ -764,7 +813,24 @@ fn main() { } }; - match runner::resume(run_id, &program) { + let library = Library::core(); + if let Err(errors) = linking::link(&mut program, &library) { + for (i, error) in errors + .iter() + .enumerate() + { + if i > 0 { + eprintln!(); + } + eprintln!( + "{}", + problem::concise_linking_error(&error, &filename, &content, &Terminal) + ); + } + std::process::exit(1); + } + + match runner::resume(run_id, &program, library) { Ok(Outcome::Quit) => { eprintln!( "paused; continue with `technique resume {}`", diff --git a/src/problem/format.rs b/src/problem/format.rs index d3a6b1dc..11a9b3e3 100644 --- a/src/problem/format.rs +++ b/src/problem/format.rs @@ -1,22 +1,25 @@ -use super::messages::{generate_error_message, generate_runner_error, generate_translation_error}; +use super::messages::{ + generate_error_message, generate_linking_error, generate_runner_error, + generate_translation_error, +}; use owo_colors::OwoColorize; use std::path::Path; use technique::{ - formatting::Render, language::LoadingError, parsing::ParsingError, runner::RunnerError, - translation::TranslationError, + formatting::Render, language::LoadingError, linking::LinkingError, parsing::ParsingError, + runner::RunnerError, translation::TranslationError, }; -/// Format a parsing error with full details including source code context -pub fn full_parsing_error<'i>( - error: &ParsingError, - filename: &'i Path, - source: &'i str, - renderer: &impl Render, +/// Render an error with full source context: a header line, the offending +/// source line, a caret underline of the given width, and the detail text. +fn full_error( + problem: String, + details: String, + filename: &Path, + source: &str, + offset: usize, + width: usize, ) -> String { - let (problem, details) = generate_error_message(error, renderer); let input = generate_filename(filename); - let offset = error.offset(); - let width = error.width(); let i = calculate_line_number(source, offset); let j = calculate_column_number(source, offset); @@ -67,6 +70,48 @@ pub fn full_parsing_error<'i>( .to_string() } +/// Format a parsing error with full details including source code context +pub fn full_parsing_error<'i>( + error: &ParsingError, + filename: &'i Path, + source: &'i str, + renderer: &impl Render, +) -> String { + let (problem, details) = generate_error_message(error, renderer); + full_error( + problem, + details, + filename, + source, + error.offset(), + error.width(), + ) +} + +/// Format a translation error with full details including source code context +pub fn full_translation_error<'i>( + error: &TranslationError<'i>, + filename: &'i Path, + source: &'i str, + renderer: &impl Render, +) -> String { + let (problem, details) = generate_translation_error(error, renderer); + let span = error.span(); + full_error(problem, details, filename, source, span.offset, span.length) +} + +/// Format a linking error with full details including source code context +pub fn full_linking_error<'i>( + error: &LinkingError<'i>, + filename: &'i Path, + source: &'i str, + renderer: &impl Render, +) -> String { + let (problem, details) = generate_linking_error(error, renderer); + let span = error.span(); + full_error(problem, details, filename, source, span.offset, span.length) +} + /// Format a parsing error with concise single-line output pub fn concise_parsing_error<'i>( error: &ParsingError, @@ -119,6 +164,33 @@ pub fn concise_translation_error<'i>( ) } +/// Format a linking error with concise single-line output. +pub fn concise_linking_error<'i>( + error: &LinkingError<'i>, + filename: &'i Path, + source: &'i str, + renderer: &impl Render, +) -> String { + let (problem, _) = generate_linking_error(error, renderer); + let input = generate_filename(filename); + let offset = error + .span() + .offset; + let i = calculate_line_number(source, offset); + let j = calculate_column_number(source, offset); + let line = i + 1; + let column = j + 1; + + format!( + "{}: {}:{}:{} {}", + "error".bright_red(), + input, + line, + column, + problem.bold(), + ) +} + /// Format a runner error with concise single-line output. pub fn concise_runner_error(error: &RunnerError, renderer: &impl Render) -> String { let (problem, _) = generate_runner_error(error, renderer); diff --git a/src/problem/messages.rs b/src/problem/messages.rs index d03e478a..0b647fc5 100644 --- a/src/problem/messages.rs +++ b/src/problem/messages.rs @@ -1,7 +1,7 @@ use crate::problem::Present; use technique::{ - formatting::Render, language::*, parsing::ParsingError, runner::RunnerError, - translation::TranslationError, + formatting::Render, language::*, linking::LinkingError, parsing::ParsingError, + runner::RunnerError, translation::TranslationError, }; /// Generate problem and detail messages for parsing errors using AST construction @@ -1019,7 +1019,7 @@ Hyphens, underscores, spaces, or subscripts are not valid in unit symbols. } } -/// Generate problem and detail messages for errors occuring during the +/// Generate problem and detail messages for errors occurring during the /// translation phase. pub fn generate_translation_error<'i>( error: &TranslationError<'i>, @@ -1059,7 +1059,84 @@ pub fn generate_translation_error<'i>( } } -/// Generate problem and detail messages for errors occuring when a proceure +/// Generate problem and detail messages for errors occurring when function +/// references are linked against the available function table. +pub fn generate_linking_error<'i>( + error: &LinkingError<'i>, + renderer: &dyn Render, +) -> (String, String) { + let examples = vec![ + Expression::Execution( + Function { + target: Identifier::new("panic"), + parameters: vec![], + }, + Span::default(), + ), + Expression::Execution( + Function { + target: Identifier::new("operate_kettle"), + parameters: vec![Expression::Number( + Numeric::Scientific(Quantity { + mantissa: Decimal { + number: 100, + precision: 0, + }, + uncertainty: None, + magnitude: None, + symbol: "°C", + }), + Span::default(), + )], + }, + Span::default(), + ), + Expression::Execution( + Function { + target: Identifier::new("enumerate"), + parameters: vec![ + Expression::String(vec![Piece::Text("men")], Span::default()), + Expression::String(vec![Piece::Text("women")], Span::default()), + Expression::String( + vec![Piece::Text("small furry creatures from Alpha Centauri")], + Span::default(), + ), + ], + }, + Span::default(), + ), + ]; + + match error { + LinkingError::ArityMismatch { + function: Identifier { value: name, .. }, + expected, + actual, + } => ( + format!( + "Wrong number of arguments to {}(), expected {} but called with {}", + name, expected, actual + ), + format!( + r#" +A function must be called with the correct number of arguments. Some example +functions calls: + + {} + {} + {} + "#, + examples[0].present(renderer), + examples[1].present(renderer), + examples[2].present(renderer) + ) + .trim_ascii() + .to_string(), + ), + } +} + +/// Generate problem and detail messages for errors occurring when a procedure /// is being evaluated by the runner. pub fn generate_runner_error(error: &RunnerError, _renderer: &dyn Render) -> (String, String) { match error { @@ -1136,6 +1213,14 @@ tablet's labels, and pairs() to get a sequence of tuples of labels and values you can iterate over. "#.trim_ascii().to_string(), ), + RunnerError::InvalidArgument { function, expected } => ( + 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::UserQuit => ( "Interrupted".to_string(), "The user quit before the procedure was completed. Use `technique resume ` to continue.".to_string(), diff --git a/src/program/types.rs b/src/program/types.rs index 4eb89283..401f7a75 100644 --- a/src/program/types.rs +++ b/src/program/types.rs @@ -78,10 +78,16 @@ impl<'i> Subroutine<'i> { } } -/// Every node of the Intermediate Representation form resulting from -/// desugaring the surface language is an `Operation` (c.f. opcode in an -/// instruction set). Later this will be instantiated into a tree that the -/// interpreter can walks recursively and reduce. +/// Every node of the Intermediate Representation resulting from desugaring +/// the surface language is an `Operation`. Collectively they form a tree that +/// the runner can walk directly, being immutable means it can be revisited +/// when needed by loop nodes. Structural nodes (`Section`, `Step`, `Loop`, +/// ...) are traversed for their effects, and value-bearing nodes are reduced +/// to a `Value` against a separate, mutable `Environment` (see +/// `evaluator::evaluate`). +/// +/// The Operation tree is not evaluated in place. All per-run state lives in +/// the `runner::Environment`. #[derive(Debug, Eq, PartialEq)] pub enum Operation<'i> { Variable(language::Identifier<'i>), @@ -147,15 +153,29 @@ pub enum SubroutineRef<'i> { /// Lowered form of `language::Function`. Functions live in a separate /// namespace from procedures: they are built-in or host-provided. The target -/// is kept as a plain Identifier here; resolution happens at a later -/// domain-linking phase, against whatever functions the executing domain -/// provides. +/// is an `ExecutableRef`, resolved against the available function table +/// during the linking phase. #[derive(Debug, Eq, PartialEq)] pub struct Executable<'i> { - pub target: language::Identifier<'i>, + pub target: ExecutableRef<'i>, pub arguments: Vec>, } +/// Index of a function in the linked `Library`. This is the resolved form of +/// the target of an `Executable`, analogous to `SubroutineId` for procedures. +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub struct ExecutableId(pub usize); + +/// Reference to a function. The translation phase emits these as +/// `Unresolved`; the linking phase replaces references where a function is in +/// the `Library` table with `Resolved`. Mirrors `SubroutineRef` for +/// procedures. +#[derive(Debug, Eq, PartialEq)] +pub enum ExecutableRef<'i> { + Unresolved(language::Identifier<'i>), + Resolved(ExecutableId), +} + /// A fragment of a string literal: either inline text or an interpolated /// expression. Defined here (rather than reusing `language::Piece`) because /// interpolations are themselves `Operation`s and may carry resolved diff --git a/src/runner/checks/evaluator.rs b/src/runner/checks/evaluator.rs index f923732e..2de1d570 100644 --- a/src/runner/checks/evaluator.rs +++ b/src/runner/checks/evaluator.rs @@ -1,14 +1,16 @@ use crate::language::{Identifier, Numeric as LangNumeric}; -use crate::program::{Entry, Fragment, Operation}; +use crate::program::{Entry, Executable, ExecutableRef, Fragment, Operation}; use crate::runner::evaluator::{evaluate, Environment}; +use crate::runner::library::Library; use crate::runner::runner::RunnerError; use crate::value; #[test] fn variable_lookup() { + let library = Library::core(); let op = Operation::Variable(Identifier::new("missing")); let mut env = Environment::new(); - match evaluate(&mut env, &op) { + match evaluate(&mut env, &library, &op) { Err(RunnerError::UnboundVariable(name)) => assert_eq!(name, "missing"), other => panic!("expected UnboundVariable, got {:?}", other), } @@ -19,20 +21,22 @@ fn variable_lookup() { value::Value::Literali("World".to_string()), ); let op = Operation::Variable(Identifier::new("name")); - let v = evaluate(&mut env, &op).expect("evaluated"); + let v = evaluate(&mut env, &library, &op).expect("evaluated"); assert_eq!(v, value::Value::Literali("World".to_string())); } #[test] fn number_evaluates_to_quanticle() { + let library = Library::core(); let op = Operation::Number(LangNumeric::Integral(42)); let mut env = Environment::new(); - let v = evaluate(&mut env, &op).expect("evaluated"); + let v = evaluate(&mut env, &library, &op).expect("evaluated"); assert_eq!(v, value::Value::Quanticle(value::Numeric::Integral(42))); } #[test] fn string_interpolation() { + let library = Library::core(); let mut env = Environment::new(); env.extend( "name".to_string(), @@ -43,7 +47,7 @@ fn string_interpolation() { Fragment::Interpolation(Operation::Variable(Identifier::new("name"))), Fragment::Text("!"), ]); - let v = evaluate(&mut env, &op).expect("evaluated"); + let v = evaluate(&mut env, &library, &op).expect("evaluated"); assert_eq!(v, value::Value::Literali("Hello, World!".to_string())); let op = Operation::String(vec![ @@ -51,7 +55,7 @@ fn string_interpolation() { Fragment::Interpolation(Operation::Variable(Identifier::new("nope"))), ]); let mut env = Environment::new(); - match evaluate(&mut env, &op) { + match evaluate(&mut env, &library, &op) { Err(RunnerError::UnboundVariable(name)) => assert_eq!(name, "nope"), other => panic!("expected UnboundVariable, got {:?}", other), } @@ -59,14 +63,16 @@ fn string_interpolation() { #[test] fn multiline_joins_with_newlines() { + let library = Library::core(); let op = Operation::Multiline(None, vec!["foo", "bar", "baz"]); let mut env = Environment::new(); - let v = evaluate(&mut env, &op).expect("evaluated"); + let v = evaluate(&mut env, &library, &op).expect("evaluated"); assert_eq!(v, value::Value::Literali("foo\nbar\nbaz".to_string())); } #[test] fn tablet_entries_evaluate() { + let library = Library::core(); let op = Operation::Tablet(vec![ Entry { label: "name", @@ -78,7 +84,7 @@ fn tablet_entries_evaluate() { }, ]); let mut env = Environment::new(); - let v = evaluate(&mut env, &op).expect("evaluated"); + let v = evaluate(&mut env, &library, &op).expect("evaluated"); assert_eq!( v, value::Value::Tabularum(vec![ @@ -96,13 +102,14 @@ fn tablet_entries_evaluate() { #[test] fn list_elements_evaluate() { + let library = Library::core(); let op = Operation::List(vec![ Operation::Number(LangNumeric::Integral(1)), Operation::Number(LangNumeric::Integral(4)), Operation::Number(LangNumeric::Integral(9)), ]); let mut env = Environment::new(); - let v = evaluate(&mut env, &op).expect("evaluated"); + let v = evaluate(&mut env, &library, &op).expect("evaluated"); assert_eq!( v, value::Value::Arraeum(vec![ @@ -115,6 +122,7 @@ fn list_elements_evaluate() { #[test] fn bind_extends_env_for_subsequent_lookup() { + let library = Library::core(); let names = [Identifier::new("greeting")]; let bind = Operation::Bind { names: &names, @@ -123,24 +131,25 @@ fn bind_extends_env_for_subsequent_lookup() { let lookup = Operation::Variable(Identifier::new("greeting")); let seq = Operation::Sequence(vec![bind, lookup]); let mut env = Environment::new(); - let v = evaluate(&mut env, &seq).expect("evaluated"); + let v = evaluate(&mut env, &library, &seq).expect("evaluated"); assert_eq!(v, value::Value::Literali("Hello".to_string())); } #[test] fn sequence_evaluation() { + let library = Library::core(); let seq = Operation::Sequence(vec![ Operation::Number(LangNumeric::Integral(1)), Operation::Number(LangNumeric::Integral(2)), Operation::Number(LangNumeric::Integral(3)), ]); let mut env = Environment::new(); - let v = evaluate(&mut env, &seq).expect("evaluated"); + let v = evaluate(&mut env, &library, &seq).expect("evaluated"); assert_eq!(v, value::Value::Quanticle(value::Numeric::Integral(3))); let seq = Operation::Sequence(vec![]); let mut env = Environment::new(); - let v = evaluate(&mut env, &seq).expect("evaluated"); + let v = evaluate(&mut env, &library, &seq).expect("evaluated"); assert_eq!(v, value::Value::Unitus); } @@ -149,6 +158,7 @@ fn multi_name_bind_destructures_parametriq() { // Build a Parametriq of three values by reducing a wrapped construction. // Simplest path: pre-stuff env with a Parametriq, then bind a tuple of // names to a Variable that looks it up. + let library = Library::core(); let mut env = Environment::new(); env.extend( "triple".to_string(), @@ -167,7 +177,7 @@ fn multi_name_bind_destructures_parametriq() { names: &names, value: Box::new(Operation::Variable(Identifier::new("triple"))), }; - let result = evaluate(&mut env, &bind).expect("evaluated"); + let result = evaluate(&mut env, &library, &bind).expect("evaluated"); assert_eq!(result, value::Value::Unitus); assert_eq!( env.lookup("a"), @@ -185,6 +195,7 @@ fn multi_name_bind_destructures_parametriq() { #[test] fn multi_name_bind_wrong_arity_errors() { + let library = Library::core(); let mut env = Environment::new(); env.extend( "pair".to_string(), @@ -202,7 +213,7 @@ fn multi_name_bind_wrong_arity_errors() { names: &names, value: Box::new(Operation::Variable(Identifier::new("pair"))), }; - match evaluate(&mut env, &bind) { + match evaluate(&mut env, &library, &bind) { Err(RunnerError::BindArityMismatch { expected, actual }) => { assert_eq!(expected, 3); assert_eq!(actual, 2); @@ -213,6 +224,7 @@ fn multi_name_bind_wrong_arity_errors() { #[test] fn multi_name_bind_against_scalar_errors_as_not_tuple() { + let library = Library::core(); let mut env = Environment::new(); env.extend( "scalar".to_string(), @@ -223,10 +235,49 @@ fn multi_name_bind_against_scalar_errors_as_not_tuple() { names: &names, value: Box::new(Operation::Variable(Identifier::new("scalar"))), }; - match evaluate(&mut env, &bind) { + match evaluate(&mut env, &library, &bind) { Err(RunnerError::BindNotTuple { expected }) => { assert_eq!(expected, 2); } other => panic!("expected BindNotTuple, got {:?}", other), } } + +#[test] +fn execute_dispatches_resolved_builtin() { + let library = Library::core(); + let id = library + .resolve("seq") + .expect("seq registered"); + let op = Operation::Execute(Executable { + target: ExecutableRef::Resolved(id), + arguments: vec![ + Operation::Number(LangNumeric::Integral(1)), + Operation::Number(LangNumeric::Integral(3)), + ], + }); + let mut env = Environment::new(); + let v = evaluate(&mut env, &library, &op).expect("evaluated"); + assert_eq!( + v, + value::Value::Arraeum(vec![ + value::Value::Quanticle(value::Numeric::Integral(1)), + value::Value::Quanticle(value::Numeric::Integral(2)), + value::Value::Quanticle(value::Numeric::Integral(3)), + ]) + ); +} + +#[test] +fn execute_unresolved_function_errors() { + let library = Library::core(); + let op = Operation::Execute(Executable { + target: ExecutableRef::Unresolved(Identifier::new("click")), + arguments: Vec::new(), + }); + let mut env = Environment::new(); + let Err(RunnerError::UnresolvedFunction(name)) = evaluate(&mut env, &library, &op) else { + panic!("expected UnresolvedFunction"); + }; + assert_eq!(name, "click"); +} diff --git a/src/runner/checks/library.rs b/src/runner/checks/library.rs new file mode 100644 index 00000000..9e622925 --- /dev/null +++ b/src/runner/checks/library.rs @@ -0,0 +1,94 @@ +use crate::runner::library::Library; +use crate::runner::runner::RunnerError; +use crate::value::{Numeric, Value}; + +fn int(n: i64) -> Value { + Value::Quanticle(Numeric::Integral(n)) +} + +fn text(s: &str) -> Value { + Value::Literali(s.to_string()) +} + +// Invoke a builtin by name through the core Library, the same path the +// evaluator takes once a call is resolved. +fn call(name: &str, args: &[Value]) -> Result { + let library = Library::core(); + let id = library + .resolve(name) + .expect("builtin registered"); + library.call(id, args) +} + +#[test] +fn seq_builds_inclusive_range() { + let result = call("seq", &[int(1), int(4)]).expect("seq"); + assert_eq!(result, Value::Arraeum(vec![int(1), int(2), int(3), int(4)])); +} + +#[test] +fn seq_is_empty_when_descending() { + let result = call("seq", &[int(6), int(1)]).expect("seq"); + assert_eq!(result, Value::Arraeum(Vec::new())); +} + +#[test] +fn seq_rejects_non_integer() { + let result = call("seq", &[text("a"), int(4)]); + let Err(RunnerError::InvalidArgument { function, .. }) = result else { + panic!("expected InvalidArgument, got {:?}", result); + }; + assert_eq!(function, "seq"); +} + +#[test] +fn zip_pairs_truncating_to_shorter() { + let xs = Value::Arraeum(vec![int(1), int(2), int(3)]); + let ys = Value::Arraeum(vec![text("a"), text("b")]); + let result = call("zip", &[xs, ys]).expect("zip"); + assert_eq!( + result, + Value::Arraeum(vec![ + Value::Parametriq(vec![int(1), text("a")]), + Value::Parametriq(vec![int(2), text("b")]), + ]) + ); +} + +#[test] +fn tablet_projections() { + let form = Value::Tabularum(vec![ + ("primary".to_string(), text("1.1.1.1")), + ("secondary".to_string(), text("8.8.8.8")), + ]); + + let values = call("values", std::slice::from_ref(&form)).expect("values"); + assert_eq!( + values, + Value::Arraeum(vec![text("1.1.1.1"), text("8.8.8.8")]) + ); + + let labels = call("labels", std::slice::from_ref(&form)).expect("labels"); + assert_eq!( + labels, + Value::Arraeum(vec![text("primary"), text("secondary")]) + ); + + let pairs = call("pairs", std::slice::from_ref(&form)).expect("pairs"); + assert_eq!( + pairs, + Value::Arraeum(vec![ + Value::Parametriq(vec![text("primary"), text("1.1.1.1")]), + Value::Parametriq(vec![text("secondary"), text("8.8.8.8")]), + ]) + ); +} + +#[test] +fn projections_reject_non_tablet() { + let result = call("values", &[int(3)]); + let Err(RunnerError::InvalidArgument { function, .. }) = result else { + panic!("expected InvalidArgument, got {:?}", result); + }; + assert_eq!(function, "values"); +} diff --git a/src/runner/checks/runner.rs b/src/runner/checks/runner.rs index 62c49e32..e43b4bc8 100644 --- a/src/runner/checks/runner.rs +++ b/src/runner/checks/runner.rs @@ -1,10 +1,13 @@ use std::collections::HashSet; use std::path::{Path, PathBuf}; -use crate::language::Identifier; +use crate::language::{Identifier, Numeric as LangNumeric}; use crate::parsing; -use crate::program::{Fragment, Operation, Ordinal, Program, Subroutine}; +use crate::program::{ + Executable, ExecutableRef, Fragment, Operation, Ordinal, Program, Subroutine, +}; use crate::runner::evaluator::Environment; +use crate::runner::library::Library; use crate::runner::prompt::{Event, Mock, UserInput}; use crate::runner::runner::{bind_parameters, Outcome, Runner, RunnerError}; use crate::runner::state::{parse_record, Appender, State, Store, Value as RecordValue}; @@ -104,6 +107,7 @@ fn step_outcomes_recorded() { HashSet::new(), prompt, Environment::new(), + Library::stub(), ); let outcome = runner .run() @@ -146,6 +150,7 @@ fn step_outcomes_recorded() { HashSet::new(), prompt, Environment::new(), + Library::stub(), ); runner .run() @@ -176,6 +181,7 @@ fn step_outcomes_recorded() { HashSet::new(), prompt, Environment::new(), + Library::stub(), ); runner .run() @@ -218,6 +224,7 @@ fn two_steps_prompted_in_source_order() { HashSet::new(), prompt, Environment::new(), + Library::stub(), ); runner .run() @@ -259,6 +266,7 @@ fn pre_completed_step_short_circuits() { completed, prompt, Environment::new(), + Library::stub(), ); runner .run() @@ -295,6 +303,7 @@ fn quit_propagates_and_stops_walking() { HashSet::new(), prompt, Environment::new(), + Library::stub(), ); let outcome = runner .run() @@ -353,6 +362,7 @@ fn section_walking() { HashSet::new(), prompt, Environment::new(), + Library::stub(), ); runner .run() @@ -399,6 +409,7 @@ fn section_walking() { HashSet::new(), prompt, Environment::new(), + Library::stub(), ); runner .run() @@ -437,6 +448,7 @@ fn parallel_step_index_starts_at_one() { HashSet::new(), prompt, Environment::new(), + Library::stub(), ); runner .run() @@ -482,6 +494,7 @@ test : HashSet::new(), prompt, Environment::new(), + Library::stub(), ); runner .run() @@ -531,6 +544,7 @@ helper : HashSet::new(), prompt, Environment::new(), + Library::stub(), ); runner .run() @@ -575,6 +589,7 @@ test : HashSet::new(), prompt, Environment::new(), + Library::stub(), ); runner .run() @@ -627,6 +642,7 @@ fn loop_inside_step_produces_one_result() { HashSet::new(), prompt, env, + Library::stub(), ); runner .run() @@ -681,6 +697,7 @@ fn repeat_loops_until_quit() { HashSet::new(), prompt, Environment::new(), + Library::stub(), ); runner .run() @@ -748,6 +765,7 @@ fn foreach_walks_body_once_per_list_element() { HashSet::new(), prompt, env, + Library::stub(), ); runner .run() @@ -771,6 +789,85 @@ fn foreach_walks_body_once_per_list_element() { assert_eq!(steps, vec![("/[1]/a", "first"), ("/[2]/a", "second")]); } +#[test] +fn foreach_over_seq_builtin_runs() { + let mut fixture = StoreFixture::new("foreach-seq"); + + // The iterable is the result of the `seq` builtin rather than a seeded + // env binding, exercising the evaluator's Execute dispatch end to end. + let library = Library::core(); + let seq = library + .resolve("seq") + .expect("seq registered"); + + let description = Operation::String(vec![Fragment::Interpolation(Operation::Variable( + Identifier::new("n"), + ))]); + let substep = Operation::Step { + ordinal: Ordinal::Dependent("a"), + attributes: Vec::new(), + description: vec![description], + body: Box::new(Operation::Sequence(Vec::new())), + responses: Vec::new(), + }; + let names = [Identifier::new("n")]; + let over = Operation::Execute(Executable { + target: ExecutableRef::Resolved(seq), + arguments: vec![ + Operation::Number(LangNumeric::Integral(1)), + Operation::Number(LangNumeric::Integral(3)), + ], + }); + let loop_op = Operation::Loop { + names: &names, + over: Some(Box::new(over)), + body: Box::new(Operation::Sequence(vec![substep])), + responses: Vec::new(), + }; + let mut sub = Subroutine::anonymous(); + sub.body = loop_op; + let mut program = Program::new(); + program + .subroutines + .push(sub); + + let prompt = Mock::with_answers([ + UserInput::Done(Value::Unitus), + UserInput::Done(Value::Unitus), + UserInput::Done(Value::Unitus), + ]); + let mut runner = Runner::new( + &program, + fixture.take_appender(), + HashSet::new(), + prompt, + Environment::new(), + library, + ); + runner + .run() + .expect("run"); + + let prompt = runner.into_prompt(); + let steps: Vec<(&str, &str)> = prompt + .events() + .iter() + .filter_map(|event| match event { + Event::Step { + qualified, + description, + } => Some((qualified.as_str(), description.as_str())), + _ => None, + }) + .collect(); + // seq(1, 3) yields [1, 2, 3]; the body walks once per element with `n` + // bound to each in turn. + assert_eq!( + steps, + vec![("/[1]/a", "1"), ("/[2]/a", "2"), ("/[3]/a", "3")] + ); +} + #[test] fn foreach_destructures_tuple_elements() { let mut fixture = StoreFixture::new("foreach-destructure"); @@ -829,6 +926,7 @@ fn foreach_destructures_tuple_elements() { HashSet::new(), prompt, env, + Library::stub(), ); runner .run() @@ -886,6 +984,7 @@ fn foreach_widens_primitive_to_singleton() { HashSet::new(), prompt, env, + Library::stub(), ); runner .run() @@ -943,6 +1042,7 @@ fn foreach_over_non_list_or_unbound_errors() { HashSet::new(), Mock::new(), env, + Library::stub(), ); match runner.run() { Err(RunnerError::NotIterable) => {} @@ -966,6 +1066,7 @@ fn foreach_over_non_list_or_unbound_errors() { HashSet::new(), Mock::new(), env, + Library::stub(), ); match runner.run() { Err(RunnerError::NotIterable) => {} @@ -980,6 +1081,7 @@ fn foreach_over_non_list_or_unbound_errors() { HashSet::new(), Mock::new(), Environment::new(), + Library::stub(), ); match runner.run() { Err(RunnerError::UnboundVariable(name)) => assert_eq!(name, "source"), @@ -1080,6 +1182,7 @@ greet(name) : HashSet::new(), prompt, env, + Library::stub(), ); runner .run() @@ -1122,6 +1225,7 @@ test : HashSet::new(), prompt, Environment::new(), + Library::stub(), ); runner .run() diff --git a/src/runner/evaluator.rs b/src/runner/evaluator.rs index 10096301..e0857b7e 100644 --- a/src/runner/evaluator.rs +++ b/src/runner/evaluator.rs @@ -3,8 +3,9 @@ use std::collections::HashMap; +use super::library::Library; use super::runner::RunnerError; -use crate::program::{Fragment, Operation}; +use crate::program::{ExecutableRef, Fragment, Operation}; use crate::value::{Numeric, Value}; /// Variable bindings established by the walker as `Bind` operations @@ -40,11 +41,14 @@ impl Environment { /// specifically at this point values of variables need to be known from the /// `Environment` otherwise the `Operation` can't be evaluated. /// -/// Non-value variants (Section / Step / Loop / Invoke / Execute) evaluate to -/// `Unitus` rather than failing — `evaluate` is only meant to be called on -/// value-bearing positions and that fallback keeps it total. +/// A resolved `Execute` dispatches through the passed in `Library` to its +/// builtin, evaluating its arguments before doing so. #[allow(dead_code)] -pub fn evaluate<'i>(env: &mut Environment, op: &Operation<'i>) -> Result { +pub fn evaluate<'i>( + env: &mut Environment, + library: &Library, + op: &Operation<'i>, +) -> Result { match op { Operation::Variable(id) => env .lookup(id.value) @@ -61,7 +65,7 @@ pub fn evaluate<'i>(env: &mut Environment, op: &Operation<'i>) -> Result text.push_str(t), - Fragment::Interpolation(inner) => match evaluate(env, inner)? { + Fragment::Interpolation(inner) => match evaluate(env, library, inner)? { Value::Literali(s) => text.push_str(&s), other => text.push_str(&other.to_string()), }, @@ -73,7 +77,7 @@ pub fn evaluate<'i>(env: &mut Environment, op: &Operation<'i>) -> Result { let mut pairs = Vec::with_capacity(entries.len()); for entry in entries { - let v = evaluate(env, &entry.value)?; + let v = evaluate(env, library, &entry.value)?; pairs.push(( entry .label @@ -86,27 +90,44 @@ pub fn evaluate<'i>(env: &mut Environment, op: &Operation<'i>) -> Result { let mut values = Vec::with_capacity(items.len()); for item in items { - values.push(evaluate(env, item)?); + values.push(evaluate(env, library, item)?); } Ok(Value::Arraeum(values)) } Operation::Bind { names, value } => { - let v = evaluate(env, value)?; + let v = evaluate(env, library, value)?; bind_names(env, names, v)?; Ok(Value::Unitus) } Operation::Sequence(ops) => { let mut last = Value::Unitus; for child in ops { - last = evaluate(env, child)?; + last = evaluate(env, library, child)?; } Ok(last) } + Operation::Execute(executable) => match &executable.target { + ExecutableRef::Resolved(id) => { + let mut args = Vec::with_capacity( + executable + .arguments + .len(), + ); + for arg in &executable.arguments { + args.push(evaluate(env, library, arg)?); + } + library.call(*id, &args) + } + ExecutableRef::Unresolved(target) => Err(RunnerError::UnresolvedFunction( + target + .value + .to_string(), + )), + }, Operation::Section { .. } | Operation::Step { .. } | Operation::Loop { .. } - | Operation::Invoke(_) - | Operation::Execute(_) => Ok(Value::Unitus), + | Operation::Invoke(_) => Ok(Value::Unitus), } } diff --git a/src/runner/library.rs b/src/runner/library.rs new file mode 100644 index 00000000..b2cadb9e --- /dev/null +++ b/src/runner/library.rs @@ -0,0 +1,198 @@ +//! The function table for the evaluator. + +use super::runner::RunnerError; +use crate::program::ExecutableId; +use crate::value::{Numeric, Value}; + +/// A native function: implemented in Rust, taking already-evaluated +/// arguments. +pub type Native = fn(&[Value]) -> Result; + +/// A function in the Library's table +struct Entry { + name: &'static str, + arity: usize, + pointer: Native, +} + +/// The set of functions available to a program, indexed by `ExecutableId`. A +/// Library is built by combining core builtins then adding whatever the given +/// domain contributes. It is used by the linking phase to perform lookups of +/// function pointers, then ownership is passed to the runner. +pub struct Library { + functions: Vec, +} + +impl Library { + /// The domain-independent builtins present under every domain. Populated + /// with the pure functions that coerce our value types; effectful + /// functions (functions supplied by the host environment) are declared + /// and implemented by the relevant domain the Technique is executing in. + pub fn core() -> Self { + let entry = |name, arity, pointer| Entry { + name, + arity, + pointer, + }; + Library { + functions: vec![ + entry("seq", 2, seq as Native), + entry("zip", 2, zip as Native), + entry("values", 1, values as Native), + entry("labels", 1, labels as Native), + entry("pairs", 1, pairs as Native), + ], + } + } + + /// Resolve a function name to its index, or `None` if no entry matches. + pub fn resolve(&self, name: &str) -> Option { + self.functions + .iter() + .position(|entry| entry.name == name) + .map(ExecutableId) + } + + /// The declared arity of the function at `id`. + pub fn arity(&self, id: ExecutableId) -> usize { + self.functions[id.0].arity + } + + /// The name of the function at `id`. + pub fn name(&self, id: ExecutableId) -> &'static str { + self.functions[id.0].name + } + + /// Call the function at `id` with (already evaluated) arguments. + pub fn call(&self, id: ExecutableId, args: &[Value]) -> Result { + (self.functions[id.0].pointer)(args) + } +} + +/// `seq(a, b)` — the inclusive integer range from `a` to `b` as a list, +/// empty when `a > b`. +fn seq(args: &[Value]) -> Result { + let a = as_integer("seq", &args[0])?; + let b = as_integer("seq", &args[1])?; + let range = (a..=b) + .map(|n| Value::Quanticle(Numeric::Integral(n))) + .collect(); + Ok(Value::Arraeum(range)) +} + +/// `zip(xs, ys)` — a list of `(x, y)` pairs, one per position, truncated to +/// the shorter input. +fn zip(args: &[Value]) -> Result { + let xs = as_list("zip", &args[0])?; + let ys = as_list("zip", &args[1])?; + let pairs = xs + .iter() + .zip(ys.iter()) + .map(|(x, y)| Value::Parametriq(vec![x.clone(), y.clone()])) + .collect(); + Ok(Value::Arraeum(pairs)) +} + +/// `values(form)` — the values of a tablet's entries, in order, as a list. +fn values(args: &[Value]) -> Result { + let entries = as_tablet("values", &args[0])?; + let values = entries + .iter() + .map(|(_, value)| value.clone()) + .collect(); + Ok(Value::Arraeum(values)) +} + +/// `labels(form)` — the labels of a tablet's entries, in order, as a list of +/// text values. +fn labels(args: &[Value]) -> Result { + let entries = as_tablet("labels", &args[0])?; + let labels = entries + .iter() + .map(|(label, _)| Value::Literali(label.clone())) + .collect(); + Ok(Value::Arraeum(labels)) +} + +/// `pairs(form)` — a tablet's entries as a list of `(label, value)` pairs, +/// so `foreach (k, v) in pairs(form)` destructures through the usual rule. +fn pairs(args: &[Value]) -> Result { + let entries = as_tablet("pairs", &args[0])?; + let pairs = entries + .iter() + .map(|(label, value)| { + Value::Parametriq(vec![Value::Literali(label.clone()), value.clone()]) + }) + .collect(); + Ok(Value::Arraeum(pairs)) +} + +fn as_integer(function: &'static str, value: &Value) -> Result { + if let Value::Quanticle(Numeric::Integral(n)) = value { + Ok(*n) + } else { + Err(RunnerError::InvalidArgument { + function, + expected: "an integer", + }) + } +} + +fn as_list<'a>(function: &'static str, value: &'a Value) -> Result<&'a [Value], RunnerError> { + if let Value::Arraeum(items) = value { + Ok(items) + } else { + Err(RunnerError::InvalidArgument { + function, + expected: "a list", + }) + } +} + +fn as_tablet<'a>( + function: &'static str, + value: &'a Value, +) -> Result<&'a [(String, Value)], RunnerError> { + if let Value::Tabularum(entries) = value { + Ok(entries) + } else { + Err(RunnerError::InvalidArgument { + function, + expected: "a tablet", + }) + } +} + +#[cfg(test)] +impl Library { + pub fn stub() -> Self { + fn unit(_: &[Value]) -> Result { + Ok(Value::Unitus) + } + let entry = |name, arity| Entry { + name, + arity, + pointer: unit as Native, + }; + Library { + functions: vec![ + entry("seq", 2), + entry("zip", 2), + entry("exec", 1), + entry("cmd", 1), + entry("now", 0), + entry("uuid", 0), + entry("timer", 1), + entry("journal", 1), + entry("click", 1), + entry("navigate", 1), + entry("select", 1), + entry("deselect", 1), + ], + } + } +} + +#[cfg(test)] +#[path = "checks/library.rs"] +mod check; diff --git a/src/runner/mod.rs b/src/runner/mod.rs index 514327f6..ce2dee25 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -8,11 +8,13 @@ use std::path::{Path, PathBuf}; use crate::program::Program; mod evaluator; +mod library; mod path; mod prompt; mod runner; mod state; +pub use library::Library; pub use runner::{Outcome, RunnerError}; pub use state::{RecordError, RunId}; @@ -31,13 +33,21 @@ pub fn start<'i>( document: &Path, program: &'i Program<'i>, arguments: &[String], + library: Library, ) -> Result<(RunId, Outcome), RunnerError> { let env = bind_parameters(program, arguments)?; let store = Store::new(PathBuf::from(STORE_ROOT)); let (run_id, run_dir) = store.create(document, now_iso8601())?; let pfftt = construct_state_path(&run_dir, document); let appender = Appender::open(pfftt, run_id)?; - let mut runner = Runner::new(program, appender, HashSet::new(), Console::new(), env); + let mut runner = Runner::new( + program, + appender, + HashSet::new(), + Console::new(), + env, + library, + ); let outcome = runner.run()?; Ok((run_id, outcome)) } @@ -54,7 +64,11 @@ pub fn locate(run_id: RunId) -> Result { /// Open an existing run and walk the given program, short-circuiting /// any step whose FQN has already been recorded. Appends a `Resume` /// record at the root path before walking. -pub fn resume<'i>(run_id: RunId, program: &'i Program<'i>) -> Result { +pub fn resume<'i>( + run_id: RunId, + program: &'i Program<'i>, + library: Library, +) -> Result { let store = Store::new(PathBuf::from(STORE_ROOT)); let (document, completed, run_dir) = store.open(run_id)?; let pfftt = construct_state_path(&run_dir, &document); @@ -72,6 +86,7 @@ pub fn resume<'i>(run_id: RunId, program: &'i Program<'i>) -> Result { prompt: P, env: Environment, path: QualifiedPath<'i>, + library: Library, } impl<'i, P: Prompt> Runner<'i, P> { @@ -81,6 +104,7 @@ impl<'i, P: Prompt> Runner<'i, P> { completed: HashSet, prompt: P, env: Environment, + library: Library, ) -> Self { Runner { program, @@ -89,6 +113,7 @@ impl<'i, P: Prompt> Runner<'i, P> { prompt, env, path: QualifiedPath::new(), + library, } } @@ -146,6 +171,7 @@ impl<'i, P: Prompt> Runner<'i, P> { } => self.walk_loop(names, over.as_deref(), body), Operation::Invoke(invocable) => self.walk_invoke(invocable), Operation::Execute(executable) => { + let function = self.executable_name(&executable.target); let qualified = self .path .render(); @@ -157,16 +183,13 @@ impl<'i, P: Prompt> Runner<'i, P> { run_id, path: qualified, state: State::Execute { - function: executable - .target - .value - .to_string(), + function: function.clone(), }, }; self.appender .append(&record)?; self.prompt - .announce(&describe_execute(executable)); + .announce(&describe_execute(&function)); Ok(Outcome::Done(Value::Unitus)) } Operation::Bind { .. } @@ -176,12 +199,26 @@ impl<'i, P: Prompt> Runner<'i, P> { | Operation::Multiline(_, _) | Operation::Tablet(_) | Operation::List(_) => { - let value = super::evaluator::evaluate(&mut self.env, op)?; + let value = super::evaluator::evaluate(&mut self.env, &self.library, op)?; Ok(Outcome::Done(value)) } } } + /// The name of a function target. FIXME an unresolved one (awaiting + /// domain linking) carries its identifier still. + fn executable_name(&self, target: &ExecutableRef<'_>) -> String { + match target { + ExecutableRef::Resolved(id) => self + .library + .name(*id) + .to_string(), + ExecutableRef::Unresolved(id) => id + .value + .to_string(), + } + } + fn walk_invoke(&mut self, invocable: &'i Invocable<'i>) -> Result { match &invocable.target { SubroutineRef::Resolved(id) => { @@ -259,7 +296,7 @@ impl<'i, P: Prompt> Runner<'i, P> { } } Some(expr) => { - let items = match super::evaluator::evaluate(&mut self.env, expr)? { + let items = match super::evaluator::evaluate(&mut self.env, &self.library, expr)? { Value::Arraeum(items) => items, // A scalar in list context is a singleton list. value @ (Value::Literali(_) | Value::Quanticle(_)) => vec![value], @@ -335,7 +372,7 @@ impl<'i, P: Prompt> Runner<'i, P> { .path .render(); let title_text = match title { - Some(op) => match super::evaluator::evaluate(&mut self.env, op)? { + Some(op) => match super::evaluator::evaluate(&mut self.env, &self.library, op)? { Value::Literali(s) => s, other => other.to_string(), }, @@ -426,7 +463,7 @@ impl<'i, P: Prompt> Runner<'i, P> { if !description_text.is_empty() { description_text.push('\n'); } - match super::evaluator::evaluate(&mut self.env, op)? { + match super::evaluator::evaluate(&mut self.env, &self.library, op)? { Value::Literali(s) => description_text.push_str(&s), other => description_text.push_str(&other.to_string()), } @@ -476,13 +513,8 @@ fn describe_loop( } } -fn describe_execute(executable: &Executable<'_>) -> String { - format!( - "{}()", - executable - .target - .value - ) +fn describe_execute(function: &str) -> String { + format!("{}()", function) } /// Lift a `UserInput` from the prompt into the runner's `Outcome`. diff --git a/src/translation/checks/translate.rs b/src/translation/checks/translate.rs index 0d5d2c83..4fa6a22f 100644 --- a/src/translation/checks/translate.rs +++ b/src/translation/checks/translate.rs @@ -7,7 +7,7 @@ use std::path::Path; use crate::language; use crate::parsing; -use crate::program::{Fragment, Operation, Ordinal, SubroutineId, SubroutineRef}; +use crate::program::{ExecutableRef, Fragment, Operation, Ordinal, SubroutineId, SubroutineRef}; use crate::translation::translate; #[test] @@ -728,12 +728,10 @@ run : let Operation::Execute(executable) = &ops[0] else { panic!("expected Execute, got {:?}", ops[0]); }; - assert_eq!( - executable - .target - .value, - "sum" - ); + let ExecutableRef::Unresolved(target) = &executable.target else { + panic!("expected Unresolved, got {:?}", executable.target); + }; + assert_eq!(target.value, "sum"); assert_eq!( executable .arguments @@ -1036,12 +1034,10 @@ run : let Operation::Execute(executable) = &ops[0] else { panic!("expected Execute, got {:?}", ops[0]); }; - assert_eq!( - executable - .target - .value, - "journal" - ); + let ExecutableRef::Unresolved(target) = &executable.target else { + panic!("expected Unresolved, got {:?}", executable.target); + }; + assert_eq!(target.value, "journal"); } #[test] @@ -1559,9 +1555,10 @@ delete_rds_instance : .iter() .map(|op| match op { Operation::Execute(executable) => { - executable - .target - .value + let ExecutableRef::Unresolved(target) = &executable.target else { + panic!("expected Unresolved, got {:?}", executable.target); + }; + target.value } other => panic!("expected Execute, got {:?}", other), }) diff --git a/src/translation/translator.rs b/src/translation/translator.rs index 1d260950..5ce5f100 100644 --- a/src/translation/translator.rs +++ b/src/translation/translator.rs @@ -6,8 +6,8 @@ use crate::language; use crate::language::{Document, Span}; use crate::program::{ - Entry, Executable, Fragment, Invocable, Operation, Ordinal, Program, Subroutine, SubroutineId, - SubroutineRef, + Entry, Executable, ExecutableRef, Fragment, Invocable, Operation, Ordinal, Program, Subroutine, + SubroutineId, SubroutineRef, }; pub fn translate<'i>(document: &'i Document<'i>) -> Result, Vec>> { @@ -703,7 +703,7 @@ impl<'i> Translator<'i> { Operation::Invoke(self.translate_invocation(invocation)) } language::Expression::Execution(function, _) => Operation::Execute(Executable { - target: function.target, + target: ExecutableRef::Unresolved(function.target), arguments: function .parameters .iter() diff --git a/tests/broken/linking/WrongArgumentCount.tq b/tests/broken/linking/WrongArgumentCount.tq new file mode 100644 index 00000000..f9feeb89 --- /dev/null +++ b/tests/broken/linking/WrongArgumentCount.tq @@ -0,0 +1,5 @@ +% technique v1 + +powerdown : + + 1. Inhibit the nodes { seq(1, 2, 3) } diff --git a/tests/integration.rs b/tests/integration.rs index 2e37703a..f32a8dad 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,5 +1,6 @@ mod common; mod formatting; +mod linking; mod parsing; mod templating; mod translation; diff --git a/tests/linking/broken.rs b/tests/linking/broken.rs new file mode 100644 index 00000000..3ab8a9e6 --- /dev/null +++ b/tests/linking/broken.rs @@ -0,0 +1,66 @@ +use std::path::Path; + +use technique::linking; +use technique::parsing; +use technique::runner::Library; +use technique::translation; + +use crate::common::list_technique_documents; + +#[test] +fn ensure_fail() { + let dir = Path::new("tests/broken/linking/"); + let files = list_technique_documents(dir); + + let library = Library::core(); + + let mut unexpected_successes = Vec::new(); + let mut earlier_failures = Vec::new(); + + for file in &files { + let content = parsing::load(&file) + .unwrap_or_else(|e| panic!("Failed to load file {:?}: {:?}", file, e)); + + // Linking-failure fixtures must parse and translate cleanly first; the + // failure is meant to come from the linking phase, not earlier ones. + let document = match parsing::parse(&file, &content) { + Ok(document) => document, + Err(errors) => { + println!("File {:?} unexpectedly failed to parse: {:?}", file, errors); + earlier_failures.push(file.clone()); + continue; + } + }; + + let mut program = match translation::translate(&document) { + Ok(program) => program, + Err(errors) => { + println!( + "File {:?} unexpectedly failed to translate: {:?}", + file, errors + ); + earlier_failures.push(file.clone()); + continue; + } + }; + + if linking::link(&mut program, &library).is_ok() { + println!("File {:?} unexpectedly linked successfully", file); + unexpected_successes.push(file.clone()); + } + } + + if !earlier_failures.is_empty() { + panic!( + "Linking-failure fixtures must parse and translate cleanly, but {} failed earlier", + earlier_failures.len() + ); + } + + if !unexpected_successes.is_empty() { + panic!( + "Broken files should not link successfully, but {} files passed", + unexpected_successes.len() + ); + } +} diff --git a/tests/linking/mod.rs b/tests/linking/mod.rs new file mode 100644 index 00000000..e41275a7 --- /dev/null +++ b/tests/linking/mod.rs @@ -0,0 +1 @@ +mod broken;