Commit Graph
4679 Commits
Author SHA1 Message Date
Chris Tate 4ab3850579 Keep named properties written to arrays through checked views (#1715) 2026-10-21 19:34:43 -05:00
Chris Tate 029906d861 Merge main 2026-10-21 19:34:10 -05:00
Chris Tate 62ddc2fd35 Read a class branch's own-property bag from its receiver 2026-10-21 19:31:56 -05:00
Chris Tate 5bcd68f1ae Address the third review round on array named properties 2026-10-21 19:31:44 -05:00
Chris Tate f8f315cc03 Warn about likely-hot functions that use untyped values (#1724) 2026-10-21 19:29:04 -05:00
Chris Tate c29ce641b2 Count only the repeated parts of loops as likely-hot work
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.
2026-10-21 19:26:00 -05:00
Chris Tate 220199dfec Evaluate an fs argument inside satisfies once after a URL path
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.
2026-10-21 19:17:07 -05:00
Chris Tate be23f71266 Compile TypeScript's UMD module wrapper as the module body 2026-10-21 19:02:52 -05:00
Chris Tateandblurname 5a3fbcc1ef Hold class values in structural constructor types declared in the program
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>
2026-10-21 18:57:17 -05:00
Chris Tate fa7bc7c7a0 Warn about likely-hot functions that use untyped values
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.
2026-10-21 18:53:43 -05:00
Chris Tate 34f371f829 Address the second review round on array named properties 2026-10-21 18:43:03 -05:00
Chris Tate 2054ea51bb Give the sanitized Effect pilot tests a longer timeout 2026-10-21 18:31:16 -05:00
Chris Tate 982ef6dbef Allow zlib callbacks to assign to their result
The error-path copy of a zlib callback reads its result from a hidden
local that was immutable, so a body that assigned a fallback Buffer to
the result (if (!out) out = Buffer.from("fallback")) failed to compile
with an internal compiler error. The local is now mutable, and an
assigned value is read back as a checked-dynamic value, as in Node.
2026-10-21 18:11:33 -05:00
Chris Tate 4fee347ca0 Address review feedback on array named properties 2026-10-21 17:57:00 -05:00
Chris Tate dfdc610c59 Replace every match of a block in one-character replace chains
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.
2026-10-21 17:36:58 -05:00
Chris Tate 4cf87366aa Refuse elements on Array.prototype (#1720) 2026-10-21 17:29:26 -05:00
Chris Tate 0950725949 Refuse elements on Array.prototype 2026-10-21 17:24:17 -05:00
Chris Tate 8061060aff Lower a Buffer view of a dynamic typed-array value 2026-10-21 17:20:50 -05:00
Chris Tate a7a42640dc Stop the type-parameter walk at recursive aliases in signatures
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.
2026-10-21 17:17:15 -05:00
Chris Tate d15e822d26 Hold any non-nullish value in an empty object type inferred from a literal
The type mapper lowered the type tsc infers from an empty object literal
(`const o = {}`) as the empty record, while a declared `{}` already lowered
as a checked-dynamic value. tsc's `{}` admits every non-nullish value in
both cases: union subtype reduction folds primitives into a literal's `{}`
(`["12", {}]` is `{}[]`, `flag ? "x" : {}` is `{}`), and `let o = {}`
accepts `o = "x"`. Such a program built and then threw a TypeError at run
time ("expected object at $[0], got string" in a JavaScript file, "a
'string' value is not representable in the target union" in TypeScript),
where Node prints the values.

An empty object type with no members, index signatures or signatures now
lowers as a checked-dynamic value, the same rule as `object` and `Object`.
As an arm of a union that keeps another non-nullish arm, which subtype
reduction would have removed, it holds only the literal's objects and
stays the empty record.

An assertion's scalar check (`as string`, `as number`, `as boolean`) on a
dynamic value whose result goes straight back into a dynamic slot keeps
the original value, since JavaScript erases assertions: passing `{} as
unknown as string` to process.loadEnvFile reports Node's
ERR_INVALID_ARG_TYPE instead of a TypeError from the check.

Object.assign's two-argument path for a dynamic target now applies the
same refusal as the general path when a class instance is copied into a
fresh object literal whose result the checker types as the class, so
`Object.assign({}, instance)` keeps its compile-time refusal now that the
literal lowers as a dynamic value.
2026-10-21 17:16:12 -05:00
Chris Tate 238b08352b Keep named properties written to arrays through checked views 2026-10-21 17:00:19 -05:00
Chris Tate d6f857bb1d Keep class methods on structural views asserted from unknown 2026-10-21 16:59:02 -05:00
Chris Tate 4124e2a501 Convert global isFinite's argument with ToNumber (#1662) 2026-10-21 16:37:03 -05:00
Chris Tate bbb37ae03b Read a spread's elements before an earlier argument converts
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.
2026-10-21 16:34:37 -05:00
Chris Tate ea4d570e59 Speed up split, indexOf, slice and Number() on ASCII fields
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.
2026-10-21 16:12:52 -05:00
Chris Tate 47671d5bed Give the fs corpus programs per-process scratch directories
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.
2026-10-21 15:54:23 -05:00
Chris Tate eca349d244 Forward nested zlib callback results and keep self-calling callbacks' error checks
A nested callback lowered inside an outer error path passed the absent
Buffer on into a Buffer slot even when its own operation succeeded, so a
callee such as (err, out) => cb(err, out) received undefined instead of
the result. The forwarded value is now the Buffer when there is one.

A named function expression passed to zlib can call itself, and a
function expression can reach itself through arguments.callee, with an
error and a Buffer. Such functions no longer count as called only by
the adapter, so their success copy keeps the runtime error checks
instead of folding them to the success case.
2026-10-21 15:32:52 -05:00
Malte UblandClaude Opus 5.5 eaefbdf44f Host the runtime's event loop on Node's libuv once Node loads (NI-3)
Scope-B milestone NI-3. When a program loads Node in-process, its pending
ticks, promise jobs, timers and immediates move onto Node's queues in
order (timers keep their deadlines relative to the moment Node began
loading; the work of a phase the switch interrupted finishes first) and
every later one is scheduled there, so the jobs of both sides interleave
as in Node. The runtime's I/O hooks are watched from Node's loop, and its
liveness is re-read before each poll phase, after each check phase and
after every microtask checkpoint. Node's loop runs on Node's stack, and
V8's limit follows whichever stack an entry comes from.

Island signal and exit listeners join the runtime's registries at their
position, so the runtime keeps owning every signal disposition; Node's own
'exit' emission is left to the runtime's exit sequence, and the exit code
island code leaves in process.exitCode becomes the program's. Bridge ABI 3.

Ordering programs (tests/corpus-node-islands-order, harness
node-island-order.test.ts), corpus programs 6804-6811 and the
node-island-process harness pin the behavior against Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 13:31:51 -07:00
Chris Tate 1c2b80040b Deliver queued worker messages before immediates, as Node does (#1682) 2026-10-21 15:05:13 -05:00
Chris Tate ca68d63c1c Finish a worker message batch before port messages, and count queued messages as they arrive
- 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.
2026-10-21 15:00:22 -05:00
Chris Tate 3b35db6b6b Resolve the stream-construction fixture's file read on close
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.
2026-10-21 14:24:57 -05:00
Chris Tate 27855a3835 Resolve conditional member types over literal class type arguments (#1669) 2026-10-21 14:23:50 -05:00
Chris Tate 644f423734 Evaluate fs arguments up to the last one with effects before a URL path conversion
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.
2026-10-21 14:22:51 -05:00
Chris Tate 558fb7c8ba Bind only conditional-dependent class type arguments as checker types
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.
2026-10-21 14:20:20 -05:00
Chris Tateandsaboursepideh a7e84cd48c Support SNICallback in tls.createServer
tls.createServer({ cert, key, SNICallback }) hands each ClientHello's
servername to the callback and serves the SecureContext it answers,
synchronously or later, or the default cert and key when it answers none.
An error answer closes that connection. This uses the SNI handling that
http2.createSecureServer already has. A callback expression with side
effects, or SNICallback combined with ALPNProtocols, minVersion, maxVersion
or the other options the runtime walk reads, fails with SC2020.

A tls.connect or https.request client whose peer closes or resets the
connection before the handshake completes now fails with Node's ECONNRESET
"Client network socket disconnected before secure TLS connection was
established" instead of closing without an error.

Refs #1438

Co-authored-by: saboursepideh <25549001+saboursepideh@users.noreply.github.com>
2026-10-21 13:56:07 -05:00
Chris Tate 3a139a8d40 Negotiate ALPN and bound the TLS version in tls.connect and tls.createServer (#1664) 2026-10-21 13:52:22 -05:00
Chris Tate 9e751b3d38 Give arrays of a known final length exact dense capacity (#1699) 2026-10-21 13:31:21 -05:00
Chris Tate 1d6f26a071 Give arrays of a known final length exact dense capacity 2026-10-21 13:26:50 -05:00
Chris Tate 23ec584deb Read nullable union chunks of push and write by their representation
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.
2026-10-21 13:24:10 -05:00
Chris Tate a4d32a8e38 Use the default TLS version bound for null minVersion and maxVersion in tls.createServer
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.
2026-10-21 13:22:56 -05:00
Chris Tate 349ab75298 Evaluate every ToNumber argument before converting, and convert conditionals
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.
2026-10-21 12:50:14 -05:00
Chris Tate 8cc2594d6e Place a beside libnode pack on cache hits, key the cache by the pack's manifest, keep the loop for synchronous Node calls, and record the Node call programs' costs
- A --libnode=beside build whose executable comes from the executable
  cache places the pack next to it again (compile, index.ts): the pack
  is an output of the build, and a rebuild after the outputs were
  deleted restored the executable without it.
- The cache key names the libnode pack by the option, the pack
  directory and a digest of its manifest.json (libnodeCacheKey), so a
  pack upgraded in place gives cached executables a new key instead of
  their old recorded Node version.
- nodeIsland.callSync marks the program's event loop live, as
  nodeIsland.call does: the called function may leave Node with ref'd
  timers or I/O, which the loop drives before the program exits.
- scr_runtime.h includes scr_node_island.h, so the LLVM backend's
  declarations of the Node island entry points have prototypes there.
- Corpus costs for 6800-6803, from CI's differential and
  llvm-differential timings of the programs.
2026-10-21 12:45:05 -05:00
Chris Tate 6d19198eed Merge main 2026-10-21 12:43:37 -05:00
Chris Tate a437102af6 Keep object literals' key order through IR rewrites and on shapes typed JSON.parse also builds (#1607) 2026-10-21 12:28:31 -05:00
Malte UblandClaude Opus 5.5 091b8f1934 Report which packages and modules would run in a Node island
Adds the report-only Node island policy, configuration and detector of
the scope-B plan (milestone NI-2). Coverage now lists the npm packages a
build would run in a Node island and why, gap islands first, without
changing code generation.

- project-config.ts reads the `scriptc` key of the entry's package.json
  with a schema; an unknown value in `scriptc.packages` or
  `scriptc.nodeIslands` is SC8004 instead of being ignored. Package shims
  and the WebAssembly scanner read through it.
- node-islands/: origin (user, npm, shim, builtin; package instances),
  rules R1.1-R2.5 and the four classes, a syntactic pre-scan for R1/R2
  markers, classification of refusals and runtime fences (reason ids at
  the refusal sites that need them), the policy (auto, explicit, off;
  forced node and native packages; marked user modules) and the report
  with island closure.
- coverage: a "Node islands" section, `--explain-island=<pkg>`, a
  `nodeIslands` field in the diagnostics envelope, and `--fail-on`
  `node-islands`, `eager-node-islands` and `island-gaps`.
- CLI: `--node-islands`, `--node-island`, `--native`, `--node` (validated,
  report-only) and `--strict-user-js` (SC8101-SC8105 for R2 constructs in
  user JavaScript).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:22:35 -07:00
Chris Tate 9a73f63b06 Pass undefined as the zlib callback result on errors
Node calls a zlib callback with (err, undefined) when the operation
fails. The callback adapter passed a NULL Buffer in the result's static
Buffer slot instead: member reads crashed and typeof printed "object".
This applied to deflate, inflate, deflateRaw, inflateRaw, gzip, gunzip
and unzip, with and without options, for callback literals and for
function values.

A function shaped (err, out), with err typed Error | null and out a
Buffer that the body reads, now tests out on entry (bytes.isAbsent).
With a Buffer, the body runs as before. With the NULL Buffer, a second
copy of the body runs with out read as any holding undefined, so typeof,
comparisons, templates, optional chains and console.log give Node's
answers and member reads throw Node's TypeError. Passed on into a Buffer
slot, out stays the NULL Buffer, which a callee with the same entry test
reads as undefined. The parameter keeps its static type, so callers and
adapters are unchanged. A copy that has no such lowering throws SC2020
when it runs.

Conditions on the error parameter fold in the error-path copy, and in
the success copy for a literal that only the adapter calls. Inside an
error-path copy, a nested callback lowers once with its result read as
any, so nested callbacks grow linearly in code size.
2026-10-21 12:17:22 -05:00
Chris Tate 33110f1938 Track unions in nonJsonPart and expect per-platform lifecycle output 2026-10-21 12:08:57 -05:00
Malte UblandClaude Opus 5.5 51e9812493 Call into Node.js from compiled programs with @scriptc/node
Scope-B milestone NI-1, API side: the explicit call API that replaces the
build container's hand-written node subprocess (api patch 09).

- The new package @scriptc/node exports callSync(specifier, name, ...args)
  and call(...). The module loads with require() from the calling module;
  arguments and results cross as JSON copies; a thrown Error crosses as its
  nearest intrinsic class with its message and string code, other thrown
  values as JSON copies; callSync refuses a function that returns a
  promise. Its Node implementation runs the calls in-process with the same
  copies, so a program prints the same under Node and compiled.
- The compiler lowers calls to the named or namespace imports
  (node-api-import.ts, lower-node-island.ts) to the nodeIsland.callSync
  and nodeIsland.call libCalls: the calling module's real path, the
  specifier, the export name and the boxed argument array, with the result
  checked against the declared type like `JSON.parse(s) as T`. Arguments
  and results whose types are not JSON data are refused at build time, and
  the functions and the namespace refuse as values.
- Corpus programs 6800-6803 (sync and async results, errors, process.exit
  from Node with the program's exit listeners, Node's pending timers
  keeping the program alive) run under Node and as binaries that load
  libnode; `// @requires-libnode` skips them where SCRIPTC_LIBNODE names no
  pack (until the pack CI matrix publishes packs). The node-island harness
  covers loading Node only on the first call, the beside packaging, a
  missing pack, a pack of another Node version or bridge ABI, and the
  executable's symbols.
- Release tooling publishes @scriptc/node with the other packages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 09:59:55 -07:00
Malte UblandClaude Opus 5.5 1081315a49 Run Node on its own stack; provide a libnode pack to the Linux CI test lanes
Calls into Node now run on one dedicated 64 MB stack with a guard page,
reached by a context switch, and the bridge sets V8's stack limit on it
(bridge ABI 2). Before, a call from an async fiber (512 KB stacks without a
guard page) failed to bootstrap Node, which the dev-mode LLVM differential
showed. The runtime's stack guard follows the switch, and the switch
carries ASan's fiber annotations.

The Node island tests load Node from a libnode pack. Until the NI-13 packs
are published, .github/actions/libnode-dev-pack assembles one for the
Linux x64 test lanes (plain and sanitized): a pinned, checksum-verified
prebuilt libnode of the repository's Node release (24.22.0) and Node's
official headers (packages/libnode-common/dev-pack.json), with the bridge
built against them by build-pack.mjs. The pack is cached by its inputs and
the bridge source, so only a cache miss downloads and builds; the normal
build path never does.

tests/harness/libnode.ts makes a missing pack a failure in those lanes:
island tests skip only off Linux x64 CI without a configured pack. The
bridge smoke test (packages/libnode-common/test/bridge-smoke.c) loads the
pack the way scr_node_island.c does and makes synchronous, throwing and
asynchronous calls through the bridge with the runtime's bootstrap.

IR, LLVM and object outputs of programs that call into Node no longer need
a pack at build time; only executables record one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 09:58:32 -07:00
Chris Tate b45b8e0898 Call an object's own or inherited toString in template literals and join (#1655) 2026-10-21 11:50:36 -05:00