mirror of
https://github.com/Ratstail91/Mementos.git
synced 2025-11-29 02:24:28 +11:00
404 lines
16 KiB
C#
404 lines
16 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace Ink.Parsed
|
|
{
|
|
public class Divert : Parsed.Object
|
|
{
|
|
public Parsed.Path target { get; protected set; }
|
|
public Parsed.Object targetContent { get; protected set; }
|
|
public List<Expression> arguments { get; protected set; }
|
|
public Runtime.Divert runtimeDivert { get; protected set; }
|
|
public bool isFunctionCall { get; set; }
|
|
public bool isEmpty { get; set; }
|
|
public bool isTunnel { get; set; }
|
|
public bool isThread { get; set; }
|
|
public bool isEnd {
|
|
get {
|
|
return target != null && target.dotSeparatedComponents == "END";
|
|
}
|
|
}
|
|
public bool isDone {
|
|
get {
|
|
return target != null && target.dotSeparatedComponents == "DONE";
|
|
}
|
|
}
|
|
|
|
public Divert (Parsed.Path target, List<Expression> arguments = null)
|
|
{
|
|
this.target = target;
|
|
this.arguments = arguments;
|
|
|
|
if (arguments != null) {
|
|
AddContent (arguments.Cast<Parsed.Object> ().ToList ());
|
|
}
|
|
}
|
|
|
|
public Divert (Parsed.Object targetContent)
|
|
{
|
|
this.targetContent = targetContent;
|
|
}
|
|
|
|
public override Runtime.Object GenerateRuntimeObject ()
|
|
{
|
|
// End = end flow immediately
|
|
// Done = return from thread or instruct the flow that it's safe to exit
|
|
if (isEnd) {
|
|
return Runtime.ControlCommand.End ();
|
|
}
|
|
if (isDone) {
|
|
return Runtime.ControlCommand.Done ();
|
|
}
|
|
|
|
runtimeDivert = new Runtime.Divert ();
|
|
|
|
// Normally we resolve the target content during the
|
|
// Resolve phase, since we expect all runtime objects to
|
|
// be available in order to find the final runtime path for
|
|
// the destination. However, we need to resolve the target
|
|
// (albeit without the runtime target) early so that
|
|
// we can get information about the arguments - whether
|
|
// they're by reference - since it affects the code we
|
|
// generate here.
|
|
ResolveTargetContent ();
|
|
|
|
|
|
CheckArgumentValidity ();
|
|
|
|
// Passing arguments to the knot
|
|
bool requiresArgCodeGen = arguments != null && arguments.Count > 0;
|
|
if ( requiresArgCodeGen || isFunctionCall || isTunnel || isThread ) {
|
|
|
|
var container = new Runtime.Container ();
|
|
|
|
// Generate code for argument evaluation
|
|
// This argument generation is coded defensively - it should
|
|
// attempt to generate the code for all the parameters, even if
|
|
// they don't match the expected arguments. This is so that the
|
|
// parameter objects themselves are generated correctly and don't
|
|
// get into a state of attempting to resolve references etc
|
|
// without being generated.
|
|
if (requiresArgCodeGen) {
|
|
|
|
// Function calls already in an evaluation context
|
|
if (!isFunctionCall) {
|
|
container.AddContent (Runtime.ControlCommand.EvalStart());
|
|
}
|
|
|
|
List<FlowBase.Argument> targetArguments = null;
|
|
if( targetContent )
|
|
targetArguments = (targetContent as FlowBase).arguments;
|
|
|
|
for (var i = 0; i < arguments.Count; ++i) {
|
|
Expression argToPass = arguments [i];
|
|
FlowBase.Argument argExpected = null;
|
|
if( targetArguments != null && i < targetArguments.Count )
|
|
argExpected = targetArguments [i];
|
|
|
|
// Pass by reference: argument needs to be a variable reference
|
|
if (argExpected != null && argExpected.isByReference) {
|
|
|
|
var varRef = argToPass as VariableReference;
|
|
if (varRef == null) {
|
|
Error ("Expected variable name to pass by reference to 'ref " + argExpected.identifier + "' but saw " + argToPass.ToString ());
|
|
break;
|
|
}
|
|
|
|
// Check that we're not attempting to pass a read count by reference
|
|
var targetPath = new Path(varRef.pathIdentifiers);
|
|
Parsed.Object targetForCount = targetPath.ResolveFromContext (this);
|
|
if (targetForCount != null) {
|
|
Error ("can't pass a read count by reference. '" + targetPath.dotSeparatedComponents+"' is a knot/stitch/label, but '"+target.dotSeparatedComponents+"' requires the name of a VAR to be passed.");
|
|
break;
|
|
}
|
|
|
|
var varPointer = new Runtime.VariablePointerValue (varRef.name);
|
|
container.AddContent (varPointer);
|
|
}
|
|
|
|
// Normal value being passed: evaluate it as normal
|
|
else {
|
|
argToPass.GenerateIntoContainer (container);
|
|
}
|
|
}
|
|
|
|
// Function calls were already in an evaluation context
|
|
if (!isFunctionCall) {
|
|
container.AddContent (Runtime.ControlCommand.EvalEnd());
|
|
}
|
|
}
|
|
|
|
|
|
// Starting a thread? A bit like a push to the call stack below... but not.
|
|
// It sort of puts the call stack on a thread stack (argh!) - forks the full flow.
|
|
if (isThread) {
|
|
container.AddContent(Runtime.ControlCommand.StartThread());
|
|
}
|
|
|
|
// If this divert is a function call, tunnel, we push to the call stack
|
|
// so we can return again
|
|
else if (isFunctionCall || isTunnel) {
|
|
runtimeDivert.pushesToStack = true;
|
|
runtimeDivert.stackPushType = isFunctionCall ? Runtime.PushPopType.Function : Runtime.PushPopType.Tunnel;
|
|
}
|
|
|
|
// Jump into the "function" (knot/stitch)
|
|
container.AddContent (runtimeDivert);
|
|
|
|
return container;
|
|
}
|
|
|
|
// Simple divert
|
|
else {
|
|
return runtimeDivert;
|
|
}
|
|
}
|
|
|
|
|
|
// When the divert is to a target that's actually a variable name
|
|
// rather than an explicit knot/stitch name, try interpretting it
|
|
// as such by getting the variable name.
|
|
public string PathAsVariableName()
|
|
{
|
|
return target.firstComponent;
|
|
}
|
|
|
|
|
|
void ResolveTargetContent()
|
|
{
|
|
if (isEmpty || isEnd) {
|
|
return;
|
|
}
|
|
|
|
if (targetContent == null) {
|
|
|
|
// Is target of this divert a variable name that will be de-referenced
|
|
// at runtime? If so, there won't be any further reference resolution
|
|
// we can do at this point.
|
|
var variableTargetName = PathAsVariableName ();
|
|
if (variableTargetName != null) {
|
|
var flowBaseScope = ClosestFlowBase ();
|
|
var resolveResult = flowBaseScope.ResolveVariableWithName (variableTargetName, fromNode: this);
|
|
if (resolveResult.found) {
|
|
|
|
// Make sure that the flow was typed correctly, given that we know that this
|
|
// is meant to be a divert target
|
|
if (resolveResult.isArgument) {
|
|
var argument = resolveResult.ownerFlow.arguments.Where (a => a.identifier.name == variableTargetName).First();
|
|
if ( !argument.isDivertTarget ) {
|
|
Error ("Since '" + argument.identifier + "' is used as a variable divert target (on "+this.debugMetadata+"), it should be marked as: -> " + argument.identifier, resolveResult.ownerFlow);
|
|
}
|
|
}
|
|
|
|
runtimeDivert.variableDivertName = variableTargetName;
|
|
return;
|
|
|
|
}
|
|
}
|
|
|
|
targetContent = target.ResolveFromContext (this);
|
|
}
|
|
}
|
|
|
|
public override void ResolveReferences(Story context)
|
|
{
|
|
if (isEmpty || isEnd || isDone) {
|
|
return;
|
|
}
|
|
|
|
if (targetContent) {
|
|
runtimeDivert.targetPath = targetContent.runtimePath;
|
|
}
|
|
|
|
// Resolve children (the arguments)
|
|
base.ResolveReferences (context);
|
|
|
|
// May be null if it's a built in function (e.g. TURNS_SINCE)
|
|
// or if it's a variable target.
|
|
var targetFlow = targetContent as FlowBase;
|
|
if (targetFlow) {
|
|
if (!targetFlow.isFunction && this.isFunctionCall) {
|
|
base.Error (targetFlow.identifier + " hasn't been marked as a function, but it's being called as one. Do you need to delcare the knot as '== function " + targetFlow.identifier + " =='?");
|
|
} else if (targetFlow.isFunction && !this.isFunctionCall && !(this.parent is DivertTarget)) {
|
|
base.Error (targetFlow.identifier + " can't be diverted to. It can only be called as a function since it's been marked as such: '" + targetFlow.identifier + "(...)'");
|
|
}
|
|
}
|
|
|
|
// Check validity of target content
|
|
bool targetWasFound = targetContent != null;
|
|
bool isBuiltIn = false;
|
|
bool isExternal = false;
|
|
|
|
if (target.numberOfComponents == 1 ) {
|
|
|
|
// BuiltIn means TURNS_SINCE, CHOICE_COUNT, RANDOM or SEED_RANDOM
|
|
isBuiltIn = FunctionCall.IsBuiltIn (target.firstComponent);
|
|
|
|
// Client-bound function?
|
|
isExternal = context.IsExternal (target.firstComponent);
|
|
|
|
if (isBuiltIn || isExternal) {
|
|
if (!isFunctionCall) {
|
|
base.Error (target.firstComponent + " must be called as a function: ~ " + target.firstComponent + "()");
|
|
}
|
|
if (isExternal) {
|
|
runtimeDivert.isExternal = true;
|
|
if( arguments != null )
|
|
runtimeDivert.externalArgs = arguments.Count;
|
|
runtimeDivert.pushesToStack = false;
|
|
runtimeDivert.targetPath = new Runtime.Path (this.target.firstComponent);
|
|
CheckExternalArgumentValidity (context);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Variable target?
|
|
if (runtimeDivert.variableDivertName != null) {
|
|
return;
|
|
}
|
|
|
|
if( !targetWasFound && !isBuiltIn && !isExternal )
|
|
Error ("target not found: '" + target + "'");
|
|
}
|
|
|
|
// Returns false if there's an error
|
|
void CheckArgumentValidity()
|
|
{
|
|
if (isEmpty)
|
|
return;
|
|
|
|
// Argument passing: Check for errors in number of arguments
|
|
var numArgs = 0;
|
|
if (arguments != null && arguments.Count > 0)
|
|
numArgs = arguments.Count;
|
|
|
|
// Missing content?
|
|
// Can't check arguments properly. It'll be due to some
|
|
// other error though, so although there's a problem and
|
|
// we report false, we don't need to report a specific error.
|
|
// It may also be because it's a valid call to an external
|
|
// function, that we check at the resolve stage.
|
|
if (targetContent == null) {
|
|
return;
|
|
}
|
|
|
|
FlowBase targetFlow = targetContent as FlowBase;
|
|
|
|
// No error, crikey!
|
|
if (numArgs == 0 && (targetFlow == null || !targetFlow.hasParameters)) {
|
|
return;
|
|
}
|
|
|
|
if (targetFlow == null && numArgs > 0) {
|
|
Error ("target needs to be a knot or stitch in order to pass arguments");
|
|
return;
|
|
}
|
|
|
|
if (targetFlow.arguments == null && numArgs > 0) {
|
|
Error ("target (" + targetFlow.name + ") doesn't take parameters");
|
|
return;
|
|
}
|
|
|
|
if( this.parent is DivertTarget ) {
|
|
if (numArgs > 0)
|
|
Error ("can't store arguments in a divert target variable");
|
|
return;
|
|
}
|
|
|
|
var paramCount = targetFlow.arguments.Count;
|
|
if (paramCount != numArgs) {
|
|
|
|
string butClause;
|
|
if (numArgs == 0) {
|
|
butClause = "but there weren't any passed to it";
|
|
} else if (numArgs < paramCount) {
|
|
butClause = "but only got " + numArgs;
|
|
} else {
|
|
butClause = "but got " + numArgs;
|
|
}
|
|
Error ("to '" + targetFlow.identifier + "' requires " + paramCount + " arguments, "+butClause);
|
|
return;
|
|
}
|
|
|
|
// Light type-checking for divert target arguments
|
|
for (int i = 0; i < paramCount; ++i) {
|
|
FlowBase.Argument flowArg = targetFlow.arguments [i];
|
|
Parsed.Expression divArgExpr = arguments [i];
|
|
|
|
// Expecting a divert target as an argument, let's do some basic type checking
|
|
if (flowArg.isDivertTarget) {
|
|
|
|
// Not passing a divert target or any kind of variable reference?
|
|
var varRef = divArgExpr as VariableReference;
|
|
if (!(divArgExpr is DivertTarget) && varRef == null ) {
|
|
Error ("Target '" + targetFlow.identifier + "' expects a divert target for the parameter named -> " + flowArg.identifier + " but saw " + divArgExpr, divArgExpr);
|
|
}
|
|
|
|
// Passing 'a' instead of '-> a'?
|
|
// i.e. read count instead of divert target
|
|
else if (varRef != null) {
|
|
|
|
// Unfortunately have to manually resolve here since we're still in code gen
|
|
var knotCountPath = new Path(varRef.pathIdentifiers);
|
|
Parsed.Object targetForCount = knotCountPath.ResolveFromContext (varRef);
|
|
if (targetForCount != null) {
|
|
Error ("Passing read count of '" + knotCountPath.dotSeparatedComponents + "' instead of a divert target. You probably meant '" + knotCountPath + "'");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (targetFlow == null) {
|
|
Error ("Can't call as a function or with arguments unless it's a knot or stitch");
|
|
return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
void CheckExternalArgumentValidity(Story context)
|
|
{
|
|
string externalName = target.firstComponent;
|
|
ExternalDeclaration external = null;
|
|
var found = context.externals.TryGetValue(externalName, out external);
|
|
System.Diagnostics.Debug.Assert (found, "external not found");
|
|
|
|
int externalArgCount = external.argumentNames.Count;
|
|
int ownArgCount = 0;
|
|
if (arguments != null) {
|
|
ownArgCount = arguments.Count;
|
|
}
|
|
|
|
if (ownArgCount != externalArgCount) {
|
|
Error ("incorrect number of arguments sent to external function '" + externalName + "'. Expected " + externalArgCount + " but got " + ownArgCount);
|
|
}
|
|
}
|
|
|
|
public override void Error (string message, Object source = null, bool isWarning = false)
|
|
{
|
|
// Could be getting an error from a nested Divert
|
|
if (source != this && source) {
|
|
base.Error (message, source);
|
|
return;
|
|
}
|
|
|
|
if (isFunctionCall) {
|
|
base.Error ("Function call " + message, source, isWarning);
|
|
} else {
|
|
base.Error ("Divert " + message, source, isWarning);
|
|
}
|
|
}
|
|
|
|
public override string ToString ()
|
|
{
|
|
if (target != null)
|
|
return target.ToString ();
|
|
else
|
|
return "-> <empty divert>";
|
|
}
|
|
|
|
}
|
|
}
|
|
|