Files
Mementos/Unity/Alternate Genre Jam/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Divert.cs
2021-06-30 21:39:19 +10:00

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>";
}
}
}