| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517 |
- using System;
- using System.Collections.Generic;
- using Newtonsoft.Json;
- namespace Yarn
- {
- internal class Compiler
- {
- struct CompileFlags {
- // should we emit code that turns (VAR_SHUFFLE_OPTIONS) off
- // after the next RunOptions bytecode?
- public bool DisableShuffleOptionsAfterNextSet;
- }
- CompileFlags flags;
- internal Program program { get; private set; }
- internal Compiler (string programName)
- {
- program = new Program ();
- }
- internal void CompileNode(Parser.Node node) {
- if (program.nodes.ContainsKey(node.name)) {
- throw new ArgumentException ("Duplicate node name " + node.name);
- }
- var compiledNode = new Node();
- compiledNode.name = node.name;
- compiledNode.tags = node.nodeTags;
- // Register the entire text of this node if we have it
- if (node.source != null)
- {
- // Dump the entire contents of this node into the string table
- // instead of compiling its contents.
- // the line number is 0 because the string starts at the start of the node
- compiledNode.sourceTextStringID = program.RegisterString(node.source, node.name, "line:"+node.name, 0, true);
- } else {
- // Compile the node.
- var startLabel = RegisterLabel();
- Emit(compiledNode, ByteCode.Label, startLabel);
- foreach (var statement in node.statements)
- {
- GenerateCode(compiledNode, statement);
- }
- // Does this node end after emitting AddOptions codes
- // without calling ShowOptions?
- // Note: this only works when we know that we don't have
- // AddOptions and then Jump up back into the code to run them.
- // TODO: A better solution would be for the parser to flag
- // whether a node has Options at the end.
- var hasRemainingOptions = false;
- foreach (var instruction in compiledNode.instructions)
- {
- if (instruction.operation == ByteCode.AddOption)
- {
- hasRemainingOptions = true;
- }
- if (instruction.operation == ByteCode.ShowOptions)
- {
- hasRemainingOptions = false;
- }
- }
- // If this compiled node has no lingering options to show at the end of the node, then stop at the end
- if (hasRemainingOptions == false)
- {
- Emit(compiledNode, ByteCode.Stop);
- }
- else {
- // Otherwise, show the accumulated nodes and then jump to the selected node
- Emit(compiledNode, ByteCode.ShowOptions);
- if (flags.DisableShuffleOptionsAfterNextSet == true)
- {
- Emit(compiledNode, ByteCode.PushBool, false);
- Emit(compiledNode, ByteCode.StoreVariable, VirtualMachine.SpecialVariables.ShuffleOptions);
- Emit(compiledNode, ByteCode.Pop);
- flags.DisableShuffleOptionsAfterNextSet = false;
- }
- Emit(compiledNode, ByteCode.RunNode);
- }
- }
- program.nodes[compiledNode.name] = compiledNode;
- }
- private int labelCount = 0;
- // Generates a unique label name to use
- string RegisterLabel(string commentary = null) {
- return "L" + labelCount++ + commentary;
- }
- void Emit(Node node, ByteCode code, object operandA = null, object operandB = null) {
- var instruction = new Instruction();
- instruction.operation = code;
- instruction.operandA = operandA;
- instruction.operandB = operandB;
- node.instructions.Add (instruction);
- if (code == ByteCode.Label) {
- // Add this label to the label table
- node.labels.Add ((string)instruction.operandA, node.instructions.Count - 1);
- }
- }
- // Statements
- void GenerateCode(Node node, Parser.Statement statement) {
- switch (statement.type) {
- case Parser.Statement.Type.CustomCommand:
- GenerateCode (node, statement.customCommand);
- break;
- case Parser.Statement.Type.ShortcutOptionGroup:
- GenerateCode (node, statement.shortcutOptionGroup);
- break;
- case Parser.Statement.Type.Block:
- // Blocks are just groups of statements
- foreach (var blockStatement in statement.block.statements) {
- GenerateCode(node, blockStatement);
- }
- break;
- case Parser.Statement.Type.IfStatement:
- GenerateCode (node, statement.ifStatement);
- break;
- case Parser.Statement.Type.OptionStatement:
- GenerateCode (node, statement.optionStatement);
- break;
- case Parser.Statement.Type.AssignmentStatement:
- GenerateCode (node, statement.assignmentStatement);
- break;
- case Parser.Statement.Type.Line:
- GenerateCode (node, statement, statement.line);
- break;
- default:
- throw new ArgumentOutOfRangeException ();
- }
- }
- void GenerateCode(Node node, Parser.CustomCommand statement) {
- // If this command is an evaluable expression, evaluate it
- if (statement.expression != null) {
- GenerateCode (node, statement.expression);
- } else {
- switch (statement.clientCommand) {
- case "stop":
- Emit (node, ByteCode.Stop);
- break;
- case "shuffleNextOptions":
- // Emit code that sets "VAR_SHUFFLE_OPTIONS" to true
- Emit (node, ByteCode.PushBool, true);
- Emit (node, ByteCode.StoreVariable, VirtualMachine.SpecialVariables.ShuffleOptions);
- Emit (node, ByteCode.Pop);
- flags.DisableShuffleOptionsAfterNextSet = true;
- break;
- default:
- Emit (node, ByteCode.RunCommand, statement.clientCommand);
- break;
- }
- }
- }
- string GetLineIDFromNodeTags(Parser.ParseNode node) {
- // TODO: This will use only the first #line: tag, ignoring all others
- foreach (var tag in node.tags)
- {
- if (tag.StartsWith("line:"))
- {
- return tag;
- }
- }
- return null;
- }
- void GenerateCode(Node node, Parser.Statement parseNode, string line) {
- // Does this line have a "#line:LINENUM" tag? Use it
- string lineID = GetLineIDFromNodeTags(parseNode);
- var parts = new List<String>();
- var currentPart = "";
- foreach (var c in line)
- {
- if (c == '$')
- {
- if (currentPart.Length > 0) parts.Add(currentPart);
- currentPart = "$";
- }
- else
- {
- if (currentPart.Length > 0 && currentPart[0] == '$' && ",.?!: ".Contains(new string(c,1)))
- {
- parts.Add(currentPart);
- currentPart = new string(c, 1);
- }
- else
- {
- currentPart += c;
- }
- }
- }
- if (currentPart.Length > 0) parts.Add(currentPart);
- if (parts.Count == 1)
- {
- var num = program.RegisterString(line, node.name, lineID, parseNode.lineNumber, true);
- Emit(node, ByteCode.RunLine, num);
- }
- else
- {
- EmitLinePart(node, parseNode, parts[0], lineID);
- for (var i = 1; i < parts.Count; ++i)
- {
- EmitLinePart(node, parseNode, parts[i], lineID);
- node.instructions.Add(new Instruction
- {
- operation = ByteCode.Concat,
- operandA = null,
- operandB = null
- });
- }
- node.instructions.Add(new Instruction
- {
- operation = ByteCode.RunLineFromStack,
- operandA = null,
- operandB = null
- });
- }
- }
- private void EmitLinePart(Node node, Parser.Statement parseNode, String part, String lineID)
- {
- if (part[0] == '$')
- {
- node.instructions.Add(new Instruction
- {
- operation = ByteCode.PushVariable,
- operandA = part,
- operandB = null
- });
- }
- else
- {
- var num = program.RegisterString(part, node.name, lineID, parseNode.lineNumber, true);
- node.instructions.Add(new Instruction
- {
- operation = ByteCode.PushString,
- operandA = num,
- operandB = null
- });
- }
- }
- void GenerateCode(Node node, Parser.ShortcutOptionGroup statement) {
- var endOfGroupLabel = RegisterLabel ("group_end");
- var labels = new List<string> ();
- int optionCount = 0;
- foreach (var shortcutOption in statement.options) {
- var optionDestinationLabel = RegisterLabel ("option_" + (optionCount+1));
- labels.Add (optionDestinationLabel);
- string endOfClauseLabel = null;
- if (shortcutOption.condition != null) {
- endOfClauseLabel = RegisterLabel ("conditional_"+optionCount);
- GenerateCode (node, shortcutOption.condition);
- Emit (node, ByteCode.JumpIfFalse, endOfClauseLabel);
- }
- var labelLineID = GetLineIDFromNodeTags(shortcutOption);
- var labelStringID = program.RegisterString (shortcutOption.label, node.name, labelLineID, shortcutOption.lineNumber, true);
- Emit (node, ByteCode.AddOption, labelStringID, optionDestinationLabel);
- if (shortcutOption.condition != null) {
- Emit (node, ByteCode.Label, endOfClauseLabel);
- Emit (node, ByteCode.Pop);
- }
- optionCount++;
- }
- Emit (node, ByteCode.ShowOptions);
- if (flags.DisableShuffleOptionsAfterNextSet == true) {
- Emit (node, ByteCode.PushBool, false);
- Emit (node, ByteCode.StoreVariable, VirtualMachine.SpecialVariables.ShuffleOptions);
- Emit (node, ByteCode.Pop);
- flags.DisableShuffleOptionsAfterNextSet = false;
- }
- Emit (node, ByteCode.Jump);
- optionCount = 0;
- foreach (var shortcutOption in statement.options) {
- Emit (node, ByteCode.Label, labels [optionCount]);
- if (shortcutOption.optionNode != null)
- GenerateCode (node, shortcutOption.optionNode.statements);
- Emit (node, ByteCode.JumpTo, endOfGroupLabel);
- optionCount++;
- }
- // reached the end of the option group
- Emit (node, ByteCode.Label, endOfGroupLabel);
- // clean up after the jump
- Emit (node, ByteCode.Pop);
- }
- void GenerateCode(Node node, IEnumerable<Yarn.Parser.Statement> statementList) {
- if (statementList == null)
- return;
- foreach (var statement in statementList) {
- GenerateCode (node, statement);
- }
- }
- void GenerateCode(Node node, Parser.IfStatement statement) {
- // We'll jump to this label at the end of every clause
- var endOfIfStatementLabel = RegisterLabel ("endif");
- foreach (var clause in statement.clauses) {
- var endOfClauseLabel = RegisterLabel ("skipclause");
- if (clause.expression != null) {
- GenerateCode (node, clause.expression);
- Emit (node, ByteCode.JumpIfFalse, endOfClauseLabel);
- }
- GenerateCode (node, clause.statements);
- Emit (node, ByteCode.JumpTo, endOfIfStatementLabel);
- if (clause.expression != null) {
- Emit (node, ByteCode.Label, endOfClauseLabel);
- }
- // Clean up the stack by popping the expression that was tested earlier
- if (clause.expression != null) {
- Emit (node, ByteCode.Pop);
- }
- }
- Emit (node, ByteCode.Label, endOfIfStatementLabel);
- }
- void GenerateCode(Node node, Parser.OptionStatement statement) {
- var destination = statement.destination;
- if (statement.label == null) {
- // this is a jump to another node
- Emit(node, ByteCode.RunNode, destination);
- } else {
- var lineID = GetLineIDFromNodeTags(statement.parent);
- var stringID = program.RegisterString (statement.label, node.name, lineID, statement.lineNumber, true);
- Emit (node, ByteCode.AddOption, stringID, destination);
- }
- }
- void GenerateCode(Node node, Parser.AssignmentStatement statement) {
- // Is it a straight assignment?
- if (statement.operation == TokenType.EqualToOrAssign) {
- // Evaluate the expression, which will result in a value
- // on the stack
- GenerateCode (node, statement.valueExpression);
- // Stack now contains [destinationValue]
- } else {
- // It's a combined operation-plus-assignment
- // Get the current value of the variable
- Emit(node, ByteCode.PushVariable, statement.destinationVariableName);
- // Evaluate the expression, which will result in a value
- // on the stack
- GenerateCode (node, statement.valueExpression);
- // Stack now contains [currentValue, expressionValue]
- switch (statement.operation) {
- case TokenType.AddAssign:
- Emit (node, ByteCode.CallFunc, TokenType.Add.ToString ());
- break;
- case TokenType.MinusAssign:
- Emit (node, ByteCode.CallFunc, TokenType.Minus.ToString ());
- break;
- case TokenType.MultiplyAssign:
- Emit (node, ByteCode.CallFunc, TokenType.Multiply.ToString ());
- break;
- case TokenType.DivideAssign:
- Emit (node, ByteCode.CallFunc, TokenType.Divide.ToString ());
- break;
- default:
- throw new ArgumentOutOfRangeException ();
- }
- // Stack now contains [destinationValue]
- }
- // Store the top of the stack in the variable
- Emit(node, ByteCode.StoreVariable, statement.destinationVariableName);
- // Clean up the stack
- Emit (node, ByteCode.Pop);
- }
- void GenerateCode(Node node, Parser.Expression expression) {
- // Expressions are either plain values, or function calls
- switch (expression.type) {
- case Parser.Expression.Type.Value:
- // Plain value? Emit that
- GenerateCode (node, expression.value);
- break;
- case Parser.Expression.Type.FunctionCall:
- // Evaluate all parameter expressions (which will
- // push them to the stack)
- foreach (var parameter in expression.parameters) {
- GenerateCode (node, parameter);
- }
- // If this function has a variable number of parameters, put
- // the number of parameters that were passed onto the stack
- if (expression.function.paramCount == -1) {
- Emit (node, ByteCode.PushNumber, expression.parameters.Count);
- }
- // And then call the function
- Emit (node, ByteCode.CallFunc, expression.function.name);
- break;
- }
- }
- void GenerateCode(Node node, Parser.ValueNode value) {
- // Push a value onto the stack
- switch (value.value.type) {
- case Value.Type.Number:
- Emit (node, ByteCode.PushNumber, value.value.AsNumber);
- break;
- case Value.Type.String:
- // TODO: we use 'null' as the line ID here because strings used in expressions
- // don't have a #line: tag we can use
- var id = program.RegisterString (value.value.AsString, node.name, null, value.lineNumber, false);
- Emit (node, ByteCode.PushString, id);
- break;
- case Value.Type.Bool:
- Emit (node, ByteCode.PushBool, value.value.AsBool);
- break;
- case Value.Type.Variable:
- Emit (node, ByteCode.PushVariable, value.value.AsString);
- break;
- case Value.Type.Null:
- Emit (node, ByteCode.PushNull);
- break;
- default:
- throw new ArgumentOutOfRangeException (); // Since the language can't actually produce an object, this is okay.
- }
- }
- }
- }
|