using System.Runtime.CompilerServices; using Jint.Collections; using Jint.Native; using Jint.Native.Function; using Jint.Native.Iterator; using Jint.Native.Object; using Jint.Runtime.Interpreter; using Jint.Runtime.Interpreter.Expressions; namespace Jint.Runtime.Environments; /// /// https://tc39.es/ecma262/#sec-function-environment-records /// internal sealed class FunctionEnvironment : DeclarativeEnvironment { private enum ThisBindingStatus { Lexical, Initialized, Uninitialized } private JsValue? _thisValue; private ThisBindingStatus _thisBindingStatus; internal readonly Function _functionObject; public FunctionEnvironment( Engine engine, Function functionObject, JsValue newTarget) : base(engine) { _functionObject = functionObject; NewTarget = newTarget; if (functionObject._functionDefinition?.Function.Type is NodeType.ArrowFunctionExpression) { _thisBindingStatus = ThisBindingStatus.Lexical; } else { _thisBindingStatus = ThisBindingStatus.Uninitialized; } } internal override bool HasThisBinding() => _thisBindingStatus != ThisBindingStatus.Lexical; internal override bool HasSuperBinding() => _thisBindingStatus != ThisBindingStatus.Lexical && !_functionObject._homeObject.IsUndefined(); public JsValue BindThisValue(JsValue value) { if (_thisBindingStatus != ThisBindingStatus.Initialized) { _thisValue = value; _thisBindingStatus = ThisBindingStatus.Initialized; return value; } Throw.ReferenceError(_functionObject._realm, "'this' has already been bound"); return null!; } internal override JsValue GetThisBinding() { if (_thisBindingStatus != ThisBindingStatus.Uninitialized) { return _thisValue!; } ThrowUninitializedThis(); return null!; } [MethodImpl(MethodImplOptions.NoInlining)] private void ThrowUninitializedThis() { var message = "Cannot access uninitialized 'this'"; if (NewTarget is ScriptFunction { _isClassConstructor: true, _constructorKind: ConstructorKind.Derived }) { // help with better error message message = "Must call super constructor in derived class before accessing 'this' or returning from derived constructor"; } Throw.ReferenceError(_engine.ExecutionContext.Realm, message); } public JsValue GetSuperBase() { var home = _functionObject._homeObject; return home.IsUndefined() ? Undefined : ((ObjectInstance) home).GetPrototypeOf() ?? Null; } // optimization to have logic near record internal structures. internal void InitializeParameters( Key[] parameterNames, bool hasDuplicates, JsCallArguments? arguments) { if (parameterNames.Length == 0) { return; } var value = hasDuplicates ? Undefined : null; var directSet = !hasDuplicates && (_dictionary is null || _dictionary.Count == 0); _dictionary ??= new HybridDictionary(parameterNames.Length, checkExistingKeys: !directSet); for (uint i = 0; i < (uint) parameterNames.Length; i++) { var paramName = parameterNames[i]; ref var binding = ref _dictionary.GetValueRefOrAddDefault(paramName, out var exists); if (directSet || !exists) { var parameterValue = arguments?.At((int) i, Undefined) ?? value; binding = new Binding(parameterValue!, canBeDeleted: false, mutable: true, strict: false); } } _dictionary.CheckExistingKeys = true; } internal void AddFunctionParameters(EvaluationContext context, IFunction functionDeclaration, JsCallArguments arguments) { var empty = _dictionary is null || _dictionary.Count == 0; ref readonly var parameters = ref functionDeclaration.Params; var count = parameters.Count; for (var i = 0; i < count; i++) { SetFunctionParameter(context, parameters[i], arguments, i, empty); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetFunctionParameter( EvaluationContext context, Node? parameter, JsCallArguments arguments, int index, bool initiallyEmpty) { if (parameter is Identifier identifier) { var argument = arguments.At(index); SetItemSafely(identifier.Name, argument, initiallyEmpty); } else { SetFunctionParameterUnlikely(context, parameter, arguments, index, initiallyEmpty); } } private void SetFunctionParameterUnlikely( EvaluationContext context, Node? parameter, JsCallArguments arguments, int index, bool initiallyEmpty) { var argument = arguments.At(index); if (parameter is RestElement restElement) { HandleRestElementArray(context, restElement, arguments, index, initiallyEmpty); } else if (parameter is ArrayPattern arrayPattern) { HandleArrayPattern(context, initiallyEmpty, argument, arrayPattern); } else if (parameter is ObjectPattern objectPattern) { HandleObjectPattern(context, initiallyEmpty, argument, objectPattern); } else if (parameter is AssignmentPattern assignmentPattern) { HandleAssignmentPatternOrExpression(context, assignmentPattern.Left, assignmentPattern.Right, argument, initiallyEmpty); } else if (parameter is AssignmentExpression assignmentExpression) { HandleAssignmentPatternOrExpression(context, assignmentExpression.Left, assignmentExpression.Right, argument, initiallyEmpty); } } private void HandleObjectPattern(EvaluationContext context, bool initiallyEmpty, JsValue argument, ObjectPattern objectPattern) { if (argument.IsNullOrUndefined()) { Throw.TypeError(_functionObject._realm, "Destructed parameter is null or undefined"); } var argumentObject = TypeConverter.ToObject(_engine.Realm, argument); ref readonly var properties = ref objectPattern.Properties; var processedProperties = properties.Count > 0 && properties[properties.Count - 1] is RestElement ? new HashSet() : null; var jsValues = _engine._jsValueArrayPool.RentArray(1); foreach (var property in properties) { var oldEnv = _engine.ExecutionContext.LexicalEnvironment; var paramVarEnv = JintEnvironment.NewDeclarativeEnvironment(_engine, oldEnv); PrivateEnvironment? privateEnvironment = null; // TODO PRIVATE check when implemented _engine.EnterExecutionContext(paramVarEnv, paramVarEnv, _engine.ExecutionContext.Realm, privateEnvironment); try { if (property is AssignmentProperty p) { var propertyName = p.GetKey(_engine); processedProperties?.Add(propertyName.ToString()); jsValues[0] = argumentObject.Get(propertyName); SetFunctionParameter(context, p.Value, jsValues, 0, initiallyEmpty); } else { if (((RestElement) property).Argument is Identifier restIdentifier) { var rest = _engine.Realm.Intrinsics.Object.Construct((argumentObject.Properties?.Count ?? 0) - processedProperties!.Count); argumentObject.CopyDataProperties(rest, processedProperties); SetItemSafely(restIdentifier.Name, rest, initiallyEmpty); } else { Throw.SyntaxError(_functionObject._realm, "Object rest parameter can only be objects"); } } } finally { _engine.LeaveExecutionContext(); } } _engine._jsValueArrayPool.ReturnArray(jsValues); } private void HandleArrayPattern(EvaluationContext context, bool initiallyEmpty, JsValue argument, ArrayPattern arrayPattern) { if (argument.IsNull()) { Throw.TypeError(_functionObject._realm, "Destructed parameter is null"); } JsArray? array; if (argument is JsArray { HasOriginalIterator: true } ai) { array = ai; } else { if (!argument.TryGetIterator(_functionObject._realm, out var iterator)) { Throw.TypeError(context.Engine.Realm, "object is not iterable"); } array = _engine.Realm.Intrinsics.Array.ArrayCreate(0); var max = arrayPattern.Elements.Count; if (max > 0 && arrayPattern.Elements[max - 1]?.Type == NodeType.RestElement) { // need to consume all max = int.MaxValue; } var protocol = new ArrayPatternProtocol(_engine, array, iterator, max); protocol.Execute(); } var arrayContents = array.ToArray(); for (var i = 0; i < arrayPattern.Elements.Count; i++) { SetFunctionParameter(context, arrayPattern.Elements[i], arrayContents, i, initiallyEmpty); } } private void HandleRestElementArray( EvaluationContext context, RestElement restElement, JsCallArguments 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; uint targetIndex = 0; var rest = new JsValue[count]; for (var argIndex = index; argIndex < arguments.Length; ++argIndex) { rest[targetIndex++] = arguments[argIndex]; } var array = new JsArray(_engine, rest); if (restElement.Argument is Identifier restIdentifier) { SetItemSafely(restIdentifier.Name, array, initiallyEmpty); } else if (restElement.Argument is DestructuringPattern pattern) { SetFunctionParameter(context, pattern, [array], 0, initiallyEmpty); } else { Throw.SyntaxError(_functionObject._realm, "Rest parameters can only be identifiers or arrays"); } } private void HandleAssignmentPatternOrExpression( EvaluationContext context, Node left, Node right, JsValue argument, bool initiallyEmpty) { var idLeft = left as Identifier; if (idLeft != null && right is Identifier idRight && string.Equals(idLeft.Name, idRight.Name, StringComparison.Ordinal)) { Throw.ReferenceNameError(_functionObject._realm, idRight.Name); } if (argument.IsUndefined()) { var expression = (Expression) right; var jintExpression = JintExpression.Build(expression); var oldEnv = _engine.ExecutionContext.LexicalEnvironment; var paramVarEnv = JintEnvironment.NewDeclarativeEnvironment(_engine, oldEnv); _engine.EnterExecutionContext(new ExecutionContext(null, paramVarEnv, paramVarEnv, null, _engine.Realm, null)); try { argument = jintExpression.GetValue(context); } finally { _engine.LeaveExecutionContext(); } if (idLeft != null && right.IsFunctionDefinition()) { ((Function) argument).SetFunctionName(idLeft.Name); } } SetFunctionParameter(context, left, [argument], 0, initiallyEmpty); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetItemSafely(Key name, JsValue argument, bool initiallyEmpty) { if (initiallyEmpty) { _dictionary ??= new HybridDictionary(); _dictionary[name] = new Binding(argument, canBeDeleted: false, mutable: true, strict: false); } else { SetItemCheckExisting(name, argument); } } private void SetItemCheckExisting(Key name, JsValue argument) { _dictionary ??= new HybridDictionary(); 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 { Throw.TypeError(_functionObject._realm, "Can't update the value of an immutable binding."); } } } private sealed class ArrayPatternProtocol : IteratorProtocol { private readonly JsArray _instance; private readonly int _max; private long _index; public ArrayPatternProtocol( Engine engine, JsArray instance, IteratorInstance iterator, int max) : base(engine, iterator, 0) { _instance = instance; _max = max; } protected override void ProcessItem(JsValue[] arguments, JsValue currentValue) { _instance.SetIndexValue((uint) _index, currentValue, updateLength: false); _index++; } protected override bool ShouldContinue => _index < _max; protected override void IterationEnd() { if (_index > 0) { _instance.SetLength((uint) _index); IteratorClose(CompletionType.Normal); } } } }