diff --git a/src/domain/engine.rs b/src/domain/engine.rs index cf10d97..06a292f 100644 --- a/src/domain/engine.rs +++ b/src/domain/engine.rs @@ -356,6 +356,7 @@ fn render_expression(expr: &Expression) -> String { } result } + Expression::Response(value, _) => format!("'{}'", value), Expression::Number(Numeric::Scientific(q), _) => q.to_string(), Expression::Number(Numeric::Integral(n), _) => n.to_string(), Expression::Pair(pair, _) => { diff --git a/src/formatting/formatter.rs b/src/formatting/formatter.rs index 869c0c3..3f564e8 100644 --- a/src/formatting/formatter.rs +++ b/src/formatting/formatter.rs @@ -1266,6 +1266,11 @@ impl<'i> Formatter<'i> { } self.add_fragment_reference(Syntax::Quote, "\""); } + Expression::Response(value, _) => { + self.add_fragment_reference(Syntax::Quote, "'"); + self.add_fragment_reference(Syntax::Response, value); + self.add_fragment_reference(Syntax::Quote, "'"); + } Expression::Number(numeric, _) => self.append_numeric(numeric), Expression::Multiline(lang, lines, _) => { self.append_char('\n'); diff --git a/src/language/types.rs b/src/language/types.rs index ff4d7b9..b262e79 100644 --- a/src/language/types.rs +++ b/src/language/types.rs @@ -437,6 +437,7 @@ pub enum Piece<'i> { pub enum Expression<'i> { Variable(Identifier<'i>, Span), String(Vec>, Span), + Response(&'i str, Span), Number(Numeric<'i>, Span), Multiline(Option<&'i str>, Vec<&'i str>, Span), Repeat(Box>, Span), @@ -456,6 +457,7 @@ impl PartialEq for Expression<'_> { match (self, other) { (Expression::Variable(a, _), Expression::Variable(b, _)) => a == b, (Expression::String(a, _), Expression::String(b, _)) => a == b, + (Expression::Response(a, _), Expression::Response(b, _)) => a == b, (Expression::Number(a, _), Expression::Number(b, _)) => a == b, (Expression::Multiline(a1, a2, _), Expression::Multiline(b1, b2, _)) => { a1 == b1 && a2 == b2 diff --git a/src/linking/linker.rs b/src/linking/linker.rs index df4aabf..8946810 100644 --- a/src/linking/linker.rs +++ b/src/linking/linker.rs @@ -113,6 +113,7 @@ fn link_operation<'i>( } Operation::Variable(_) | Operation::Number(_) + | Operation::Response(_) | Operation::Multiline(_, _) | Operation::Prose(_) | Operation::Hole diff --git a/src/parsing/checks/parser.rs b/src/parsing/checks/parser.rs index d943b42..fa07ef4 100644 --- a/src/parsing/checks/parser.rs +++ b/src/parsing/checks/parser.rs @@ -271,6 +271,30 @@ fn unit_as_expression() { ); } +#[test] +fn response_as_expression() { + // A single-quoted value is a response literal usable anywhere an + // expression is expected: bare in a code block, or as an argument. + let mut input = Parser::new(); + input.initialize("'Monarchy'"); + assert_eq!( + input.read_expression(), + Ok(Expression::Response("Monarchy", Span::default())) + ); + + input.initialize("evaluate('Democracy')"); + assert_eq!( + input.read_expression(), + Ok(Expression::Execution( + Function { + target: Identifier::new("evaluate"), + parameters: vec![Expression::Response("Democracy", Span::default())] + }, + Span::default() + )) + ); +} + #[test] fn declaration_simple() { let mut input = Parser::new(); diff --git a/src/parsing/parser.rs b/src/parsing/parser.rs index d36c146..75ab537 100644 --- a/src/parsing/parser.rs +++ b/src/parsing/parser.rs @@ -1527,6 +1527,13 @@ impl<'i> Parser<'i> { })?; let span = self.span_since(start); Ok(Expression::String(parts, span)) + } else if is_enum_response(content) { + let value = + self.take_block_chars("a response literal", '\'', '\'', false, |inner| { + Ok(inner.source) + })?; + let span = self.span_since(start); + Ok(Expression::Response(value, span)) } else if is_invocation(content) { let invocation = self.read_invocation()?; let span = self.span_since(start); @@ -2590,6 +2597,16 @@ impl<'i> Parser<'i> { })?; let span = outer.span_since(param_start); params.push(Expression::String(parts, span)); + } else if content.starts_with('\'') { + let value = outer.take_block_chars( + "a response literal", + '\'', + '\'', + false, + |inner| Ok(inner.source), + )?; + let span = outer.span_since(param_start); + params.push(Expression::Response(value, span)); } else if is_numeric_quantity(content) { let numeric = outer.read_numeric_quantity()?; let span = outer.span_since(param_start); diff --git a/src/program/types.rs b/src/program/types.rs index 22a27e4..175cfd0 100644 --- a/src/program/types.rs +++ b/src/program/types.rs @@ -122,6 +122,7 @@ pub enum Operation<'i> { Variable(language::Identifier<'i>), Number(language::Numeric<'i>), String(Vec>), + Response(&'i str), Multiline(Option<&'i str>, Vec<&'i str>), Tablet(Vec>), List(Vec>), diff --git a/src/resolution/resolver.rs b/src/resolution/resolver.rs index d102167..6e1d984 100644 --- a/src/resolution/resolver.rs +++ b/src/resolution/resolver.rs @@ -143,6 +143,7 @@ fn resolve_operation<'i>( } Operation::Variable(_) | Operation::Number(_) + | Operation::Response(_) | Operation::Multiline(_, _) | Operation::Prose(_) | Operation::Hole @@ -210,6 +211,7 @@ fn gather_iterated<'i>(op: &Operation<'i>, iterated: &mut HashSet<&'i str>) { } Operation::Variable(_) | Operation::Number(_) + | Operation::Response(_) | Operation::Multiline(_, _) | Operation::Prose(_) | Operation::Hole @@ -271,6 +273,7 @@ fn mark_iterated<'i>(op: &mut Operation<'i>, iterated: &HashSet<&str>) { } Operation::Variable(_) | Operation::Number(_) + | Operation::Response(_) | Operation::Multiline(_, _) | Operation::Prose(_) | Operation::Hole @@ -363,6 +366,7 @@ fn check_scope<'i>( } } Operation::Number(_) + | Operation::Response(_) | Operation::Multiline(_, _) | Operation::Prose(_) | Operation::Hole diff --git a/src/runner/checks/driver.rs b/src/runner/checks/driver.rs index f7d7f0d..c7116a5 100644 --- a/src/runner/checks/driver.rs +++ b/src/runner/checks/driver.rs @@ -1,8 +1,8 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crate::runner::driver::{ - Automatic, Console, Driver, Event, Kind, Mock, Prompt, Standing, UserInput, draw, edit, - is_list_forma, + Automatic, Console, Driver, Event, Kind, Mock, Prompt, Standing, UserInput, draw, draw_action, + edit, is_list_forma, }; use crate::value::{Numeric, Value}; @@ -136,7 +136,12 @@ fn automatic_declines_action_and_choice_as_skip() { // taken as Done just like a plain Computable. let mut p = Automatic::with_handle(Vec::new()); assert_eq!( - p.action("/I/1", "click", "Click", "Actions"), + p.action( + "/I/1", + "click", + "Click", + &Value::Literali("Actions".to_string()) + ), UserInput::Skip ); assert_eq!( @@ -387,6 +392,40 @@ fn esc_menu_disables_edit_for_unit_and_complex() { ); } +#[test] +fn action_response_label_is_coloured() { + // A response literal argument (e.g. `scroll('BOTTOM')`) shows on the action + // line in the Response orange (0xf5, 0x79, 0x00), not the default white a + // plain string label gets. + let it = Prompt::begin(&[], Value::Unitus); + + let mut out: Vec = Vec::new(); + draw_action( + &mut out, + "I/1", + "Scroll to", + &Value::Enumerati("BOTTOM".to_string()), + &it, + ) + .expect("draw"); + let coloured = String::from_utf8(out).expect("utf8"); + assert!(coloured.contains("BOTTOM")); + assert!(coloured.contains("245;121;0")); + + let mut out: Vec = Vec::new(); + draw_action( + &mut out, + "I/1", + "Type", + &Value::Literali("hello".to_string()), + &it, + ) + .expect("draw"); + let plain = String::from_utf8(out).expect("utf8"); + assert!(plain.contains("hello")); + assert!(!plain.contains("245;121;0")); +} + #[test] fn menu_shows_greyed_edit_when_unavailable() { // Edit is always listed so it stays discoverable; for a non-editable value diff --git a/src/runner/driver.rs b/src/runner/driver.rs index be17063..93c13fc 100644 --- a/src/runner/driver.rs +++ b/src/runner/driver.rs @@ -142,7 +142,7 @@ pub trait Driver { /// trace; Skip / Fail decline and record the step; Quit stops. Unlike /// `command` there is no edit buffer. `Automatic` returns `Done` without /// prompting. - fn action(&mut self, qualified: &str, name: &str, verb: &str, label: &str) -> UserInput; + fn action(&mut self, qualified: &str, name: &str, verb: &str, value: &Value) -> UserInput; /// Open a Section: the grey `↘ /fqp` descent bracket (matching the `↙` /// the section's close marker) followed by its prose heading — @@ -242,7 +242,7 @@ pub trait Verdict { qualified: &str, name: &str, verb: &str, - label: &str, + value: &Value, ) -> UserInput; fn acquire( @@ -426,9 +426,9 @@ impl Verdict for Interactive { qualified: &str, name: &str, verb: &str, - label: &str, + value: &Value, ) -> UserInput { - prompt_action(out.surface(), qualified, name, verb, label) + prompt_action(out.surface(), qualified, name, verb, value) } fn acquire( @@ -515,9 +515,9 @@ impl Verdict for Batch { _qualified: &str, _name: &str, verb: &str, - label: &str, + value: &Value, ) -> UserInput { - out.show_action(verb, label); + out.show_action(verb, &value.label()); // An unattended run cannot attest a physical action was performed. UserInput::Skip } @@ -620,9 +620,15 @@ impl Driver for Interface { .command(&mut self.out, qualified, script) } - fn action(&mut self, qualified: &str, name: &str, verb: &str, label: &str) -> UserInput { + fn action( + &mut self, + qualified: &str, + name: &str, + verb: &str, + value: &Value, + ) -> UserInput { self.verdict - .action(&mut self.out, qualified, name, verb, label) + .action(&mut self.out, qualified, name, verb, value) } fn seal(&mut self, qualified: &str, produced: Value, kind: Kind) -> UserInput { @@ -779,11 +785,11 @@ fn prompt_action( qualified: &str, name: &str, verb: &str, - label: &str, + value: &Value, ) -> UserInput { let qualified = display_path(qualified); let result = interact(out, Prompt::begin(&[], Value::Unitus), |o, i| { - draw_action(o, &qualified, verb, label, i) + draw_action(o, &qualified, verb, value, i) }); let _ = queue!( &mut out, @@ -1485,9 +1491,10 @@ fn draw_action( mut out: &mut dyn Write, qualified: &str, verb: &str, - label: &str, + value: &Value, interaction: &Prompt, ) -> io::Result<()> { + let label = value.label(); queue!( &mut out, cursor::MoveToColumn(0), @@ -1498,10 +1505,17 @@ fn draw_action( write!(out, "{}", verb)?; queue!(&mut out, ResetColor)?; if !label.is_empty() { - write!(out, " {}", label)?; + match value { + Value::Enumerati(_) => { + queue!(&mut out, SetForegroundColor(RESPONSE))?; + write!(out, " {}", label)?; + queue!(&mut out, ResetColor)?; + } + _ => write!(out, " {}", label)?, + } } write!(out, " {} ", PROMPT_SYMBOL.blue())?; - let prefix = action_prefix_width(qualified, verb, label); + let prefix = action_prefix_width(qualified, verb, &label); let (cursor_col, end_col) = draw_tail(out, interaction, prefix)?; place_cursor(out, cursor_col, end_col) } @@ -1925,15 +1939,21 @@ impl Driver for Transcript { .command(qualified, script) } - fn action(&mut self, qualified: &str, name: &str, verb: &str, label: &str) -> UserInput { + fn action( + &mut self, + qualified: &str, + name: &str, + verb: &str, + value: &Value, + ) -> UserInput { self.emit(Trace::Execute { path: qualified.to_string(), - script: format!("{} {}", verb, label) + script: format!("{} {}", verb, value.label()) .trim_end() .to_string(), }); self.inner - .action(qualified, name, verb, label) + .action(qualified, name, verb, value) } fn section(&mut self, qualified: &str, numeral: &str, title: &str) { @@ -2154,13 +2174,13 @@ impl Driver for Mock { /// Records the action and auto-confirms it (`Done`) without draining the /// answer queue — like `command`, an action is orthogonal to the step /// verdicts a test drives. - fn action(&mut self, qualified: &str, name: &str, verb: &str, label: &str) -> UserInput { + fn action(&mut self, qualified: &str, name: &str, verb: &str, value: &Value) -> UserInput { self.events .push(Event::Action { qualified: qualified.to_string(), name: name.to_string(), verb: verb.to_string(), - label: label.to_string(), + label: value.label(), }); UserInput::Done(Value::Unitus) } diff --git a/src/runner/evaluator.rs b/src/runner/evaluator.rs index 646cb1a..a3473e9 100644 --- a/src/runner/evaluator.rs +++ b/src/runner/evaluator.rs @@ -113,6 +113,7 @@ fn kind(value: &Value) -> &'static str { match value { Value::Unitus => "unit", Value::Literali(_) => "string", + Value::Enumerati(_) => "response", Value::Quanticle(_) => "quantity", Value::Tabularum(_) => "tablet", Value::Arraeum(_) => "list", @@ -147,6 +148,7 @@ pub fn evaluate<'i>( ) }), Operation::Number(n) => Ok(Value::Quanticle(Numeric::from(n))), + Operation::Response(value) => Ok(Value::Enumerati(value.to_string())), Operation::String(fragments) => { let mut text = String::new(); for fragment in fragments { diff --git a/src/runner/runner.rs b/src/runner/runner.rs index a83b26f..367e8c5 100644 --- a/src/runner/runner.rs +++ b/src/runner/runner.rs @@ -416,10 +416,10 @@ impl<'i, D: Driver> Runner<'i, D> { } } Kind::Action => { - let (verb, label) = self.action_parts(env, executable)?; + let (verb, value) = self.action_parts(env, executable)?; match self .driver - .action(&qualified, &function, &verb, &label) + .action(&qualified, &function, &verb, &value) { UserInput::Done(_) => { let value = super::evaluator::dispatch( @@ -487,6 +487,7 @@ impl<'i, D: Driver> Runner<'i, D> { } => self.walk_bind(env, names, value, inferred.as_ref()), Operation::Variable(_) | Operation::Number(_) + | Operation::Response(_) | Operation::String(_) | Operation::Multiline(_, _) | Operation::Tablet(_) @@ -562,13 +563,13 @@ impl<'i, D: Driver> Runner<'i, D> { } /// An action's parts for the user to confirm: its imperative verb (the - /// library's `display` name, e.g. `Click`) and the bare label its single - /// argument evaluates to, with string literals shown unquoted. + /// library's `display` name, e.g. `Click`) and the value its single + /// argument evaluates to. fn action_parts( &mut self, env: &mut Environment, executable: &'i Executable<'i>, - ) -> Result<(String, String), RunnerError> { + ) -> Result<(String, Value), RunnerError> { let verb = match &executable.target { ExecutableRef::Resolved(id) => self .library @@ -577,19 +578,14 @@ impl<'i, D: Driver> Runner<'i, D> { .unwrap_or_else(|| self.executable_name(&executable.target)), _ => self.executable_name(&executable.target), }; - let label = match executable + let value = match executable .arguments .first() { - Some(arg) => { - match super::evaluator::evaluate(&self.library, &self.context, env, arg)? { - Value::Literali(text) => text, - other => other.to_string(), - } - } - None => String::new(), + Some(arg) => super::evaluator::evaluate(&self.library, &self.context, env, arg)?, + None => Value::Unitus, }; - Ok((verb, label)) + Ok((verb, value)) } fn walk_invoke( diff --git a/src/runner/state.rs b/src/runner/state.rs index fd0ca0d..3bfe63f 100644 --- a/src/runner/state.rs +++ b/src/runner/state.rs @@ -747,6 +747,7 @@ fn parse_optional_value(rest: Option<&str>) -> Result, Reco // // Unitus -> () // Literali(s) -> "" +// Enumerati(s) -> '' // Quanticle(n) -> canonical numeric text (via formatting::render_numeric) // Arraeum(items) -> [item, item] (empty: []) // Tabularum(pairs) -> ["label" = value] (empty: [=], unambiguous vs []) @@ -766,6 +767,11 @@ fn write_value(out: &mut String, value: &value::Value) { escape_literal(out, text); out.push('"'); } + value::Value::Enumerati(text) => { + out.push('\''); + out.push_str(text); + out.push('\''); + } value::Value::Quanticle(numeric) => out.push_str(&render_value_numeric(numeric)), value::Value::Futurae(name) => { out.push('{'); @@ -867,6 +873,12 @@ pub(crate) fn deserialize_value(text: &str) -> Result let inner = &text[1..text.len() - 1]; Ok(value::Value::Literali(unescape_literal(inner)?)) } + '\'' => { + if text.len() < 2 || !text.ends_with('\'') { + return Err(RecordError::MalformedState); + } + Ok(value::Value::Enumerati(text[1..text.len() - 1].to_string())) + } '{' => { if !text.ends_with('}') { return Err(RecordError::MalformedState); @@ -931,12 +943,14 @@ pub(crate) fn deserialize_value(text: &str) -> Result } // Split `text` on `delim`, but only at top level: not inside double quotes -// (honouring `\"` escapes), nor inside nested `[]`, `()`, or `{}`. +// (honouring `\"` escapes), not inside single-quoted Enumerati values (which +// carry no escapes or interior quote), nor inside nested `[]`, `()`, or `{}`. fn split_top_level(text: &str, delim: char) -> Result, RecordError> { let mut parts = Vec::new(); let bytes = text.as_bytes(); let mut depth = 0i32; let mut in_quote = false; + let mut in_squote = false; let mut start = 0usize; let mut i = 0usize; while i < bytes.len() { @@ -952,8 +966,16 @@ fn split_top_level(text: &str, delim: char) -> Result, RecordError> { i += 1; continue; } + if in_squote { + if c == '\'' { + in_squote = false; + } + i += 1; + continue; + } match c { '"' => in_quote = true, + '\'' => in_squote = true, '[' | '(' | '{' => depth += 1, ']' | ')' | '}' => depth -= 1, _ if c == delim && depth == 0 => { @@ -964,7 +986,7 @@ fn split_top_level(text: &str, delim: char) -> Result, RecordError> { } i += 1; } - if in_quote || depth != 0 { + if in_quote || in_squote || depth != 0 { return Err(RecordError::MalformedState); } parts.push(&text[start..]); @@ -981,6 +1003,7 @@ fn split_once_top_level_equals(text: &str) -> Option<(&str, &str)> { let bytes = text.as_bytes(); let mut depth = 0i32; let mut in_quote = false; + let mut in_squote = false; let mut i = 0usize; while i < bytes.len() { let c = bytes[i] as char; @@ -995,8 +1018,16 @@ fn split_once_top_level_equals(text: &str) -> Option<(&str, &str)> { i += 1; continue; } + if in_squote { + if c == '\'' { + in_squote = false; + } + i += 1; + continue; + } match c { '"' => in_quote = true, + '\'' => in_squote = true, '[' | '(' | '{' => depth += 1, ']' | ')' | '}' => depth -= 1, ' ' if depth == 0 @@ -1073,6 +1104,11 @@ mod codec_check { roundtrip(Value::Quanticle(Numeric::Integral(42))); roundtrip(Value::Quanticle(Numeric::Integral(-7))); roundtrip(Value::Quanticle(Numeric::Integral(0))); + roundtrip(Value::Enumerati("BOTTOM".to_string())); + assert_eq!( + serialize_value(&Value::Enumerati("BOTTOM".to_string())), + "'BOTTOM'" + ); } #[test] @@ -1098,6 +1134,15 @@ mod codec_check { "k".to_string(), Value::Literali("v".to_string()), )])])); + roundtrip(Value::Tabularum(vec![( + "system".to_string(), + Value::Enumerati("Monarchy".to_string()), + )])); + // A response value carrying a comma must not split mid-value. + roundtrip(Value::Arraeum(vec![ + Value::Enumerati("Not applicable, see note".to_string()), + Value::Literali("after".to_string()), + ])); roundtrip(Value::Tabularum(vec![ ("reason".to_string(), Value::Literali("boom".to_string())), ("count".to_string(), Value::Quanticle(Numeric::Integral(3))), diff --git a/src/translation/translator.rs b/src/translation/translator.rs index a59fe9d..e0176bb 100644 --- a/src/translation/translator.rs +++ b/src/translation/translator.rs @@ -731,6 +731,7 @@ impl<'i> Translator<'i> { .collect(); Operation::String(fragments) } + language::Expression::Response(value, _) => Operation::Response(value), language::Expression::Multiline(lang, lines, _) => { Operation::Multiline(*lang, lines.clone()) } diff --git a/src/value/types.rs b/src/value/types.rs index 7c01a74..4b47fa9 100644 --- a/src/value/types.rs +++ b/src/value/types.rs @@ -19,6 +19,7 @@ use crate::language; pub enum Value { Unitus, Literali(String), + Enumerati(String), Quanticle(Numeric), Tabularum(Vec<(String, Value)>), Arraeum(Vec), @@ -75,6 +76,7 @@ impl Display for Value { match self { Value::Unitus => Ok(()), Value::Literali(text) => write!(f, "\"{}\"", text), + Value::Enumerati(text) => write!(f, "'{}'", text), Value::Quanticle(numeric) => write!(f, "{}", numeric), Value::Tabularum(pairs) => { f.write_str("[")?; @@ -120,6 +122,17 @@ impl Display for Value { } } +impl Value { + /// Unquoted text for a prompt label: `Literali`/`Enumerati` unwrap + /// their string, everything else falls back to `Display`. + pub fn label(&self) -> String { + match self { + Value::Literali(text) | Value::Enumerati(text) => text.clone(), + other => other.to_string(), + } + } +} + // Numeric rendering goes through `crate::formatting`'s number renderer. // To call into it we briefly reconstruct a borrowed `language::Numeric` impl Display for Numeric { diff --git a/tests/samples/parsing/ResponseLiterals.tq b/tests/samples/parsing/ResponseLiterals.tq new file mode 100644 index 0000000..9794812 --- /dev/null +++ b/tests/samples/parsing/ResponseLiterals.tq @@ -0,0 +1,22 @@ +political_systems : +{ + click('Dictatorship') + click('Theocracy') + click('Democracy') + + 'Monarchy' +} + +the_wild : [Animals] -> Freedom + +The wild open spaces of Connecticut are our destination. What will we find +there? + +{ + [ + "monarch" = "King Julian the First" + "nation" = "Madagascar" + "system" = 'Monarchy' + "founded" = 2005 + ] +}