using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Esprima.Ast;
using Jint.Native;
using Jint.Native.Array;
using Jint.Native.Function;
using Jint.Native.Iterator;
using Jint.Native.Object;
using Jint.Runtime.Interpreter.Expressions;
namespace Jint.Runtime.Environments
{
///
/// https://tc39.es/ecma262/#sec-function-environment-records
///
internal sealed class FunctionEnvironmentRecord : DeclarativeEnvironmentRecord
{
private enum ThisBindingStatus
{
Lexical,
Initialized,
Uninitialized
}
private JsValue _thisValue;
private ThisBindingStatus _thisBindingStatus;
internal readonly FunctionInstance _functionObject;
public FunctionEnvironmentRecord(
Engine engine,
FunctionInstance functionObject,
JsValue newTarget) : base(engine)
{
_functionObject = functionObject;
NewTarget = newTarget;
if (functionObject._functionDefinition.Function is ArrowFunctionExpression)
{
_thisBindingStatus = ThisBindingStatus.Lexical;
}
else
{
_thisBindingStatus = ThisBindingStatus.Uninitialized;
}
}
public override bool HasThisBinding() => _thisBindingStatus != ThisBindingStatus.Lexical;
public override bool HasSuperBinding() =>
_thisBindingStatus != ThisBindingStatus.Lexical && !_functionObject._homeObject.IsUndefined();
public JsValue BindThisValue(JsValue value)
{
if (_thisBindingStatus == ThisBindingStatus.Initialized)
{
ExceptionHelper.ThrowReferenceError(_functionObject._realm, (string) null);
}
_thisValue = value;
_thisBindingStatus = ThisBindingStatus.Initialized;
return value;
}
public override JsValue GetThisBinding()
{
if (_thisBindingStatus != ThisBindingStatus.Uninitialized)
{
return _thisValue;
}
ExceptionHelper.ThrowReferenceError(_engine.ExecutionContext.Realm, (string) null);
return null;
}
public JsValue GetSuperBase()
{
var home = _functionObject._homeObject;
return home.IsUndefined()
? Undefined
: ((ObjectInstance) home).GetPrototypeOf();
}
// optimization to have logic near record internal structures.
internal void InitializeParameters(
Key[] parameterNames,
bool hasDuplicates,
JsValue[] arguments)
{
var value = hasDuplicates ? Undefined : null;
var directSet = !hasDuplicates && _dictionary.Count == 0;
for (var i = 0; (uint) i < (uint) parameterNames.Length; i++)
{
var paramName = parameterNames[i];
if (directSet || !_dictionary.ContainsKey(paramName))
{
var parameterValue = value;
if (arguments != null)
{
parameterValue = (uint) i < (uint) arguments.Length ? arguments[i] : Undefined;
}
_dictionary[paramName] = new Binding(parameterValue, canBeDeleted: false, mutable: true, strict: false);
}
}
}
internal void AddFunctionParameters(IFunction functionDeclaration, JsValue[] arguments)
{
bool empty = _dictionary.Count == 0;
ref readonly var parameters = ref functionDeclaration.Params;
var count = parameters.Count;
for (var i = 0; i < count; i++)
{
SetFunctionParameter(parameters[i], arguments, i, empty);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetFunctionParameter(
Node parameter,
JsValue[] arguments,
int index,
bool initiallyEmpty)
{
if (parameter is Identifier identifier)
{
var argument = (uint) index < (uint) arguments.Length ? arguments[index] : Undefined;
SetItemSafely(identifier.Name, argument, initiallyEmpty);
}
else
{
SetFunctionParameterUnlikely(parameter, arguments, index, initiallyEmpty);
}
}
private void SetFunctionParameterUnlikely(
Node parameter,
JsValue[] arguments,
int index,
bool initiallyEmpty)
{
var argument = arguments.Length > index ? arguments[index] : Undefined;
if (parameter is RestElement restElement)
{
HandleRestElementArray(restElement, arguments, index, initiallyEmpty);
}
else if (parameter is ArrayPattern arrayPattern)
{
HandleArrayPattern(initiallyEmpty, argument, arrayPattern);
}
else if (parameter is ObjectPattern objectPattern)
{
HandleObjectPattern(initiallyEmpty, argument, objectPattern);
}
else if (parameter is AssignmentPattern assignmentPattern)
{
HandleAssignmentPatternOrExpression(assignmentPattern.Left, assignmentPattern.Right, argument, initiallyEmpty);
}
else if (parameter is AssignmentExpression assignmentExpression)
{
HandleAssignmentPatternOrExpression(assignmentExpression.Left, assignmentExpression.Right, argument, initiallyEmpty);
}
}
private void HandleObjectPattern(bool initiallyEmpty, JsValue argument, ObjectPattern objectPattern)
{
if (argument.IsNullOrUndefined())
{
ExceptionHelper.ThrowTypeError(_functionObject._realm, "Destructed parameter is null or undefined");
}
if (!argument.IsObject())
{
return;
}
var argumentObject = argument.AsObject();
var processedProperties = objectPattern.Properties.Count > 0 &&
objectPattern.Properties[objectPattern.Properties.Count - 1] is RestElement
? new HashSet()
: null;
var jsValues = _engine._jsValueArrayPool.RentArray(1);
foreach (var property in objectPattern.Properties)
{
var oldEnv = _engine.ExecutionContext.LexicalEnvironment;
var paramVarEnv = JintEnvironment.NewDeclarativeEnvironment(_engine, oldEnv);
PrivateEnvironmentRecord privateEnvironment = null; // TODO PRIVATE check when implemented
_engine.EnterExecutionContext(paramVarEnv, paramVarEnv, _engine.ExecutionContext.Realm, privateEnvironment);
try
{
if (property is Property p)
{
JsString propertyName = JsString.Empty;
if (p.Key is Identifier propertyIdentifier)
{
propertyName = JsString.Create(propertyIdentifier.Name);
}
else if (p.Key is Literal propertyLiteral)
{
propertyName = JsString.Create(propertyLiteral.Raw);
}
else if (p.Key is CallExpression callExpression)
{
var jintCallExpression = new JintCallExpression(_engine, callExpression);
var jsValue = jintCallExpression.GetValue();
propertyName = TypeConverter.ToJsString(jsValue);
}
else
{
ExceptionHelper.ThrowArgumentOutOfRangeException("property", "unknown object pattern property type");
}
processedProperties?.Add(propertyName.ToString());
jsValues[0] = argumentObject.Get(propertyName);
SetFunctionParameter(p.Value, jsValues, 0, initiallyEmpty);
}
else
{
if (((RestElement) property).Argument is Identifier restIdentifier)
{
var rest = _engine.Realm.Intrinsics.Object.Construct(argumentObject.Properties.Count - processedProperties.Count);
argumentObject.CopyDataProperties(rest, processedProperties);
SetItemSafely(restIdentifier.Name, rest, initiallyEmpty);
}
else
{
ExceptionHelper.ThrowSyntaxError(_functionObject._realm, "Object rest parameter can only be objects");
}
}
}
finally
{
_engine.LeaveExecutionContext();
}
}
_engine._jsValueArrayPool.ReturnArray(jsValues);
}
private void HandleArrayPattern(bool initiallyEmpty, JsValue argument, ArrayPattern arrayPattern)
{
if (argument.IsNull())
{
ExceptionHelper.ThrowTypeError(_functionObject._realm, "Destructed parameter is null");
}
ArrayInstance array = null;
var arrayContents = System.Array.Empty();
if (argument.IsArray())
{
array = argument.AsArray();
}
else if (argument.IsObject() && argument.TryGetIterator(_functionObject._realm, out var iterator))
{
array = _engine.Realm.Intrinsics.Array.ConstructFast(0);
var protocol = new ArrayPatternProtocol(_engine, array, iterator, arrayPattern.Elements.Count);
protocol.Execute();
}
if (!ReferenceEquals(array, null))
{
arrayContents = new JsValue[array.GetLength()];
for (uint i = 0; i < (uint) arrayContents.Length; i++)
{
arrayContents[i] = array.Get(i);
}
}
for (uint arrayIndex = 0; arrayIndex < arrayPattern.Elements.Count; arrayIndex++)
{
SetFunctionParameter(arrayPattern.Elements[(int) arrayIndex], arrayContents, (int) arrayIndex, initiallyEmpty);
}
}
private void HandleRestElementArray(
RestElement restElement,
JsValue[] arguments,
int index,
bool initiallyEmpty)
{
// index + 1 == parameters.count because rest is last
int restCount = arguments.Length - (index + 1) + 1;
uint count = restCount > 0 ? (uint) restCount : 0;
var rest = _engine.Realm.Intrinsics.Array.ConstructFast(count);
uint targetIndex = 0;
for (var argIndex = index; argIndex < arguments.Length; ++argIndex)
{
rest.SetIndexValue(targetIndex++, arguments[argIndex], updateLength: false);
}
if (restElement.Argument is Identifier restIdentifier)
{
SetItemSafely(restIdentifier.Name, rest, initiallyEmpty);
}
else if (restElement.Argument is BindingPattern bindingPattern)
{
SetFunctionParameter(bindingPattern, new JsValue[]
{
rest
}, index, initiallyEmpty);
}
else
{
ExceptionHelper.ThrowSyntaxError(_functionObject._realm, "Rest parameters can only be identifiers or arrays");
}
}
private void HandleAssignmentPatternOrExpression(
Node left,
Node right,
JsValue argument,
bool initiallyEmpty)
{
var idLeft = left as Identifier;
if (idLeft != null
&& right is Identifier idRight
&& idLeft.Name == idRight.Name)
{
ExceptionHelper.ThrowReferenceError(_functionObject._realm, idRight.Name);
}
if (argument.IsUndefined())
{
var expression = right.As();
var jintExpression = JintExpression.Build(_engine, expression);
var oldEnv = _engine.ExecutionContext.LexicalEnvironment;
var paramVarEnv = JintEnvironment.NewDeclarativeEnvironment(_engine, oldEnv);
_engine.EnterExecutionContext(new ExecutionContext(paramVarEnv, paramVarEnv, null, _engine.Realm, null));
try
{
argument = jintExpression.GetValue();
}
finally
{
_engine.LeaveExecutionContext();
}
if (idLeft != null && right.IsFunctionWithName())
{
((FunctionInstance) argument).SetFunctionName(idLeft.Name);
}
}
SetFunctionParameter(left, new[]
{
argument
}, 0, initiallyEmpty);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetItemSafely(Key name, JsValue argument, bool initiallyEmpty)
{
if (initiallyEmpty)
{
_dictionary[name] = new Binding(argument, canBeDeleted: false, mutable: true, strict: false);
}
else
{
SetItemCheckExisting(name, argument);
}
}
private void SetItemCheckExisting(Key name, JsValue argument)
{
if (!_dictionary.TryGetValue(name, out var existing))
{
_dictionary[name] = new Binding(argument, canBeDeleted: false, mutable: true, strict: false);
}
else
{
if (existing.Mutable)
{
_dictionary[name] = existing.ChangeValue(argument);
}
else
{
ExceptionHelper.ThrowTypeError(_functionObject._realm, "Can't update the value of an immutable binding.");
}
}
}
private sealed class ArrayPatternProtocol : IteratorProtocol
{
private readonly ArrayInstance _instance;
private readonly int _max;
private long _index = -1;
public ArrayPatternProtocol(
Engine engine,
ArrayInstance instance,
IIterator iterator,
int max) : base(engine, iterator, 0)
{
_instance = instance;
_max = max;
}
protected override void ProcessItem(JsValue[] args, JsValue currentValue)
{
_index++;
var jsValue = ExtractValueFromIteratorInstance(currentValue);
_instance.SetIndexValue((uint) _index, jsValue, updateLength: false);
}
protected override bool ShouldContinue => _index < _max;
protected override void IterationEnd()
{
if (_index >= 0)
{
_instance.SetLength((uint) _index);
IteratorClose(CompletionType.Normal);
}
}
}
}
}