A `for` loop's initializer and a `for...of` loop's iterable run once, but
SC6101 counted any dynamic operation inside a loop statement's span, so
untyped setup there produced the warning and failed --fail-on=dynamic.
Loop work is now found by walking the IR: a loop's condition, update and
body repeat, its initializer and iterable do not. Locations could not
tell them apart, because lowering gives the index loop of a `for...of`
the whole statement's span.
The arguments after a URL path that are evaluated ahead of the
conversion were keyed by the expression without its parentheses. When
the fs lowering read through a satisfies wrapper, it lowered the inner
expression again, so fs.chmodSync(url, mode() satisfies number) called
mode() twice. The saved arguments are now keyed and looked up without
their type-only wrappers (satisfies, as, type assertions, !), so each
runs once.
A constructor type whose instances are not one program class's, such as
`new () => unknown`, a type that also names static members (`{ readonly
schema: unknown; new (): unknown }`), an abstract constructor type over a
record, or construct overloads, did not map and refused with SC2001. Such
a type declared in the program now lowers as a checked-dynamic value, as
an `unknown` slot already holds a class value: the value keeps its class
identity, `new` through it constructs that class, static reads see that
class's statics, and the constructed value checks into the signature's
instance type. Library and package constructor types keep their fences.
Refs #1372
Co-authored-by: blurname <50226652+blurname@users.noreply.github.com>
A build now names the functions of the program's own files that compile
to the dynamic tier and likely run often, so a slow program with a little
untyped code says where the time goes:
warning SC6101: 3 functions that likely run often use untyped values, which runs slower than typed code:
advance (nbody.js:22): untyped JavaScript file, in a loop
...
- The functions come from the tier assignment lowering already makes
(Lowerer.dynamicFunctions). Without a profile, a function counts as
likely hot when a dynamic operation runs inside one of its loops, when
a loop calls it, or when it calls itself (lowering/dynamic-tier.ts).
- A function is named only when its source says why its values are
untyped: an `any` (for a for...of variable, the value it iterates), an
untyped JSON.parse result, or an untyped JavaScript file. Generated
%dyn.* helpers, module top levels and functions that are dynamic for an
operation without a static lowering are left out.
- At most five functions are named, most likely hot first, with the
total; dynamic-tier code in npm packages is counted per package on one
line. All of the warning's text is in diagnostics/dynamic-tier.ts.
- SC6101 opens the SC61xx band, performance notes, with a new
"performance" category in the diagnostics envelope; SC60xx stay the
divergences.
- scriptc build prints it after the divergence warnings, and the
executable cache stores it with the entry, so a cached build (including
the CLI's routed fast path) repeats it. scriptc coverage reports it, and
a comment-only edit no longer replays a verdict that carries it, since
its text holds line numbers.
- --no-dynamic-tier-warning leaves it out of build and coverage;
coverage --fail-on=dynamic fails on it.
- Docs: coverage.mdx (Dynamic tier, Gating builds and CI, the envelope
categories) and cli.mdx.
Chains of global one-character replacements (the HTML-escape idiom) run
as one scr_str_replace_chars call. It searched for the next match eight
bytes at a time and started the search over after each replacement.
It now reads 16-byte blocks with SSE2 or NEON (8-byte words elsewhere on
little-endian hosts) and replaces every match of a block before reading
the next one. The table of characters and replacements is kept for the
next call with the same immortal operands, which are the literals the
compiler passes, instead of being rebuilt from the replacement text on
every call.
mentionsTypeParameter walked union constituents and type arguments
without remembering the types on its path. A parameter typed with an
alias that reaches itself through its own type arguments, such as
`type Tree = string | Tree[]` in `function f<T>(x: T, ...trees:
Tree[])`, recursed until the compiler ran out of stack. The walk now
skips a type it has already entered; a type parameter reached through
any other path is still found.
When the Math functions evaluate their arguments into temporaries before
converting (an earlier argument whose ToNumber can run valueOf), a spread
argument's array is now copied where it is evaluated. Before, the
temporary held the array itself and its elements were read after the
conversions, so a valueOf that changed the array changed the values
passed, while JavaScript reads a spread's elements during argument
evaluation.
Number() of a string with an optional sign and 1 to 15 digits returns
the integer directly; every other string keeps the full StringToNumber
grammar, out of line.
split() with a separator collects its first batch of pieces before
creating the result, so the result is an inline array of exactly that
size, filled in place. A split's default limit skips the uint32
conversion, and pieces of a proven-ASCII string are created ASCII,
including the scratch pieces of split loops.
indexOf() from the start of a proven-ASCII string searches bytes
directly, and slice() of one skips the UTF-16 index map. The one-byte
case of the byte search reaches memchr without a stack frame.
5568-fs-cp-errors built its fixture in a /tmp directory named after the
program file, which it deletes and recreates. The Node oracle and the
native runs of one program run at the same time, so two runs with the
same name deleted each other's files and the program exited 1 on
random shards. Its root, and that of the five other corpus programs
that name a /tmp/scr-* directory after the program file alone, now
ends in the process id, as 5571-fs-cp-deep's does.
- Port messages that a 'message' listener posts while more worker
messages are queued are delivered after those messages and before
setImmediate callbacks, as in Node. The ports' poll phase waits while a
batch's next message is queued, and the loop pass continues once more
after the batch when a port has something to deliver.
- A mailbox counts its queued events per kind as they are posted and
taken, so starting a batch reads the count instead of scanning the
queue under the lock. Messages that alternate with a worker's captured
stdout or stderr start a batch each, which made delivery quadratic:
20000 such messages took 5.6 s to drain, and now take 0.5 s (Node 0.57 s).
- The continue hook passes on that the batch's next message is at the
head, so the dispatch that follows does not lock the mailbox again.
- Corpus 6212 checks the order of worker messages, a MessageChannel
message posted by the first listener, and setImmediate against Node.
6211's costs are now taken from its CI timings.
readFileStream resolved on 'end' while its 'close' listener printed
"released". 'close' follows 'end' once the descriptor is closed, so the
line landed wherever that completion fell among the child processes
the fixture starts next, and the comparison with Node depended on
timing. The read now resolves from its 'close' listener, after the line
is printed.
A synchronous fs call with a URL path evaluated only the later arguments
with effects ahead of the path conversion. When such an argument
reassigned the variable an earlier argument reads, as in
fs.chownSync(url, uid, next()), the earlier argument was read after the
reassignment. Every argument from the URL through the last one with
effects is now evaluated in source order before the conversion.
The npm fixture node24-fs-descriptors now reaches its SC2020 refusal for
writeSync with a hex encoding, since openSync accepts its file URL.
A generic class instance now binds a type parameter to its checker type
only when a conditional type in the class depends on it, and that checker
type keys the instance. Before, every type argument was bound while only
literal arguments of those parameters were in the key, so two instances
such as Holder<R, "a"> and Holder<R, "b"> with a `T[K]` field shared the
first instance's layout. The other parameters get no checker binding, so
member types that depend on their literal identity stay unresolved, as
before.
Detection now follows the type aliases and interfaces a member's type
refers to, so a conditional wrapped in a union alias, such as
`(K extends "required" ? T : T | undefined) | null`, also separates the
instances.
Readable.push and Writable.write with a union chunk (Buffer, string and
null arms) read the chunk's tag and payload from a tagged union box. A
union with one reference arm and null, such as `string | null` or
`Buffer | null`, is the arm pointer itself, so `push(line)` with
`line: string | null` and `push(done ? null : text)` read past the
string and crashed. Both now read the tag and payload with unionTag and
unionPeek, as the other union consumers do. unshift and end refuse
union chunks and are unchanged.
tls.createServer validated minVersion and maxVersion with its own loop,
which threw ERR_TLS_INVALID_PROTOCOL_VERSION for null. It now uses the
same check as tls.connect, where null and undefined select the default
bound, as in Node.
The Math functions and String.fromCharCode/fromCodePoint evaluate all of
their arguments before they convert any, as in Node. When an argument's
conversion can run program code (valueOf or toString on a dynamic value)
and a later argument follows, the arguments now evaluate into temporaries
first and convert in order after (lowerToNumberArguments). Before, a
dynamic argument converted before the next one was evaluated.
A conditional argument whose branches are not both numbers boxes its
value and converts it like any dynamic value, so `isFinite(flag ? "12" :
"abc")` in a JavaScript file returns a boolean instead of reaching a
run-time fence.
A statement inside an expression can end its block (an argument call that
always throws). The expression's later statements and result now emit in
a block with no predecessors, so the instructions there read only values
defined in it.
An event that typed code only listened to with a listener taking no
parameters had no bridge entry: the scan marked a listener as typed only
when one of its parameters was not a checked-dynamic value. Emitting the
event through an `any` reference then threw instead of calling the
listener. A TypeScript listener with no parameters now marks the event as
typed. A JavaScript function without parameters still registers as before,
since it can read `arguments`.