-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
640 lines (607 loc) · 32.8 KB
/
Copy pathindex.html
File metadata and controls
640 lines (607 loc) · 32.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>SxfeScript</title>
<meta name="description" content="sxn is a JavaScript runtime that starts in 8.4 ms and has no JIT to warm up. It runs .js, .ts and .sx directly, loads node_modules, and serves HTTP. SxfeScript adds ownership and borrows on top.">
<link rel="icon" type="image/png" href="logo.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Newsreader:opsz,wght@6..72,400;6..72,500;6..72,600&family=Public+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="site.css">
<style>
/* Tokens, base typography, nav, buttons, the copy button and the footer
live in site.css, shared with every generated documentation page. What
is left here is the landing page's own layout. */
/* ---- hero ---- */
header.hero { padding: 76px 0 40px; }
.kicker {
font-family: "JetBrains Mono", monospace; font-size: 11.5px; letter-spacing: .16em;
text-transform: uppercase; color: var(--accent); margin: 0 0 18px;
}
h1.title {
font-size: clamp(40px, 5vw, 64px); line-height: 1.02; font-weight: 600; margin: 0 0 22px;
letter-spacing: -.02em;
}
p.lede { font-size: clamp(17px, 2vw, 19.5px); color: var(--ink-mid); max-width: 58ch; margin: 0 0 30px; }
.cta-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 44px; }
.install-tabs { display: flex; gap: 2px; margin-bottom: 8px; }
.install-tab {
font-family: "JetBrains Mono", monospace; font-size: 12.5px; font-weight: 600;
padding: 7px 14px; border: 1px solid var(--rule); background: var(--card);
color: var(--ink-soft); cursor: pointer;
}
.install-tab:first-child { border-radius: 7px 0 0 7px; }
.install-tab:last-child { border-radius: 0 7px 7px 0; border-left: none; }
.install-tab.active { background: var(--ink); color: var(--paper); border-color: var(--ink); }
pre.install-cmd {
margin: 0 0 10px; padding: 14px 52px 14px 18px; font-size: 14px;
display: inline-flex; align-items: center; gap: 20px; position: relative;
/* The command is one long unbreakable line; without this the inline-flex
box sizes to it and drags the whole column wider than a phone. */
max-width: 100%;
}
pre.install-cmd[hidden] { display: none; }
pre.install-cmd code { color: var(--safe); }
/* One line of text, so the button centers rather than sitting at the top
the way it does on a multi-line block. */
pre.install-cmd .copy-btn { top: 50%; transform: translateY(-50%); }
p.install-note { font-size: 13px; color: var(--ink-soft); margin: 0 0 44px; }
/* minmax(0, ...) rather than a bare fraction, and min-width on the items:
a grid track's automatic minimum is its content's min-content width, and
a <pre> of code has no wrap opportunities, so an unqualified 1fr grew the
column past the viewport on a phone and the page was clipped rather than
laid out. The same reason .doc carries min-width: 0. */
.hero-grid {
display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, .85fr);
gap: 48px; align-items: start;
}
.hero-grid > * { min-width: 0; }
@media (max-width: 860px) { .hero-grid { grid-template-columns: minmax(0, 1fr); } }
.compare-hero { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; }
.compare-hero > * { min-width: 0; }
.compare-hero pre { margin: 0; font-size: 12.6px; }
.compare-hero .highlight { margin: 0; }
.compare-label {
font-family: "JetBrains Mono", monospace; font-size: 11px; letter-spacing: .08em;
text-transform: uppercase; color: var(--ink-mid); margin: 0 0 8px;
}
/* ---- runnable examples ---- */
.ex-tabs { display: flex; gap: 2px; margin-bottom: 14px; flex-wrap: wrap; }
.ex-tab {
font-family: "JetBrains Mono", monospace; font-size: 12.5px; font-weight: 600;
padding: 7px 14px; border: 1px solid var(--rule); border-radius: 7px;
background: var(--card); color: var(--ink-soft); cursor: pointer;
}
.ex-tab:hover { color: var(--ink); }
.ex-tab.active { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.ex-panel[hidden] { display: none; }
.ex-panel .ex-file {
font-family: "JetBrains Mono", monospace; font-size: 11.5px; letter-spacing: .06em;
color: var(--ink-soft); margin: 0 0 8px;
}
.ex-panel pre { position: relative; margin: 0; font-size: 13px; }
/* The code block is inlined from the example file at build time and
arrives wrapped in .highlight, the way the doc pages get it. */
.ex-panel .highlight { margin: 0; }
.ex-run {
font-family: "JetBrains Mono", monospace; font-size: 11px; letter-spacing: .1em;
text-transform: uppercase; color: var(--ink-soft); margin: 18px 0 8px;
}
pre.ex-out { background: var(--card); box-shadow: none; }
pre.ex-out code { color: var(--ink-mid); }
/* ---- sections ---- */
section { padding: 56px 0; border-top: 1px solid var(--rule-soft); }
section:first-of-type { border-top: none; }
section.band {
border-top: 1px solid var(--rule-soft); border-bottom: 1px solid var(--rule-soft);
background: var(--card-2);
margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw);
padding-left: calc(50vw - 50%); padding-right: calc(50vw - 50%);
}
h2.h {
font-size: clamp(26px, 3.6vw, 32px); font-weight: 600; margin: 0 0 8px;
}
p.lede-s { color: var(--ink-mid); max-width: 62ch; margin: 0 0 32px; }
.grid-2 { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; }
.grid-2 > * { min-width: 0; }
@media (max-width: 760px) { .grid-2 { grid-template-columns: minmax(0, 1fr); } }
.card {
background: var(--card); border: 1px solid var(--rule); border-radius: 10px;
padding: 20px; transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.card:hover {
transform: translateY(-2px); box-shadow: var(--shadow);
border-color: color-mix(in srgb, var(--accent) 32%, var(--rule));
}
.card h3 {
margin: 0 0 8px; font-size: 16.5px; font-weight: 600; font-family: "Public Sans", sans-serif;
display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
}
.card p { margin: 0; font-size: 14.5px; color: var(--ink-mid); }
.badge {
font-family: "JetBrains Mono", monospace; font-size: 11px; font-weight: 600; letter-spacing: .02em;
color: var(--accent); background: var(--accent-wash); border-radius: 5px; padding: 2px 7px;
}
a.badge { text-decoration: none; }
a.badge:hover { background: color-mix(in srgb, var(--accent) 22%, var(--accent-wash)); }
/* comparison table */
.scroll { overflow-x: auto; border: 1px solid var(--rule); border-radius: 8px; background: var(--card); }
table { border-collapse: collapse; width: 100%; min-width: 560px; }
th, td { text-align: left; padding: 12px 16px; border-bottom: 1px solid var(--rule-soft); vertical-align: top; font-size: 14.5px; }
thead th {
font-family: "JetBrains Mono", monospace; font-size: 10.5px; letter-spacing: .09em; text-transform: uppercase;
color: var(--ink-mid); font-weight: 600; background: var(--card-2); border-bottom: 2px solid var(--rule);
}
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover td { background: var(--card-2); }
td.yes { color: var(--safe); font-weight: 600; }
td.yes.quiet { color: var(--ink-mid); font-weight: 400; }
td.no { color: var(--ink-mid); }
td.yes::before { content: "\2713 "; }
td.no::before { content: "- "; }
td code { background: none; border: 0; padding: 0; }
td.num, th.num {
text-align: right; font-family: "JetBrains Mono", monospace; font-variant-numeric: tabular-nums;
font-size: 14px; white-space: nowrap;
}
ul.check, ul.plain { padding-left: 0; list-style: none; margin: 0; }
ul.check li, ul.plain li { padding: 5px 0 5px 26px; position: relative; font-size: 15px; color: var(--ink-mid); }
ul.check li::before { content: "\2713"; position: absolute; left: 0; color: var(--safe); font-weight: 700; }
ul.plain li::before { content: "\00b7"; position: absolute; left: 4px; color: var(--ink-soft); font-weight: 700; }
</style>
</head>
<body>
<nav class="top">
<div class="row">
<a class="wordmark" href="#top"><img src="logo.png" alt="" width="30" height="30"><span>Sxfe<span class="wm-accent">Script</span></span></a>
<ul>
<li><a class="navlink" href="docs/">Docs</a></li>
<li><a class="navlink" href="#code">Code</a></li>
<li><a class="navlink" href="#benchmarks">Benchmarks</a></li>
<li><a class="navlink" href="#vs-typescript">vs TypeScript</a></li>
<li><a class="navlink" href="#arcsx">ArcSX runtime</a></li>
<li><a class="navlink" href="#contributing">Contributing</a></li>
</ul>
<a class="gh-link" href="{{REPO_URL}}">GitHub</a>
</div>
</nav>
<div class="wrap">
<header class="hero" id="top">
<div class="hero-grid">
<div>
<p class="kicker">sxn · the ArcSX runtime · SxfeScript</p>
<h1 class="title">A JavaScript runtime that starts fast and stays even.</h1>
<p class="lede">
<code>sxn</code> runs your <code>.js</code>, <code>.ts</code> and <code>.sx</code>
files with no build step, loads <code>node_modules</code>, and serves HTTP.
It cold-starts in 8.1 ms against Node's 43.5, and its worst
garbage-collection pause is 0.04 ms against Node's 0.22 and Bun's 2.55
— because there is no JIT to warm up. That is deliberate: iOS will not
let a third-party app generate machine code, so this one is built to be fast
without it.
</p>
<div class="cta-row">
<a class="btn btn-primary" href="docs/quickstart/">Get started</a>
<a class="btn btn-ghost" href="docs/why/">Why sxn</a>
<a class="btn btn-ghost" href="#benchmarks">Benchmarks</a>
<a class="btn btn-ghost" href="{{REPO_URL}}">View the source</a>
</div>
<div class="install-tabs" role="tablist" aria-label="Install command">
<button type="button" class="install-tab active" role="tab" aria-selected="true" data-os="unix">macOS / Linux</button>
<button type="button" class="install-tab" role="tab" aria-selected="false" data-os="windows">Windows</button>
</div>
<pre class="install-cmd" data-os="unix"><code>curl -fsSL https://sxfescript.github.io/latest/install.sh | bash</code><button type="button" class="copy-btn" aria-label="Copy install command">Copy</button></pre>
<pre class="install-cmd" data-os="windows" hidden><code>irm https://sxfescript.github.io/latest/install.ps1 | iex</code><button type="button" class="copy-btn" aria-label="Copy install command">Copy</button></pre>
<p class="install-note">arm64 or x64, either way. Every release also has a
plain <code>.zip</code>/<code>.tar.gz</code> on
<a href="{{REPO_URL}}/releases">Releases</a>.</p>
</div>
<div class="compare-hero">
<div>
<p class="compare-label">TypeScript: erased at build time</p>
<!-- highlight: ts -->
function withdraw(acct: Account, n: number) {
// nothing here is true at runtime.
// acct could be null. n could be NaN.
// tsc already stopped watching.
acct.balance -= n;
}
<!-- /highlight -->
</div>
<div>
<p class="compare-label">SxfeScript: checked, then run</p>
<!-- highlight: sx -->
function withdraw(acct: &mut Account, n: i32) {
// acct is an exclusive borrow, and the
// caller must own it as `let mut`.
acct.balance -= n;
}
<!-- /highlight -->
</div>
</div>
</div>
</header>
<section id="benchmarks" class="band">
<h2 class="h">sxn vs Node vs Bun</h2>
<p class="lede-s">
Matched workloads, same tree, same tests, on an Apple M4. Lower is better in
every row. <strong>No category is hidden</strong> — sxn takes seven of the
nine, and of the two it does not, one is Node's and one is Bun's.
</p>
<!-- section: README.md#mac-apple-m4 table -->
<p style="margin-top:22px; color:var(--ink-mid); font-size:14.5px; max-width:66ch;">
A second machine, a Ryzen 7 5700G running Ubuntu, agrees row for row. Both
machines' specs, how each row is measured, and the harness you can run yourself
are on the <a href="docs/benchmarks/">benchmarks page</a>.
</p>
<h3 style="margin-top:44px;">What you get in the box</h3>
<div class="scroll">
<table>
<thead><tr><th></th><th>sxn</th><th>Node</th><th>Bun</th></tr></thead>
<tbody>
<tr><td>Run <code>.ts</code> with no build step</td><td class="yes">Directly</td><td class="no">Type stripping only</td><td class="yes">Directly</td></tr>
<tr><td>Ownership and borrows in the language</td><td class="yes">Yes</td><td class="no">Not modeled</td><td class="no">Not modeled</td></tr>
<tr><td>Standard library</td><td class="yes quiet"><a href="docs/node/">Superset ↗</a></td><td class="yes">The reference</td><td class="yes quiet">Most of it</td></tr>
<tr><td><code>.node</code> native addons</td><td class="yes"><a href="docs/native/">120 Node-API entry points ↗</a></td><td class="yes">Yes</td><td class="yes quiet">Partial</td></tr>
<tr><td>WinterTC Minimum Common API</td><td class="yes">62 of 62, less WebAssembly</td><td class="yes quiet">Partial</td><td class="yes quiet">Most</td></tr>
<tr><td>Call a C function without an addon</td><td class="yes"><code>Sxn.ffi</code></td><td class="no">Needs an addon</td><td class="yes"><code>bun:ffi</code></td></tr>
<tr><td>Precompile to skip parsing</td><td class="yes"><code>.sxbc</code>, still needs <code>sxn</code> installed</td><td class="no">No</td><td class="no">No</td></tr>
<tr><td>Standalone binary, no runtime to install</td><td class="no">No</td><td class="yes quiet">Experimental (SEA)</td><td class="yes"><code>bun build --compile</code></td></tr>
<tr><td>JIT</td><td class="no">None, deliberately</td><td class="yes">Yes</td><td class="yes">Yes</td></tr>
<tr><td>Runs on iOS</td><td class="yes">Yes</td><td class="no">No</td><td class="no">No</td></tr>
</tbody>
</table>
</div>
<p style="margin-top:22px; color:var(--ink-mid); font-size:14.5px; max-width:66ch;">
And when to pick something else: a hot numeric loop a JIT can compile away, the
widest possible package compatibility, or a bundler and test runner in the same
binary. <a href="docs/why/">Why sxn</a> is honest about all three.
</p>
</section>
<section id="code">
<h2 class="h">What it looks like</h2>
<p class="lede-s">
Each of these is a real <code>.sx</code> file in
<a href="{{REPO_URL}}/tree/main/examples"><code>examples/</code></a>, and the
output under it is what that file actually prints. Nothing here needs a
config file, a bundler, or a step before <code>sxn</code> runs it.
</p>
<div class="ex-tabs" role="tablist" aria-label="Example">
<button type="button" class="ex-tab active" role="tab" aria-selected="true" data-ex="server">HTTP server</button>
<button type="button" class="ex-tab" role="tab" aria-selected="false" data-ex="sx">Ownership</button>
<button type="button" class="ex-tab" role="tab" aria-selected="false" data-ex="fetch">fetch & streams</button>
<button type="button" class="ex-tab" role="tab" aria-selected="false" data-ex="ffi">Calling C</button>
</div>
<div class="ex-panel" data-ex="server">
<p class="ex-file">examples/server.sx</p>
<!-- source: examples/server.sx -->
<p class="ex-run">sxn examples/server.sx</p>
<pre class="ex-out"><code>listening on http://127.0.0.1:56690
POST /notes -> 201 {"id":2,"text":"written by the example"}
GET /notes -> 200 [{"id":1,"text":"the first note"},{"id":2,"text":"written by the example"}]</code></pre>
</div>
<div class="ex-panel" data-ex="sx" hidden>
<p class="ex-file">examples/hello.sx</p>
<!-- source: examples/hello.sx -->
<p class="ex-run">sxn examples/hello.sx</p>
<pre class="ex-out"><code>counter: 2</code></pre>
</div>
<div class="ex-panel" data-ex="fetch" hidden>
<p class="ex-file">examples/fetch.sx</p>
<!-- source: examples/fetch.sx -->
<p class="ex-run">sxn examples/fetch.sx</p>
<pre class="ex-out"><code>200 text/html
559 bytes
1 chunk(s), 559 characters</code></pre>
</div>
<div class="ex-panel" data-ex="ffi" hidden>
<p class="ex-file">examples/ffi.sx</p>
<!-- source: examples/ffi.sx -->
<p class="ex-run">sxn examples/ffi.sx</p>
<pre class="ex-out"><code>pow(2, 10) = 1024
sqrt(144) = 12</code></pre>
</div>
<p style="margin-top:26px;">
More of them, each with its output, on the
<a href="docs/examples/">examples page</a>. The
<a href="docs/quickstart/">quick start</a> goes from an empty directory to
a running server.
</p>
</section>
<section id="what-is">
<h2 class="h">What SxfeScript actually adds</h2>
<p class="lede-s">
Every ordinary JavaScript value keeps its ordinary semantics. SxfeScript layers four
things on top, and all four are optional. A file that uses none of them is a
normal <code>.js</code> file that happens to have a <code>.sx</code> extension.
</p>
<div class="grid-2">
<div class="card">
<h3>Ownership & borrows</h3>
<p><code>let mut</code> creates a mutable owner, <code>let</code> an immutable one.
<code>&value</code> borrows it shared; <code>&mut value</code> borrows it
exclusively and requires a mutable owner. A borrow can't be returned, stored somewhere
longer-lived, or carried across an <code>await</code>.</p>
<p>The exclusive-borrow rule is checked before the code runs: <code>&mut</code> on
an immutable binding is a compile error, not a runtime surprise. The rest of the model
is parsed today and checked by the control-flow pass that is still being written —
<a href="docs/implementation/">the ledger</a> says which is which, and it is worth
reading before relying on a rule.</p>
</div>
<div class="card">
<h3>The <span class="kw-safe">safe</span> / <span class="kw-unsafe">unsafe</span> boundary</h3>
<p><code>safe let</code> marks a binding as type-stable: the object shape it points to
rejects property addition, deletion, and incompatible writes. Reaching outside that
(a raw pointer, an FFI call) means writing <code>unsafe</code> and meaning it,
the same contract Rust makes explicit.</p>
</div>
<div class="card">
<h3>Erasable TypeScript annotations</h3>
<p>Type aliases, interfaces, <code>declare</code>, generics on functions, optional
parameters, <code>as</code>/<code>satisfies</code>, union types: all of it parsed
natively, no <code>tsc</code>, no bundler, no separate compile step. They emit no code
— but a declared scalar type is remembered rather than discarded, and spent on
the bytecode that comes out.</p>
</div>
<div class="card">
<h3>Fixed-layout structs</h3>
<p>A primitive-only interface (<code>i32</code>, <code>f32</code>, <code>f64</code>,
<code>bool</code>) defines an affine struct with declared field order and natural
alignment: the same layout on every supported target, for code that has to cross
into native memory.</p>
</div>
</div>
</section>
<section id="vs-typescript">
<h2 class="h">SxfeScript vs. TypeScript</h2>
<p class="lede-s">
TypeScript's types exist for the compiler and your editor, and stop existing the moment
<code>tsc</code> finishes. A null you were promised isn't there can still be null.
SxfeScript's <span class="kw-safe">safe</span> qualifier is the same idea pushed one layer
deeper: the guarantee is checked before the code runs, and object shapes that would
violate it are rejected rather than merely un-typed.
</p>
<div class="scroll">
<table>
<thead><tr><th>Question</th><th>TypeScript</th><th>SxfeScript</th></tr></thead>
<tbody>
<tr>
<td>What happens to the types at runtime?</td>
<td class="no">Erased entirely: a type is a compile-time fiction</td>
<td class="yes"><code>safe</code> bindings keep a runtime descriptor and incompatible writes are rejected. The declared type reaches codegen too: <code>i32</code> arithmetic wraps by definition, a small function with a scalar signature is inlined into its caller, and a binding typed as a primitive-only <code>interface</code> is compiled to one local per field instead of an object</td>
</tr>
<tr>
<td>Do you need a build step to run a file?</td>
<td class="no"><code>tsc</code>, or a bundler standing in for it</td>
<td class="yes">No. <code>sxn app.ts</code> parses the types and runs it directly — declared ones are compiled, not discarded</td>
</tr>
<tr>
<td>Aliasing and mutation</td>
<td class="no">Not modeled at all</td>
<td class="yes">Explicit: an owner, a shared borrow, or an exclusive borrow, one at a time. The exclusive borrow is checked at compile time today; the rest is parsed and waiting on the control-flow pass</td>
</tr>
<tr>
<td>Calling into native code</td>
<td class="no">Whatever your runtime's FFI story is. TypeScript has no opinion</td>
<td class="yes">An explicit <code>unsafe extern</code> boundary; everywhere else stays safe by default</td>
</tr>
<tr>
<td>Can a "safe" line still corrupt memory?</td>
<td class="no">Not applicable. There's no runtime safety claim to keep</td>
<td class="yes quiet">Not while it stays <code>safe</code>; <code>unsafe</code> is the one place that trust is spent explicitly</td>
</tr>
</tbody>
</table>
</div>
<p style="margin-top:18px; color:var(--ink-mid); font-size:14.5px; max-width:66ch;">
TypeScript documents intent for humans and tooling. SxfeScript's safe subset makes a
narrower, different claim: a runtime contract, smaller in scope than TypeScript's type
system today (see the honest gap list in
<a href="docs/implementation/">spec/IMPLEMENTATION.md</a>), and
still growing, but real where it applies rather than a promise the runtime never checks.
</p>
</section>
<section id="arcsx" class="band">
<h2 class="h">ArcSX: the runtime underneath</h2>
<p class="lede-s">
ArcSX is a QuickJS-ng fork, built as <code>sxn</code>. It runs <code>.sx</code>,
<code>.ts</code>, and ordinary <code>.js</code>/<code>.mjs</code>/<code>.cjs</code>
directly, with two compatibility layers built on top of the same engine core: one for the
browser-shaped WinterTC web APIs, one for Node.
</p>
<p style="color:var(--ink-mid); font-size:14.5px; max-width:68ch; margin-bottom:12px;">
What that's actually good for, measured against Node 25 and Bun 1.2 on the same Apple
M4 (full methodology, a second machine, and the interpreter-level work behind these
numbers are in the <a href="docs/readme/">README</a>):
</p>
<div class="scroll">
<table>
<thead><tr><th>What you're waiting on</th><th class="num">sxn</th><th class="num">Node</th><th class="num">Bun</th><th>In plain terms</th></tr></thead>
<tbody>
<tr>
<td>Starting a new process</td>
<td class="num yes">7.5 ms</td><td class="num">42.5 ms</td><td class="num">9.4 ms</td>
<td>About 5x faster than Node to start, and a hair ahead of Bun. Matters for a CLI tool or a serverless cold start</td>
</tr>
<tr>
<td>Processing binary data (<code>Buffer</code>)</td>
<td class="num yes">19.4 ms</td><td class="num">24.5 ms</td><td class="num">27.1 ms</td>
<td>Faster than both, at the operation almost every server-side script does constantly</td>
</tr>
<tr>
<td>Encoding text (<code>TextEncoder</code>)</td>
<td class="num yes">4.7 ms</td><td class="num">39.8 ms</td><td class="num">6.2 ms</td>
<td>8x faster than Node, ahead of Bun too</td>
</tr>
<tr>
<td>Worst single GC-style pause</td>
<td class="num yes">0.01 ms</td><td class="num">0.28 ms</td><td class="num">3.13 ms</td>
<td>No JIT means no warm-up stalls. The most predictable of the three, which is what a real-time or low-latency workload actually needs</td>
</tr>
<tr>
<td>Firing many event listeners</td>
<td class="num">6.7 ms</td><td class="num yes">5.4 ms</td><td class="num">9.2 ms</td>
<td>The one row Node wins, narrowly. Its JIT inlines the hot path in a way an interpreter structurally can't</td>
</tr>
<tr>
<td>A megabyte of JSON in and out</td>
<td class="num">48.0 ms</td><td class="num">29.3 ms</td><td class="num yes">24.8 ms</td>
<td>Bun's row, and Node is ahead too. Parsing is C in all three; what surrounds it is not</td>
</tr>
</tbody>
</table>
</div>
<p style="color:var(--ink-soft); font-size:13px; margin: 10px 0 30px;">
All six rows, every runtime, same machine, same workload, run side by side by
<code>benchmarks/wintertc/run.sh</code> in the repo. Nothing here is cherry-picked
or estimated.
</p>
<div class="grid-2">
<div class="card">
<h3>WinterTC surface <span class="badge">62 / 62</span></h3>
<p>Every name in the Minimum Common API except WebAssembly:
<code>fetch</code>, <code>Sxn.serve</code> (HTTP, SSE, WebSocket upgrade), Web
Streams including BYOB reads and the compression streams,
<code>URLPattern</code>, Web Crypto, <code>structuredClone</code>. This half
travels wherever the engine is embedded. It's not tied to Node emulation.</p>
</div>
<div class="card">
<h3>Node compatibility <a class="badge" href="docs/node/">superset ↗</a></h3>
<p>CommonJS, <code>node:</code> builtins, and <code>.node</code> native addons over
120 Node-API entry points — enough that <code>next-swc</code>, the 130 MB
Rust binary Next.js compiles JSX with, loads and runs. Weak references and the
old V8 <code>NODE_MODULE</code> interface are the two things that are not there.</p>
</div>
<div class="card">
<h3>Calling native code</h3>
<p><code>Sxn.ffi</code> calls a C function through libffi. It's an engine capability,
not a Node one. The design reasoning for that split, and what it means for
mobile, is in <a href="docs/native/">spec/NATIVE.md</a>.</p>
</div>
<div class="card">
<h3>Precompiled bytecode</h3>
<p><code>sxn compile app.js</code> writes a <code>.sxbc</code> file that skips
parsing entirely on later runs, measured 12% faster on a one-line script, 37%
on a 618 KB generated file. <code>--compile-cache</code> does the same thing
automatically, on every launch. <a href="docs/bytecode/">spec/BYTECODE.md</a>.</p>
</div>
<div class="card">
<h3>Memory that comes back</h3>
<p>A long-running server can build up garbage its own allocator never gets a
chance to collect. This one sweeps it up while the server sits idle between
requests — 171 MB reclaimed in one pass in testing, with no request
held up to do it. Call <code>Sxn.gc()</code> to sweep on demand, or pass
<code>--no-idle-gc</code> to turn the automatic sweep off.</p>
</div>
</div>
<p style="margin-top:26px; color:var(--ink-mid); font-size:14.5px; max-width:66ch;">
Every number above is measured, not estimated. The full benchmark methodology,
both test machines' specs, and the interpreter-level optimizations behind them are in
the <a href="docs/readme/">project README</a>.
</p>
</section>
<section id="contributing">
<h2 class="h">Contributing</h2>
<p class="lede-s">
This project is young, opinionated in places, and actively looking for people who'll
push back on those opinions. If something here reads as "obviously the right call" and
you don't think it is, that's exactly the kind of issue worth opening.
</p>
<div class="grid-2">
<div class="card">
<h3>What's genuinely open</h3>
<ul class="check">
<li>The shape and scope of <code>safe</code>/<code>unsafe</code></li>
<li>Which TypeScript forms get real support next</li>
<li>Ownership and borrow-checking rules and their error messages</li>
<li><code>node:</code> and WinterTC coverage priorities</li>
<li>Naming, ergonomics, anything that reads as a mistake</li>
</ul>
</div>
<div class="card">
<h3>What isn't</h3>
<ul class="plain">
<li>Adding a JIT, or anything else that generates machine code at runtime.
iOS won't grant that entitlement to a third-party app, and running there is the point</li>
</ul>
<p style="margin-top:14px;">
Everything above is a real conversation. That one line is a constraint the project
is built around, not a preference.
</p>
</div>
</div>
<p style="margin-top:8px;">
Ideological disagreement is welcome too. If you think the ownership model is solving
the wrong problem, or that the safe/unsafe split should work differently, open an issue
and make the case. The specs in <code>spec/</code> are the actual design surface of this
project, not settled history:
</p>
<ul class="plain" style="margin-bottom: 20px;">
<li><a href="docs/language/">spec/LANGUAGE.md</a>: the language contract</li>
<li><a href="docs/abi/">spec/ABI.md</a>: the native/JS boundary</li>
<li><a href="docs/implementation/">spec/IMPLEMENTATION.md</a>: what's real today, and what isn't yet</li>
<li><a href="docs/runtime/">spec/RUNTIME.md</a> & <a href="docs/node/">spec/NODE.md</a>: the two compatibility surfaces</li>
</ul>
<div class="cta-row">
<a class="btn btn-primary" href="{{REPO_URL}}/issues/new">Open an issue</a>
<a class="btn btn-ghost" href="{{REPO_URL}}/pulls">See open pull requests</a>
</div>
</section>
<footer>
<div class="row">
<p>SxfeScript / ArcSX · the docs pages are generated from the repo's own markdown.</p>
<div class="links">
<a href="{{REPO_URL}}">Source</a>
<a href="docs/">Docs</a>
<a href="llms.txt">llms.txt</a>
<a href="{{REPO_URL}}/issues">Issues</a>
</div>
</div>
</footer>
</div>
<script>
(function () {
var tabs = document.querySelectorAll(".install-tab");
var cmds = document.querySelectorAll("pre.install-cmd");
function show(os) {
tabs.forEach(function (t) { t.classList.toggle("active", t.dataset.os === os); t.setAttribute("aria-selected", t.dataset.os === os); });
cmds.forEach(function (c) { c.hidden = c.dataset.os !== os; });
}
tabs.forEach(function (t) { t.addEventListener("click", function () { show(t.dataset.os); }); });
if (/Win/.test(navigator.platform || navigator.userAgent)) show("windows");
// Every example block gets a copy button too, so the button is attached
// here rather than written out four more times in the markup. The output
// blocks are excluded: nobody wants to copy a program's output.
document.querySelectorAll(".ex-panel pre:not(.ex-out)").forEach(function (pre) {
var btn = document.createElement("button");
btn.type = "button";
btn.className = "copy-btn";
btn.setAttribute("aria-label", "Copy code");
pre.appendChild(btn);
});
document.querySelectorAll(".copy-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
var code = btn.parentNode.querySelector("code").textContent;
navigator.clipboard.writeText(code).then(function () {
btn.classList.add("copied");
setTimeout(function () { btn.classList.remove("copied"); }, 1800);
});
});
});
var exTabs = document.querySelectorAll(".ex-tab");
var exPanels = document.querySelectorAll(".ex-panel");
exTabs.forEach(function (t) {
t.addEventListener("click", function () {
exTabs.forEach(function (o) {
o.classList.toggle("active", o === t);
o.setAttribute("aria-selected", o === t);
});
exPanels.forEach(function (p) { p.hidden = p.dataset.ex !== t.dataset.ex; });
});
});
})();
</script>
</body>
</html>