Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions Source/Parser/Expressions/FunctionCallExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -341,17 +341,6 @@ private static ExpressionBase GetParameter(InterpreterScope parameterScope, Inte
}
}

var anonymousFunction = value as AnonymousUserFunctionDefinitionExpression;
if (anonymousFunction != null)
{
// a function normally can only see its variables and global variables. an
// anonymous function may also see variables in the current function scope.
// identify any local variables the anonymous function needs so we can copy
// them to the function call scope when the function is invoked.
anonymousFunction.IdentifyCaptureVariables(parameterScope);
parameterScope.AddFunction(anonymousFunction);
}

return value;
}

Expand Down
81 changes: 64 additions & 17 deletions Source/Parser/Expressions/FunctionDefinitionExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using RATools.Parser.Functions;
using RATools.Parser.Internal;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;

Expand Down Expand Up @@ -34,6 +35,7 @@ protected FunctionDefinitionExpression(FunctionDefinitionExpression source)
Parameters = source.Parameters;
Expressions = source.Expressions;
DefaultParameters = source.DefaultParameters;
Location = source.Location;

MakeReadOnly();
}
Expand Down Expand Up @@ -625,6 +627,11 @@ protected UserFunctionDefinitionExpression(VariableDefinitionExpression name)
{
}

protected UserFunctionDefinitionExpression(UserFunctionDefinitionExpression source)
: base(source)
{
}

/// <summary>
/// Parses a function definition.
/// </summary>
Expand Down Expand Up @@ -806,7 +813,7 @@ protected ErrorExpression ParseShorthandBody(PositionalTokenizer tokenizer)
/// Evaluates an expression
/// </summary>
/// <returns><see cref="ErrorExpression"/> indicating the failure, or the result of evaluating the expression.</returns>
public ExpressionBase Evaluate(InterpreterScope scope)
public virtual ExpressionBase Evaluate(InterpreterScope scope)
{
return this;
}
Expand Down Expand Up @@ -936,6 +943,13 @@ protected AnonymousUserFunctionDefinitionExpression(VariableDefinitionExpression
CapturedVariables = Enumerable.Empty<VariableReferenceExpression>();
}

protected AnonymousUserFunctionDefinitionExpression(AnonymousUserFunctionDefinitionExpression source, IEnumerable<VariableReferenceExpression> capturedVariables)
: base(source)
{
CapturedVariables = capturedVariables;
_isConstant = true;
}

/// <summary>
/// Determines if the tokenizer is pointing at a parameter list for an anonymous function.
/// </summary>
Expand Down Expand Up @@ -1025,31 +1039,64 @@ public static ExpressionBase ParseAnonymous(PositionalTokenizer tokenizer, Expre

public IEnumerable<VariableReferenceExpression> CapturedVariables { get; private set; }

public void IdentifyCaptureVariables(InterpreterScope scope)
{
// Initialize a new scope object with a FunctionCall context so we can determine which
// variables have to be captured. The FunctionCall context will only see the globals.
var captureScope = new InterpreterScope(scope);
captureScope.Context = new FunctionCallExpression("NonAnonymousFunction", new ExpressionBase[0]);
/// <summary>
/// Gets whether this is non-changing.
/// </summary>
/// <remarks>
/// An anonymous function may see variables in the current function scope.
/// If this function is dependent on scoped variables, the definition will not be constant.
/// </remarks>
public override bool IsConstant { get { return _isConstant; } }
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private bool _isConstant = false;

var capturedVariables = new List<VariableReferenceExpression>();
/// <summary>
/// Evaluates an expression.
/// </summary>
/// <remarks>
/// An anonymous function may see variables in the current function scope.
/// If this function is dependent on scoped variables, this will return a scope-specific function definition containing the captured variables.
/// </remarks>
public override ExpressionBase Evaluate(InterpreterScope scope)
{
if (_isConstant) // previously determined there's no dependencies
return this;

// A function normally can only see its variables and global variables. an
// anonymous function may also see variables in the current function scope.
// Identify any local variables the anonymous function needs so we can copy
// them to the function call scope when the function is invoked.
var possibleDependencies = new HashSet<string>();
((INestedExpressions)this).GetDependencies(possibleDependencies);
foreach (var dependency in possibleDependencies)
if (possibleDependencies.Count > 0)
{
if (captureScope.GetVariable(dependency) == null)
// Initialize a new scope object with a FunctionCall context so we can determine which
// variables have to be captured. The FunctionCall context will only see the globals.
var captureScope = new InterpreterScope(scope);
captureScope.Context = new FunctionCallExpression("GlobalFilter", new ExpressionBase[0]);

var capturedVariables = new List<VariableReferenceExpression>();

foreach (var dependency in possibleDependencies)
{
// the variable is not visible to the function scope. check to see if it's visible
// in the calling scope. if it is, create a copy for the function call.
var variable = scope.GetVariableReference(dependency);
if (variable != null)
capturedVariables.Add(variable);
if (captureScope.GetVariable(dependency) == null)
{
// The variable is not visible to the function scope. Check to see if it's visible
// in the calling scope. If it is, create a reference for the function call.
var variable = scope.GetVariableReference(dependency);
if (variable != null)
capturedVariables.Add(variable);
}
}

// If variables were captured, create a unique instance of the definition with the captured values.
if (capturedVariables.Count > 0)
return new AnonymousUserFunctionDefinitionExpression(this, capturedVariables.ToArray());
}

if (capturedVariables.Count > 0)
CapturedVariables = capturedVariables.ToArray();
// No dependencies. Flag as constant and return the definition.
_isConstant = true;
return this;
}
}

Expand Down
27 changes: 27 additions & 0 deletions Tests/Parser/AchievementScriptInterpreterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1399,5 +1399,32 @@ public void TestReservedWordFunctionParameter()
var interpreter = AchievementScriptTests.Parse("function f(class, i) => class + i", false);
Assert.That(interpreter.ErrorMessage, Is.EqualTo("1:25 class is a reserved word"));
}

[Test]
public void TestLambdaCaptureInReturn()
{
var scope = AchievementScriptTests.Evaluate(
"function f(arg) { return () => arg }\r\n" +
"str = format(\"{0} {1}\", f(\"good\")(), f(\"bad\")())");

var str = scope.GetVariable("str");
Assert.That(str, Is.InstanceOf<StringConstantExpression>());
Assert.That(((StringConstantExpression)str).Value, Is.EqualTo("good bad"));
}

[Test]
public void TestLambdaCaptureInClassInstance()
{
var scope = AchievementScriptTests.Evaluate(
"class RememberArg { arg_f = 0 }\r\n" +
"function f(arg) { return RememberArg(() => arg) }\r\n" +
"f_good = f(\"good\")\r\n" +
"f_bad = f(\"bad\")\r\n" +
"str = format(\"{0} {1}\", f_good.arg_f(), f_bad.arg_f())");

var str = scope.GetVariable("str");
Assert.That(str, Is.InstanceOf<StringConstantExpression>());
Assert.That(((StringConstantExpression)str).Value, Is.EqualTo("good bad"));
}
}
}
Loading