Skip to content

Commit

Permalink
Merge pull request #183 from CohenArthur/basic-string-interpolation
Browse files Browse the repository at this point in the history
Basic string interpolation
  • Loading branch information
CohenArthur authored Apr 29, 2021
2 parents bc64bc7 + d8f7e98 commit 22892ee
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 2 deletions.
2 changes: 1 addition & 1 deletion src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use nom::multi::many0;

mod box_construct;
mod constant_construct;
mod constructs;
pub mod constructs;
mod shunting_yard;
mod tokens;

Expand Down
5 changes: 4 additions & 1 deletion src/value/jk_constant.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::string_interpolation::JkStringFmt;
use crate::instruction::{InstrKind, Instruction, Operator, TypeDec};
use crate::{
FromObjectInstance, Interpreter, JkError, JkString, ObjectInstance, Rename, ToObjectInstance,
Expand Down Expand Up @@ -219,7 +220,9 @@ impl Instruction for JkString {
fn execute(&self, interpreter: &mut Interpreter) -> Result<InstrKind, JkError> {
interpreter.debug("CONSTANT", &self.0.to_string());

Ok(InstrKind::Expression(Some(self.to_instance())))
let interpolated = JkString::from(JkStringFmt::interpolate(&self.0, interpreter)?);

Ok(InstrKind::Expression(Some(interpolated.to_instance())))
}
}

Expand Down
1 change: 1 addition & 0 deletions src/value/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use crate::{instruction::Operator, Instruction, JkError, ObjectInstance};

mod jk_constant;
mod string_interpolation;

pub use jk_constant::JkConstant;

Expand Down
239 changes: 239 additions & 0 deletions src/value/string_interpolation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
//! String interpolation is performed during execution of a `JkString`.
//! The format used is the following: {expression}. This will replace {expression} with
//! the current value of the `expression`'s execution in the interpreter at that time.
//! In order to use the '{' or '}' character themselves, use '{{' and '}}'.
use crate::parser::constructs::Construct;
use crate::{InstrKind, Instruction, Interpreter, JkErrKind, JkError};

use nom::bytes::complete::{is_not, take_while};
use nom::character::complete::char;
use nom::combinator::opt;
use nom::multi::many0;
use nom::sequence::delimited;
use nom::IResult;

const L_DELIM: char = '{';
const R_DELIM: char = '}';

pub struct JkStringFmt;

impl JkStringFmt {
/// Consume input until a certain character is met. This is useful for interpolation,
/// as consume_until("Some non interpolated text {some_var}", '{') will return
/// "Some non interpolated text " and leave "{some_var}", which can easily be
/// separated into a parsable expression
fn consume_until(input: &str, limit: char) -> IResult<&str, Option<&str>> {
match opt(take_while(|c| c != limit))(input) {
Ok((i, Some(data))) => match data.len() {
0 => Ok((i, None)),
_ => Ok((i, Some(data))),
},
Ok(v) => Ok(v),
Err(e) => Err(e),
}
}

/// Parse a "pre expression" in a string to interpolate. The pre expressions are
/// highlighted in the following string, spaces included
///
/// ```
/// "Hey my name is {name} and I am {age} years old"
/// ^^^^^^^^^^^^^^^ ^^^^^^^^^^ ^^^^^^^^^^
/// ```
fn pre_expr(input: &str) -> IResult<&str, Option<&str>> {
JkStringFmt::consume_until(input, L_DELIM)
}

/// Parse an expression in a string to interpolate. The expressions are
/// highlighted in the following string, delimiters included. The expressions go
/// through the same parsing rules as regular jinko expressions. They must be valid
/// code.
///
/// ```
/// "Hey my name is {name} and I am {age} years old"
/// ^^^^^^ ^^^^^
/// ```
fn expr(input: &str) -> IResult<&str, Option<Box<dyn Instruction>>> {
let (input, expr) = match opt(delimited(char(L_DELIM), is_not("{}"), char(R_DELIM)))(input)?
{
(i, Some("")) | (i, None) => return Ok((i, None)), // Early return, no need to parse an empty input
(i, Some(e)) => (i, e),
};

let (_, expr) = Construct::instruction(expr)?;

Ok((input, Some(expr)))
}

/// Everything in the string is either a `pre_expr` or an `expr`
/// The string "Hello {name}" has a pre_expr "Hello" and an expression "name".
/// The string "{name}, how are you?" has an empty pre_expr, an expression "name", a
/// pre_expr ", how are you?" and an empty expr
fn parser(input: &str) -> IResult<&str, (Option<&str>, Option<Box<dyn Instruction>>)> {
use nom::error::{ErrorKind, ParseError};
use nom::Err;

let (input, pre_expr) = JkStringFmt::pre_expr(input)?;
let (input, expr) = JkStringFmt::expr(input)?;

match (pre_expr, &expr) {
// Stop multi-parsing when we couldn't parse anything anymore
(None, None) => Err(Err::Error(ParseError::from_error_kind(
input,
ErrorKind::OneOf,
))),
_ => Ok((input, (pre_expr, expr))),
}
}

/// Execute a parsed expression in the current context. This function returns the
/// expression's execution's result as a String
fn interpolate_expr(
expr: Box<dyn Instruction>,
s: &str,
interpreter: &mut Interpreter,
) -> Result<String, JkError> {
// We must check if the expression is a statement or not. If it is
// an expression, then it is invalid in an Interpolation context
match expr.kind() {
InstrKind::Statement => Err(JkError::new(
JkErrKind::Interpreter,
format!("invalid argument to string interpolation: {}", expr.print()),
None,
String::from(s),
)), // FIXME: Fix loc and input
InstrKind::Expression(_) => {
match expr.execute(interpreter)? {
// FIXME: Call some jinko code, in order to enable custom types,
// instead of `to_string()` in Rust
InstrKind::Expression(Some(res)) => Ok(res.to_string()),
// We just checked that we couldn't execute
// anything other than an expression. This pattern should never
// be encountered
_ => unreachable!(),
}
}
}
}

/// Interpolates the string passed as parameter
pub fn interpolate(s: &str, interpreter: &mut Interpreter) -> Result<String, JkError> {
// We know the final string will be roughly the same size as `s`. We can pre-allocate
// to save some performance
let mut result = String::with_capacity(s.len());

let (_, expressions) = many0(JkStringFmt::parser)(s)?;

for (pre_expr, expr) in expressions {
result.push_str(pre_expr.unwrap_or(""));
match expr {
Some(expr) => {
result.push_str(&JkStringFmt::interpolate_expr(expr, s, interpreter)?)
}
None => {}
}
}

Ok(result)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn no_interpolation() {
let s = "nothing to change here";
let mut interpreter = Interpreter::new();

assert_eq!(JkStringFmt::interpolate(s, &mut interpreter).unwrap(), s);
}

/// Creates two variables, a and b, in an interpreter
fn setup(i: &mut Interpreter) {
let e = Construct::instruction("a = 1").unwrap().1;
e.execute(i).unwrap();

let e = Construct::instruction("b = 2").unwrap().1;
e.execute(i).unwrap();
}

#[test]
fn empty() {
let mut interpreter = Interpreter::new();
setup(&mut interpreter);

let s = "";
assert_eq!(JkStringFmt::interpolate(s, &mut interpreter).unwrap(), "");
}

#[test]
fn only_expr() {
let mut interpreter = Interpreter::new();
setup(&mut interpreter);

let s = "{a}";
assert_eq!(JkStringFmt::interpolate(s, &mut interpreter).unwrap(), "1");
}

#[test]
fn pre_expr_plus_expr() {
let mut interpreter = Interpreter::new();
setup(&mut interpreter);

let s = "Hey {a}";
assert_eq!(
JkStringFmt::interpolate(s, &mut interpreter).unwrap(),
"Hey 1"
);
}

#[test]
fn expr_plus_pre_expr() {
let mut interpreter = Interpreter::new();
setup(&mut interpreter);

let s = "{a} Hey";
assert_eq!(
JkStringFmt::interpolate(s, &mut interpreter).unwrap(),
"1 Hey"
);
}

#[test]
fn e_plus_pe_plus_e() {
let mut interpreter = Interpreter::new();
setup(&mut interpreter);

let s = "{a} Hey {b}";
assert_eq!(
JkStringFmt::interpolate(s, &mut interpreter).unwrap(),
"1 Hey 2"
);
}

#[test]
fn pe_plus_e_plus_pe() {
let mut interpreter = Interpreter::new();
setup(&mut interpreter);

let s = "Hey {b} Hey";
assert_eq!(
JkStringFmt::interpolate(s, &mut interpreter).unwrap(),
"Hey 2 Hey"
);
}

#[test]
fn stmt_as_interp() {
let mut interpreter = Interpreter::new();
setup(&mut interpreter);
let e = Construct::instruction("mut x = 1").unwrap().1;
e.execute(&mut interpreter).unwrap();

let s = "Hey {x = 12}";
assert!(JkStringFmt::interpolate(s, &mut interpreter).is_err());
}
}

0 comments on commit 22892ee

Please sign in to comment.