Commit Graph
128 Commits
Author SHA1 Message Date
BFlat 6121418e0c Merge pull request #6783 from dotCooCoo/fix/js-flat-charges-heap
Scripts: operators, JSON.stringify and join count a flattened string against the heap limit
2026-11-12 15:12:46 +07:00
BFlat 9a4c36dfed Merge pull request #6771 from dotCooCoo/fix/e4x-scope-name-assign
JavaScript: a bare E4X name inside with or a filter can be assigned
2026-11-12 14:17:08 +07:00
dotCooCoo 49c8a380a1 Scripts: flattening a rope keeps it rooted while its copy is reserved
flat_charged and the property-key path now root the rope itself, so a string held only by
native code (a getter's result for ++o.x, a valueOf result, a toString key) still counts
what it holds when the reservation collects. The per-call roots for these cases are removed.
2026-11-11 23:02:45 -08:00
dotCooCoo c2c560eeaa Scripts: a String object's character is read along its rope when the copy does not fit
When the flattened copy of a String object's concatenated string would pass the heap limit
before a collection can run, the indexed read walks the rope to the unit instead, taking a
unit of fuel for each node it visits, so no copy is made.
2026-11-11 22:57:35 -08:00
dotCooCoo 40283f4a38 Scripts: loose equality and the JSON space string keep a converted string rooted while it is flattened 2026-11-11 22:49:15 -08:00
dotCooCoo 1f991536d6 Merge main into fix/js-flat-charges-heap 2026-11-11 22:44:59 -08:00
dotCooCoo 46efc4a651 Scripts: a String object counts its rope's copy when a character is read
- wrapping a string no longer flattens it, so Object(s).length and new String(s) on a large concatenated string make no copy and cannot collect while a call's operands are off the stack
- reading a String object's character by index, `in` and hasOwnProperty count the flattened copy at the next limit check, and the run ends before the copy when the fuel for it is not left
- unary plus on an object whose valueOf returns a concatenated string, and a property key from toString, keep that string rooted while its copy is reserved
- the unshift comment cites step 6 of section 15.4.4.13
2026-11-11 22:39:04 -08:00
BFlat af22fd6d5b Merge pull request #6776 from dotCooCoo/fix/eval-source-takes-fuel
JavaScript: source that eval and Function compile takes fuel before it is parsed
2026-11-12 13:17:47 +07:00
dotCooCoo 56f9b6181e Merge main into fix/e4x-scope-name-assign 2026-11-11 22:03:32 -08:00
dotCooCoo 3dc597f259 Scripts: a call and new String keep their operands rooted while a rope is flattened
Boxing the string receiver of a non-strict function flattens it, and reserving the copy can
collect. The callee, receiver and arguments are already off the stack at that point, so they
stay in the temporary roots until the frame holds them; before, an argument object could be
freed and its cell reused by the wrapper. new String(value) roots the converted string the
same way, so what the rope holds is still counted while its copy is reserved.
2026-11-11 21:53:50 -08:00
BFlat 105536fff4 Merge pull request #6765 from dotCooCoo/fix/js-finally-sites-gc-test
Scripts: a finally block left early drops its saved throw site
2026-11-12 12:40:29 +07:00
BFlat df41593465 Merge pull request #6772 from dotCooCoo/fix/e4x-xml-declaration-check
Scripts: E4X checks the XML declaration and takes fuel for every run the parser scans
2026-11-12 12:40:21 +07:00
dotCooCoo 44eb3be713 Scripts: a flattened string stays counted while the next copy is made
- the relational operators root both strings while flattening them, so the first copy still counts when the second one starts a collection
- join roots its separator for the whole join and each element while it is written, and JSON.stringify roots a string while it writes it
- a String object flattens its string when it is made, with the copy counted, so reading one of its characters copies nothing
2026-11-11 21:21:45 -08:00
dotCooCoo 0996268f87 JavaScript: a host evaluator's whole source must fit in the heap before it is encoded
The fuel and heap checks for a host evaluator's source were split into a third
before encoding and the rest after, so a source whose whole UTF-16 text did not
fit in the heap could still be encoded and parsed. The whole encoded size is now
reserved before encoding. A source over the source limit is still refused as a
syntax error.
2026-11-11 20:54:00 -08:00
BFlat b945662f75 Merge pull request #6700 from dotCooCoo/fix/script-files-followups
Scripts: File and Folder exists checks the kind, encode keeps slashes, every call is counted
2026-11-12 11:42:26 +07:00
BFlat cd92345707 Merge pull request #6759 from dotCooCoo/fix/js-tosource-property-flags
Scripts: toSource refuses read-only, non-configurable and non-extensible data it cannot write back
2026-11-12 11:42:09 +07:00
dotCooCoo b172e3672d JavaScript: for each evaluates its target before reading the value
A for each loop read the property's value before evaluating its target,
so a target such as (o.p = 2, t).v, t[(o.p = 3, 'w')] or a bare
@[(o.p = 2, 'k')] stored the value from before the target ran. The
target's object and name are now evaluated first and the value is read
after them (ECMA-357 section 12.3).
2026-11-11 20:39:02 -08:00
dotCooCoo e2eb364f83 Scripts: operators, JSON.stringify and join count a flattened string against the heap limit
Flattening a concatenated string copies its units into a new buffer. The
string built-ins reserve that copy with flat_charged, but several other
paths flattened without it, so a script could build copies the heap limit
never saw:

- the relational operators (`<`, `>`, `<=`, `>=`) on two strings, `==`
  between a string and a number, unary `+` and every other ToNumber of a
  string, and reading a character by index (`s[0]`)
- a property name made from an object whose toString returns a long
  string, and a long string used as a property name, which now reserves
  its copy before it is made instead of after
- direct `eval` of a concatenated source
- `JSON.stringify` of a string value and its `space` argument
- `Array.prototype.join`'s separator and each string element it writes

Property names and eval source reserve the copy's heap bytes without
taking fuel.
2026-11-11 20:35:33 -08:00
dotCooCoo d151f3f5d0 JavaScript: source that eval and Function compile takes fuel before it is parsed
Direct and indirect eval, Function and host evaluators parsed and compiled
their source before taking any fuel or checking the heap, so a script with a
small fuel limit could make the interpreter parse megabytes of source in a
handful of instructions. Interp::prepare_source now takes one unit of fuel for
each UTF-16 code unit and checks that the source text fits in the heap before
the source is flattened, parsed or compiled. A host evaluator's string takes
the fuel for a third of its bytes before it is encoded, and the rest after.

Limits::fuel and the maxFuel row in docs/mcp.md say so.
2026-11-11 19:52:06 -08:00
dotCooCoo db97cc1a8f JavaScript: a bare E4X name inside with or a filter can be assigned
With Options::e4x set, an E4X name standing alone (@a, @ns::a, @[expr],
ns::a, *) is now a valid target for =, compound assignment, ++, --,
delete and for/for each loops, as well as a read. The target is the
nearest XML or XMLList value in scope whose [[HasProperty]] accepts the
name (ECMA-357 section 11.1). The name is evaluated once, and the
target resolves before the right-hand side runs.

When no XML value in scope holds the name, a read or write throws a
ReferenceError and creates no child or attribute. A read no longer
falls back to the first XML value in scope, so a filter such as
list.(@id == '1') throws when an item has no @id and no outer XML
value holds one, instead of treating that item as not matching.
2026-11-11 19:35:59 -08:00
dotCooCoo 781357921e Scripts: E4X checks the XML declaration and takes fuel for every run the parser scans
An XML declaration at the start of XML or XMLList text was skipped up to
the next "?>" without being read. It is now checked against XML 1.0's
grammar (https://www.w3.org/TR/xml/#NT-XMLDecl): a version of "1." and
digits, then an optional encoding name and an optional standalone "yes"
or "no", in that order, with optional white space before "?>". Any other
declaration gives "SyntaxError: invalid XML declaration", and a character
XML does not allow gives "invalid character in XML text". White space
before the declaration is still accepted.

The parser now takes fuel for the length of the declaration and the
white space before it, element and attribute names, white space in open
and closing tags, and the text it searched when an attribute value, CDATA
section, comment or processing instruction is not closed, or when "]]>"
or "<" stops a text run or attribute value. A long run in any of these
used the same fuel as a short one.
2026-11-11 19:35:39 -08:00
dotCooCoo dea30d6328 Scripts: a finally block left early drops its saved throw site
A finally block that break or continue leaves no longer keeps the saved
site of the exception it ran for. A Jump now removes the sites at or
above the stack's length, as a return already does, so a collection
after the stack grows back over that slot does not root and count the
old code.

New tests in crates/js/src/host_tests.rs: an error passed on by a
finally block that collects garbage under a 1 MiB heap is reported at
its throw; the saved site keeps direct eval code live through a
collection; a host abort inside a finally block leaves no saved site;
and break, continue and return out of a finally block drop the site.
2026-11-11 19:14:05 -08:00
BFlat 433a3009b7 Merge pull request #6701 from dotCooCoo/fix/xml-namespace-scope-fuel
JavaScript: XML namespace lookups take fuel for each ancestor they climb
2026-11-12 10:03:17 +07:00
dotCooCoo 19ed0889b1 Merge main into fix/script-files-followups
# Conflicts:
#	crates/engine/src/script/files.rs
2026-11-11 18:48:18 -08:00
dotCooCoo 98ad53e799 Merge main into fix/js-tosource-property-flags 2026-11-11 18:34:29 -08:00
dotCooCoo d28b72a358 Scripts: toSource refuses read-only, non-configurable and non-extensible data it cannot write back
A literal makes an extensible object whose properties are writable and
configurable, and an array whose length is writable. toSource wrote a
property defined with writable or configurable false, a sealed, frozen or
non-extensible object or array, and an array with a read-only length as an
ordinary literal, so evaluating the text gave an open value.

toSource now throws a TypeError for each of these, at any depth, as it
already does for getters, hidden properties and other prototypes.
Properties defined with every flag on are written as before.
2026-11-11 18:33:42 -08:00
BFlat 8719585cc6 Merge pull request #6663 from dotCooCoo/feat/script-target-engine
Scripts: #targetengine runs a script in a named engine that keeps its globals
2026-11-12 09:22:45 +07:00
BFlat e067daef3c Merge pull request #6662 from dotCooCoo/fix/script-finally-return-drops-throw-site
Scripts: a return from a finally block drops the discarded error's throw site
2026-11-12 09:22:36 +07:00
dotCooCoo a354503683 JavaScript: XML namespace lookups take fuel for each ancestor they climb
`inScopeNamespaces()`, `namespaceDeclarations()` and `namespace()` walk
from the node up to its root. The walk cloned each ancestor's whole node,
children and attributes included, to read its namespaces, and charged
fuel only for the namespaces it found, so a deep tree with no declared
namespaces cost no fuel. It now takes one unit of fuel per ancestor,
copies only the namespace list and parent link, and checks prefixes
already seen with a set. `namespaceDeclarations()` compares the node's
declarations with the ancestors' through a map instead of a scan.
2026-11-11 14:54:26 -08:00
dotCooCoo 5bd1bbf536 Scripts: File and Folder exists checks the kind, encode keeps slashes, every call is counted
- `File.encode` and `Folder.encode` leave `/` unescaped, as the File
  object reference lists it among the characters kept
  (https://extendscript.docsforadobe.dev/file-system-access/file-object/).
- `exists` is true for a `File` only when the path names something
  other than a folder, and for a `Folder` only when it names a folder.
- Fields of the records the app returns are read as own properties, so
  a property a script puts on `Object.prototype` does not turn a
  successful `open` into a failure.
- `File.encode`, `File.decode`, `File.isEncodingAvailable` and the
  `Folder` locations count as file calls and charge the host-call limit.
- On the web the locations are `null` and `File.tempName` names a file
  at the root, without asking the platform for a temporary folder.
2026-11-11 14:46:43 -08:00
dotCooCoo db6b55ab0a Scripts: a return looks only at the last saved finally site
The saved throw sites of running finally blocks are kept in rising slot
order, so leaving a frame or a block now removes entries from the end of
the list and stops at the first one below the slot. Before, every return
looked at every saved site, so a call made inside many nested finally
blocks took time in proportion to their count.
2026-11-11 14:03:21 -08:00
dotCooCoo 283d127e2a JavaScript: characters assigned in Unicode 17 are identifier characters
vectorcraft-js and Check Script took identifier categories from
unicode-general-category 1.1.0, which carries Unicode 16.0, so letters
and marks assigned in Unicode 17.0 (U+088F at the start of a name,
U+1ACF after its first character) were refused. Both lexers now read
general categories from unicode-properties 0.1.4 (Unicode 17.0), which
Cargo.lock already holds. The version is exact because 0.1.5 adds the
Unicode-3.0 license, which is not on the license allowlist. The
Other_ID_Start, Other_ID_Continue and Pattern_Syntax lists are the same
in PropList 17.0.

The exhaustive identifier test now expects the DerivedCoreProperties
17.0 totals (145,916 ID_Start, 149,240 ID_Continue), and new tests read
U+088F and U+1ACF written out and as escapes in both lexers.
2026-11-11 13:57:54 -08:00
dotCooCoo 8e90c833f6 Scripts: a kept engine's next run starts after the last uncaught exception is dropped
Resetting Folder.current for the next run of a named engine now drops the
last run's uncaught exception and $.error first, so memory only they held
no longer makes the reset fail with the heap limit on every later run.

A File an earlier run left open now counts as closed in a later run:
setting its encoding stores it for the next open, and reading the encoding
gives the stored one.

docs/mcp.md: engines stay on the thread that made them, and a
#targetengine line in a file $.evalFile runs makes that call throw a
SyntaxError when it runs.
2026-11-11 13:50:12 -08:00
dotCooCoo f04aa61fc1 Scripts: a return from a finally block drops the discarded error's throw site
A `return` inside a finally block discards the pending exception, but the
saved throw site for it stayed in the list of collection roots. The code it
pointed to then stayed live after the function was gone, which could stop a
script with the heap limit when that code came from `Function` or `eval`.
Returning from a frame now drops the saved sites for the slots it leaves.

Follows #6653.
2026-11-11 12:19:32 -08:00
dotCooCoo 3a7c46975c Merge main into feat/script-target-engine 2026-11-11 12:16:55 -08:00
BFlat 6e873b806c Merge pull request #6657 from dotCooCoo/fix/js-random-seed-test
Scripts: test that the OS random value seeds Math.random
2026-11-12 03:16:14 +07:00
dotCooCoo 7fbac74885 Merge main into feat/script-target-engine 2026-11-11 12:09:30 -08:00
BFlat dd6b00c228 Merge pull request #6653 from dotCooCoo/fix/script-throw-site-through-finally
Scripts: an uncaught error that passes through a finally block is reported where it was thrown
2026-11-12 02:53:22 +07:00
dotCooCoo 27c24a4f53 Scripts: test that the OS random value seeds Math.random
random_seed passes the OS random source's result to random_seed_from, so a test can
give it a known value and check the seed it makes, instead of only reading the source text.
2026-11-11 11:25:25 -08:00
BFlat 1c049233af Merge pull request #6645 from dotCooCoo/fix/js-random-seed-web
Scripts: Math.random gets a new seed on every web page load
2026-11-12 02:24:16 +07:00
dotCooCoo f1c17603bb Scripts: #targetengine runs a script in a named engine that keeps its globals
A script whose first lines name an engine with `#targetengine name` (or
`//@targetengine name`) runs in that engine, and a later script that names
it sees the globals and functions earlier runs left, until the engine is
dropped or the session ends. A script without the line, and
`#targetengine main`, still get a new engine for each run.

- A session keeps at most 8 named engines, each with a 64 MiB heap. A run
  that goes past the heap ends with the heap limit, and the engine stays
  usable. Names are 1 to 64 bytes. The line comes once, in the script
  itself, before its code and its #include lines.
- An uncaught error, a limit or script.stop puts the documents back and
  keeps the globals the run set. An internal error drops the engine. A run
  stopped at a dialog or left paused ends where it was before the engine
  is kept. An error in a function an earlier run defined is reported
  without a line and column.
- `vc`, `$`, `alert`, `confirm` and `prompt` are made once for each engine
  and act for the run that calls them, including copies an earlier run kept
  in a global. `vc.args` gives a read-only copy of the calling run's
  `args`, and what a run sets on `$` lasts for that run only.
- `script.engines` lists the named engines with their heap, and
  `script.engine.drop {name}` drops one. Scripts cannot run either.
- The engines stay on the thread that made them. A session used on
  another thread starts there with no named engines, and the old ones are
  dropped on their own thread.
- The web build has no named engines: a run that names one other than
  `main` ends with an error before any of it runs.
- `Realm::end` ends a run where it is, `Realm::error_source` counts from
  the realm's last script, and `HostItem::FrozenGetter` gives a frozen
  value each time it is read.
- The script checker lists `#targetengine` as a supported directive.

Docs: docs/mcp.md, docs/target-app-parity.md, docs/gaps.md.
2026-11-11 11:17:42 -08:00
dotCooCoo 1f835d94c4 Merge main into fix/script-throw-site-through-finally 2026-11-11 10:44:55 -08:00
dotCooCoo 8d094fc811 Scripts: an uncaught error that passes through a finally block is reported where it was thrown
An error a finally block passed on unchanged was reported at the end of
that block, in script.run's report and in the Script Console, because the
block's end raised the error again at a new site. The interpreter now keeps
the site of the exception each finally block runs for, keyed by the stack
slot that holds it, and the block's end throws the exception on from that
site. This holds through nested finally blocks, called functions, resumed
runs and marked host errors. An error the finally block throws is reported
at that throw, and one its return, break or continue discards is not
reported.
2026-11-11 10:44:16 -08:00
BFlat 9840b65a85 Merge pull request #6626 from dotCooCoo/fix/js-xml-copy-and-insert-fuel
JavaScript: new XML copies a one-item list's node, and insertChildAfter and insertChildBefore take fuel per child scanned
2026-11-12 01:35:54 +07:00
BFlat 1e76791ae1 Merge pull request #6615 from dotCooCoo/feat/script-file-folder
Scripts: File and Folder read files in the script's own folder
2026-11-12 01:35:45 +07:00
dotCooCoo 6fbaad6c5e Scripts: Math.random gets a new seed on every web page load
Each realm seeded Math.random from std's RandomState. On
wasm32-unknown-unknown its keys come from stack and heap addresses,
which are the same on every page load, so scripts in the web build
drew the same sequence each time the page opened.

The seed now comes from getrandom (crypto.getRandomValues in the
browser, as crates/pdf/src/encrypt.rs already does), with RandomState
kept as the fallback when it fails.
2026-11-11 10:31:32 -08:00
dotCooCoo 28042c2e92 JavaScript: test that new XML copies a list item's attributes and children
The test for new XML with a one-item list now starts from an item that
has a parent, an attribute and a child. It checks that the copy has no
parent and that changing the copy's attribute and child leaves the
original unchanged, so a copy that drops or shares the children fails.
2026-11-11 09:47:33 -08:00
dotCooCoo 53bd6ecd9a JavaScript: new XML copies a one-item list's node, and insertChildAfter and insertChildBefore take fuel per child scanned
new XML(list) now returns a deep copy of the list's single item, as
ECMA-357 section 13.4.2 specifies for an XMLList argument; before, it
returned the item itself, so changes to the new value changed the list.

insertChildAfter and insertChildBefore take one unit of fuel for each
child they compare with the reference, as childIndex does.

The test for the missing xpath method now checks the XML and XMLList
prototypes; reading .xpath on a value selects child elements and
returned an empty list in every case.
2026-11-11 09:21:00 -08:00
dotCooCoo de95640d93 JavaScript: XML namespace declarations and QName strings count against the heap limit
addNamespace and setNamespace now add a declaration's bytes to the heap total when the element
gains a new prefix, and QName.prototype.toString adds the bytes of the string it returns. Before,
both only checked for room, so a loop of distinct prefixes could grow an element past the heap
limit until a collection recounted it.
2026-11-11 08:09:22 -08:00
dotCooCoo bc6c20d0c9 JavaScript: a limit during an XML path assignment or list copy keeps the tree whole
- A list copy holds each new copy as a root while the list is charged
  for it, so a collection at that point no longer frees the copy. Before,
  `x.setChildren(<c/> + <d/>)` near the heap limit could leave one child
  twice or end with an internal error.
- An assignment to a path such as `x.a.b = v` or `x.a.b[0] = v` removes
  the elements it added for the path when a later step stops at a limit
  or throws.
- Comparing XML values counts the attribute tables and the work stack
  against the heap limit.
2026-11-11 08:00:42 -08:00