using Jint.Collections;
using Jint.Native;
using Jint.Native.Function;
using Jint.Native.Object;
using Jint.Runtime.Descriptors;
namespace Jint.Runtime.Interpreter.Expressions;
///
/// https://tc39.es/ecma262/#sec-object-initializer
///
internal sealed class JintObjectExpression : JintExpression
{
private JintExpression[] _valueExpressions = Array.Empty();
private ObjectProperty?[] _properties = Array.Empty();
// check if we can do a shortcut when all are object properties
// and don't require duplicate checking
private bool _canBuildFast;
private bool _initialized;
private sealed class ObjectProperty
{
internal readonly string? _key;
private JsString? _keyJsString;
internal readonly Property _value;
private JintFunctionDefinition? _functionDefinition;
public ObjectProperty(string? key, Property property)
{
_key = key;
_value = property;
}
public JsString? KeyJsString => _keyJsString ??= _key != null ? JsString.Create(_key) : null;
public JintFunctionDefinition GetFunctionDefinition(Engine engine)
{
if (_functionDefinition is not null)
{
return _functionDefinition;
}
var function = _value.Value as IFunction;
if (function is null)
{
ExceptionHelper.ThrowSyntaxError(engine.Realm);
}
_functionDefinition = new JintFunctionDefinition(function);
return _functionDefinition;
}
}
private JintObjectExpression(ObjectExpression expression) : base(expression)
{
}
public static JintExpression Build(ObjectExpression expression)
{
return expression.Properties.Count == 0
? JintEmptyObjectExpression.Instance
: new JintObjectExpression(expression);
}
private void Initialize()
{
_canBuildFast = true;
var expression = (ObjectExpression) _expression;
ref readonly var properties = ref expression.Properties;
_valueExpressions = new JintExpression[properties.Count];
_properties = new ObjectProperty[properties.Count];
for (var i = 0; i < _properties.Length; i++)
{
string? propName = null;
var property = properties[i];
if (property is Acornima.Ast.ObjectProperty p)
{
if (p.Key is Literal literal)
{
propName = AstExtensions.LiteralKeyToString(literal);
}
if (!p.Computed && p.Key is Identifier identifier)
{
propName = identifier.Name;
_canBuildFast &= !string.Equals(propName, "__proto__", StringComparison.Ordinal);
}
_properties[i] = new ObjectProperty(propName, p);
if (p.Kind is PropertyKind.Init)
{
var propertyValue = p.Value;
_valueExpressions[i] = Build((Expression) propertyValue);
_canBuildFast &= !propertyValue.IsFunctionDefinition();
}
else
{
_canBuildFast = false;
}
}
else if (property is SpreadElement spreadElement)
{
_canBuildFast = false;
_properties[i] = null;
_valueExpressions[i] = Build(spreadElement.Argument);
}
else
{
ExceptionHelper.ThrowArgumentOutOfRangeException("property", "cannot handle property " + property);
}
_canBuildFast &= propName != null;
}
}
protected override object EvaluateInternal(EvaluationContext context)
{
if (!_initialized)
{
Initialize();
_initialized = true;
}
return _canBuildFast
? BuildObjectFast(context)
: BuildObjectNormal(context);
}
///
/// Version that can safely build plain object with only normal init/data fields fast.
///
private JsObject BuildObjectFast(EvaluationContext context)
{
var obj = new JsObject(context.Engine);
var properties = new PropertyDictionary(_properties.Length, checkExistingKeys: true);
for (var i = 0; i < _properties.Length; i++)
{
var objectProperty = _properties[i];
var valueExpression = _valueExpressions[i];
var propValue = valueExpression.GetValue(context).Clone();
properties[objectProperty!._key!] = new PropertyDescriptor(propValue, PropertyFlag.ConfigurableEnumerableWritable);
}
obj.SetProperties(properties);
return obj;
}
///
/// https://tc39.es/ecma262/#sec-object-initializer-runtime-semantics-propertydefinitionevaluation
///
private object BuildObjectNormal(EvaluationContext context)
{
var engine = context.Engine;
var obj = engine.Realm.Intrinsics.Object.Construct(_properties.Length);
for (var i = 0; i < _properties.Length; i++)
{
var objectProperty = _properties[i];
if (objectProperty is null)
{
// spread
if (_valueExpressions[i].GetValue(context) is ObjectInstance source)
{
source.CopyDataProperties(obj, null);
}
continue;
}
var property = objectProperty._value;
if (property.Method)
{
ClassDefinition.MethodDefinitionEvaluation(engine, obj, property, enumerable: true);
continue;
}
JsValue? propName = objectProperty.KeyJsString;
if (propName is null)
{
var value = property.TryGetKey(engine);
if (context.IsAbrupt())
{
return value;
}
propName = TypeConverter.ToPropertyKey(value);
}
if (property.Kind == PropertyKind.Init)
{
var expr = _valueExpressions[i];
var completion = expr.GetValue(context);
if (context.IsAbrupt())
{
return completion;
}
var propValue = completion.Clone();
if (string.Equals(objectProperty._key, "__proto__", StringComparison.Ordinal) && !objectProperty._value.Computed && !objectProperty._value.Shorthand)
{
if (propValue.IsObject() || propValue.IsNull())
{
obj.SetPrototypeOf(propValue);
}
continue;
}
if (expr._expression.IsAnonymousFunctionDefinition())
{
var closure = (Function) propValue;
closure.SetFunctionName(propName);
}
obj.CreateDataPropertyOrThrow(propName, propValue);
}
else if (property.Kind is PropertyKind.Get or PropertyKind.Set)
{
var function = objectProperty.GetFunctionDefinition(engine);
var closure = engine.Realm.Intrinsics.Function.OrdinaryFunctionCreate(
engine.Realm.Intrinsics.Function.PrototypeObject,
function,
function.ThisMode,
engine.ExecutionContext.LexicalEnvironment,
engine.ExecutionContext.PrivateEnvironment);
closure.SetFunctionName(propName, property.Kind == PropertyKind.Get ? "get" : "set");
closure.MakeMethod(obj);
var propDesc = new GetSetPropertyDescriptor(
get: property.Kind == PropertyKind.Get ? closure : null,
set: property.Kind == PropertyKind.Set ? closure : null,
PropertyFlag.Enumerable | PropertyFlag.Configurable);
obj.DefinePropertyOrThrow(propName, propDesc);
}
}
return obj;
}
internal sealed class JintEmptyObjectExpression : JintExpression
{
public static JintEmptyObjectExpression Instance = new(new ObjectExpression(NodeList.From(Array.Empty())));
private JintEmptyObjectExpression(Expression expression) : base(expression)
{
}
protected override object EvaluateInternal(EvaluationContext context)
{
return new JsObject(context.Engine);
}
public override JsValue GetValue(EvaluationContext context)
{
return new JsObject(context.Engine);
}
}
}