From b5e287e0659da324e2397a9ef958174d21d3668d Mon Sep 17 00:00:00 2001
From: JT <jonathan.d.turner@gmail.com>
Date: Fri, 30 Jul 2021 15:26:06 +1200
Subject: [PATCH] WIP string interp

---
 src/eval.rs      |  32 ++++++++++-
 src/main.rs      |   3 ++
 src/parser.rs    | 136 ++++++++++++++++++++++++++++++++++++++++++++++-
 src/signature.rs |  11 ++++
 4 files changed, 180 insertions(+), 2 deletions(-)

diff --git a/src/eval.rs b/src/eval.rs
index ff03a08121..adc4795054 100644
--- a/src/eval.rs
+++ b/src/eval.rs
@@ -1,4 +1,4 @@
-use std::collections::HashMap;
+use std::{collections::HashMap, fmt::Display};
 
 use crate::{
     parser::Operator, Block, BlockId, Call, Expr, Expression, ParserState, Span, Statement, VarId,
@@ -20,6 +20,24 @@ pub enum Value {
     Block(BlockId),
     Unknown,
 }
+
+impl Display for Value {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Value::Bool { val, .. } => {
+                write!(f, "{}", val)
+            }
+            Value::Int { val, .. } => {
+                write!(f, "{}", val)
+            }
+            Value::String { val, .. } => write!(f, "{}", val),
+            Value::List(..) => write!(f, "<list>"),
+            Value::Block(..) => write!(f, "<block>"),
+            Value::Unknown => write!(f, "<unknown>"),
+        }
+    }
+}
+
 impl Value {
     pub fn add(&self, rhs: &Value) -> Result<Value, ShellError> {
         match (self, rhs) {
@@ -138,6 +156,18 @@ fn eval_call(state: &State, stack: &mut Stack, call: &Call) -> Result<Value, She
             }
             _ => Err(ShellError::Mismatch("bool".into(), Span::unknown())),
         }
+    } else if decl.signature.name == "build-string" {
+        let mut output = vec![];
+
+        for expr in &call.positional {
+            let val = eval_expression(state, stack, expr)?;
+
+            output.push(val.to_string());
+        }
+        Ok(Value::String {
+            val: output.join(""),
+            span: call.head,
+        })
     } else {
         Ok(Value::Unknown)
     }
diff --git a/src/main.rs b/src/main.rs
index c2ddd36986..95c0d594b8 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -49,6 +49,9 @@ fn main() -> std::io::Result<()> {
         );
         working_set.add_decl(sig.into());
 
+        let sig = Signature::build("build-string").rest(SyntaxShape::String, "list of string");
+        working_set.add_decl(sig.into());
+
         let sig = Signature::build("def")
             .required("def_name", SyntaxShape::String, "definition name")
             .required("params", SyntaxShape::Signature, "parameters")
diff --git a/src/parser.rs b/src/parser.rs
index d9d81b7ee9..5edadc0a17 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -894,7 +894,141 @@ impl<'a> ParserWorkingSet<'a> {
     }
 
     pub(crate) fn parse_dollar_expr(&mut self, span: Span) -> (Expression, Option<ParseError>) {
-        self.parse_variable_expr(span)
+        let contents = self.get_span_contents(span);
+
+        if contents.starts_with(b"$\"") {
+            self.parse_string_interpolation(span)
+        } else {
+            self.parse_variable_expr(span)
+        }
+    }
+
+    pub fn parse_string_interpolation(&mut self, span: Span) -> (Expression, Option<ParseError>) {
+        #[derive(PartialEq, Eq, Debug)]
+        enum InterpolationMode {
+            String,
+            Expression,
+        }
+        let mut error = None;
+
+        let contents = self.get_span_contents(span);
+
+        let start = if contents.starts_with(b"$\"") {
+            span.start + 2
+        } else {
+            span.start
+        };
+
+        let end = if contents.ends_with(b"\"") && contents.len() > 2 {
+            span.end - 1
+        } else {
+            span.end
+        };
+
+        let inner_span = Span { start, end };
+        let contents = self.get_span_contents(inner_span).to_vec();
+
+        let mut output = vec![];
+        let mut mode = InterpolationMode::String;
+        let mut token_start = start;
+        let mut depth = 0;
+
+        let mut b = start;
+
+        #[allow(clippy::needless_range_loop)]
+        while b != end {
+            if contents[b - start] == b'(' && mode == InterpolationMode::String {
+                depth = 1;
+                mode = InterpolationMode::Expression;
+                if token_start < b {
+                    let span = Span {
+                        start: token_start,
+                        end: b,
+                    };
+                    let str_contents = self.get_span_contents(span);
+                    output.push(Expression {
+                        expr: Expr::String(String::from_utf8_lossy(str_contents).to_string()),
+                        span,
+                        ty: Type::String,
+                    });
+                }
+                token_start = b;
+            } else if contents[b - start] == b'(' && mode == InterpolationMode::Expression {
+                depth += 1;
+            } else if contents[b - start] == b')' && mode == InterpolationMode::Expression {
+                match depth {
+                    0 => {}
+                    1 => {
+                        mode = InterpolationMode::String;
+
+                        if token_start < b {
+                            let span = Span {
+                                start: token_start,
+                                end: b + 1,
+                            };
+
+                            let (expr, err) = self.parse_full_column_path(span);
+                            error = error.or(err);
+                            output.push(expr);
+                        }
+
+                        token_start = b + 1;
+                    }
+                    _ => depth -= 1,
+                }
+            }
+            b += 1;
+        }
+
+        match mode {
+            InterpolationMode::String => {
+                if token_start < end {
+                    let span = Span {
+                        start: token_start,
+                        end,
+                    };
+                    let str_contents = self.get_span_contents(span);
+                    output.push(Expression {
+                        expr: Expr::String(String::from_utf8_lossy(str_contents).to_string()),
+                        span,
+                        ty: Type::String,
+                    });
+                }
+            }
+            InterpolationMode::Expression => {
+                if token_start < end {
+                    let span = Span {
+                        start: token_start,
+                        end,
+                    };
+
+                    let (expr, err) = self.parse_full_column_path(span);
+                    error = error.or(err);
+                    output.push(expr);
+                }
+            }
+        }
+
+        if let Some(decl_id) = self.find_decl(b"build-string") {
+            (
+                Expression {
+                    expr: Expr::Call(Box::new(Call {
+                        head: span,
+                        named: vec![],
+                        positional: output,
+                        decl_id,
+                    })),
+                    span,
+                    ty: Type::String,
+                },
+                error,
+            )
+        } else {
+            (
+                Expression::garbage(span),
+                Some(ParseError::UnknownCommand(span)),
+            )
+        }
     }
 
     pub fn parse_variable_expr(&mut self, span: Span) -> (Expression, Option<ParseError>) {
diff --git a/src/signature.rs b/src/signature.rs
index 8e12543a0c..a678982c4c 100644
--- a/src/signature.rs
+++ b/src/signature.rs
@@ -102,6 +102,17 @@ impl Signature {
         self
     }
 
+    pub fn rest(mut self, shape: impl Into<SyntaxShape>, desc: impl Into<String>) -> Signature {
+        self.rest_positional = Some(PositionalArg {
+            name: "rest".into(),
+            desc: desc.into(),
+            shape: shape.into(),
+            var_id: None,
+        });
+
+        self
+    }
+
     /// Add an optional named flag argument to the signature
     pub fn named(
         mut self,