Skip to content

Commit 5ebb3d4

Browse files
committed
CLike: Implement templated strings
1 parent 80178f4 commit 5ebb3d4

File tree

11 files changed

+283
-5
lines changed

11 files changed

+283
-5
lines changed

src/MoonSharp.Interpreter/Execution/InstructionFieldUsage.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ internal static InstructionFieldUsage GetFieldUsage(this OpCode op)
5656
case OpCode.Ret:
5757
case OpCode.MkTuple:
5858
case OpCode.CloseUp:
59+
case OpCode.StrFormat:
5960
return InstructionFieldUsage.NumVal;
6061
case OpCode.Jump:
6162
case OpCode.Jf:

src/MoonSharp.Interpreter/Execution/VM/ByteCode.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ public int Emit_Literal(DynValue value)
144144
throw new InvalidOperationException(value.Type.ToString());
145145
}
146146

147+
public int Emit_StrFormat(int argCount)
148+
{
149+
return AppendInstruction(new Instruction() {OpCode = OpCode.StrFormat, NumVal = argCount});
150+
}
151+
147152
public int Emit_Jump(OpCode jumpOpCode, int idx, int optPar = 0)
148153
{
149154
return AppendInstruction(new Instruction() { OpCode = jumpOpCode, NumVal = idx, NumVal2 = optPar });

src/MoonSharp.Interpreter/Execution/VM/OpCode.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ internal enum OpCode
4949
JFor, // Peeks at the top, top-1 and top-2 values of the v-stack which it assumes to be numbers. Then if top-1 is less than zero, checks if top is <= top-2, otherwise it checks that top is >= top-2. Then if the condition is false, it jumps.
5050
JtOrPop, // Peeks at the topmost value of the v-stack as a boolean. If true, it performs a jump, otherwise it removes the topmost value from the v-stack.
5151
JfOrPop, // Peeks at the topmost value of the v-stack as a boolean. If false, it performs a jump, otherwise it removes the topmost value from the v-stack.
52-
52+
//
53+
StrFormat, // Format using string.Format
5354
// Operators
5455
Concat, // Concatenation of the two topmost operands on the v-stack
5556
LessEq, // Compare <= of the two topmost operands on the v-stack

src/MoonSharp.Interpreter/Execution/VM/Processor/Processor_InstructionLoop.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@ private DynValue Processing_Loop(int instructionPtr, bool canAwait = false)
230230
top = DynValue.NewBoolean(top.CastToBool());
231231
break;
232232
}
233+
case OpCode.StrFormat:
234+
ExecStrFormat(i);
235+
break;
233236
case OpCode.Args:
234237
ExecArgs(i);
235238
break;
@@ -1156,6 +1159,25 @@ bool ToConcatString(ref DynValue v, out string s, ref int metamethodCounter)
11561159
return false;
11571160
}
11581161
}
1162+
1163+
private void ExecStrFormat(Instruction i)
1164+
{
1165+
string[] formatValues = new string[i.NumVal];
1166+
if (i.NumVal > 0)
1167+
{
1168+
for (int j = 0; j < i.NumVal; j++)
1169+
{
1170+
var off = (i.NumVal - j - 1);
1171+
int mCount = 0;
1172+
if (!ToConcatString(ref m_ValueStack.Peek(off), out formatValues[j], ref mCount))
1173+
{
1174+
formatValues[j] = m_ValueStack.Peek(off).ToPrintString();
1175+
}
1176+
}
1177+
}
1178+
m_ValueStack.RemoveLast(i.NumVal);
1179+
m_ValueStack.Set(0, DynValue.NewString(string.Format(m_ValueStack.Peek(0).String, formatValues)));
1180+
}
11591181

11601182
private int ExecAddStr(Instruction i, int instructionPtr)
11611183
{

src/MoonSharp.Interpreter/Tree/Expression_.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,10 @@ private static Expression PrefixExp(ScriptLoadingContext lcontext)
306306
switch (T.Type)
307307
{
308308
case TokenType.String when lcontext.Syntax == ScriptSyntax.CLike:
309+
case TokenType.String_EndTemplate:
309310
return new LiteralExpression(lcontext, T);
311+
case TokenType.String_TemplateFragment:
312+
return new TemplatedStringExpression(lcontext, T);
310313
case TokenType.Brk_Open_Round:
311314
lcontext.Lexer.Next();
312315
Expression e = Expr(lcontext);

src/MoonSharp.Interpreter/Tree/Expressions/LiteralExpression.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public LiteralExpression(ScriptLoadingContext lcontext, Token t)
3232
break;
3333
case TokenType.String:
3434
case TokenType.String_Long:
35+
case TokenType.String_EndTemplate:
3536
m_Value = DynValue.NewString(t.Text);
3637
break;
3738
case TokenType.True:
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Text;
4+
using MoonSharp.Interpreter.Execution;
5+
using MoonSharp.Interpreter.Execution.VM;
6+
7+
namespace MoonSharp.Interpreter.Tree.Expressions
8+
{
9+
class TemplatedStringExpression : Expression
10+
{
11+
private List<Expression> arguments = new List<Expression>();
12+
private string formatString;
13+
14+
static string EscapeForFormat(string s) => s.Replace("{", "{{").Replace("}", "}}");
15+
16+
public TemplatedStringExpression(ScriptLoadingContext lcontext, Token startToken) : base(lcontext)
17+
{
18+
var builder = new StringBuilder();
19+
builder.Append(EscapeForFormat(startToken.Text));
20+
lcontext.Lexer.Next();
21+
int i = 0;
22+
while (lcontext.Lexer.Current.Type != TokenType.String_EndTemplate) {
23+
if (lcontext.Lexer.Current.Type == TokenType.Eof) {
24+
throw new SyntaxErrorException(lcontext.Lexer.Current, "`` expected")
25+
{
26+
IsPrematureStreamTermination = true
27+
};
28+
}
29+
if (lcontext.Lexer.Current.Type != TokenType.String_TemplateFragment) {
30+
builder.Append("{").Append(i++).Append("}");
31+
arguments.Add(Expr(lcontext));
32+
}
33+
else {
34+
builder.Append(EscapeForFormat(lcontext.Lexer.Current.Text));
35+
lcontext.Lexer.Next();
36+
}
37+
}
38+
builder.Append(EscapeForFormat(lcontext.Lexer.Current.Text));
39+
lcontext.Lexer.Next();
40+
formatString = builder.ToString();
41+
}
42+
43+
public override void Compile(ByteCode bc)
44+
{
45+
bc.Emit_Literal(DynValue.NewString(formatString));
46+
foreach (var exp in arguments) {
47+
exp.CompilePossibleLiteral(bc);
48+
}
49+
bc.Emit_StrFormat(arguments.Count);
50+
}
51+
52+
public override DynValue Eval(ScriptExecutionContext context)
53+
{
54+
return DynValue.NewString(string.Format(formatString, arguments.Select(x =>
55+
{
56+
var dyn = x.Eval(context);
57+
if (dyn.Type == DataType.String) return dyn.String;
58+
else if (dyn.Type == DataType.Number) return dyn.Number.ToString();
59+
else if (dyn.Type == DataType.Boolean) {
60+
return dyn.Boolean ? "true" : "false";
61+
}
62+
else {
63+
//TODO: I think this is incorrect
64+
throw new DynamicExpressionException("Cannot call __tostring in dynamic expression");
65+
}
66+
})));
67+
}
68+
69+
public override bool EvalLiteral(out DynValue dv)
70+
{
71+
dv = DynValue.Nil;
72+
return false;
73+
}
74+
}
75+
}

src/MoonSharp.Interpreter/Tree/Lexer/Lexer.cs

Lines changed: 144 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Text;
34

45
namespace MoonSharp.Interpreter.Tree
@@ -60,23 +61,58 @@ public void Next()
6061
m_Current = FetchNewToken();
6162
}
6263

64+
private List<int> templateStringState = new List<int>();
65+
66+
void PushTemplateString()
67+
{
68+
templateStringState.Add(0);
69+
}
70+
71+
bool InTemplateString() => templateStringState.Count > 0;
72+
73+
void TemplateStringAddBracket()
74+
{
75+
if (InTemplateString()) {
76+
templateStringState[templateStringState.Count - 1]++;
77+
}
78+
}
79+
80+
bool ReturnToTemplateString()
81+
{
82+
var c = templateStringState[templateStringState.Count - 1];
83+
if (c == 0) return true;
84+
templateStringState[templateStringState.Count - 1]--;
85+
return false;
86+
}
87+
88+
void PopTemplateString()
89+
{
90+
templateStringState.RemoveAt(templateStringState.Count - 1);
91+
}
92+
93+
6394
struct Snapshot
6495
{
6596
public int Cursor;
6697
public Token Current;
6798
public int Line;
6899
public int Col;
100+
public int[] TemplateStringState;
69101
}
102+
70103

71104
private Snapshot s;
105+
106+
72107

73108
public void SavePos()
74109
{
75110
s = new Snapshot() {
76111
Cursor = m_Cursor,
77112
Current = m_Current,
78113
Line = m_Line,
79-
Col = m_Col
114+
Col = m_Col,
115+
TemplateStringState = templateStringState.ToArray(),
80116
};
81117
}
82118

@@ -86,6 +122,7 @@ public void RestorePos()
86122
m_Current = s.Current;
87123
m_Line = s.Line;
88124
m_Col = s.Col;
125+
templateStringState = new List<int>(s.TemplateStringState);
89126
}
90127

91128
public Token PeekNext()
@@ -94,6 +131,12 @@ public Token PeekNext()
94131
Token current = m_Current;
95132
int line = m_Line;
96133
int col = m_Col;
134+
//Save the template string state
135+
int stateC = templateStringState.Count;
136+
int lastC = 0;
137+
if (templateStringState.Count > 0) {
138+
lastC = templateStringState[templateStringState.Count - 1];
139+
}
97140

98141
Next();
99142
Token t = Current;
@@ -102,7 +145,19 @@ public Token PeekNext()
102145
m_Current = current;
103146
m_Line = line;
104147
m_Col = col;
105-
148+
//Restore the template string state
149+
if (templateStringState.Count < stateC)
150+
{
151+
templateStringState.Add(lastC);
152+
}
153+
else if (templateStringState.Count > stateC)
154+
{
155+
templateStringState.RemoveAt(templateStringState.Count - 1);
156+
}
157+
else if (stateC != 0)
158+
{
159+
templateStringState[templateStringState.Count - 1] = lastC;
160+
}
106161
return t;
107162
}
108163

@@ -463,8 +518,17 @@ private Token ReadToken()
463518
case ')':
464519
return CreateSingleCharToken(TokenType.Brk_Close_Round, fromLine, fromCol);
465520
case '{':
521+
TemplateStringAddBracket();
466522
return CreateSingleCharToken(TokenType.Brk_Open_Curly, fromLine, fromCol);
467-
case '}':
523+
case '}' when InTemplateString(): {
524+
if (ReturnToTemplateString()) {
525+
return ReadTemplateString(fromLine, fromCol, false);
526+
}
527+
else {
528+
return CreateSingleCharToken(TokenType.Brk_Close_Curly, fromLine, fromCol);
529+
}
530+
}
531+
case '}' when !InTemplateString():
468532
return CreateSingleCharToken(TokenType.Brk_Close_Curly, fromLine, fromCol);
469533
case ',':
470534
return CreateSingleCharToken(TokenType.Comma, fromLine, fromCol);
@@ -509,6 +573,19 @@ private Token ReadToken()
509573
}
510574
case ':':
511575
return PotentiallyDoubleCharOperator(':', TokenType.Colon, TokenType.DoubleColon, fromLine, fromCol);
576+
case '`':
577+
{
578+
char next = CursorCharNext();
579+
if (next == '`')
580+
{
581+
PushTemplateString();
582+
return ReadTemplateString(fromLine, fromCol, true);
583+
}
584+
throw new SyntaxErrorException(CreateToken(TokenType.Invalid, fromLine, fromCol), "unexpected symbol near '{0}'", CursorChar())
585+
{
586+
IsPrematureStreamTermination = true
587+
};
588+
}
512589
case '"':
513590
case '\'':
514591
return ReadSimpleStringToken(fromLine, fromCol);
@@ -534,6 +611,70 @@ private Token ReadToken()
534611
}
535612
}
536613

614+
615+
Token ReadTemplateString(int fromLine, int fromCol, bool isStart)
616+
{
617+
StringBuilder text = new StringBuilder(32);
618+
619+
for (char c = CursorCharNext(); CursorNotEof(); c = CursorCharNext())
620+
{
621+
redo_Loop:
622+
623+
if (c == '\\')
624+
{
625+
text.Append(c);
626+
c = CursorCharNext();
627+
text.Append(c);
628+
629+
if (c == '\r')
630+
{
631+
c = CursorCharNext();
632+
if (c == '\n')
633+
text.Append(c);
634+
else
635+
goto redo_Loop;
636+
}
637+
else if (c == 'z')
638+
{
639+
c = CursorCharNext();
640+
641+
if (char.IsWhiteSpace(c))
642+
SkipWhiteSpace();
643+
644+
c = CursorChar();
645+
646+
goto redo_Loop;
647+
}
648+
}
649+
else if (c == '$' && CursorMatches("${"))
650+
{
651+
CursorCharNext();
652+
CursorCharNext();
653+
Token t = CreateToken(TokenType.String_TemplateFragment, fromLine, fromCol);
654+
t.Text = LexerUtils.UnescapeLuaString(t, text.ToString());
655+
return t;
656+
}
657+
else if (c == '`' && CursorMatches("``"))
658+
{
659+
CursorCharNext();
660+
CursorCharNext();
661+
PopTemplateString();
662+
Token t = CreateToken(isStart ? TokenType.String_Long : TokenType.String_EndTemplate, fromLine, fromCol);
663+
t.Text = LexerUtils.UnescapeLuaString(t, text.ToString());
664+
return t;
665+
}
666+
else
667+
{
668+
text.Append(c);
669+
}
670+
}
671+
672+
throw new SyntaxErrorException(
673+
CreateToken(TokenType.Invalid, fromLine, fromCol),
674+
"unfinished string near '{0}'", text.ToString()) { IsPrematureStreamTermination = true };
675+
}
676+
677+
537678
private string ReadLongString(int fromLine, int fromCol, string startpattern, string subtypeforerrors)
538679
{
539680
// here we are at the first '=' or second '['

src/MoonSharp.Interpreter/Tree/Lexer/LexerUtils.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ public static string UnescapeLuaString(Token token, string str)
170170
else if (c == '\'') { sb.Append('\''); escape = false; zmode = false; }
171171
else if (c == '[') { sb.Append('['); escape = false; zmode = false; }
172172
else if (c == ']') { sb.Append(']'); escape = false; zmode = false; }
173+
else if (c == '{') { sb.Append('{'); escape = false; zmode = false; }
173174
else if (c == 'x') { hex = true; }
174175
else if (c == 'u') { unicode_state = 1; }
175176
else if (c == 'z') { zmode = true; escape = false; }

src/MoonSharp.Interpreter/Tree/Lexer/TokenType.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ enum TokenType
9494

9595
String,
9696
String_Long,
97-
97+
String_TemplateFragment,
98+
String_EndTemplate,
9899
Number,
99100
Number_HexFloat,
100101
Number_Hex,

0 commit comments

Comments
 (0)