Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ Its rules are split across the files in the index above, and every one of them i
Each of these cost a real integrator or a real bug.

- **Constraints bound one entry into the engine, never a host-driven sequence of them.** Every public entry that runs script — `Execute`, `Evaluate`, `Invoke`, `Engine.Call`, the `JsValue.Call` extension helpers — funnels through `Engine.ExecuteWithConstraints` (`Jint/Engine.cs`), which calls `ResetConstraints()` before the callback and again in its `finally` for any entry that is not nested (nesting is `_hostEntryDepth > 0 || _executionContexts.Count > 1`). So `foreach (var row in rows) predicate.Call(row);` — the single most common embedding shape — hands every element a fresh statement budget, a fresh allocation budget and, worst, a **freshly armed timeout deadline**. Measured: `LimitStatements(100)` does not stop 1000 host `Call`s, `LimitExecutionTime(200ms)` does not fire across 3 s of continuous host-driven execution, and `LimitMemory` never sees more than one call's allocations — while the identical work inside one `Execute` throws in every case. The reset itself is not the mistake: per-run reset is exactly what `Constraint.Reset`'s doc promises and what makes a reused engine usable, and the nested case is handled deliberately (a host callback re-entering the engine from inside a running script does *not* re-arm, or `while (true) hostCallback()` would run forever). What no embedder expects is that a single function call is a **run**. `Engine.Constraints.Check()` from the host loop does not close the gap — `TimeConstraint` re-arms its deadline on the way *out* of every run, so a host-side check measures the time since the last call returned. All of it is pinned from the embedder's side in `Jint.Tests.PublicInterface/HostCallLoopConstraintTests.cs` and `HostMemoryLimitTests.cs`; those tests assert the behaviour as it is, so changing it is a deliberate act that updates them. **What an embedder must do instead — the host-side bound, the in-script loop, and the two in-box constraints that survive the per-entry reset — is [`Jint/Constraints/AGENTS.md`](Jint/Constraints/AGENTS.md#bounding-a-host-driven-sequence), which is also the file to read before changing any of it.**
- **`FastSetProperty` / `FastSetDataProperty` always create an *own* property.** They shadow anything of that name on the prototype chain, invoke no inherited setter, and run no `[[DefineOwnProperty]]` validation (so they can never raise `TypeError`). Storing a raw descriptor under a string key is a dictionary-mode operation, so a shape-mode receiver is permanently deoptimized and forfeits the shape inline cache. Two refinements: symbol keys do not deopt, and a `BuiltinShapeMode` receiver can survive an in-place slot replacement. Use them for setup-time writes only — `Set` for steady-state mutation, `JsObject.Create` to build objects.
- **`GetOwnProperties` is not the enumeration hook.** `Object.keys`/`values`/`entries`, `for..in`, object spread and rest, `Object.assign`, `JSON.stringify` and `JsonSerializer` all list keys through `GetOwnPropertyKeys` and filter them with `ProbeOwnProperty`; none of them calls `GetOwnProperties`. Its real callers are the CLR conversion path (`ToObject` under `Options.Interop.CreateClrObject`), `GetSmallestIndex`, the debugger's `GetAllBindingNames`, and the debug view. A host that overrides only `GetOwnProperties` to expose projected properties therefore ships an object whose keys are invisible to every script-visible enumeration — a real integrator did exactly that. Overriding `GetOwnPropertyKeys` (plus `ProbeOwnProperty`, so existence and enumerability are answered without materializing a descriptor) is the pair that works.
- **`DefineOwnPropertyUnchecked` / `DefineOwnDataPropertyUnchecked` always create an *own* property.** They shadow anything of that name on the prototype chain, invoke no inherited setter, and run no `[[DefineOwnProperty]]` validation (so they can never raise `TypeError`). Storing a raw descriptor under a string key is a dictionary-mode operation, so a shape-mode receiver is permanently deoptimized and forfeits the shape inline cache. Two refinements: symbol keys do not deopt, and a `BuiltinShapeMode` receiver can survive an in-place slot replacement. Use them for setup-time writes only — `Set` for steady-state mutation, `JsObject.Create` to build objects.
- **The enumeration hook is `GetOwnPropertyKeys`, and it is the only one.** `Object.keys`/`values`/`entries`, `for..in`, object spread and rest, `Object.assign`, `JSON.stringify` and `JsonSerializer` all list keys through it and filter them with `ProbeOwnProperty`, and so — since [#3461](https://github.com/sebastienros/jint/pull/3461) — do `GetOwnProperties`' own consumers, the CLR conversion path (`ToObject` under `Options.Interop.CreateClrObject`), the debugger's `GetAllBindingNames` and the debug view. `GetOwnProperties` was a second `virtual` whose name read like the hook, and a real integrator overrode only that and shipped an object whose keys were invisible to every script-visible enumeration; it is now derived from `GetOwnPropertyKeys` + `GetOwnProperty` and non-virtual, so a host declares its keys once. Override `GetOwnPropertyKeys` (plus `ProbeOwnProperty`, so existence and enumerability are answered without materializing a descriptor), or derive from `NamedPropertyObject` / `ArrayLikeObject` and write neither.
- **Sharing a `JsValue` across engines is unsupported**, and nothing validates or guards it. An `ObjectInstance` holds a hard reference to its creating engine and realm. README.md's "Embedding performance" section states this for embedders; there is still no XML doc on `JsValue` / `Engine.SetValue` saying it, and no test pins it.

The other seventeen are in the files indexed above. **Do not add a new gotcha here.** Add it to the file for the area it governs; if none fits, say so in the pull request rather than growing this one.
Expand Down
8 changes: 4 additions & 4 deletions Jint.Benchmark/HostLayoutLazySlotBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ namespace Jint.Benchmark;
/// </description></item>
/// <item><description>
/// <see cref="EnvelopeKind.DictionaryCustomValue"/> is what a host had to write before: the same 11 cheap
/// members through <c>FastSetDataProperty</c> and the same 4 lazy ones as
/// <see cref="PropertyFlag.CustomJsValue"/> descriptors through <c>FastSetProperty</c>. It decodes exactly
/// members through <c>DefineOwnDataPropertyUnchecked</c> and the same 4 lazy ones as
/// <see cref="PropertyFlag.CustomJsValue"/> descriptors through <c>DefineOwnPropertyUnchecked</c>. It decodes exactly
/// as little as the lazy layout does, so its distance from <see cref="EnvelopeKind.LayoutLazy"/> is purely
/// the price of the dictionary representation: a per-item property dictionary and 15 descriptors on the
/// build side, and a batch that never shares a hidden class — so the projection loop's member sites see a
Expand Down Expand Up @@ -278,12 +278,12 @@ private static JsObject CreateDictionary(Engine engine, RawEnvelope raw)
FillEager(values, raw);
for (var i = 0; i < EagerNames.Length; i++)
{
obj.FastSetDataProperty(EagerNames[i], values[i]!);
obj.DefineOwnDataPropertyUnchecked(EagerNames[i], values[i]!);
}

for (var i = 0; i < LazyNames.Length; i++)
{
obj.FastSetProperty(LazyNames[i], new LazyMemberDescriptor(obj, raw, i));
obj.DefineOwnPropertyUnchecked(LazyNames[i], new LazyMemberDescriptor(obj, raw, i));
}

return obj;
Expand Down
10 changes: 0 additions & 10 deletions Jint.Benchmark/HostObjectAccessBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -452,16 +452,6 @@ public override List<JsValue> GetOwnPropertyKeys(Types types = Types.String | Ty
return keys;
}

public override IEnumerable<KeyValuePair<JsValue, PropertyDescriptor>> GetOwnProperties()
{
for (var slot = 0; slot < SlotCount; slot++)
{
yield return new KeyValuePair<JsValue, PropertyDescriptor>(
_keys[slot],
new PropertyDescriptor(Project(slot), writable: true, enumerable: true, configurable: true));
}
}

protected JsValue Project(int slot)
{
var projected = _projected[slot];
Expand Down
12 changes: 6 additions & 6 deletions Jint.Benchmark/HostPrototypeShapeBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ namespace Jint.Benchmark;
/// <item><description>
/// <see cref="BuildPrototypes"/> — per-engine setup, the headline. <see cref="HostPrototypeKind.Dictionary"/>
/// is today's pattern: a plain <see cref="ObjectInstance"/> populated through
/// <see cref="ObjectInstance.FastSetProperty(string, PropertyDescriptor)"/> with one eagerly-created
/// <see cref="ObjectInstance.DefineOwnPropertyUnchecked(string, PropertyDescriptor)"/> with one eagerly-created
/// <see cref="ClrFunction"/> per operation and a getter/setter pair per attribute.
/// <see cref="HostPrototypeKind.Shape"/> is the same members declared once per process as a
/// <see cref="JsObjectShape"/> and instantiated per engine. The <c>Allocated</c> column is the point: the
Expand Down Expand Up @@ -353,15 +353,15 @@ private static ObjectInstance BuildDictionaryPrototype(Engine engine, MemberTabl
switch (member.Kind)
{
case MemberKind.Operation:
prototype.FastSetProperty(
prototype.DefineOwnPropertyUnchecked(
member.Name,
new PropertyDescriptor(
new ClrFunction(engine, member.Name, member.Implementation!, 0),
PropertyFlag.ConfigurableEnumerableWritable));
break;

case MemberKind.Attribute:
prototype.FastSetProperty(
prototype.DefineOwnPropertyUnchecked(
member.Name,
new GetSetPropertyDescriptor(
new ClrFunction(engine, "get " + member.Name, member.Implementation!, 0),
Expand All @@ -371,15 +371,15 @@ private static ObjectInstance BuildDictionaryPrototype(Engine engine, MemberTabl
break;

default:
prototype.FastSetProperty(
prototype.DefineOwnPropertyUnchecked(
member.Name,
new PropertyDescriptor(member.Constant!, PropertyFlag.OnlyEnumerable));
break;
}
}

prototype.FastSetProperty("constructor", new PropertyDescriptor(JsValue.Undefined, PropertyFlag.NonEnumerable));
prototype.FastSetProperty(
prototype.DefineOwnPropertyUnchecked("constructor", new PropertyDescriptor(JsValue.Undefined, PropertyFlag.NonEnumerable));
prototype.DefineOwnPropertyUnchecked(
ToStringTagKey,
new PropertyDescriptor(new JsString(table.Name), PropertyFlag.Configurable));

Expand Down
6 changes: 3 additions & 3 deletions Jint.Tests.PublicInterface/GlobalSnapshotTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ protected override JsValue? CustomValue
public void AHostDescriptorHoldingItsValueOutsideTheEngineIsNotReverted()
{
var engine = new Engine();
engine.Global.FastSetProperty("cfg", new HostStateDescriptor(JsNumber.Create(1)));
engine.Global.DefineOwnPropertyUnchecked("cfg", new HostStateDescriptor(JsNumber.Create(1)));
var snapshot = engine.Advanced.CaptureGlobalSnapshot();

engine.Evaluate("cfg = 5;");
Expand All @@ -609,7 +609,7 @@ public void AHostDescriptorHoldingItsValueOutsideTheEngineIsNotReverted()
public void AHostDescriptorsAttributeFlagsAreStillReverted()
{
var engine = new Engine();
engine.Global.FastSetProperty("cfg", new HostStateDescriptor(JsNumber.Create(1)));
engine.Global.DefineOwnPropertyUnchecked("cfg", new HostStateDescriptor(JsNumber.Create(1)));
var snapshot = engine.Advanced.CaptureGlobalSnapshot();

engine.Evaluate("Object.defineProperty(globalThis, 'cfg', { enumerable: false, configurable: false });");
Expand Down Expand Up @@ -856,7 +856,7 @@ public void MutationsInsideAnObjectGraphBehindARestoredBindingSurvive()
{
var engine = new Engine();
var holder = new JsObject(engine);
holder.FastSetDataProperty("x", 1);
holder.DefineOwnDataPropertyUnchecked("x", 1);
engine.SetValue("holder", holder);

var snapshot = engine.Advanced.CaptureGlobalSnapshot();
Expand Down
7 changes: 1 addition & 6 deletions Jint.Tests.PublicInterface/HostDescriptorEnvelopeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ public void APlainObjectCarryingAHostOwnedDescriptorObservesEveryRefill()
var engine = new Engine();
var body = new PropertyDescriptor(JsValue.Undefined, PropertyFlag.ConfigurableEnumerableWritable);
var envelope = new JsObject(engine);
envelope.FastSetProperty("body", body);
envelope.DefineOwnPropertyUnchecked("body", body);
engine.SetValue("envelope", envelope);
engine.SetValue("refill", new Action<string>(value => body.Value = value));

Expand Down Expand Up @@ -288,11 +288,6 @@ public override List<JsValue> GetOwnPropertyKeys(Types types = Types.String | Ty
return keys;
}

public override IEnumerable<KeyValuePair<JsValue, PropertyDescriptor>> GetOwnProperties()
{
_handedOut.Add(_body);
yield return new KeyValuePair<JsValue, PropertyDescriptor>(new JsString(BodyName), _body);
}
}

/// <summary>
Expand Down
Loading