Commit Graph
6750 Commits
Author SHA1 Message Date
Chris Tate 383b69cf2d Represent record-only unions used as Map keys and Set elements as record pointers 2026-10-24 18:39:02 -05:00
Chris Tate e2487106e5 Compare optional string index reads as code units (#1923) 2026-10-24 18:37:12 -05:00
Chris Tate 924fc1055c Find checker reply element ends in C and list dynamic imports from module references (#1885) 2026-10-24 18:34:58 -05:00
Chris Tate ecd103d1fb Keep the while-loop bound off counters written inside expressions
flow follows a while body's statements and applies each increment it
meets as a statement. An increment inside an expression, such as an if
condition's comma expression `if ((i++, i > 0))`, lowers to a statement
nested in the expression; whileBound accepted it as an increment, but
flow never applied it, so a read after it kept the pre-increment bound
and lost its bounds check. whileBound now gives no bound when any
expression in the body holds a write of the counter.
2026-10-24 18:32:42 -05:00
Chris Tate 9e55835014 Compile more npm and CommonJS JavaScript dependencies statically (#2122) 2026-10-24 18:30:56 -05:00
Chris Tate 8b4f83ac02 Collect the native compiler profile on the release commit (#2138) 2026-10-24 18:25:03 -05:00
Chris Tate 61c32fbc48 Keep Map.get results owned when a copy or a temporary map can outlive the entry
A borrowed Map.get local holds the entry's own reference, so every use
must run before anything that can release the entry. Two cases escaped
that proof:

- A local declared from the borrowed local (`const copy = held`) is a
  stable alias of it, with no reference of its own. Its reads after a
  write to the map were not counted as uses. Reference locals declared
  from the local, at any depth of the statement that declares them, now
  count as its uses.
- A read from a map that no local holds (`fresh().get(k)`) borrowed an
  entry of a map released at the end of the declaration. The map must
  now be a local or a field chain rooted at one, whose changes the proof
  sees as edge changes.

Both keep the owned read. Corpus 8181 covers the two cases, and the
emission test checks that their reads retain the value.
2026-10-24 18:24:13 -05:00
Chris Tate c8ee28de80 Write slices stored in records that are only read into stack buffers
A slice whose every reader only borrows it is written into a stack buffer
instead of a heap allocation. A slice stored in a record field counted as
kept, so a tokenizer's `const text = source.slice(start, i); const t =
token(kind, text, start); sum += weight(t);` allocated and freed a string
per token even though `t` is read only for `t.text.length`.

A slice local stored directly in a field of a stable record local, built
by a record literal or by a factory that only returns a record literal of
its parameters, is now lent when the record is only projected: its field
reads are lending reads, and it is passed only to record parameters whose
bodies read that field only in lending positions (a fixpoint, like the one
for lent string parameters). A record that is returned, stored, captured or
passed anywhere else keeps the slice on the heap. includes, indexOf and
lastIndexOf on string arrays now lend their search argument.
2026-10-24 18:23:17 -05:00
Chris Tate 3b7b9afae5 Reap the checker process within a millisecond of closing it (#2154) 2026-10-24 18:22:33 -05:00
Chris Tate 6997d8b211 Allocate stream ticks from the small-block allocator, and emit stream events by immortal names (#2152) 2026-10-24 18:22:03 -05:00
Chris Tate 6c18c58708 Merge main 2026-10-24 18:21:25 -05:00
Chris Tate ba81871ff0 Run row sums two rows per pass (#2148) 2026-10-24 18:09:22 -05:00
Chris Tate 6b3a47c386 Merge main 2026-10-24 18:07:52 -05:00
Chris Tate 5dc3a051b7 Prove typed array reads in while loops bounded by the length in bounds
findBytesBounds proved element reads only inside for loops, and only for
`counter + c` indexes. A while loop such as

    while (i < len) {
      c = line[right - i]!;   // right = len - 1
      i++;
      if (c === 10) {
        if (i === len) break;
        c = line[right - i]!;
        i++;
      }
      ...
    }

kept a bounds check on every read. The out-of-bounds path also produced a
NaN that LLVM threaded into a following switch, which then stayed an
indirect jump instead of becoming a lookup table.

- A while condition that compares an integer counter with a typed array's
  length (`i < len`, `i <= right`, `len > i`) starts a bound. The body may
  write the counter only by statements that add a positive literal, and
  may not write the receiver. Calls in the body are allowed: they cannot
  write either binding (both are uncaptured locals), and a typed array's
  length is fixed at construction.
- The body is followed in order. An increment raises the bound and the
  counter's minimum by its step. An if condition that compares the counter
  with the length narrows the bound in its branches (`if (i === len) break`
  restores `i < len` after an increment). Branches join to the weaker bound,
  and a statement that advances the counter inside a nested loop or
  labeled block leaves the counter unbounded after it.
- Indexes of the form `L + d - counter` (`len - 1 - i`, `right - i`) are
  proven for both for and while bounds, from the counter's upper bound and
  its minimum.
2026-10-24 18:06:45 -05:00
Chris Tate 29e5bacb9c Pump untyped iterable pipeline sources (#2111) 2026-10-24 18:03:36 -05:00
Chris Tate 8da01d3ab3 State that SCRIPTC_CHECKER_SERVER needs a tsc-ts that serves --listen
The tsc-ts 0.0.9 that scriptc includes rejects --listen, so with it every
build that sets SCRIPTC_CHECKER_SERVER=1 checks with its own tsc-ts
process. The variable's description now says so.
2026-10-24 18:02:25 -05:00
Chris Tate fcc81c034c Read the final result's value only for sync pipeline sources
for await reads the value of a done result only through the
async-from-sync wrapper; an async iterator's done result is not read.
The source's generator read it for both, so an async iterator whose
done result has a throwing value getter rejected the pipeline. It now
reads and awaits the final value only when the iterator is sync.
2026-10-24 17:59:56 -05:00
Chris Tate 9c670e8d93 Count the arguments a function receives when it is called through a value (#1880) 2026-10-24 17:58:41 -05:00
Chris Tate 6856dcf285 Lower each closure argument of an implicit-any call once (#2150) 2026-10-24 17:56:00 -05:00
Chris Tate 05bc5299b0 Reap the checker process within a millisecond of closing it
Closing a checker process checked for its exit once and then slept 10 ms
before checking again, although the process exits a fraction of a
millisecond after its input closes. The checks now start 0.1 ms apart and
double, so the process is reaped as soon as it exits. The total wait before
the kill stays at about 50 ms (51.1 ms).
2026-10-24 17:54:15 -05:00
Chris Tate c0a4496900 Check with a tsc-ts server shared by later builds (opt-in)
With SCRIPTC_CHECKER_SERVER=1 on Linux and macOS, the native compiler's
checker connections go to a shared `tsc-ts --api --listen` server, which
keeps the files it parsed for the next build. When the variable is unset,
nothing changes.

checkerServerName names the server by a digest of the tsc-ts executable's
identity, the working directory, the file system callbacks, the timing
flag and the TSC_TS_* environment. scriptc_ts7_attach takes the server's
lock file with a non-blocking flock, connects to its socket in this
user's private server directory ($XDG_RUNTIME_DIR/scriptc or
/tmp/scriptc-<uid>), and starts the server when none answers: in its own
process group, with its standard streams on /dev/null and no other
descriptor of the build, with --idle-exit 300 and TSC_TS_LISTEN_MEMORY=2048
unless the environment sets it. A build that finds the server in use,
cannot start or reach it, or gets no answer to a ping checks with its own
tsc-ts process, as without the variable; so does a tsc-ts without
--listen, which exits as it starts. The syntax-only source parser, which
reads no libraries, always uses its own process and leaves the server to
the program's checker.
2026-10-24 17:50:45 -05:00
Chris Tate 9d08b301fc Merge main 2026-10-24 17:50:19 -05:00
Chris Tate 65555dace3 Drop struct type names before the thin link instead of renaming promoted locals
The partitions of a program build have no locals for ThinLTO to promote:
symbols that other partitions refer to are already hidden. What changed
the modules after importing, and with them the object cache keys, of
partitions an edit did not touch was the numbering of struct types. Every
partition declares the program's types under the same names, and the thin
link loads the importing partition and the partitions it imports from into
one context, which tells same-named types apart by appending a counter
that runs across the whole link. An edit that adds a type anywhere shifted
the counter.

Partitions now drop the names of their struct types before their summary
is written; importing matches unnamed types by their bodies. The
promoted-locals option, the per-partition source file name and the
partition index in the summary key are removed.
2026-10-24 17:47:58 -05:00
Chris Tate e25d871a34 Allocate stream ticks from the small-block allocator, and emit stream events by immortal names
Each deferred stream step (afterWrite, emitReadable, resume and the
others) queued a ScrStreamTick and a process.nextTick entry, both from
libc calloc and free. They now come from the runtime's small-block
allocator, as do the nextTick entries scheduled through
scr_next_tick and scr_process_write_callback.

The stream unit emitted each event with a newly allocated name string.
The names it emits ("data", "end", "drain", "finish" and the others)
are now immortal static strings, so an emit allocates no name.
2026-10-24 17:45:59 -05:00
Chris Tate 587b8f40c0 Type untyped JavaScript Maps, Sets and arrays from their uses (#2089) 2026-10-24 17:45:47 -05:00
Chris Tate da738a78ba Construct readline.Interface and builtin classes bound through require() members in JavaScript (#2095) 2026-10-24 17:43:27 -05:00
Chris Tate a5c7a4e983 Report how well the native compiler's profile matches its functions (#2117) 2026-10-24 17:39:30 -05:00
Chris Tate 01f5a3455a Test row-pair overlaps only for nests of two or more rows
The paired copy's test read the stored and read arrays' headers whatever
the row range, so a nest whose arrays the program assigns only for a
nonempty range dereferenced their NULL headers on an empty range, where
the loop as written never reads them. The overlap tests now run only
when the nest has at least two rows, and an array that is not there
answers that it is not disjoint, so the loop runs as written.
2026-10-24 17:38:44 -05:00
Chris Tate fe6dd9a5c5 Skip a byte order mark at the start of package.json
The run-time resolver parsed package.json as it read it, so a file that
starts with a UTF-8 byte order mark failed to parse and computed require
and require.resolve ended with the invalid-package-config refusal. Node's
reader skips the mark; the resolver now does too.

The corpus program writes the package with the marked package.json to a
fresh directory at run time and resolves it by its absolute path, so no
committed file contains the mark.
2026-10-24 17:38:25 -05:00
Chris Tate a711d74ecf Keep copied array callbacks exact with local classes, switches and contracts
- A binding that a local class captures counts as written: the class's
  methods may assign it while the loop runs, so the call keeps its helper.
- Copied bodies rename the labels of switch and for-of statements and the
  binding of for-of loops.
- The known-union and record-temporary cleanups rewrite switch cases and
  for-of bodies, where they left declarations of removed locals.
- Copied locals keep their source bindings. In programs that write
  contract types, a parameter with a source binding keeps its own local,
  so an Int32 parameter is checked against the elements the loop passes.
2026-10-24 17:35:51 -05:00
Chris Tate 43ff8a1122 Inline destructured literal results and read ASCII slice ranges inline
- A function whose whole body is `return [a, b]` or `return { a, b }`,
  with number, boolean or string fields, is now inlined where its result
  is only destructured (`const [head, tail] = split(text, at)`). The
  parameters are declared from the arguments, then each field straight
  into its destructured binding, in the literal's order; an unread field
  still evaluates into a binding of its own. No record is built, and a
  slice field read only through its length or char codes becomes a
  scalar slice that copies nothing. Arguments may own their value, but
  as for numeric records nothing inside an argument may hold a
  temporary of its own.
- A scalar slice of a proven-ASCII string computes its range inline:
  the bounds clamp over the byte length exactly as scr_str_slice_range
  does, and no boundary can split a surrogate pair. Other strings keep
  the runtime call, which gains the same ASCII fast path.
- value-records' tuples section, `[head, tail] = split(...)` read only
  through lengths, goes from two heap slices per step to none.
2026-10-24 17:35:25 -05:00
Chris Tate 6e4bfb3c91 Await the final value of an untyped sync pipeline source
for await over a sync iterator awaits the value of the result that ends
it too, so a sync iterator that returns a rejected promise rejects the
pump. The source's generator skipped that value and reported success.
It now awaits a final value that is an object (a promise included)
before it stops, without closing the finished iterator.
2026-10-24 17:34:40 -05:00
Chris Tate bac7a80377 Raise slow CI job time limits and prune closed pull request caches (#1994) 2026-10-24 17:33:56 -05:00
Chris Tate 5155793155 Decode passphrases with the key's encoding, and refuse RSA-PSS keys restricted to other digests
createPrivateKey decoded a string passphrase as UTF-8 whatever the
options' encoding, so a correct passphrase given in that encoding failed
to decrypt, and an encrypted export read its passphrase the same way.
Both now decode it with the encoding, as Node does.

An RSA-PSS key whose parameters name a digest the runtime does not sign
with, such as sha512-224 or sha512-256, was imported without its digest,
MGF1 and salt restrictions, so it allowed operations the key forbids. It
now throws a coded SC2020 error that names the digest. Parameters that
are malformed still leave the key unrestricted, as OpenSSL reads them.
2026-10-24 17:33:30 -05:00
Chris Tate 2edebe7e2f Read spread tuple fields before later rest arguments run
The packing path for `(T | undefined)[]` rest parameters saved plain
arguments to temporaries but read tuple fields only when it built the
pack, after every later argument had run. A later argument that wrote
the tuple changed values already spread into the call. Each tuple field
is now saved where its spread appears, in argument order.
2026-10-24 17:30:32 -05:00
Chris Tate 7fb950deaf Keep rejectUnauthorized on unless false, and certificates independent of raw
A server with requestCert read a runtime rejectUnauthorized through
JavaScript truthiness, so null, 0 or "" let clients without a valid
certificate through, and tls.connect read it the same way. Both now treat
only false as off, as Node's options.rejectUnauthorized !== false does; a
standalone TLSSocket keeps Node's truthiness test.

An X509Certificate's methods read the record's raw Buffer, so writing
into the Buffer that certificate.raw answers changed later checks, the
PEM and the public key. The record now keeps a hidden copy of the DER
that the methods read, and raw stays the same Buffer at every read, as
in Node.
2026-10-24 17:29:34 -05:00
Chris Tate cc6b114468 Take the argument count before parameters bind
A function that reads arguments.length and is called through a value
reads the count its adapter stored, then clears the slot. That read ran
after the parameter prologue, so a parameter binding that threw would
leave the slot set for the next direct call. The count is now taken, and
the slot cleared, before any parameter binds.
2026-10-24 17:23:08 -05:00
Chris Tate b6c6be080c Parse npm-static declaration and package files only when a rewrite can apply (#2146) 2026-10-24 17:22:04 -05:00
Chris Tate adbd9cd1e9 Lower each closure argument of an implicit-any call once
To key an implicit-any function's instance, storedImplicitArgumentType
lowers a JavaScript arrow or function-expression argument to read its
function type, then discards the value; the call's argument list then
lowers the closure again. Every such closure was lowered twice, and a
closure nested in another one four times, and each lowering lifted its
own copy of the body.

The first lowering now keeps its value (Lowerer.speculatedClosures), and
the argument's own lowering in the same function and with the same type
bindings takes it instead of lowering the closure again. A lowering that
reported diagnostics is not kept.
2026-10-24 17:18:30 -05:00
Chris Tate aa9664bf8a Propagate exceptions by unwinding from directly called functions on Linux (#2115) 2026-10-24 17:15:36 -05:00
Chris Tate bc9fdb9c08 Answer stream, StringDecoder and handle-object members in untyped code as Node does (#2105) 2026-10-24 17:11:01 -05:00
Chris Tate fba274439c Report constant object graphs in untyped JavaScript (static objects, stage 1) (#2147) 2026-10-24 17:08:34 -05:00
Chris Tate 055c9ffa25 Evaluate the arguments of deferred require.resolve calls before they throw
Preflight admits a createRequire loader's require.resolve call with a
computed request inside a try block, but lowering refused the call, so
in JavaScript source the whole statement became a runtime fence. The
fence threw before the request expression ran, and before anything else
in the statement: `try { where = load.resolve(candidate(name)); }`
skipped candidate()'s side effects, so the catch clause saw a different
state than in Node.

Such calls (a computed request, another argument shape, or
runtime-computed options, on require.resolve and require.resolve.paths)
now evaluate their arguments in order and then throw the fence, as a
computed require call does. The addon-probe fixture checks that a
computed candidate path runs before each failed lookup.
2026-10-24 17:07:44 -05:00
Chris Tate 34ada100c4 Format the string comparison emission test 2026-10-24 17:07:05 -05:00
Chris Tate 40adc2d161 Hoist a call only when its arguments write no binding
A hoisted call runs before the rest of its write. An argument such as
`i++` in `acc = acc * 3 + i + tally(i++)` would then move that write
ahead of the write's earlier read of `i`, so such a call stays where it
is and the write is not speculated.
2026-10-24 17:06:14 -05:00
Chris Tate 01e6d55fc5 Keep optional-element arrays through nested conversions, union arms, rest packing and receiver reassignment
- Checked conversions from dynamic values key their builders and matchers
  by the type with `?` after every `(T | undefined)[]`, at any depth.
  `number[][]` and `(number | undefined)[][]` share a typeKey, so the
  second conversion reused the first one's builder and rejected undefined
  elements.
- The dynamic matcher of a `(T | undefined)[]` takes undefined elements,
  so a union with such an arm matches arrays that hold undefined.
- An optional-element rest parameter packs tuple-spread values and plain
  arguments together as present undefined elements, each evaluated once in
  argument order, instead of converting each value to T.
- The element-state scan counts an argument in a rest position that may be
  undefined, and a spread tuple with undefined elements, as undefined
  elements of the rest array, so reads in the callee check element states.
- A union index read keeps the receiver in a temporary when the key can
  assign it, so `a[(a = other, 0)]` reads the array `a` held before the key.
2026-10-24 17:05:18 -05:00
Chris Tate 9406ba5b89 Run row sums two rows per pass
A loop nest that sums one term per row,

  for (let i = start; i < end; i++) {
    let t = 0;
    for (let j = from; j < to; j++) t += term;
    out[i] = t;
  }

where the term only reads numbers, the counters and typed-array
elements, now runs two rows per pass when both row bounds are int32,
the start is at most the end, and the stored array shares no byte with
any array the term reads. Each row keeps its order of additions, so each
sum has the bits of the loop as written; an odd row count runs its last
row as written, and any other case runs the nest as written. Element
reads that do not depend on the row are read once per column for both
rows. With two independent sums in the inner loop, neither waits on the
other's addition, and LLVM computes both rows' terms in one vector.

bytes.disjoint compares two typed arrays' element ranges in memory.
2026-10-24 17:04:14 -05:00
Chris Tate 3d4aeddb82 Keep the reply scanner in a constant object
A native build of a program that installs the C scanner but decodes no
reply, such as the self-hosted source parser and CommonJS checks, failed
at startup with "Cannot access 'valueEnd' before initialization": the
build left the module variable uninitialized because its only other use
was the assignment in useNativeJsonValueEnd, which checks it first.

The scanner is now a property of a constant object that the decoders
and useNativeJsonValueEnd read, so it is initialized in every program.
2026-10-24 17:03:27 -05:00
Chris Tate 65e62fe1cb Compare integer values without converting them to doubles (#2060) 2026-10-24 17:02:42 -05:00
Chris Tate 8d9705e22d Run CommonJS top-level return, untyped assert forms, and untyped instanceof Object as Node does (#2101) 2026-10-24 17:01:39 -05:00