TypeConverter.cs 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.Runtime.CompilerServices;
  5. using Esprima.Ast;
  6. using Jint.Extensions;
  7. using Jint.Native;
  8. using Jint.Native.Number;
  9. using Jint.Native.Number.Dtoa;
  10. using Jint.Native.Object;
  11. using Jint.Native.String;
  12. using Jint.Native.Symbol;
  13. using Jint.Pooling;
  14. using Jint.Runtime.Interop;
  15. namespace Jint.Runtime
  16. {
  17. [Flags]
  18. public enum Types
  19. {
  20. None = 0,
  21. Undefined = 1,
  22. Null = 2,
  23. Boolean = 4,
  24. String = 8,
  25. Number = 16,
  26. Symbol = 64,
  27. Object = 128
  28. }
  29. [Flags]
  30. internal enum InternalTypes
  31. {
  32. // should not be used, used for empty match
  33. None = 0,
  34. Undefined = 1,
  35. Null = 2,
  36. // primitive types range start
  37. Boolean = 4,
  38. String = 8,
  39. Number = 16,
  40. Integer = 32,
  41. Symbol = 64,
  42. // primitive types range end
  43. Object = 128,
  44. // internal usage
  45. ObjectEnvironmentRecord = 512,
  46. RequiresCloning = 1024,
  47. Primitive = Boolean | String | Number | Integer | Symbol,
  48. InternalFlags = ObjectEnvironmentRecord | RequiresCloning
  49. }
  50. public static class TypeConverter
  51. {
  52. // how many decimals to check when determining if double is actually an int
  53. private const double DoubleIsIntegerTolerance = double.Epsilon * 100;
  54. internal static readonly string[] intToString = new string[1024];
  55. private static readonly string[] charToString = new string[256];
  56. static TypeConverter()
  57. {
  58. for (var i = 0; i < intToString.Length; ++i)
  59. {
  60. intToString[i] = i.ToString();
  61. }
  62. for (var i = 0; i < charToString.Length; ++i)
  63. {
  64. var c = (char) i;
  65. charToString[i] = c.ToString();
  66. }
  67. }
  68. /// <summary>
  69. /// https://tc39.es/ecma262/#sec-toprimitive
  70. /// </summary>
  71. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  72. public static JsValue ToPrimitive(JsValue input, Types preferredType = Types.None)
  73. {
  74. return input is not ObjectInstance oi
  75. ? input
  76. : ToPrimitiveObjectInstance(oi, preferredType);
  77. }
  78. private static JsValue ToPrimitiveObjectInstance(ObjectInstance oi, Types preferredType)
  79. {
  80. var hint = preferredType switch
  81. {
  82. Types.String => JsString.StringString,
  83. Types.Number => JsString.NumberString,
  84. _ => JsString.DefaultString
  85. };
  86. var exoticToPrim = oi.GetMethod(GlobalSymbolRegistry.ToPrimitive);
  87. if (exoticToPrim is object)
  88. {
  89. var str = exoticToPrim.Call(oi, new JsValue[] { hint });
  90. if (str.IsPrimitive())
  91. {
  92. return str;
  93. }
  94. if (str.IsObject())
  95. {
  96. ExceptionHelper.ThrowTypeError(oi.Engine.Realm, "Cannot convert object to primitive value");
  97. }
  98. }
  99. return OrdinaryToPrimitive(oi, preferredType == Types.None ? Types.Number : preferredType);
  100. }
  101. /// <summary>
  102. /// https://tc39.es/ecma262/#sec-ordinarytoprimitive
  103. /// </summary>
  104. internal static JsValue OrdinaryToPrimitive(ObjectInstance input, Types hint = Types.None)
  105. {
  106. JsString property1;
  107. JsString property2;
  108. if (hint == Types.String)
  109. {
  110. property1 = (JsString) "toString";
  111. property2 = (JsString) "valueOf";
  112. }
  113. else if (hint == Types.Number)
  114. {
  115. property1 = (JsString) "valueOf";
  116. property2 = (JsString) "toString";
  117. }
  118. else
  119. {
  120. ExceptionHelper.ThrowTypeError(input.Engine.Realm);
  121. return null;
  122. }
  123. if (input.Get(property1) is ICallable method1)
  124. {
  125. var val = method1.Call(input, Arguments.Empty);
  126. if (val.IsPrimitive())
  127. {
  128. return val;
  129. }
  130. }
  131. if (input.Get(property2) is ICallable method2)
  132. {
  133. var val = method2.Call(input, Arguments.Empty);
  134. if (val.IsPrimitive())
  135. {
  136. return val;
  137. }
  138. }
  139. ExceptionHelper.ThrowTypeError(input.Engine.Realm);
  140. return null;
  141. }
  142. /// <summary>
  143. /// https://tc39.es/ecma262/#sec-toboolean
  144. /// </summary>
  145. public static bool ToBoolean(JsValue o)
  146. {
  147. var type = o._type & ~InternalTypes.InternalFlags;
  148. switch (type)
  149. {
  150. case InternalTypes.Boolean:
  151. return ((JsBoolean) o)._value;
  152. case InternalTypes.Undefined:
  153. case InternalTypes.Null:
  154. return false;
  155. case InternalTypes.Integer:
  156. return (int) ((JsNumber) o)._value != 0;
  157. case InternalTypes.Number:
  158. var n = ((JsNumber) o)._value;
  159. return n != 0 && !double.IsNaN(n);
  160. case InternalTypes.String:
  161. return !((JsString) o).IsNullOrEmpty();
  162. default:
  163. return true;
  164. }
  165. }
  166. /// <summary>
  167. /// https://tc39.es/ecma262/#sec-tonumber
  168. /// </summary>
  169. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  170. public static double ToNumber(JsValue o)
  171. {
  172. return o.IsNumber()
  173. ? ((JsNumber) o)._value
  174. : ToNumberUnlikely(o);
  175. }
  176. private static double ToNumberUnlikely(JsValue o)
  177. {
  178. var type = o._type & ~InternalTypes.InternalFlags;
  179. switch (type)
  180. {
  181. case InternalTypes.Undefined:
  182. return double.NaN;
  183. case InternalTypes.Null:
  184. return 0;
  185. case InternalTypes.Object when o is IPrimitiveInstance p:
  186. return ToNumber(ToPrimitive(p.PrimitiveValue, Types.Number));
  187. case InternalTypes.Boolean:
  188. return ((JsBoolean) o)._value ? 1 : 0;
  189. case InternalTypes.String:
  190. return ToNumber(o.ToString());
  191. case InternalTypes.Symbol:
  192. // TODO proper TypeError would require Engine instance and a lot of API changes
  193. ExceptionHelper.ThrowTypeErrorNoEngine("Cannot convert a Symbol value to a number");
  194. return 0;
  195. default:
  196. return ToNumber(ToPrimitive(o, Types.Number));
  197. }
  198. }
  199. private static double ToNumber(string input)
  200. {
  201. // eager checks to save time and trimming
  202. if (string.IsNullOrEmpty(input))
  203. {
  204. return 0;
  205. }
  206. var first = input[0];
  207. if (input.Length == 1 && first >= '0' && first <= '9')
  208. {
  209. // simple constant number
  210. return first - '0';
  211. }
  212. var s = StringPrototype.TrimEx(input);
  213. if (s.Length == 0)
  214. {
  215. return 0;
  216. }
  217. if (s.Length == 8 || s.Length == 9)
  218. {
  219. if ("+Infinity" == s || "Infinity" == s)
  220. {
  221. return double.PositiveInfinity;
  222. }
  223. if ("-Infinity" == s)
  224. {
  225. return double.NegativeInfinity;
  226. }
  227. }
  228. // todo: use a common implementation with JavascriptParser
  229. try
  230. {
  231. if (s.Length > 2 && s[0] == '0' && char.IsLetter(s[1]))
  232. {
  233. var fromBase = 0;
  234. if (s[1] == 'x' || s[1] == 'X')
  235. {
  236. fromBase = 16;
  237. }
  238. if (s[1] == 'o' || s[1] == 'O')
  239. {
  240. fromBase = 8;
  241. }
  242. if (s[1] == 'b' || s[1] == 'B')
  243. {
  244. fromBase = 2;
  245. }
  246. if (fromBase > 0)
  247. {
  248. return Convert.ToInt32(s.Substring(2), fromBase);
  249. }
  250. }
  251. var start = s[0];
  252. if (start != '+' && start != '-' && start != '.' && !char.IsDigit(start))
  253. {
  254. return double.NaN;
  255. }
  256. var n = double.Parse(s,
  257. NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign |
  258. NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite |
  259. NumberStyles.AllowExponent, CultureInfo.InvariantCulture);
  260. if (s.StartsWith("-") && n == 0)
  261. {
  262. return -0.0;
  263. }
  264. return n;
  265. }
  266. catch (OverflowException)
  267. {
  268. return s.StartsWith("-") ? double.NegativeInfinity : double.PositiveInfinity;
  269. }
  270. catch
  271. {
  272. return double.NaN;
  273. }
  274. }
  275. /// <summary>
  276. /// https://tc39.es/ecma262/#sec-tolength
  277. /// </summary>
  278. public static ulong ToLength(JsValue o)
  279. {
  280. var len = ToInteger(o);
  281. if (len <= 0)
  282. {
  283. return 0;
  284. }
  285. return (ulong) Math.Min(len, NumberConstructor.MaxSafeInteger);
  286. }
  287. /// <summary>
  288. /// https://tc39.es/ecma262/#sec-tointegerorinfinity
  289. /// </summary>
  290. public static double ToIntegerOrInfinity(JsValue argument)
  291. {
  292. var number = ToNumber(argument);
  293. if (double.IsNaN(number) || number == 0)
  294. {
  295. return 0;
  296. }
  297. if (double.IsInfinity(number))
  298. {
  299. return number;
  300. }
  301. var integer = (long) Math.Floor(Math.Abs(number));
  302. if (number < 0)
  303. {
  304. integer *= -1;
  305. }
  306. return integer;
  307. }
  308. /// <summary>
  309. /// https://tc39.es/ecma262/#sec-tointeger
  310. /// </summary>
  311. public static double ToInteger(JsValue o)
  312. {
  313. var number = ToNumber(o);
  314. if (double.IsNaN(number))
  315. {
  316. return 0;
  317. }
  318. if (number == 0 || double.IsInfinity(number))
  319. {
  320. return number;
  321. }
  322. return (long) number;
  323. }
  324. internal static double ToInteger(string o)
  325. {
  326. var number = ToNumber(o);
  327. if (double.IsNaN(number))
  328. {
  329. return 0;
  330. }
  331. if (number == 0 || double.IsInfinity(number))
  332. {
  333. return number;
  334. }
  335. return (long) number;
  336. }
  337. internal static int DoubleToInt32Slow(double o)
  338. {
  339. // Computes the integral value of the number mod 2^32.
  340. var doubleBits = BitConverter.DoubleToInt64Bits(o);
  341. var sign = (int) (doubleBits >> 63); // 0 if positive, -1 if negative
  342. var exponent = (int) ((doubleBits >> 52) & 0x7FF) - 1023;
  343. if ((uint) exponent >= 84)
  344. {
  345. // Anything with an exponent that is negative or >= 84 will convert to zero.
  346. // This includes infinities and NaNs, which have exponent = 1024
  347. // The 84 comes from 52 (bits in double mantissa) + 32 (bits in integer)
  348. return 0;
  349. }
  350. var mantissa = (doubleBits & 0xFFFFFFFFFFFFFL) | 0x10000000000000L;
  351. var int32Value = exponent >= 52 ? (int) (mantissa << (exponent - 52)) : (int) (mantissa >> (52 - exponent));
  352. return (int32Value + sign) ^ sign;
  353. }
  354. /// <summary>
  355. /// http://www.ecma-international.org/ecma-262/5.1/#sec-9.5
  356. /// </summary>
  357. public static int ToInt32(JsValue o)
  358. {
  359. if (o._type == InternalTypes.Integer)
  360. {
  361. return o.AsInteger();
  362. }
  363. var doubleVal = ToNumber(o);
  364. if (doubleVal >= -(double) int.MinValue && doubleVal <= int.MaxValue)
  365. {
  366. // Double-to-int cast is correct in this range
  367. return (int) doubleVal;
  368. }
  369. return DoubleToInt32Slow(doubleVal);
  370. }
  371. /// <summary>
  372. /// http://www.ecma-international.org/ecma-262/5.1/#sec-9.6
  373. /// </summary>
  374. public static uint ToUint32(JsValue o)
  375. {
  376. if (o._type == InternalTypes.Integer)
  377. {
  378. return (uint) o.AsInteger();
  379. }
  380. var doubleVal = ToNumber(o);
  381. if (doubleVal >= 0.0 && doubleVal <= uint.MaxValue)
  382. {
  383. // Double-to-uint cast is correct in this range
  384. return (uint) doubleVal;
  385. }
  386. return (uint) DoubleToInt32Slow(doubleVal);
  387. }
  388. /// <summary>
  389. /// http://www.ecma-international.org/ecma-262/5.1/#sec-9.7
  390. /// </summary>
  391. public static ushort ToUint16(JsValue o)
  392. {
  393. return o._type == InternalTypes.Integer
  394. ? (ushort) (uint) o.AsInteger()
  395. : (ushort) (uint) ToNumber(o);
  396. }
  397. /// <summary>
  398. /// https://tc39.es/ecma262/#sec-toint16
  399. /// </summary>
  400. internal static double ToInt16(JsValue o)
  401. {
  402. return o._type == InternalTypes.Integer
  403. ? (short) o.AsInteger()
  404. : (short) (long) ToNumber(o);
  405. }
  406. /// <summary>
  407. /// https://tc39.es/ecma262/#sec-toint8
  408. /// </summary>
  409. internal static double ToInt8(JsValue o)
  410. {
  411. return o._type == InternalTypes.Integer
  412. ? (sbyte) o.AsInteger()
  413. : (sbyte) (long) ToNumber(o);
  414. }
  415. /// <summary>
  416. /// https://tc39.es/ecma262/#sec-touint8
  417. /// </summary>
  418. internal static double ToUint8(JsValue o)
  419. {
  420. return o._type == InternalTypes.Integer
  421. ? (byte) o.AsInteger()
  422. : (byte) (long) ToNumber(o);
  423. }
  424. /// <summary>
  425. /// https://tc39.es/ecma262/#sec-touint8clamp
  426. /// </summary>
  427. internal static byte ToUint8Clamp(JsValue o)
  428. {
  429. if (o._type == InternalTypes.Integer)
  430. {
  431. var intValue = o.AsInteger();
  432. if (intValue is > -1 and < 256)
  433. {
  434. return (byte) intValue;
  435. }
  436. }
  437. return ToUint8ClampUnlikely(o);
  438. }
  439. private static byte ToUint8ClampUnlikely(JsValue o)
  440. {
  441. var number = ToNumber(o);
  442. if (double.IsNaN(number))
  443. {
  444. return 0;
  445. }
  446. if (number <= 0)
  447. {
  448. return 0;
  449. }
  450. if (number >= 255)
  451. {
  452. return 255;
  453. }
  454. var f = Math.Floor(number);
  455. if (f + 0.5 < number)
  456. {
  457. return (byte) (f + 1);
  458. }
  459. if (number < f + 0.5)
  460. {
  461. return (byte) f;
  462. }
  463. if (f % 2 != 0)
  464. {
  465. return (byte) (f + 1);
  466. }
  467. return (byte) f;
  468. }
  469. /// <summary>
  470. /// https://tc39.es/ecma262/#sec-tobigint
  471. /// </summary>
  472. public static double ToBigInt(JsValue value)
  473. {
  474. ExceptionHelper.ThrowNotImplementedException("BigInt not implemented");
  475. return 0;
  476. }
  477. /// <summary>
  478. /// https://tc39.es/ecma262/#sec-tobigint64
  479. /// </summary>
  480. internal static double ToBigInt64(JsValue value)
  481. {
  482. ExceptionHelper.ThrowNotImplementedException("BigInt not implemented");
  483. return 0;
  484. }
  485. /// <summary>
  486. /// https://tc39.es/ecma262/#sec-tobiguint64
  487. /// </summary>
  488. internal static double ToBigUint64(JsValue value)
  489. {
  490. ExceptionHelper.ThrowNotImplementedException("BigInt not implemented");
  491. return 0;
  492. }
  493. /// <summary>
  494. /// https://tc39.es/ecma262/#sec-canonicalnumericindexstring
  495. /// </summary>
  496. internal static double? CanonicalNumericIndexString(JsValue value)
  497. {
  498. if (value is JsNumber jsNumber)
  499. {
  500. return jsNumber._value;
  501. }
  502. if (value is JsString jsString)
  503. {
  504. if (jsString.ToString() == "-0")
  505. {
  506. return -0d;
  507. }
  508. var n = ToNumber(value);
  509. if (!JsValue.SameValue(ToString(n), value))
  510. {
  511. return null;
  512. }
  513. return n;
  514. }
  515. return null;
  516. }
  517. /// <summary>
  518. /// https://tc39.es/ecma262/#sec-toindex
  519. /// </summary>
  520. public static uint ToIndex(Realm realm, JsValue value)
  521. {
  522. if (value.IsUndefined())
  523. {
  524. return 0;
  525. }
  526. var integerIndex = ToIntegerOrInfinity(value);
  527. if (integerIndex < 0)
  528. {
  529. ExceptionHelper.ThrowRangeError(realm);
  530. }
  531. var index = ToLength(integerIndex);
  532. if (integerIndex != index)
  533. {
  534. ExceptionHelper.ThrowRangeError(realm, "Invalid index");
  535. }
  536. return (uint) Math.Min(uint.MaxValue, index);
  537. }
  538. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  539. internal static string ToString(long i)
  540. {
  541. return i >= 0 && i < intToString.Length
  542. ? intToString[i]
  543. : i.ToString();
  544. }
  545. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  546. internal static string ToString(int i)
  547. {
  548. return i >= 0 && i < intToString.Length
  549. ? intToString[i]
  550. : i.ToString();
  551. }
  552. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  553. internal static string ToString(uint i)
  554. {
  555. return i < (uint) intToString.Length
  556. ? intToString[i]
  557. : i.ToString();
  558. }
  559. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  560. internal static string ToString(char c)
  561. {
  562. return c >= 0 && c < charToString.Length
  563. ? charToString[c]
  564. : c.ToString();
  565. }
  566. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  567. internal static string ToString(ulong i)
  568. {
  569. return i >= 0 && i < (ulong) intToString.Length
  570. ? intToString[i]
  571. : i.ToString();
  572. }
  573. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  574. internal static string ToString(double d)
  575. {
  576. if (d > long.MinValue && d < long.MaxValue && Math.Abs(d % 1) <= DoubleIsIntegerTolerance)
  577. {
  578. // we are dealing with integer that can be cached
  579. return ToString((long) d);
  580. }
  581. using var stringBuilder = StringBuilderPool.Rent();
  582. // we can create smaller array as we know the format to be short
  583. return NumberPrototype.NumberToString(d, new DtoaBuilder(17), stringBuilder.Builder);
  584. }
  585. /// <summary>
  586. /// http://www.ecma-international.org/ecma-262/6.0/#sec-topropertykey
  587. /// </summary>
  588. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  589. public static JsValue ToPropertyKey(JsValue o)
  590. {
  591. const InternalTypes stringOrSymbol = InternalTypes.String | InternalTypes.Symbol;
  592. return (o._type & stringOrSymbol) != 0
  593. ? o
  594. : ToPropertyKeyNonString(o);
  595. }
  596. [MethodImpl(MethodImplOptions.NoInlining)]
  597. private static JsValue ToPropertyKeyNonString(JsValue o)
  598. {
  599. const InternalTypes stringOrSymbol = InternalTypes.String | InternalTypes.Symbol;
  600. var primitive = ToPrimitive(o, Types.String);
  601. return (primitive._type & stringOrSymbol) != 0
  602. ? primitive
  603. : ToStringNonString(primitive);
  604. }
  605. /// <summary>
  606. /// http://www.ecma-international.org/ecma-262/6.0/#sec-tostring
  607. /// </summary>
  608. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  609. public static string ToString(JsValue o)
  610. {
  611. return o.IsString() ? o.ToString() : ToStringNonString(o);
  612. }
  613. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  614. internal static JsString ToJsString(JsValue o)
  615. {
  616. if (o is JsString s)
  617. {
  618. return s;
  619. }
  620. return JsString.Create(ToStringNonString(o));
  621. }
  622. private static string ToStringNonString(JsValue o)
  623. {
  624. var type = o._type & ~InternalTypes.InternalFlags;
  625. switch (type)
  626. {
  627. case InternalTypes.Boolean:
  628. return ((JsBoolean) o)._value ? "true" : "false";
  629. case InternalTypes.Integer:
  630. return ToString((int) ((JsNumber) o)._value);
  631. case InternalTypes.Number:
  632. return ToString(((JsNumber) o)._value);
  633. case InternalTypes.Symbol:
  634. ExceptionHelper.ThrowTypeErrorNoEngine("Cannot convert a Symbol value to a string");
  635. return null;
  636. case InternalTypes.Undefined:
  637. return Undefined.Text;
  638. case InternalTypes.Null:
  639. return Null.Text;
  640. case InternalTypes.Object when o is IPrimitiveInstance p:
  641. return ToString(ToPrimitive(p.PrimitiveValue, Types.String));
  642. case InternalTypes.Object when o is IObjectWrapper p:
  643. return p.Target?.ToString();
  644. default:
  645. return ToString(ToPrimitive(o, Types.String));
  646. }
  647. }
  648. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  649. public static ObjectInstance ToObject(Realm realm, JsValue value)
  650. {
  651. if (value is ObjectInstance oi)
  652. {
  653. return oi;
  654. }
  655. return ToObjectNonObject(realm, value);
  656. }
  657. private static ObjectInstance ToObjectNonObject(Realm realm, JsValue value)
  658. {
  659. var type = value._type & ~InternalTypes.InternalFlags;
  660. switch (type)
  661. {
  662. case InternalTypes.Boolean:
  663. return realm.Intrinsics.Boolean.Construct((JsBoolean) value);
  664. case InternalTypes.Number:
  665. case InternalTypes.Integer:
  666. return realm.Intrinsics.Number.Construct((JsNumber) value);
  667. case InternalTypes.String:
  668. return realm.Intrinsics.String.Construct(value.ToString());
  669. case InternalTypes.Symbol:
  670. return realm.Intrinsics.Symbol.Construct((JsSymbol) value);
  671. case InternalTypes.Null:
  672. case InternalTypes.Undefined:
  673. ExceptionHelper.ThrowTypeError(realm, "Cannot convert undefined or null to object");
  674. return null;
  675. default:
  676. ExceptionHelper.ThrowTypeError(realm, "Cannot convert given item to object");
  677. return null;
  678. }
  679. }
  680. [MethodImpl(MethodImplOptions.NoInlining)]
  681. internal static void CheckObjectCoercible(
  682. Engine engine,
  683. JsValue o,
  684. Node sourceNode,
  685. string referenceName)
  686. {
  687. if (!engine._referenceResolver.CheckCoercible(o))
  688. {
  689. ThrowMemberNullOrUndefinedError(engine, o, sourceNode, referenceName);
  690. }
  691. }
  692. [MethodImpl(MethodImplOptions.NoInlining)]
  693. private static void ThrowMemberNullOrUndefinedError(
  694. Engine engine,
  695. JsValue o,
  696. Node sourceNode,
  697. string referencedName)
  698. {
  699. referencedName ??= "unknown";
  700. var message = $"Cannot read property '{referencedName}' of {o}";
  701. throw new JavaScriptException(engine.Realm.Intrinsics.TypeError, message).SetCallstack(engine, sourceNode.Location);
  702. }
  703. public static void CheckObjectCoercible(Engine engine, JsValue o)
  704. {
  705. if (o._type < InternalTypes.Boolean)
  706. {
  707. ExceptionHelper.ThrowTypeError(engine.Realm);
  708. }
  709. }
  710. internal readonly record struct MethodMatch(MethodDescriptor Method, JsValue[] Arguments, int Score = 0) : IComparable<MethodMatch>
  711. {
  712. public int CompareTo(MethodMatch other) => Score.CompareTo(other.Score);
  713. }
  714. internal static IEnumerable<MethodMatch> FindBestMatch(
  715. Engine engine,
  716. MethodDescriptor[] methods,
  717. Func<MethodDescriptor, JsValue[]> argumentProvider)
  718. {
  719. List<MethodMatch> matchingByParameterCount = null;
  720. foreach (var method in methods)
  721. {
  722. var parameterInfos = method.Parameters;
  723. var arguments = argumentProvider(method);
  724. if (arguments.Length <= parameterInfos.Length
  725. && arguments.Length >= parameterInfos.Length - method.ParameterDefaultValuesCount)
  726. {
  727. var score = CalculateMethodScore(engine, method, arguments);
  728. if (score == 0)
  729. {
  730. // perfect match
  731. yield return new MethodMatch(method, arguments);
  732. yield break;
  733. }
  734. if (score < 0)
  735. {
  736. // discard
  737. continue;
  738. }
  739. matchingByParameterCount ??= new List<MethodMatch>();
  740. matchingByParameterCount.Add(new MethodMatch(method, arguments, score));
  741. }
  742. }
  743. if (matchingByParameterCount == null)
  744. {
  745. yield break;
  746. }
  747. if (matchingByParameterCount.Count > 1)
  748. {
  749. matchingByParameterCount.Sort();
  750. }
  751. foreach (var match in matchingByParameterCount)
  752. {
  753. yield return match;
  754. }
  755. }
  756. /// <summary>
  757. /// Method's match score tells how far away it's from ideal candidate. 0 = ideal, bigger the the number,
  758. /// the farther away the candidate is from ideal match. Negative signals impossible match.
  759. /// </summary>
  760. private static int CalculateMethodScore(Engine engine, MethodDescriptor method, JsValue[] arguments)
  761. {
  762. if (method.Parameters.Length == 0 && arguments.Length == 0)
  763. {
  764. // perfect
  765. return 0;
  766. }
  767. var score = 0;
  768. for (var i = 0; i < arguments.Length; i++)
  769. {
  770. var jsValue = arguments[i];
  771. var paramType = method.Parameters[i].ParameterType;
  772. var parameterScore = CalculateMethodParameterScore(engine, jsValue, paramType);
  773. if (parameterScore < 0)
  774. {
  775. return parameterScore;
  776. }
  777. score += parameterScore;
  778. }
  779. return score;
  780. }
  781. /// <summary>
  782. /// Determines how well parameter type matches target method's type.
  783. /// </summary>
  784. private static int CalculateMethodParameterScore(
  785. Engine engine,
  786. JsValue jsValue,
  787. Type paramType)
  788. {
  789. var objectValue = jsValue.ToObject();
  790. var objectValueType = objectValue?.GetType();
  791. if (objectValueType == paramType)
  792. {
  793. return 0;
  794. }
  795. if (objectValue == null)
  796. {
  797. if (!TypeIsNullable(paramType))
  798. {
  799. // this is bad
  800. return -1;
  801. }
  802. return 0;
  803. }
  804. if (paramType == typeof(JsValue))
  805. {
  806. // JsValue is convertible to. But it is still not a perfect match
  807. return 1;
  808. }
  809. if (paramType == typeof(object))
  810. {
  811. // a catch-all, prefer others over it
  812. return 5;
  813. }
  814. if (paramType.IsEnum &&
  815. jsValue is JsNumber jsNumber
  816. && jsNumber.IsInteger()
  817. && Enum.IsDefined(paramType, jsNumber.AsInteger()))
  818. {
  819. // we can do conversion from int value to enum
  820. return 0;
  821. }
  822. if (paramType.IsAssignableFrom(objectValueType))
  823. {
  824. // is-a-relation
  825. return 1;
  826. }
  827. if (jsValue.IsArray() && objectValueType.IsArray)
  828. {
  829. // we have potential, TODO if we'd know JS array's internal type we could have exact match
  830. return 2;
  831. }
  832. if (CanChangeType(objectValue, paramType))
  833. {
  834. // forcing conversion isn't ideal not ideal, but works, especially for int -> double for example
  835. return 1;
  836. }
  837. if (engine.Options.Interop.AllowOperatorOverloading)
  838. {
  839. foreach (var m in objectValueType.GetOperatorOverloadMethods())
  840. {
  841. if (paramType.IsAssignableFrom(m.ReturnType) && m.Name is "op_Implicit" or "op_Explicit")
  842. {
  843. // implicit/explicit operator conversion is OK, but not ideal
  844. return 1;
  845. }
  846. }
  847. }
  848. if (ReflectionExtensions.TryConvertViaTypeCoercion(paramType, engine.Options.Interop.ValueCoercion, jsValue, out _))
  849. {
  850. // gray JS zone where we start to do odd things
  851. return 10;
  852. }
  853. // will rarely succeed
  854. return 100;
  855. }
  856. private static bool CanChangeType(object value, Type targetType)
  857. {
  858. if (value is null && !targetType.IsValueType)
  859. {
  860. return true;
  861. }
  862. if (value is not IConvertible)
  863. {
  864. return false;
  865. }
  866. try
  867. {
  868. Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture);
  869. return true;
  870. }
  871. catch
  872. {
  873. // nope
  874. return false;
  875. }
  876. }
  877. internal static bool TypeIsNullable(Type type)
  878. {
  879. return !type.IsValueType || Nullable.GetUnderlyingType(type) != null;
  880. }
  881. }
  882. }