tosijs-ui 1.6.21 → 1.7.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,6 +18,45 @@ export declare function contextVarName(key: string): string;
18
18
  * the AsyncFunction body.
19
19
  */
20
20
  export declare function rewriteImports(code: string, contextKeys: string[]): string;
21
+ /**
22
+ * Blank out the CONTENTS of strings, template literals, comments and regex literals,
23
+ * preserving length and newlines so every index still lines up with the original.
24
+ *
25
+ * The scanner below counts brackets and matches `^const` over raw text, which made it
26
+ * wrong in two ways that both failed silently:
27
+ *
28
+ * const label = 'Items (' → an unbalanced '(' inside a string ran the depth
29
+ * counter away, swallowing the REST OF THE FILE, so
30
+ * every later binding vanished from completions.
31
+ * `const in a template` → matched at column 0 inside a template literal and
32
+ * yielded the name `in`, and the epilogue then emitted
33
+ * `try{__tosiScope.in=in}` — a PARSE error, which no
34
+ * try/catch can absorb, killing the whole example.
35
+ *
36
+ * Identifiers never live inside literals, so masking loses nothing we want.
37
+ */
38
+ export declare function maskLiterals(code: string): string;
39
+ /**
40
+ * Best-effort list of the identifiers a snippet binds at the TOP level (column 0, so
41
+ * genuinely function-scope, not inside a block). Handles `const/let/var` — including
42
+ * MULTI-LINE destructuring, MULTIPLE declarators (`const a = 1, b = 2`), nested
43
+ * patterns, aliases, defaults and rest — plus `function`/`class` declarations.
44
+ *
45
+ * This is a scanner, not a parse. tjs-lang already owns an acorn-based
46
+ * `collectScopeSymbols()` that does this properly, but it has no public export
47
+ * (filed upstream); when it lands, drive this off it. Meanwhile imperfect is safe:
48
+ * the capture epilogue guards every name individually, so an over- or under-match
49
+ * degrades completions rather than breaking the example.
50
+ */
51
+ export declare function extractTopLevelBindingNames(code: string): string[];
52
+ /**
53
+ * Build an epilogue that captures the values of `names` (as they stand at the end
54
+ * of a run) into `captureVar(scopeObject)`. Each binding is read behind its own
55
+ * try/catch so a stale/over-matched name can't abort the whole capture, and the
56
+ * whole thing is a trailing block so it never shadows user code. Returns '' when
57
+ * there's nothing to capture.
58
+ */
59
+ export declare function buildScopeCapture(names: string[], captureVar: string): string;
21
60
  /**
22
61
  * Execute code as an async function with injected context
23
62
  */
@@ -41,6 +41,302 @@ export function rewriteImports(code, contextKeys) {
41
41
  }
42
42
  return result;
43
43
  }
44
+ const OPEN = '([{';
45
+ const CLOSE = ')]}';
46
+ /** Index of the first `ch` at bracket depth 0, or -1. */
47
+ function indexAtDepth0(src, ch) {
48
+ let depth = 0;
49
+ for (let i = 0; i < src.length; i++) {
50
+ const c = src[i];
51
+ if (OPEN.includes(c))
52
+ depth++;
53
+ else if (CLOSE.includes(c))
54
+ depth--;
55
+ else if (c === ch && depth === 0) {
56
+ if (ch === '=') {
57
+ // Only a bare assignment counts — skip ==, =>, <=, >=, !=.
58
+ const next = src[i + 1];
59
+ const prev = src[i - 1];
60
+ if (next === '=' || next === '>' || (prev && '=!<>'.includes(prev)))
61
+ continue;
62
+ }
63
+ return i;
64
+ }
65
+ }
66
+ return -1;
67
+ }
68
+ /** Split on `,` at bracket depth 0, so nested commas (`{ a: { b, c } }`) don't split. */
69
+ function splitAtDepth0(src) {
70
+ const parts = [];
71
+ let depth = 0;
72
+ let start = 0;
73
+ for (let i = 0; i < src.length; i++) {
74
+ const c = src[i];
75
+ if (OPEN.includes(c))
76
+ depth++;
77
+ else if (CLOSE.includes(c))
78
+ depth--;
79
+ else if (c === ',' && depth === 0) {
80
+ parts.push(src.slice(start, i));
81
+ start = i + 1;
82
+ }
83
+ }
84
+ parts.push(src.slice(start));
85
+ return parts.filter((p) => p.trim() !== '');
86
+ }
87
+ /**
88
+ * Blank out the CONTENTS of strings, template literals, comments and regex literals,
89
+ * preserving length and newlines so every index still lines up with the original.
90
+ *
91
+ * The scanner below counts brackets and matches `^const` over raw text, which made it
92
+ * wrong in two ways that both failed silently:
93
+ *
94
+ * const label = 'Items (' → an unbalanced '(' inside a string ran the depth
95
+ * counter away, swallowing the REST OF THE FILE, so
96
+ * every later binding vanished from completions.
97
+ * `const in a template` → matched at column 0 inside a template literal and
98
+ * yielded the name `in`, and the epilogue then emitted
99
+ * `try{__tosiScope.in=in}` — a PARSE error, which no
100
+ * try/catch can absorb, killing the whole example.
101
+ *
102
+ * Identifiers never live inside literals, so masking loses nothing we want.
103
+ */
104
+ export function maskLiterals(code) {
105
+ const out = code.split('');
106
+ const blank = (from, to) => {
107
+ for (let i = from; i < to && i < out.length; i++) {
108
+ if (out[i] !== '\n')
109
+ out[i] = ' ';
110
+ }
111
+ };
112
+ // A `/` starts a regex (not division) only where a value can't precede it.
113
+ const regexAllowedAfter = /[=(,:[!&|?{};+\-*%~^<>]$/;
114
+ let i = 0;
115
+ while (i < code.length) {
116
+ const c = code[i];
117
+ const next = code[i + 1];
118
+ if (c === '/' && next === '/') {
119
+ const end = code.indexOf('\n', i);
120
+ blank(i, end === -1 ? code.length : end);
121
+ i = end === -1 ? code.length : end;
122
+ }
123
+ else if (c === '/' && next === '*') {
124
+ const end = code.indexOf('*/', i + 2);
125
+ blank(i, end === -1 ? code.length : end + 2);
126
+ i = end === -1 ? code.length : end + 2;
127
+ }
128
+ else if (c === "'" || c === '"') {
129
+ let j = i + 1;
130
+ while (j < code.length && code[j] !== c) {
131
+ if (code[j] === '\\')
132
+ j++;
133
+ if (code[j] === '\n')
134
+ break; // unterminated — don't run away
135
+ j++;
136
+ }
137
+ blank(i + 1, j); // keep the quotes — see NB below
138
+ i = j + 1;
139
+ }
140
+ else if (c === '`') {
141
+ // Template literal: skip to the matching backtick, stepping over ${…} (which can
142
+ // nest braces and contain more backticks).
143
+ let j = i + 1;
144
+ let braces = 0;
145
+ while (j < code.length) {
146
+ if (code[j] === '\\')
147
+ j += 2;
148
+ else if (braces === 0 && code[j] === '`')
149
+ break;
150
+ else if (code[j] === '$' && code[j + 1] === '{') {
151
+ braces++;
152
+ j += 2;
153
+ }
154
+ else if (braces > 0 && code[j] === '{') {
155
+ braces++;
156
+ j++;
157
+ }
158
+ else if (braces > 0 && code[j] === '}') {
159
+ braces--;
160
+ j++;
161
+ }
162
+ else
163
+ j++;
164
+ }
165
+ blank(i + 1, j); // keep the backticks
166
+ i = j + 1;
167
+ }
168
+ else if (c === '/') {
169
+ const before = code.slice(0, i).replace(/\s+$/, '');
170
+ if (before === '' || regexAllowedAfter.test(before)) {
171
+ let j = i + 1;
172
+ let inClass = false;
173
+ while (j < code.length) {
174
+ if (code[j] === '\\')
175
+ j++;
176
+ else if (code[j] === '[')
177
+ inClass = true;
178
+ else if (code[j] === ']')
179
+ inClass = false;
180
+ else if (code[j] === '/' && !inClass)
181
+ break;
182
+ else if (code[j] === '\n')
183
+ break; // unterminated — it was division after all
184
+ j++;
185
+ }
186
+ blank(i + 1, j); // keep the slashes
187
+ i = j + 1;
188
+ }
189
+ else
190
+ i++; // division
191
+ }
192
+ else
193
+ i++;
194
+ }
195
+ return out.join('');
196
+ }
197
+ // Reserved words are identifier-SHAPED but can't be read as one: emitting
198
+ // `try{__tosiScope.in=in}` is a SyntaxError that no try/catch absorbs, and it takes the
199
+ // whole example down with it. Masking should already prevent these from being matched;
200
+ // this is the belt to that suspenders.
201
+ const RESERVED = new Set(('break case catch class const continue debugger default delete do else enum export ' +
202
+ 'extends false finally for function if import in instanceof new null return super ' +
203
+ 'switch this throw true try typeof var void while with yield let static implements ' +
204
+ 'interface package private protected public await').split(' '));
205
+ const isCapturableName = (name) => /^[A-Za-z_$][\w$]*$/.test(name) && !RESERVED.has(name);
206
+ /** Pull the bound identifiers out of one declaration's left-hand side. */
207
+ function patternNames(lhs) {
208
+ lhs = lhs.trim();
209
+ if (!lhs)
210
+ return [];
211
+ if (/^[{[]/.test(lhs)) {
212
+ // Object/array destructuring. Split depth-0 commas (NOT a naive `split(',')`,
213
+ // which shreds `{ a: { b, c } }`), then per element: drop a rest `...`, take the
214
+ // TARGET of `key: target` (the target is what's bound, and may itself be a
215
+ // pattern), drop any `= default`, and recurse into nested patterns.
216
+ const names = [];
217
+ for (let part of splitAtDepth0(lhs.slice(1, -1))) {
218
+ part = part.trim().replace(/^\.\.\./, '');
219
+ if (!part)
220
+ continue;
221
+ const colon = indexAtDepth0(part, ':');
222
+ if (colon !== -1)
223
+ part = part.slice(colon + 1).trim();
224
+ const eq = indexAtDepth0(part, '=');
225
+ if (eq !== -1)
226
+ part = part.slice(0, eq).trim();
227
+ if (/^[{[]/.test(part)) {
228
+ names.push(...patternNames(part));
229
+ }
230
+ else {
231
+ const m = part.match(/^[A-Za-z_$][\w$]*/);
232
+ if (m)
233
+ names.push(m[0]);
234
+ }
235
+ }
236
+ return names;
237
+ }
238
+ const m = lhs.match(/^[A-Za-z_$][\w$]*/);
239
+ return m ? [m[0]] : [];
240
+ }
241
+ /**
242
+ * Read a `const/let/var` declaration starting at `from` — across newlines, so a
243
+ * wrapped destructure (`const {\n app,\n} = …`) is captured whole. Ends at a depth-0
244
+ * `;`, or at a newline where the statement is plainly complete (the previous
245
+ * non-space char isn't an operator/comma continuing it). Anything unbalanced keeps
246
+ * reading, because depth > 0.
247
+ */
248
+ function readDeclaration(code, from) {
249
+ let depth = 0;
250
+ let i = from;
251
+ for (; i < code.length; i++) {
252
+ const c = code[i];
253
+ if (OPEN.includes(c))
254
+ depth++;
255
+ else if (CLOSE.includes(c))
256
+ depth--;
257
+ else if (depth === 0 && c === ';')
258
+ break;
259
+ else if (depth === 0 && c === '\n') {
260
+ const soFar = code.slice(from, i).trimEnd();
261
+ const last = soFar[soFar.length - 1];
262
+ if (last !== undefined && !',=+-*/%&|?:<>!~^'.includes(last))
263
+ break;
264
+ }
265
+ }
266
+ // FAIL SAFE. Running to EOF with depth > 0 means the brackets never balanced, and
267
+ // consuming the tail would silently drop every binding after this point (the old
268
+ // behaviour, and the whole bug). Masking should make this unreachable; if it happens
269
+ // anyway, give up on THIS declaration only — take the first line and let the scan
270
+ // resume — rather than swallowing the rest of the example.
271
+ if (i >= code.length && depth > 0) {
272
+ const eol = code.indexOf('\n', from);
273
+ return code.slice(from, eol === -1 ? code.length : eol);
274
+ }
275
+ return code.slice(from, i);
276
+ }
277
+ /**
278
+ * Best-effort list of the identifiers a snippet binds at the TOP level (column 0, so
279
+ * genuinely function-scope, not inside a block). Handles `const/let/var` — including
280
+ * MULTI-LINE destructuring, MULTIPLE declarators (`const a = 1, b = 2`), nested
281
+ * patterns, aliases, defaults and rest — plus `function`/`class` declarations.
282
+ *
283
+ * This is a scanner, not a parse. tjs-lang already owns an acorn-based
284
+ * `collectScopeSymbols()` that does this properly, but it has no public export
285
+ * (filed upstream); when it lands, drive this off it. Meanwhile imperfect is safe:
286
+ * the capture epilogue guards every name individually, so an over- or under-match
287
+ * degrades completions rather than breaking the example.
288
+ */
289
+ export function extractTopLevelBindingNames(code) {
290
+ // Scan MASKED source: literals and comments are blanked (same length, same newlines),
291
+ // so a stray bracket or a `const` inside a string can't derail the scan. Indices still
292
+ // line up with `code`, and identifiers only ever live outside literals.
293
+ const masked = maskLiterals(code);
294
+ const names = new Set();
295
+ const declRe = /^(?:export\s+)?(const|let|var|function\*?|class)\s+/gm;
296
+ let m;
297
+ while ((m = declRe.exec(masked)) !== null) {
298
+ const kind = m[1];
299
+ const from = m.index + m[0].length;
300
+ if (kind === 'class' || kind.startsWith('function')) {
301
+ const id = masked.slice(from).match(/^[A-Za-z_$][\w$]*/);
302
+ if (id)
303
+ names.add(id[0]);
304
+ continue;
305
+ }
306
+ const decl = readDeclaration(masked, from);
307
+ // Each depth-0 comma starts another declarator (`const a = 1, b = 2`).
308
+ for (const declarator of splitAtDepth0(decl)) {
309
+ const eq = indexAtDepth0(declarator, '=');
310
+ const lhs = eq === -1 ? declarator : declarator.slice(0, eq);
311
+ for (const n of patternNames(lhs))
312
+ names.add(n);
313
+ }
314
+ // Don't rescan inside the declaration we just consumed.
315
+ declRe.lastIndex = from + decl.length;
316
+ }
317
+ return [...names].filter(isCapturableName);
318
+ }
319
+ /**
320
+ * Build an epilogue that captures the values of `names` (as they stand at the end
321
+ * of a run) into `captureVar(scopeObject)`. Each binding is read behind its own
322
+ * try/catch so a stale/over-matched name can't abort the whole capture, and the
323
+ * whole thing is a trailing block so it never shadows user code. Returns '' when
324
+ * there's nothing to capture.
325
+ */
326
+ export function buildScopeCapture(names, captureVar) {
327
+ // Filter HERE too, not just at extraction. Every other guard in this file is a
328
+ // runtime try/catch, but an unreadable name (`in`, `class`, `2bad`) makes the
329
+ // epilogue itself unparseable — and a SyntaxError takes the whole example down
330
+ // before a single line of it runs. This is the last line of defence, so it must not
331
+ // trust its caller.
332
+ const capturable = names.filter(isCapturableName);
333
+ if (capturable.length === 0)
334
+ return '';
335
+ const assigns = capturable
336
+ .map((n) => `try{__tosiScope.${n}=${n}}catch(_e){}`)
337
+ .join('');
338
+ return `\n;{const __tosiScope={};${assigns}${captureVar}(__tosiScope)}`;
339
+ }
44
340
  /**
45
341
  * Execute code as an async function with injected context
46
342
  */
@@ -61,7 +357,7 @@ export async function executeCode(code, context, transform) {
61
357
  // self-contained) and is loaded only for `ts` examples; from-ts in turn fetches
62
358
  // the TypeScript compiler lazily at runtime, so tsc is never pulled in until a TS
63
359
  // example actually transforms. Pinned to match the dev dep.
64
- const TJS_VERSION = '0.9.0';
360
+ const TJS_VERSION = '0.9.1';
65
361
  // Where to fetch a tjs-lang browser bundle from, in priority order:
66
362
  // 1. SAME-ORIGIN — the doc-site build copies the bundles next to the iife and
67
363
  // sets `__TJS_LOCAL_BASE`, so the transpiler ships in lockstep with the page,
@@ -71,7 +367,8 @@ const TJS_VERSION = '0.9.0';
71
367
  // it (minutes–hours) and any one can blip, so we try several. The module-cache
72
368
  // service worker caches all three hosts.
73
369
  function bundleUrls(file) {
74
- const localBase = globalThis.__TJS_LOCAL_BASE;
370
+ const localBase = globalThis
371
+ .__TJS_LOCAL_BASE;
75
372
  return [
76
373
  ...(typeof localBase === 'string' ? [`${localBase}${file}`] : []),
77
374
  `https://cdn.jsdelivr.net/npm/tjs-lang@${TJS_VERSION}/dist/${file}`,
@@ -119,7 +416,9 @@ async function loadTjsTestApiImpl() {
119
416
  for (const load of sources) {
120
417
  try {
121
418
  const m = (await load());
122
- if (m && typeof m.extractTests === 'function' && typeof m.testUtils === 'string') {
419
+ if (m &&
420
+ typeof m.extractTests === 'function' &&
421
+ typeof m.testUtils === 'string') {
123
422
  return { extractTests: m.extractTests, testUtils: m.testUtils };
124
423
  }
125
424
  }
@@ -202,7 +501,9 @@ export async function loadTransform(dialect = 'js') {
202
501
  if (dialect === 'ts') {
203
502
  // async: fromTS lazy-loads the TypeScript compiler on first use.
204
503
  return (async () => {
205
- const tjsSource = fromTS ? (await fromTS(code, { emitTJS: true })).code : code;
504
+ const tjsSource = fromTS
505
+ ? (await fromTS(code, { emitTJS: true })).code
506
+ : code;
206
507
  const result = {
207
508
  code: tjs(tjsSource, { dialect: 'tjs', runTests: false }).code,
208
509
  };
@@ -227,6 +227,7 @@ export declare class LiveExample extends Component<ExampleParts> {
227
227
  prefix: string;
228
228
  storageKey: string;
229
229
  context: ExampleContext;
230
+ private capturedScope;
230
231
  uuid: string;
231
232
  remoteId: string;
232
233
  private remoteSync?;
@@ -262,6 +263,15 @@ export declare class LiveExample extends Component<ExampleParts> {
262
263
  private renderTjsTests;
263
264
  private computeGeneratedJs;
264
265
  private ensureProductTabs;
266
+ private captureScope;
267
+ /**
268
+ * Live bindings for tjs runtime-value autocomplete: the example's context modules
269
+ * (keyed by the identifier the rewritten code uses, e.g. `tosijs`, `tosijsui`),
270
+ * the currently-rendered `preview` element, and the latest run's top-level locals
271
+ * (so `const app = tosi(…)` gives real `app.` / `app.items.` completions, proxy
272
+ * members and all). Read lazily on each completion, so it reflects the latest run.
273
+ */
274
+ private liveBindings;
265
275
  updateUndo: () => void;
266
276
  private updateTestResultsVisibility;
267
277
  undo: () => void;
@@ -162,10 +162,22 @@ function step(! px: Float32Array, py: Float32Array, vx: Float32Array, vy: Float3
162
162
  }
163
163
  }
164
164
 
165
- const W = 480, H = 300, cols = 100, rows = 60, N = cols * rows
166
- const px = new Float32Array(N), py = new Float32Array(N)
167
- const vx = new Float32Array(N), vy = new Float32Array(N)
168
- const hx = new Float32Array(N), hy = new Float32Array(N)
165
+ // 100k particles, not 6k. At 6k each step took ~0.02ms — right at the resolution floor
166
+ // of performance.now(), so the readout was mostly measuring the timer. At this size both
167
+ // paths land in the hundreds of microseconds and the comparison actually means something.
168
+ const W = 480, H = 300, cols = 400, rows = 250, N = cols * rows
169
+
170
+ // Allocate the particle arrays INSIDE wasm memory. This is the whole ballgame: if a
171
+ // typed array's buffer isn't the wasm memory, every call copies it in and out again —
172
+ // six 6,000-float arrays per frame — and you end up timing memcpy, not SIMD. (With
173
+ // plain `new Float32Array(N)` this demo measured ~4x SLOWER than its own JS fallback.)
174
+ // `wasmBuffer` is a bump allocator handing out views into the shared wasm memory, so
175
+ // the wrapper passes a byte offset and copies nothing. Fall back gracefully if the
176
+ // wasm block didn't compile, so the JS twin still runs.
177
+ const f32 = (n) => globalThis.wasmBuffer ? wasmBuffer(Float32Array, n) : new Float32Array(n)
178
+ const px = f32(N), py = f32(N)
179
+ const vx = f32(N), vy = f32(N)
180
+ const hx = f32(N), hy = f32(N)
169
181
  for (let i = 0; i < N; i++) {
170
182
  const c = i % cols, r = (i / cols) | 0
171
183
  hx[i] = (c + 0.5) * W / cols
@@ -180,6 +192,15 @@ canvas.height = H
180
192
  canvas.style.cssText = 'width:100%;max-width:480px;border-radius:8px;display:block;background:#0b0e14;touch-action:none;cursor:crosshair'
181
193
  const ctx = canvas.getContext('2d')
182
194
 
195
+ // Plot straight into an ImageData buffer (one 32-bit store per particle) instead of
196
+ // 100k fillRect() calls — otherwise the DRAW would dominate and we'd be benchmarking
197
+ // canvas, not the kernel.
198
+ const img = ctx.createImageData(W, H)
199
+ const pix = new Uint32Array(img.data.buffer)
200
+ const BG = 0xff140e0b // #0b0e14, little-endian ABGR
201
+ const TEAL = 0xffc5d14f // #4fd1c5
202
+ const AMBER = 0xff55adf6 // #f6ad55
203
+
183
204
  let tx = W / 2, ty = H / 2, idle = 0, frame = 0
184
205
  canvas.addEventListener('pointermove', (e) => {
185
206
  const r = canvas.getBoundingClientRect()
@@ -188,21 +209,35 @@ canvas.addEventListener('pointermove', (e) => {
188
209
  idle = 0
189
210
  })
190
211
 
191
- // tjs registers the compiled function as globalThis.__tjs_wasm_0; nulling it makes
192
- // step() fall to its JS twin. (A public WASM on/off toggle would be tidier — noted
193
- // in TJS-FEEDBACK.md.)
194
- let useWasm = true, savedWasm = null
212
+ // tjs-lang 0.9.1+ gates every wasm call on `globalThis.__tjs_wasm_enabled`, so this
213
+ // flips the kernel between WASM and its JS twin without touching tjs internals.
214
+ // (Don't reach for `__tjs_wasm_0` — it's private AND index-keyed per transpile, so
215
+ // a second wasm example on the page registers the same name and you'd clobber it.)
216
+ let useWasm = true
195
217
  const btn = document.createElement('button')
196
218
  btn.style.cssText = 'margin:8px 0;padding:6px 12px;border-radius:6px;cursor:pointer'
197
219
  const readout = document.createElement('p')
220
+ let acc = 0, frames = 0
198
221
  function applyMode() {
199
- if (useWasm && savedWasm) { globalThis.__tjs_wasm_0 = savedWasm; savedWasm = null }
200
- if (!useWasm && globalThis.__tjs_wasm_0) { savedWasm = globalThis.__tjs_wasm_0; globalThis.__tjs_wasm_0 = null }
222
+ globalThis.__tjs_wasm_enabled = useWasm
223
+ acc = 0; frames = 0 // don't average across a mode switch
201
224
  btn.textContent = useWasm ? '⚡ WebAssembly SIMD — click for JavaScript' : '🐢 JavaScript — click for WASM SIMD'
202
225
  }
203
226
  btn.onclick = () => { useWasm = !useWasm; applyMode() }
204
227
 
205
- let acc = 0, frames = 0
228
+ // Warm BOTH paths before timing anything. The JS twin never executes until you click
229
+ // over to it, so an un-warmed comparison times cold, un-JITed JavaScript and flatters
230
+ // WASM (it read ~2x too good). A warmed V8 does this kernel in ~0.024 ms/step; the
231
+ // zero-copy SIMD kernel does ~0.015 — a real but modest ~1.6x, which is the honest
232
+ // number this demo should show.
233
+ function warmUp() {
234
+ for (const on of [false, true]) {
235
+ globalThis.__tjs_wasm_enabled = on
236
+ for (let i = 0; i < 150; i++) step(px, py, vx, vy, hx, hy, N, W / 2, H / 2)
237
+ }
238
+ // the warm-up perturbed the particles — put them back on the grid
239
+ for (let i = 0; i < N; i++) { px[i] = hx[i]; py[i] = hy[i]; vx[i] = 0; vy[i] = 0 }
240
+ }
206
241
  function loop() {
207
242
  if (!document.body.contains(canvas)) return // example removed → stop
208
243
  frame++
@@ -215,10 +250,15 @@ function loop() {
215
250
  step(px, py, vx, vy, hx, hy, N, tx, ty)
216
251
  acc += performance.now() - t0
217
252
  frames++
218
- ctx.fillStyle = 'rgba(11,14,20,0.3)'
219
- ctx.fillRect(0, 0, W, H)
220
- ctx.fillStyle = useWasm ? '#4fd1c5' : '#f6ad55'
221
- for (let i = 0; i < N; i++) ctx.fillRect(px[i], py[i], 1.6, 1.6)
253
+
254
+ pix.fill(BG)
255
+ const dot = useWasm ? TEAL : AMBER
256
+ for (let i = 0; i < N; i++) {
257
+ const x = px[i] | 0, y = py[i] | 0
258
+ if (x >= 0 && x < W && y >= 0 && y < H) pix[y * W + x] = dot
259
+ }
260
+ ctx.putImageData(img, 0, 0)
261
+
222
262
  if (frames >= 20) {
223
263
  readout.textContent = N.toLocaleString() + ' particles · ' + (acc / frames).toFixed(3) +
224
264
  ' ms/step (' + (useWasm ? 'WASM SIMD' : 'JS') + ') · move the pointer'
@@ -229,12 +269,11 @@ function loop() {
229
269
  }
230
270
 
231
271
  ;(async () => {
232
- // The wasm bootstrap is async — wait for the compiled function before starting,
233
- // so the first frames run in WASM rather than the fallback.
234
- const t0 = performance.now()
235
- while (!globalThis.__tjs_wasm_0 && performance.now() - t0 < 3000) {
236
- await new Promise((r) => setTimeout(r, 0))
237
- }
272
+ // The wasm bootstrap is async. tjs-lang 0.9.1+ exposes an awaitable ready signal,
273
+ // so we start only once the kernel is instantiated — otherwise the first frames
274
+ // silently run the JS fallback while the button claims "WebAssembly SIMD".
275
+ await globalThis.__tjs_wasm_ready?.()
276
+ warmUp()
238
277
  applyMode()
239
278
  requestAnimationFrame(loop)
240
279
  })()
@@ -242,6 +281,17 @@ function loop() {
242
281
  preview.append(canvas, btn, readout)
243
282
  ```
244
283
 
284
+ ```test
285
+ // Guard the claim the button makes. A `wasm {}` block that fails to compile falls
286
+ // back to JS *silently*, so without this the demo can advertise "⚡ WebAssembly SIMD"
287
+ // while running the JS twin, and every test stays green.
288
+ test('the wasm kernel actually compiled (no silent fallback to JS)', async () => {
289
+ await globalThis.__tjs_wasm_ready?.()
290
+ expect(typeof globalThis.__tjs_wasm_ready).toBe('function')
291
+ expect(globalThis.__tjs_wasm_0).toBeTruthy()
292
+ })
293
+ ```
294
+
245
295
  ## CSS Isolation with `iframe`
246
296
 
247
297
  Add the `iframe` attribute to render the preview inside an iframe, giving the example
@@ -417,6 +467,9 @@ export class LiveExample extends Component {
417
467
  prefix = 'lx';
418
468
  storageKey = STORAGE_KEY;
419
469
  context = {};
470
+ // The example's top-level locals from the latest run, captured in-run for tjs
471
+ // runtime-value autocomplete (see `liveBindings`). Populated via `onScope`.
472
+ capturedScope = {};
420
473
  uuid = crypto.randomUUID();
421
474
  remoteId = '';
422
475
  remoteSync;
@@ -602,6 +655,11 @@ export class LiveExample extends Component {
602
655
  // put the source editor in the dialect's mode — so a `tjs` example gets
603
656
  // first-class tjs editing (highlighting + autocomplete), `ts` gets TypeScript.
604
657
  this.parts.js.setAttribute('name', this.dialect);
658
+ // Runtime-value autocomplete: give the tjs completion source the example's live
659
+ // bindings (context modules + the rendered preview) so it can suggest their REAL
660
+ // members — including tosijs proxy members that static analysis can't see. Set
661
+ // before `.mode` so the tjs extension loads with the config in one shot.
662
+ this.parts.js.tjsAutocomplete = { getLiveBindings: () => this.liveBindings() };
605
663
  this.parts.js.mode = this.dialect;
606
664
  this.jsOutEditor = codeEditor({
607
665
  name: 'JS',
@@ -619,6 +677,30 @@ export class LiveExample extends Component {
619
677
  this.jsOutEditor.value = this.lastGeneratedJs;
620
678
  this.renderTjsTests();
621
679
  }
680
+ // Capture the latest run's top-level locals (arrow property so `this` is bound
681
+ // when passed as execution's `onScope`).
682
+ captureScope = (scope) => {
683
+ this.capturedScope = scope;
684
+ };
685
+ /**
686
+ * Live bindings for tjs runtime-value autocomplete: the example's context modules
687
+ * (keyed by the identifier the rewritten code uses, e.g. `tosijs`, `tosijsui`),
688
+ * the currently-rendered `preview` element, and the latest run's top-level locals
689
+ * (so `const app = tosi(…)` gives real `app.` / `app.items.` completions, proxy
690
+ * members and all). Read lazily on each completion, so it reflects the latest run.
691
+ */
692
+ liveBindings() {
693
+ const bindings = {};
694
+ for (const [key, value] of Object.entries(this.context)) {
695
+ bindings[contextVarName(key)] = value;
696
+ }
697
+ const preview = this.parts.example?.querySelector('.preview');
698
+ if (preview)
699
+ bindings.preview = preview;
700
+ // Run locals last so they win over same-named context entries.
701
+ Object.assign(bindings, this.capturedScope);
702
+ return bindings;
703
+ }
622
704
  updateUndo = () => {
623
705
  const { activeTab } = this;
624
706
  const { undo, redo } = this.parts;
@@ -1201,6 +1283,7 @@ export class LiveExample extends Component {
1201
1283
  exampleElement: example,
1202
1284
  widgetsElement: exampleWidgets,
1203
1285
  onError,
1286
+ onScope: this.captureScope,
1204
1287
  });
1205
1288
  }
1206
1289
  else {
@@ -1214,6 +1297,7 @@ export class LiveExample extends Component {
1214
1297
  styleElement: styleEl,
1215
1298
  widgetsElement: exampleWidgets,
1216
1299
  onError,
1300
+ onScope: this.captureScope,
1217
1301
  });
1218
1302
  }
1219
1303
  if (this.persistToDom) {
@@ -15,7 +15,23 @@ export interface ExecutionOptions {
15
15
  context: ExampleContext;
16
16
  transform: TransformFn;
17
17
  onError?: (error: Error) => void;
18
+ /**
19
+ * Receives the example's top-level locals after a successful run, so tjs
20
+ * autocomplete can introspect the REAL values (e.g. a `const app = tosi(…)`
21
+ * proxy) the user just created. Captured in-run — no re-execution, so no
22
+ * doubled side effects.
23
+ */
24
+ onScope?: (scope: Record<string, unknown>) => void;
18
25
  }
26
+ /**
27
+ * Append a scope-capture epilogue to already-transformed example code when a
28
+ * consumer wants the run's locals. Returns the (possibly unchanged) code plus the
29
+ * extra context entry to inject. The epilogue no-ops if the example binds nothing.
30
+ */
31
+ export declare function withScopeCapture(transformedCode: string, onScope?: (scope: Record<string, unknown>) => void): {
32
+ code: string;
33
+ extraContext: Record<string, unknown>;
34
+ };
19
35
  /**
20
36
  * Execute code inline (directly in the page)
21
37
  */