Skip to content

Release 1.0.0 - #129

Open
FlorianRappl wants to merge 111 commits into
mainfrom
devel
Open

Release 1.0.0#129
FlorianRappl wants to merge 111 commits into
mainfrom
devel

Conversation

@FlorianRappl

Copy link
Copy Markdown
Contributor

Types of Changes

Prerequisites

Please make sure you can check the following two boxes:

  • I have read the CONTRIBUTING document
  • My code follows the code style of this project

Contribution Type

What types of changes does your code introduce? Put an x in all the boxes that apply:

  • Bug fix (non-breaking change which fixes an issue, please reference the issue id)
  • New feature (non-breaking change which adds functionality, make sure to open an associated issue first)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • My change requires a change to the documentation
  • I have updated the documentation accordingly
  • I have added tests to cover my changes
  • All new and existing tests passed

Description

Note that I mentioned "Breaking change" even though I don't think anything will break when updating to this release. In general people will be amazed how much is now working... at least that's my hope.

Shoutout to everyone who made this possible. Years ago I thought this project is a dead-end and finished - now I think this might be the foundation for something really great. Without people like @lahma, @arekdygas, and @tomvanenckevort the project could not have been brought to this level. Much appreciated everyone!

(PR is there to be merged once final tests and cosmetics are in place - if you see anything or want to improve it just open a PR to devel)

FlorianRappl and others added 30 commits November 25, 2021 22:45
Added support for JS modules and importmaps
* small cleanup to API usage
* add missing license expression
* treat warnings as errors to catch obsolete members
* enable deterministic build
@FlorianRappl FlorianRappl added this to the v1.0 milestone Jul 27, 2026
FlorianRappl and others added 12 commits July 27, 2026 16:05
The release brings the host-object hooks the DOM proxies use in the next
commit, and one change that applies without any code here: an object that
overrides GetOwnProperty but not Get is now recognised as having ordinary
read semantics, so a dotted DOM read probes the node once instead of twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A node is asked three different things about a property name, and until now
all three arrived at GetOwnProperty and got a PropertyDescriptor built for
them - including the two that only ever throw it away.

Jint 4.15 splits them up, and the node answers each directly:

* TryGetOwnPropertyValue hands the value over, so an indexed read no longer
  allocates a descriptor for the engine to unwrap and drop. Returning false
  is an authoritative "no own property of that name", which is exactly what
  the discarded descriptor used to prove.
* ProbeOwnProperty answers existence and enumerability, which is what "in",
  hasOwnProperty, Object.assign, spread and JSON.stringify actually want. An
  indexed entry no longer has to be converted into a JsValue to be counted.

The three share one indexer lookup so that they cannot disagree - the answer
one gives has to be the answer the others would give at the same instant, and
a Debug build of Jint checks that on every read.

The lookup itself now takes the property key rather than its string form, and
two things fall out of that:

* a key is no longer turned into a String before the type is known to have an
  indexer at all, and most DOM types have none. A symbol used to compose
  "Symbol(...)" on every probe, and library code probes Symbol.toStringTag
  constantly - 83.7 to 2.9 bytes per read;
* the string indexer's HasProperty check is handed the key it was given
  instead of building a JsString for it. That is on the path of every read of
  an ordinary member of an indexed collection - the length a loop tests - and
  it is 64 bytes a read, which 4.15 exposes because a host prototype is no
  longer eligible for the engine's prototype cache.

Behaviour is unchanged. The eight tests added to DomTests pass on devel with
4.14.0 as well; they pin the agreement between the three answers, which is the
thing this commit could get wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A node list, an HTML collection, a token list and a named node map are all the
same thing to script - a live, read-only, indexed view - and until now every read
of one went through the general node proxy: parse the key, reflect the numeric
indexer off the prototype, invoke it, wrap the result in a descriptor. An index
past the end was signalled by a CLR exception, so a bounds test cost about a
kilobyte.

Jint 4.15 has a base class for exactly this shape. A collection supplies the
members WebIDL says such a thing has - Length, TryGetIndex, and a containment
test - and the engine derives the whole property model from them. Nothing is
cached, so the view stays live.

Containment is the length comparison and nothing else, so every question that
only wants a yes or a no - "in", hasOwnProperty, the key enumerations, the hole
test the Array.prototype generics run per element - stops projecting an element,
wrapping it in a JS value and looking it up in the identity cache just to drop it
again.

Length and the indexed getter are the one place in this binding where reflection
was too slow to leave alone: with the descriptor gone, a MethodInfo.Invoke and its
object[] would have been all that was left of an indexed read. They are compiled
once per type instead. What carries the DOM attributes and what can be called are
not always the same member - IHtmlCollection<T> re-implements
"T IReadOnlyList<T>.this[Int32]" explicitly, and that one is private and abstract
- so the attributed declaration identifies the collection while the public method
of the same name on the concrete class is what runs.

Deciding which proxy to build is a property of the type - an indexed getter plus
a length, the same pair SetIndexer looks for - so it is resolved once per type
for the whole process and never repeats.

There was one proxy class for every DOM object and there are now two, which
cannot share a base: they agree on IDomProxy instead, which is what every caller
outside the proxies actually wanted. Named entries - document.forms.login,
el.attributes.title - are not part of the array-like model and stay on the string
indexer, with indices and length delegated to the base as that base requires. A
named entry whose indexer has no setter is described by a plain value now rather
than by an accessor pair: with nothing to forward a write to, the pair only cost
two function objects per read.

Measured, allocation per operation, net8.0, loop baseline subtracted:

| operation                        | devel + 4.14.0 | this branch |
| -------------------------------- | -------------- | ----------- |
| list[0]                          |        326.6 B |     132.1 B |
| list[999] out of range           |       1086.1 B |       0.5 B |
| attrs['id']                      |       1021.6 B |     278.4 B |
| list.length                      |         51.9 B |       0.0 B |
| 0 in list                        |        326.6 B |       0.0 B |
| list.hasOwnProperty(0)           |        472.0 B |       0.5 B |
| Array.prototype.forEach.call(..) |       1724.5 B |     568.2 B |

What script sees changes in four ways, all toward what a browser does: for...of,
spread, Array.from and destructuring work on a collection; Object.keys and
for...in report the indices; delete c[0] and defineProperty on an index are
refused; and Array.isArray stays false with the prototype chain untouched, so
instanceof and Symbol.toStringTag are exactly as before.

One deviation is worth stating plainly: "length" becomes an own property of the
collection rather than an inherited one, so hasOwnProperty('length') answers true
where a browser answers false. It is the model the base class defines, it is what
an Array reports too, and reads, "in", enumerability and the value are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Read the DOM through the host object surface Jint 4.15 adds
… the package needs

Two follow-ups to #130.

The collection proxy's GetOwnProperty asked the string indexer about a name the
array-like base had already answered: an index past the end is an authoritative
miss - the value and existence hooks commit to it, the engine trusts them and
does not ask again - so an element whose id happens to be numeric surfaced as a
descriptor for a key that reads as undefined, answers false to "in" and
hasOwnProperty, and never enumerates. WebIDL resolves the same collision the
same way: an object supporting indexed properties never serves an array-index
name from its named getter. The named lookup now steps aside for canonical
array-index keys, classified exactly as the engine classifies them.

The package manifest still declared Jint [4.0.0,5.0.0), but the assembly now
derives from a base class and overrides hooks that exist only in 4.15.0, and
NuGet resolves the lowest version a range admits - a consumer taking the
package defaults got an engine the types in it cannot load against. The floor
is now the version the code is actually written to.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uV6H9cTntzsoKiaJRBn4f
Keep array-index keys out of the named getter, and declare the Jint the package needs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants