-
-
Notifications
You must be signed in to change notification settings - Fork 66
/
MiniscriptInterpreter.cs
326 lines (291 loc) · 11.5 KB
/
MiniscriptInterpreter.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
/* MiniscriptInterpreter.cs
The only class in this file is Interpreter, which is your main interface
to the MiniScript system. You give Interpreter some MiniScript source
code, and tell it where to send its output (via delegate functions called
TextOutputMethod). Then you typically call RunUntilDone, which returns
when either the script has stopped or the given timeout has passed.
For details, see Chapters 1-3 of the MiniScript Integration Guide.
*/
using System;
using System.Collections.Generic;
namespace Miniscript {
/// <summary>
/// TextOutputMethod: a delegate used to return text from the script
/// (e.g. normal output, errors, etc.) to your C# code.
/// </summary>
/// <param name="output"></param>
public delegate void TextOutputMethod(string output, bool addLineBreak);
/// <summary>
/// Interpreter: an object that contains and runs one MiniScript script.
/// </summary>
public class Interpreter {
/// <summary>
/// standardOutput: receives the output of the "print" intrinsic.
/// </summary>
public TextOutputMethod standardOutput {
get {
return _standardOutput;
}
set {
_standardOutput = value;
if (vm != null) vm.standardOutput = value;
}
}
/// <summary>
/// implicitOutput: receives the value of expressions entered when
/// in REPL mode. If you're not using the REPL() method, you can
/// safely ignore this.
/// </summary>
public TextOutputMethod implicitOutput;
/// <summary>
/// errorOutput: receives error messages from the runtime. (This happens
/// via the ReportError method, which is virtual; so if you want to catch
/// the actual exceptions rather than get the error messages as strings,
/// you can subclass Interpreter and override that method.)
/// </summary>
public TextOutputMethod errorOutput;
/// <summary>
/// hostData is just a convenient place for you to attach some arbitrary
/// data to the interpreter. It gets passed through to the context object,
/// so you can access it inside your custom intrinsic functions. Use it
/// for whatever you like (or don't, if you don't feel the need).
/// </summary>
public object hostData;
/// <summary>
/// done: returns true when we don't have a virtual machine, or we do have
/// one and it is done (has reached the end of its code).
/// </summary>
public bool done {
get { return vm == null || vm.done; }
}
/// <summary>
/// vm: the virtual machine this interpreter is running. Most applications will
/// not need to use this, but it's provided for advanced users.
/// </summary>
public TAC.Machine vm;
TextOutputMethod _standardOutput;
string source;
Parser parser;
/// <summary>
/// Constructor taking some MiniScript source code, and the output delegates.
/// </summary>
public Interpreter(string source=null, TextOutputMethod standardOutput=null, TextOutputMethod errorOutput=null) {
this.source = source;
if (standardOutput == null) standardOutput = (s,eol) => Console.WriteLine(s);
if (errorOutput == null) errorOutput = (s,eol) => Console.WriteLine(s);
this.standardOutput = standardOutput;
this.errorOutput = errorOutput;
}
/// <summary>
/// Constructor taking source code in the form of a list of strings.
/// </summary>
public Interpreter(List<string> source) : this(string.Join("\n", source.ToArray())) {
}
/// <summary>
/// Constructor taking source code in the form of a string array.
/// </summary>
public Interpreter(string[] source) : this(string.Join("\n", source)) {
}
/// <summary>
/// Stop the virtual machine, and jump to the end of the program code.
/// Also reset the parser, in case it's stuck waiting for a block ender.
/// </summary>
public void Stop() {
if (vm != null) vm.Stop();
if (parser != null) parser.PartialReset();
}
/// <summary>
/// Reset the interpreter with the given source code.
/// </summary>
/// <param name="source"></param>
public void Reset(string source="") {
this.source = source;
parser = null;
vm = null;
}
/// <summary>
/// Compile our source code, if we haven't already done so, so that we are
/// either ready to run, or generate compiler errors (reported via errorOutput).
/// </summary>
public void Compile() {
if (vm != null) return; // already compiled
if (parser == null) parser = new Parser();
try {
parser.Parse(source);
vm = parser.CreateVM(standardOutput);
vm.interpreter = new WeakReference(this);
} catch (MiniscriptException mse) {
ReportError(mse);
if (vm == null) parser = null;
}
}
/// <summary>
/// Reset the virtual machine to the beginning of the code. Note that this
/// does *not* reset global variables; it simply clears the stack and jumps
/// to the beginning. Useful in cases where you have a short script you
/// want to run over and over, without recompiling every time.
/// </summary>
public void Restart() {
if (vm != null) vm.Reset();
}
/// <summary>
/// Run the compiled code until we either reach the end, or we reach the
/// specified time limit. In the latter case, you can then call RunUntilDone
/// again to continue execution right from where it left off.
///
/// Or, if returnEarly is true, we will also return if we reach an intrinsic
/// method that returns a partial result, indicating that it needs to wait
/// for something. Again, call RunUntilDone again later to continue.
///
/// Note that this method first compiles the source code if it wasn't compiled
/// already, and in that case, may generate compiler errors. And of course
/// it may generate runtime errors while running. In either case, these are
/// reported via errorOutput.
/// </summary>
/// <param name="timeLimit">maximum amout of time to run before returning, in seconds</param>
/// <param name="returnEarly">if true, return as soon as we reach an intrinsic that returns a partial result</param>
public void RunUntilDone(double timeLimit=60, bool returnEarly=true) {
int startImpResultCount = 0;
try {
if (vm == null) {
Compile();
if (vm == null) return; // (must have been some error)
}
startImpResultCount = vm.globalContext.implicitResultCounter;
double startTime = vm.runTime;
vm.yielding = false;
while (!vm.done && !vm.yielding) {
// ToDo: find a substitute for vm.runTime, or make it go faster, because
// right now about 14% of our run time is spent just in the vm.runtime call!
// Perhaps Environment.TickCount? (Just watch out for the wraparound every 25 days!)
if (vm.runTime - startTime > timeLimit) return; // time's up for now!
vm.Step(); // update the machine
if (returnEarly && vm.GetTopContext().partialResult != null) return; // waiting for something
}
} catch (MiniscriptException mse) {
ReportError(mse);
Stop(); // was: vm.GetTopContext().JumpToEnd();
}
CheckImplicitResult(startImpResultCount);
}
/// <summary>
/// Run one step of the virtual machine. This method is not very useful
/// except in special cases; usually you will use RunUntilDone (above) instead.
/// </summary>
public void Step() {
try {
Compile();
vm.Step();
} catch (MiniscriptException mse) {
ReportError(mse);
Stop(); // was: vm.GetTopContext().JumpToEnd();
}
}
/// <summary>
/// Read Eval Print Loop. Run the given source until it either terminates,
/// or hits the given time limit. When it terminates, if we have new
/// implicit output, print that to the implicitOutput stream.
/// </summary>
/// <param name="sourceLine">Source line.</param>
/// <param name="timeLimit">Time limit.</param>
public void REPL(string sourceLine, double timeLimit=60) {
if (parser == null) parser = new Parser();
if (vm == null) {
vm = parser.CreateVM(standardOutput);
vm.interpreter = new WeakReference(this);
} else if (vm.done && !parser.NeedMoreInput()) {
// Since the machine and parser are both done, we don't really need the
// previously-compiled code. So let's clear it out, as a memory optimization.
vm.GetTopContext().ClearCodeAndTemps();
parser.PartialReset();
}
if (sourceLine == "#DUMP") {
vm.DumpTopContext();
return;
}
double startTime = vm.runTime;
int startImpResultCount = vm.globalContext.implicitResultCounter;
vm.storeImplicit = (implicitOutput != null);
vm.yielding = false;
try {
if (sourceLine != null) parser.Parse(sourceLine, true);
if (!parser.NeedMoreInput()) {
while (!vm.done && !vm.yielding) {
if (vm.runTime - startTime > timeLimit) return; // time's up for now!
vm.Step();
}
CheckImplicitResult(startImpResultCount);
}
} catch (MiniscriptException mse) {
ReportError(mse);
// Attempt to recover from an error by jumping to the end of the code.
Stop(); // was: vm.GetTopContext().JumpToEnd();
}
}
/// <summary>
/// Report whether the virtual machine is still running, that is,
/// whether it has not yet reached the end of the program code.
/// </summary>
/// <returns></returns>
public bool Running() {
return vm != null && !vm.done;
}
/// <summary>
/// Return whether the parser needs more input, for example because we have
/// run out of source code in the middle of an "if" block. This is typically
/// used with REPL for making an interactive console, so you can change the
/// prompt when more input is expected.
/// </summary>
/// <returns></returns>
public bool NeedMoreInput() {
return parser != null && parser.NeedMoreInput();
}
/// <summary>
/// Get a value from the global namespace of this interpreter.
/// </summary>
/// <param name="varName">name of global variable to get</param>
/// <returns>Value of the named variable, or null if not found</returns>
public Value GetGlobalValue(string varName) {
if (vm == null) return null;
TAC.Context c = vm.globalContext;
if (c == null) return null;
try {
return c.GetVar(varName);
} catch (UndefinedIdentifierException) {
return null;
}
}
/// <summary>
/// Set a value in the global namespace of this interpreter.
/// </summary>
/// <param name="varName">name of global variable to set</param>
/// <param name="value">value to set</param>
public void SetGlobalValue(string varName, Value value) {
if (vm != null) vm.globalContext.SetVar(varName, value);
}
/// <summary>
/// Helper method that checks whether we have a new implicit result, and if
/// so, invokes the implicitOutput callback (if any). This is how you can
/// see the result of an expression in a Read-Eval-Print Loop (REPL).
/// </summary>
/// <param name="previousImpResultCount">previous value of implicitResultCounter</param>
protected void CheckImplicitResult(int previousImpResultCount) {
if (implicitOutput != null && vm.globalContext.implicitResultCounter > previousImpResultCount) {
Value result = vm.globalContext.GetVar(ValVar.implicitResult.identifier);
if (result != null) {
implicitOutput.Invoke(result.ToString(vm), true);
}
}
}
/// <summary>
/// Report a MiniScript error to the user. The default implementation
/// simply invokes errorOutput with the error description. If you want
/// to do something different, then make an Interpreter subclass, and
/// override this method.
/// </summary>
/// <param name="mse">exception that was thrown</param>
protected virtual void ReportError(MiniscriptException mse) {
errorOutput.Invoke(mse.Description(), true);
}
}
}