Skip to content

Deviations from real JS

lamassu-js aims to match Node closely enough for differential testing over the shared subset — not full spec conformance. This page tracks every place that's known to differ, so you're not surprised by one in production.

Objects

  • Object.prototype only has three methodshasOwnProperty, toString, valueOf. Real JS also has isPrototypeOf, propertyIsEnumerable, toLocaleString, and the legacy __proto__ accessor; none of those are implemented. toString always returns "[object Object]" — there's no per-kind Symbol.toStringTag-style internal class tag, though in practice this is only reachable for plain objects, since Array/Map/Set/Date/RegExp all define their own toString that shadows it.
  • Object(primitiveValue) returns the primitive unchanged rather than boxing it into a wrapper object — this engine has no boxed-primitive type at all. Object()/Object(null)/Object(undefined) (a new empty object) and Object(anObject) (identity) both match spec.
  • Key order follows hash order, not insertion order. Affects Object.keys/values/entries, for...of over Object.entries(...), and JSON.stringify of plain objects. (Planned to resolve once the engine's property storage moves to hidden-class "shapes," which store properties in insertion order — see the roadmap in the project's docs/plan.md.)
  • Integrity is whole-object, not per-property. Object.freeze and Object.seal are enforced — on property assignment, indexed array writes, length, delete, the in-place array mutators (push/pop/shift/ unshift/reverse/fill/sort), and the host's js_object_set — and because the engine is strict-mode throughout, a refused write throws a TypeError rather than failing quietly. Object.isFrozen and Object.isSealed report the state. What's missing is anything finer: there are no per-property attributes here, so there is no writable/configurable distinction and no Object.defineProperty to set one. That covers everything guest code can observe of an ordinary frozen object; it only differs if you were reaching for a partially frozen one.

Arrays

  • No holes. Array storage is a flat vector, not a sparse structure — every index up to .length is a real, materialized slot. Array(n) and arr.length = n both throw RangeError past a fixed growth cap rather than eagerly allocating an attacker-chosen size; see Built-ins → Array.
  • Array.prototype.sort is a stable O(n²) binary insertion sort — fine for templating-sized arrays; revisit if you're sorting something large.
  • Array.from supports strings and arrays, not arbitrary array-likes or iterators.

Numbers

  • Transcendental Math functions are ~1e-12 accurate, not correctly rounded — a custom freestanding kernel, no libm. sqrt/floor/ceil/trunc/round are exact (native ops).
  • Number-to-string formatting is exact for safe integers and for magnitudes within ~1e±22; extreme magnitudes may differ from Node in the last digit (shortest-round-trip via float scaling, not a full big-integer dtoa).

Strings

Case mapping (toUpperCase/toLowerCase) is ASCII-only — no full Unicode case folding.

Regular expressions

String.prototype.matchAll returns a plain array rather than a lazy iterator (for...of over the result behaves the same either way). String.prototype.split ignores its limit argument.

Promises & async

  • Promise(executor) is callable without new — a deliberate accommodation, alongside real new/constructor-function support and prototype chains for everything else.
  • Unhandled promise rejections are silent. The engine tracks a handled flag internally but doesn't report unhandled rejections; a host hook could be added by an embedder that needs one.
  • A pending top-level-await module returns from the host's run call still pending — see Async & host calls.

Modules

  • Cross-module cycles degrade to undefined for a binding read before the exporting module has initialized it, rather than throwing a real-JS-style TDZ error. Hoisted export function declarations are available throughout, so mutually recursive function modules work fine — this only bites a cyclic read of a let/const binding before its module finishes evaluating.
  • Named and export * re-exports are snapshots, taken once the source module finishes evaluating (dependency order guarantees this has already happened). A later mutation of a re-exported mutable binding isn't observed through the re-export. export * as ns from is live by reference instead (it aliases the source's namespace object). For a templating workload this is exact in practice, since re-exports are almost always functions or constants.
  • Module evaluation drains the whole microtask queue after each module body runs — there's no cross-module async evaluation ordering beyond plain dependency order.

Not a deviation, but easy to trip on

for...in, var, classes, eval, the Function constructor, Reflect, Proxy, Symbol, generators, typed arrays, and Intl aren't "different," they're absent — see Supported syntax for the full list of non-goals.

Released under the MIT License.