1
0
mirror of https://gitlab.com/OpenMW/openmw.git synced 2025-01-10 06:39:49 +00:00
OpenMW/old_d_version/monster/modules/console.d
2009-12-19 21:31:22 +00:00

554 lines
14 KiB
D

/*
Monster - an advanced game scripting language
Copyright (C) 2007-2009 Nicolay Korslund
Email: <korslund@gmail.com>
WWW: http://monster.snaptoad.com/
This file (console.d) is part of the Monster script language package.
Monster is distributed as free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License
version 3, as published by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
version 3 along with this program. If not, see
http://www.gnu.org/licenses/ .
*/
module monster.modules.console;
// This file implements an optimized way of running Monster
// interactively, what is sometimes called "line mode" or "interactive
// mode". It is ideally suited for making ingame consoles.
// The main idea is to retain all reusable data structures and
// minimize the number of heap allocations at runtime. The current
// implemention is not perfected in that regard, but later
// implementations will be.
// All input into the console is given through the input() function,
// and all output is sent to the output() callback function. You can
// also poll for output manually using output();
import monster.util.growarray;
import monster.compiler.tokenizer;
import monster.compiler.statement;
import monster.compiler.variables;
import monster.compiler.functions;
import monster.compiler.scopes;
import monster.compiler.bytecode;
import monster.compiler.assembler;
import monster.compiler.types;
import monster.compiler.expression;
import std.stdio;
import std.string;
import monster.monster;
// Console results
enum CR
{
Ok = 1, // Command was executed
Error = 2, // An error occurred
More = 3, // An unterminated multi-line statement was entered, need
// more input
Empty = 4, // The line was empty (nothing was executed)
}
class Console
{
private:
Tokenizer tn;
GrowArray!(Token) tokArr;
GrowArray!(char) outBuf;
Function fn;
FuncScope sc;
MonsterObject *obj;
Variable* varList[];
uint varSize;
// The thread that we run console commands in. It's put in the
// background when not in use.
Thread *trd;
// The thread that was running when we started (if any)
Thread *store;
int paren, curl, square;
void delegate(char[] str) output_cb;
bool hasCallback;
char[] norm_prompt = ">>> ";
int tab = 4;
char[] ml_prompt = "... ";
char[] cmt_prompt = "(comment) ";
public:
bool allowVar = true;
this(MonsterObject *ob = null)
{
tn = new Tokenizer();
// Set the context object
obj = ob;
if(obj is null)
obj = Function.getIntMO();
// Next set up the function and the scope
fn.name.str = "__console";
fn.owner = obj.cls;
sc = new FuncScope(obj.cls.sc, &fn);
// Get a new thread
trd = Thread.getPaused();
}
void put(char[] str, bool newLine=false)
{
if(hasCallback)
{
output_cb(str);
if(newLine)
output_cb("\n");
}
else
{
outBuf ~= str;
if(newLine)
outBuf ~= '\n';
}
}
void putln(char[] str) { put(str, true); }
private:
Statement[] parse(TokenArray toks, Scope sc)
{
Statement b;
Statement[] res;
repeat:
b = null;
if(CodeBlock.canParse(toks)) b = new CodeBlock;
else if(IfStatement.canParse(toks)) b = new IfStatement;
else if(DoWhileStatement.canParse(toks)) b = new DoWhileStatement;
else if(WhileStatement.canParse(toks)) b = new WhileStatement;
else if(ForStatement.canParse(toks)) b = new ForStatement;
else if(ForeachStatement.canParse(toks)) b = new ForeachStatement;
else if(ImportStatement.canParse(toks)) b = new ImportStatement(true);
if(b !is null)
{
// Parse and resolve
b.parse(toks);
b.resolve(sc);
}
else
{
// If this is not one of the above, default to a console
// statement.
auto es = new ConsoleStatement;
b = es;
// Parse and resolve in one operation.
es.parseResolve(toks, sc, allowVar);
}
assert(b !is null);
res ~= b;
// Are there more tokens waiting for us?
if(toks.length > 0)
goto repeat;
return res;
}
int sumParen()
{
// Clean up if we had an unmatched end bracket somewhere
if(paren < 0) paren = 0;
if(curl < 0) curl = 0;
if(square < 0) square = 0;
return paren + curl + square;
}
bool isComment()
{
return tn.mode != Tokenizer.Normal;
}
// Resets the console to a usable state. Does not delete variables.
void reset()
{
paren = 0;
curl = 0;
square = 0;
tn.mode = Tokenizer.Normal;
if(cthread is trd)
{
// Reset the function stack.
trd.fstack.killAll();
assert(trd.fstack.isEmpty);
assert(!trd.fstack.hasNatives);
// Our variables should still be on the stack though.
if(stack.getPos > varSize)
stack.popInts(stack.getPos - varSize);
else
assert(stack.getPos == varSize);
// Make sure the thread is still in the 'paused' mode
trd.moveTo(&scheduler.paused);
// Background the thread - this will also capture the stack
trd.background();
}
assert(trd !is cthread);
// Restore the previous thread (if any)
if(store !is null)
store.foreground();
store = null;
}
// Push the input into the compiler and run it
CR runInput(char[] str)
{
// Set up the tokenizer
tn.setLine(str);
// Reset the token buffer, unless we're in a multiline command
if(paren == 0 && curl == 0 && square == 0)
tokArr.length = 0;
// Phase I, tokenize
Token t = tn.getNextFromLine();
// Mark the first token as a newline / separator
t.newline = true;
while(t.type != TT.EMPTY)
{
if(t.type == TT.LeftParen)
paren++;
else if(t.type == TT.LeftCurl)
curl++;
else if(t.type == TT.LeftSquare)
square++;
else if(t.type == TT.RightParen)
paren--;
else if(t.type == TT.RightCurl)
curl--;
else if(t.type == TT.RightSquare)
square--;
tokArr ~= t;
t = tn.getNextFromLine();
}
if(paren < 0 || curl < 0 || square < 0)
fail("Unmatched end bracket(s)");
// Wait for more input inside a bracket
if(sumParen() > 0)
return CR.More;
// Ditto for block comments
if(isComment())
return CR.More;
// Ignore empty token lists
if(tokArr.length == 0)
return CR.Empty;
// Phase II & III, parse and resolve
TokenArray toks = tokArr.arrayCopy();
Statement[] sts = parse(toks, sc);
delete toks;
assert(sts.length >= 1);
// First, background the current thread (if any) and bring up
// our own. This is necessary in order to keep the stack
// variables we make.
store = cthread;
if(store !is null)
store.background();
assert(trd !is null);
trd.foreground();
// We have to push ourselves on the function stack, or
// Function.call() will see that it's empty and kill the thread
// upon exit
trd.fstack.pushExt("Console");
// The rest must be performed separately for each statement on
// the line.
foreach(st; sts)
{
// Phase IV, compile
tasm.newFunc();
// Expression to print, if any
Expression printExp = null;
// Is it a special console statement?
auto cs = cast(ConsoleStatement)st;
if(cs !is null)
{
// Get the client expression, if any.
ExprStatement es = cast(ExprStatement)cs.client;
if(es !is null)
{
// It's a normal expression
if(es.right is null)
printExp = es.left;
}
// Not an expression, maybe a function?
else if(cs.func !is null)
{
// Yup, store it
printExp = cs.func;
}
}
// Ok, we got an expression. But is the type usable?
if(printExp !is null)
{
auto tp = printExp.type;
auto strt = ArrayType.getString();
assert(tp !is null);
assert(strt !is null);
if(tp == strt || tp.canCastTo(strt))
{
// Yup, it is! Cast the expression to string.
strt.typeCast(printExp, "console output");
printExp.eval();
}
else
// Type isn't usable, so set printExp to null to flag
// this.
printExp = null;
}
if(printExp is null)
// No expression is being used, so compile the statement
// normally.
st.compile();
// Gather all the statements into one function and get the
// bytecode.
fn.bcode = tasm.assemble(fn.lines);
fn.bcode ~= cast(ubyte)BC.Exit; // Non-optimal hack
// Phase V, call the function
fn.call(obj);
// Finally, get the expression result, if any, and print it.
if(printExp !is null)
putln(stack.popString8());
// In the case of new a variable declaration, we have to
// make sure they are accessible to any subsequent calls to
// the function. Since the stack frame gets set at each
// call, we have to access the variables outside the
// function frame, ie. the same way we treat function
// parameters. We do this by giving the variables negative
// indices.
if(cs !is null)
{
auto vs = cast(VarDeclStatement)cs.client;
if(vs !is null)
{
// Add the new vars to the list
foreach(v; vs.vars)
{
varList ~= v.var;
// Add the size as well
varSize += v.var.type.getSize;
}
// Recalculate all the indices backwards from zero
int place = 0;
foreach_reverse(v; varList)
{
place -= v.type.getSize;
v.number = place;
}
// Reset the scope stack counters
sc.reset();
}
}
}
trd.fstack.pop();
// Reset the console to a usable state
reset();
return CR.Ok;
}
public:
void prompt()
{
int sum = sumParen();
bool isBracket = (sum != 0);
sum = sum * tab + norm_prompt.length;
if(isComment)
{
sum -= cmt_prompt.length;
while(sum-->0)
put(" ");
put(cmt_prompt);
}
else if(isBracket)
{
sum -= ml_prompt.length;
while(sum-->0)
put(" ");
put(ml_prompt);
}
else
put(norm_prompt);
}
void addImports(char[][] str...)
{
assert(sc !is null);
sc.registerImport(str);
}
// Get the accumulated output since the last call. Includes
// newlines. Will only work if you have not set an output callback
// function.
char[] output()
{
assert(!hasCallback);
char[] res = outBuf.arrayCopy();
outBuf.length = 0;
return res;
}
// Sets the command prompt (default is "> ")
void setPrompt(char[] prmt)
{ norm_prompt = prmt; }
// Sets the multi-line prompt (default is "... ")
void setMLPrompt(char[] prmt)
{ ml_prompt = prmt; }
// Set tab size (default 4)
void setTabSize(int i)
{ tab = i; }
// Get input. Will sometimes expect multi-line input, for example if
// case a line contains an open brace or an unterminated block
// comment. In that case the console will produce another prompt
// (when you call prompt()), and also return true.
CR input(char[] str)
{
str = str.strip();
if(str == "") return CR.Empty;
try return runInput(str);
catch(MonsterException e)
{
putln(e.toString);
reset();
return CR.Error;
}
}
}
// Statement that handles variable declarations, function calls and
// expression statements in consoles. Since these are gramatically
// similar, we need the type to determine which it is.
class ConsoleStatement : Statement
{
// Used for variables and expression statements
Statement client;
// Used for function calls
FunctionCallExpr func;
void parseResolve(ref TokenArray toks, Scope sc, bool allowVar)
{
assert(toks.length != 0);
// Get the first expression
auto first = Expression.identify(toks);
// And the type right away
first.resolve(sc);
auto type = first.type;
assert(type !is null);
// Type? If so, it's a variable declaration
if((type.isMeta || type.isVar) && allowVar)
{
if(type.isMeta) type = type.getBase();
client = new VarDeclStatement(type);
}
// Function?
else if(type.isIntFunc)
func = new FunctionCallExpr(first, toks, true);
// It's an expression statement
else
client = new ExprStatement(first);
if(client !is null)
{
client.parse(toks);
client.resolve(sc);
}
else
{
assert(func !is null);
func.resolve(sc);
}
}
override:
void parse(ref TokenArray) { assert(0); }
void resolve(Scope sc) { assert(0); }
void compile()
{
if(client !is null)
{
client.compile();
return;
}
assert(func !is null);
func.evalPop();
}
}