Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "technique"
version = "0.6.1"
version = "0.6.2"
edition = "2021"
description = "A domain specific language for procedures."
authors = [ "Andrew Cowie" ]
Expand Down
30 changes: 30 additions & 0 deletions src/domain/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,13 @@ impl<'i> Scope<'i> {
}
}

/// Returns tablet pairs from an inline code block in a step's description,
/// the folded-inline counterpart of `tablet()`.
pub fn inline_tablet(&self) -> Option<Vec<&Pair<'i>>> {
self.description()
.find_map(|paragraph| paragraph.tablet())
}

/// Returns true if this scope represents a step (dependent or parallel).
pub fn is_step(&self) -> bool {
match self {
Expand Down Expand Up @@ -385,6 +392,29 @@ impl<'i> Paragraph<'i> {
targets
}

/// Returns tablet pairs if a `CodeInline` in this paragraph is a single
/// list whose elements are all labelled values.
pub fn tablet(&self) -> Option<Vec<&Pair<'i>>> {
for d in &self.0 {
if let Descriptive::CodeInline(Expression::List(elements, _)) = d {
let pairs: Vec<&Pair<'i>> = elements
.iter()
.filter_map(|element| {
if let Expression::Pair(pair, _) = element {
Some(pair.as_ref())
} else {
None
}
})
.collect();
if !pairs.is_empty() && pairs.len() == elements.len() {
return Some(pairs);
}
}
}
None
}

/// Returns rendered code inline expressions from this paragraph.
/// Each entry is (expression, body_lines) where body_lines captures
/// multiline content for separate styling.
Expand Down
22 changes: 10 additions & 12 deletions src/domain/recipe/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,19 +138,17 @@ fn collect_ingredients(items: &mut Vec<Ingredient>, scope: &language::Scope, pla
return;
}

// Steps may contain tablet children
// A step's tablet folds into its description as inline code
if scope.is_step() {
for child in scope.children() {
if let Some(pairs) = child.tablet() {
for pair in pairs {
items.push(Ingredient {
label: pair
.label
.to_string(),
quantity: format_value(&pair.value),
source: place.map(String::from),
});
}
if let Some(pairs) = scope.inline_tablet() {
for pair in pairs {
items.push(Ingredient {
label: pair
.label
.to_string(),
quantity: format_value(&pair.value),
source: place.map(String::from),
});
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/formatting/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -765,15 +765,18 @@ impl<'i> Formatter<'i> {
Descriptive::CodeInline(expr) => match expr {
_ if is_tablet_list_expr(expr) => {
line.flush();
self.append_char('\n');
self.indent();
self.add_fragment_reference(Syntax::Structure, "{");
self.append_char('\n');
self.increase(4);
self.indent();
self.append_expression(expr);
self.append_char('\n');
self.decrease(4);
self.indent();
self.add_fragment_reference(Syntax::Structure, "}");
line = self.builder();
line.add_word(Syntax::Structure, "}");
}
Expression::Multiline(_, _, _) => {
line.flush();
Expand Down
54 changes: 42 additions & 12 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ enum Output {
Terminal,
Native,
Silent,
Store,
}

#[derive(Eq, Debug, PartialEq)]
Expand Down Expand Up @@ -197,7 +198,6 @@ fn main() {
)
.arg(
Arg::new("output")
.short('o')
.long("output")
.value_name("type")
.value_parser(["native", "none"])
Expand Down Expand Up @@ -268,16 +268,6 @@ fn main() {
PDF using a template. This allows you to transform the code of \
the procedure into the intended layout suitable to the \
domain of your application.")
.arg(
Arg::new("output")
.short('o')
.long("output")
.value_name("type")
.value_parser(["pdf", "typst"])
.default_value("pdf")
.action(ArgAction::Set)
.help("Whether to write PDF to a file on disk, or print the Typst markup that would be used to create that PDF (for debugging)."),
)
.arg(
Arg::new("domain")
.short('d')
Expand Down Expand Up @@ -305,11 +295,19 @@ fn main() {
)
.arg(
Arg::new("keep")
.short('k')
.long("keep")
.action(ArgAction::SetTrue)
.help("Keep the generated intermediate files in place after rendering. This allows you to do iterative development of the template and styling with the Typst compiler without having to regenerate the input document every time. The intermediate pieces are written as hidden files in the same directory as the source document."),
)
.arg(
Arg::new("output")
.long("output")
.value_name("type")
.value_parser(["pdf", "typst"])
.default_value("pdf")
.action(ArgAction::Set)
.help("Whether to write PDF to a file on disk, or print the Typst markup that would be used to create that PDF (for debugging)."),
)
.arg(
Arg::new("filename")
.required(true)
Expand Down Expand Up @@ -350,6 +348,14 @@ fn main() {
.action(ArgAction::Set)
.help("Whether to walk the procedure interactively, prompting the operator at each step, or automatically, taking each step's computed value and running to completion or first failure."),
)
.arg(
Arg::new("output")
.long("output")
.value_parser(["pfftt", "native"])
.default_value("pfftt")
.action(ArgAction::Set)
.help("Whether to write the recorded trace to disk in PFFTT format, as is the default, or to instead print a diagnostic trace of the steps as the are completed (for debugging)."),
)
.arg(
Arg::new("raw-control-chars")
.short('R')
Expand Down Expand Up @@ -458,6 +464,7 @@ fn main() {
println!("{:#?}", technique);
}
Output::Silent => {}
_ => {}
}
std::process::exit(0);
}
Expand Down Expand Up @@ -490,6 +497,7 @@ fn main() {
println!("{:#?}", program);
}
Output::Silent => {}
_ => {}
}
std::process::exit(0);
}
Expand Down Expand Up @@ -524,6 +532,7 @@ fn main() {
println!("{:#?}", program);
}
Output::Silent => {}
_ => {}
}
std::process::exit(0);
}
Expand Down Expand Up @@ -739,6 +748,17 @@ fn main() {

debug!(?arguments);

let output = submatches
.get_one::<String>("output")
.unwrap();
let output = match output.as_str() {
"native" => Output::Native,
"pfftt" => Output::Store,
_ => panic!("Unrecognized --output value"),
};

debug!(?output);

let mode = match submatches
.get_one::<String>("mode")
.map(String::as_str)
Expand Down Expand Up @@ -833,6 +853,16 @@ fn main() {
std::process::exit(1);
}

if let Output::Native = output {
match runner::inspect(mode, colour, &program, &arguments, library) {
Ok(_) => std::process::exit(0),
Err(error) => {
eprintln!("{}", problem::concise_runner_error(&error, &Terminal));
std::process::exit(1);
}
}
}

match runner::start(
mode, colour, filename, &program, &arguments, library, &names,
) {
Expand Down
17 changes: 16 additions & 1 deletion src/parsing/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2364,7 +2364,7 @@ impl<'i> Parser<'i> {
|| is_enum_response(line)
|| malformed_step_pattern(line)
|| malformed_response_pattern(line)
|| is_code_block(line)
|| is_loop_block_line(line)
},
|outer| {
let mut results = vec![];
Expand Down Expand Up @@ -3185,6 +3185,21 @@ fn is_code_block(content: &str) -> bool {
re.is_match(content)
}

/// Does this line open a control-structure code block (`{ foreach ... }` or
/// `{ repeat ... }`)? Only a control structure owns the substeps below it and
/// so opens a scope; otherwise a plain expression block (literal value,
/// function call, tablet etc) is inline.
fn is_loop_block_line(content: &str) -> bool {
if let Some(rest) = content
.trim_ascii_start()
.strip_prefix('{')
{
is_foreach_keyword(rest) || is_repeat_keyword(rest)
} else {
false
}
}

/// Is this code block is a control structure (`foreach` or `repeat`) which
/// owns the steps below it as its body. A plain code does not.
fn is_loop_block(expressions: &[Expression]) -> bool {
Expand Down
2 changes: 1 addition & 1 deletion src/runner/checks/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ fn console_step_writes_fqn_and_description() {
}

#[test]
fn automatic_settles_done_when_effectful_skip_otherwise() {
fn automatic_settles_done_when_computable_skip_otherwise() {
let mut p = Automatic::with_handle(Vec::new());
assert_eq!(
p.ask("/I/1", &[], Value::Literali("ran".to_string()), true),
Expand Down
67 changes: 66 additions & 1 deletion src/runner/checks/evaluator.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::language::{Identifier, Numeric as LangNumeric};
use crate::program::{Entry, Executable, ExecutableRef, Fragment, Operation};
use crate::runner::context::Context;
use crate::runner::evaluator::{combine, evaluate, Environment};
use crate::runner::evaluator::{coerce_to_list, combine, evaluate, Environment};
use crate::runner::library::Library;
use crate::runner::runner::RunnerError;
use crate::value;
Expand Down Expand Up @@ -379,3 +379,68 @@ fn combine_cross_kind_errors() {
assert_eq!(left, "quantity");
assert_eq!(right, "string");
}

#[test]
fn coerce_list_passthrough_and_widening() {
// A list yields its members unchanged.
let list = value::Value::Arraeum(vec![
value::Value::Literali("a".to_string()),
value::Value::Literali("b".to_string()),
]);
assert_eq!(
coerce_to_list(list).expect("coerced"),
vec![
value::Value::Literali("a".to_string()),
value::Value::Literali("b".to_string()),
]
);

// Unit is the empty list; a bare scalar widens to a singleton.
assert_eq!(
coerce_to_list(value::Value::Unitus).expect("coerced"),
Vec::<value::Value>::new()
);
assert_eq!(
coerce_to_list(value::Value::Literali("solo".to_string())).expect("coerced"),
vec![value::Value::Literali("solo".to_string())]
);
}

#[test]
fn coerce_parses_bracketed_literal() {
// A `[ … ]` literal acquired at a prompt parses into its elements, quoted
// or bare; empty brackets are the empty list.
let quoted =
coerce_to_list(value::Value::Literali(r#"["east", "west"]"#.to_string())).expect("coerced");
assert_eq!(
quoted,
vec![
value::Value::Literali("east".to_string()),
value::Value::Literali("west".to_string()),
]
);

let bare = coerce_to_list(value::Value::Literali("[east, west]".to_string())).expect("coerced");
assert_eq!(
bare,
vec![
value::Value::Literali("east".to_string()),
value::Value::Literali("west".to_string()),
]
);

let empty = coerce_to_list(value::Value::Literali("[]".to_string())).expect("coerced");
assert_eq!(empty, Vec::<value::Value>::new());
}

#[test]
fn coerce_rejects_tablet() {
let tablet = value::Value::Tabularum(vec![(
"k".to_string(),
value::Value::Literali("v".to_string()),
)]);
match coerce_to_list(tablet) {
Err(RunnerError::NotIterable) => {}
other => panic!("expected NotIterable, got {:?}", other),
}
}
Loading