tjs-lang 0.8.2 → 0.8.4
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.
- package/CLAUDE.md +5 -2
- package/demo/autocomplete.test.ts +37 -0
- package/demo/docs.json +701 -11
- package/demo/src/autocomplete.ts +40 -2
- package/demo/src/introspection-bridge.ts +140 -0
- package/demo/src/introspection-doc.test.ts +63 -0
- package/demo/src/playground-shared.ts +53 -0
- package/demo/src/tjs-playground.ts +21 -0
- package/dist/experiments/predicates/css.predicates.d.ts +13 -0
- package/dist/experiments/predicates/verify.d.ts +39 -0
- package/dist/index.js +101 -97
- package/dist/index.js.map +4 -4
- package/dist/src/lang/index.d.ts +2 -0
- package/dist/src/lang/predicate-schema.d.ts +39 -0
- package/dist/src/lang/predicate.d.ts +103 -0
- package/dist/src/lang/transpiler.d.ts +2 -0
- package/dist/src/vm/runtime.d.ts +22 -0
- package/dist/tjs-eval.js +29 -29
- package/dist/tjs-eval.js.map +3 -3
- package/dist/tjs-from-ts.js +1 -1
- package/dist/tjs-from-ts.js.map +2 -2
- package/dist/tjs-lang.js +86 -82
- package/dist/tjs-lang.js.map +4 -4
- package/dist/tjs-vm.js +45 -45
- package/dist/tjs-vm.js.map +3 -3
- package/editors/codemirror/ajs-language.ts +130 -44
- package/editors/codemirror/completion-source.test.ts +114 -0
- package/editors/introspect-value.test.ts +61 -0
- package/editors/introspect-value.ts +86 -0
- package/editors/scope-symbols.test.ts +113 -0
- package/editors/scope-symbols.ts +173 -0
- package/package.json +2 -1
- package/src/lang/index.ts +22 -0
- package/src/lang/predicate-schema.test.ts +97 -0
- package/src/lang/predicate-schema.ts +168 -0
- package/src/lang/predicate.test.ts +184 -0
- package/src/lang/predicate.ts +550 -0
- package/src/lang/runtime.test.ts +46 -0
- package/src/lang/suggest.test.ts +84 -0
- package/src/lang/transpiler.ts +26 -0
- package/src/runtime.test.ts +6 -6
- package/src/vm/atom-effects.test.ts +72 -0
- package/src/vm/atoms/batteries.ts +12 -0
- package/src/vm/equality.test.ts +59 -0
- package/src/vm/runtime.ts +77 -59
package/demo/src/autocomplete.ts
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
* Uses __tjs metadata for rich type information.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import {
|
|
9
|
+
collectScopeSymbols,
|
|
10
|
+
type ScopeSymbol,
|
|
11
|
+
} from '../../editors/scope-symbols'
|
|
12
|
+
|
|
8
13
|
export interface Completion {
|
|
9
14
|
/** The text to insert */
|
|
10
15
|
label: string
|
|
@@ -277,6 +282,40 @@ function isInTestBlock(source: string, position: number): boolean {
|
|
|
277
282
|
return testMatches.length > 0
|
|
278
283
|
}
|
|
279
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Completions from the scope-aware symbol model (acorn-based, destructuring-aware).
|
|
287
|
+
* Subsumes the old regex `extractFunctions`/`extractVariables`, which missed all
|
|
288
|
+
* destructuring. Falls back to the regex extractors only if parsing yields
|
|
289
|
+
* nothing (so the list never goes blank on pathological input).
|
|
290
|
+
*/
|
|
291
|
+
function completionsFromScope(source: string, position: number): Completion[] {
|
|
292
|
+
const symbols = collectScopeSymbols(source, position)
|
|
293
|
+
if (symbols.length === 0) {
|
|
294
|
+
return [...extractFunctions(source), ...extractVariables(source, position)]
|
|
295
|
+
}
|
|
296
|
+
return symbols.map((s) => scopeSymbolToCompletion(s))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function scopeSymbolToCompletion(s: ScopeSymbol): Completion {
|
|
300
|
+
const kind: Completion['kind'] =
|
|
301
|
+
s.kind === 'function' ? 'function' : 'variable'
|
|
302
|
+
// For a destructured binding, show where it came from — `h1 ∈ elements`.
|
|
303
|
+
const detail =
|
|
304
|
+
s.origin?.member && s.origin.expr
|
|
305
|
+
? `∈ ${s.origin.expr}`
|
|
306
|
+
: s.kind === 'import' && s.origin?.module
|
|
307
|
+
? `import '${s.origin.module}'`
|
|
308
|
+
: s.kind === 'parameter'
|
|
309
|
+
? 'parameter'
|
|
310
|
+
: undefined
|
|
311
|
+
return {
|
|
312
|
+
label: s.name,
|
|
313
|
+
kind,
|
|
314
|
+
detail,
|
|
315
|
+
snippet: kind === 'function' ? `${s.name}($1)` : undefined,
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
280
319
|
/**
|
|
281
320
|
* Extract function names from source
|
|
282
321
|
*/
|
|
@@ -418,8 +457,7 @@ export function getCompletions(ctx: CompletionContext): Completion[] {
|
|
|
418
457
|
completions = [
|
|
419
458
|
...TJS_KEYWORDS,
|
|
420
459
|
...RUNTIME_COMPLETIONS,
|
|
421
|
-
...
|
|
422
|
-
...extractVariables(source, position),
|
|
460
|
+
...completionsFromScope(source, position),
|
|
423
461
|
]
|
|
424
462
|
|
|
425
463
|
// Add metadata-based completions
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Introspection bridge — the runtime-truth half of the autocomplete plan.
|
|
3
|
+
*
|
|
4
|
+
* Chrome-console style: run the user's code in a hidden, disposable sandbox
|
|
5
|
+
* iframe and keep a live `eval` handle into its module scope. Member completion
|
|
6
|
+
* (`todoApp.items.` → the real array's members; `elements.` → tosijs's
|
|
7
|
+
* proxy-generated creators) then comes from the ACTUAL values, not a static
|
|
8
|
+
* model. The sandbox reuses the playground's run pipeline (transpile →
|
|
9
|
+
* rewriteImports → buildIframeDoc → SW-served URL) so imports resolve the same
|
|
10
|
+
* way the preview does.
|
|
11
|
+
*
|
|
12
|
+
* Lifecycle: `refresh(source)` rebuilds the sandbox (debounced by the caller, on
|
|
13
|
+
* statement boundaries) and caches the last good one — if the source doesn't
|
|
14
|
+
* transpile, the previous sandbox stays queryable. `members(path)` asks the
|
|
15
|
+
* sandbox to introspect a value, with a short timeout so the editor never hangs.
|
|
16
|
+
*/
|
|
17
|
+
import { tjs } from '../../src/lang'
|
|
18
|
+
import { rewriteImports, registerIframeContent } from './imports'
|
|
19
|
+
import { buildIframeDoc } from './playground-shared'
|
|
20
|
+
import type { IntrospectMember } from '../../editors/introspect-value'
|
|
21
|
+
|
|
22
|
+
export class IntrospectionBridge {
|
|
23
|
+
private iframe: HTMLIFrameElement
|
|
24
|
+
private ready = false
|
|
25
|
+
private lastSource: string | null = null
|
|
26
|
+
private pending = new Map<number, (members: IntrospectMember[]) => void>()
|
|
27
|
+
private nextId = 1
|
|
28
|
+
private readonly onMessage: (e: MessageEvent) => void
|
|
29
|
+
|
|
30
|
+
constructor() {
|
|
31
|
+
const iframe = document.createElement('iframe')
|
|
32
|
+
iframe.setAttribute('aria-hidden', 'true')
|
|
33
|
+
iframe.setAttribute('tabindex', '-1')
|
|
34
|
+
// Off-screen and inert, but still a real DOM so tosijs `elements` work.
|
|
35
|
+
iframe.style.cssText =
|
|
36
|
+
'position:absolute;width:1px;height:1px;left:-9999px;top:-9999px;border:0;visibility:hidden;pointer-events:none;'
|
|
37
|
+
document.body.appendChild(iframe)
|
|
38
|
+
this.iframe = iframe
|
|
39
|
+
|
|
40
|
+
this.onMessage = (e: MessageEvent) => {
|
|
41
|
+
const d = e.data
|
|
42
|
+
if (!d || e.source !== iframe.contentWindow) return
|
|
43
|
+
if (d.type === 'tjs-bridge-ready') {
|
|
44
|
+
this.ready = true
|
|
45
|
+
} else if (d.type === 'tjs-introspect-result') {
|
|
46
|
+
const resolve = this.pending.get(d.id)
|
|
47
|
+
if (resolve) {
|
|
48
|
+
this.pending.delete(d.id)
|
|
49
|
+
resolve(Array.isArray(d.members) ? d.members : [])
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
window.addEventListener('message', this.onMessage)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Rebuild the sandbox from the current source. No-op if unchanged or if the
|
|
58
|
+
* source doesn't transpile (keeps the last good sandbox). Caller should
|
|
59
|
+
* debounce this to statement boundaries.
|
|
60
|
+
*/
|
|
61
|
+
async refresh(source: string): Promise<void> {
|
|
62
|
+
if (source === this.lastSource) return
|
|
63
|
+
|
|
64
|
+
let code: string
|
|
65
|
+
try {
|
|
66
|
+
const result = tjs(source)
|
|
67
|
+
code = result.code
|
|
68
|
+
} catch {
|
|
69
|
+
return // not transpilable yet — keep the last good sandbox
|
|
70
|
+
}
|
|
71
|
+
this.lastSource = source
|
|
72
|
+
|
|
73
|
+
const rewritten = rewriteImports(code)
|
|
74
|
+
const importStatements: string[] = []
|
|
75
|
+
const body = rewritten.replace(
|
|
76
|
+
/^import\s+(?:.*?from\s+)?['"][^'"]+['"];?\s*$/gm,
|
|
77
|
+
(match) => {
|
|
78
|
+
importStatements.push(match)
|
|
79
|
+
return ''
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const doc = buildIframeDoc({
|
|
84
|
+
cssContent: '',
|
|
85
|
+
htmlContent: '',
|
|
86
|
+
importMapScript: '',
|
|
87
|
+
jsCode: body,
|
|
88
|
+
importStatements,
|
|
89
|
+
introspectionBridge: true,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
this.ready = false
|
|
93
|
+
// Reject in-flight queries against the old sandbox.
|
|
94
|
+
for (const resolve of this.pending.values()) resolve([])
|
|
95
|
+
this.pending.clear()
|
|
96
|
+
|
|
97
|
+
const sessionId = `tjs-introspect-${Date.now()}-${Math.random()
|
|
98
|
+
.toString(36)
|
|
99
|
+
.slice(2)}`
|
|
100
|
+
const registered = await registerIframeContent(sessionId, doc)
|
|
101
|
+
if (registered) {
|
|
102
|
+
this.iframe.src = `/iframe/${sessionId}`
|
|
103
|
+
} else {
|
|
104
|
+
const blob = new Blob([doc], { type: 'text/html' })
|
|
105
|
+
this.iframe.src = URL.createObjectURL(blob)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Introspect a dotted path in the live sandbox scope. Resolves to the value's
|
|
111
|
+
* real members, or `undefined` if the sandbox isn't ready / times out (so the
|
|
112
|
+
* caller can fall back to static completions).
|
|
113
|
+
*/
|
|
114
|
+
members(
|
|
115
|
+
path: string,
|
|
116
|
+
timeoutMs = 500
|
|
117
|
+
): Promise<IntrospectMember[] | undefined> {
|
|
118
|
+
const win = this.iframe.contentWindow
|
|
119
|
+
if (!this.ready || !win) return Promise.resolve(undefined)
|
|
120
|
+
|
|
121
|
+
const id = this.nextId++
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
const timer = setTimeout(() => {
|
|
124
|
+
this.pending.delete(id)
|
|
125
|
+
resolve(undefined)
|
|
126
|
+
}, timeoutMs)
|
|
127
|
+
this.pending.set(id, (members) => {
|
|
128
|
+
clearTimeout(timer)
|
|
129
|
+
resolve(members)
|
|
130
|
+
})
|
|
131
|
+
win.postMessage({ type: 'tjs-introspect', id, path }, '*')
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
dispose(): void {
|
|
136
|
+
window.removeEventListener('message', this.onMessage)
|
|
137
|
+
this.iframe.remove()
|
|
138
|
+
this.pending.clear()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
// buildIframeDoc reads location.origin (browser global) — stub it for headless.
|
|
4
|
+
;(globalThis as any).location ??= { origin: 'http://test' }
|
|
5
|
+
|
|
6
|
+
import { buildIframeDoc } from './playground-shared'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Structural guard for the introspection sandbox document. The critical
|
|
10
|
+
* invariant: user code must run at MODULE TOP LEVEL (not inside a try block) so
|
|
11
|
+
* its top-level `const`/`let` are module-scoped and the bridge's direct `eval`
|
|
12
|
+
* can see them. A regression here silently breaks `todoApp.` completion.
|
|
13
|
+
*/
|
|
14
|
+
describe('buildIframeDoc — introspection bridge document', () => {
|
|
15
|
+
const doc = buildIframeDoc({
|
|
16
|
+
cssContent: '',
|
|
17
|
+
htmlContent: '',
|
|
18
|
+
importMapScript: '',
|
|
19
|
+
jsCode: 'const todoApp = { items: [] }',
|
|
20
|
+
importStatements: ["import { tosi } from 'https://cdn/tosijs'"],
|
|
21
|
+
introspectionBridge: true,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('runs user code at module top level — NOT inside a try block', () => {
|
|
25
|
+
expect(doc).toContain('const todoApp = { items: [] }')
|
|
26
|
+
// The real invariant: after the bridge is installed, the user code runs with
|
|
27
|
+
// no try-wrap between (a block would re-scope its declarations away from
|
|
28
|
+
// eval). (The injected introspector has its own internal try/catch — that's
|
|
29
|
+
// fine and lives before the ready post.)
|
|
30
|
+
const readyIdx = doc.indexOf('tjs-bridge-ready')
|
|
31
|
+
const codeIdx = doc.indexOf('const todoApp')
|
|
32
|
+
expect(codeIdx).toBeGreaterThan(readyIdx)
|
|
33
|
+
expect(doc.slice(readyIdx, codeIdx)).not.toContain('try {')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('installs the message listener and the injected introspector', () => {
|
|
37
|
+
expect(doc).toContain("d.type !== 'tjs-introspect'")
|
|
38
|
+
expect(doc).toContain('eval(d.path)')
|
|
39
|
+
expect(doc).toContain('function introspectValue') // injected source
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('posts bridge-ready and puts imports first', () => {
|
|
43
|
+
expect(doc).toContain('tjs-bridge-ready')
|
|
44
|
+
const importIdx = doc.indexOf('import { tosi }')
|
|
45
|
+
const listenerIdx = doc.indexOf('tjs-introspect')
|
|
46
|
+
const codeIdx = doc.indexOf('const todoApp')
|
|
47
|
+
// imports → listener → user code
|
|
48
|
+
expect(importIdx).toBeGreaterThan(-1)
|
|
49
|
+
expect(importIdx).toBeLessThan(listenerIdx)
|
|
50
|
+
expect(listenerIdx).toBeLessThan(codeIdx)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('does not install the bridge for a normal preview doc', () => {
|
|
54
|
+
const preview = buildIframeDoc({
|
|
55
|
+
cssContent: '',
|
|
56
|
+
htmlContent: '',
|
|
57
|
+
importMapScript: '',
|
|
58
|
+
jsCode: 'console.log(1)',
|
|
59
|
+
})
|
|
60
|
+
expect(preview).not.toContain('tjs-introspect')
|
|
61
|
+
expect(preview).toContain('try {') // normal preview DOES wrap in try
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { CodeMirror } from '../../editors/codemirror/component'
|
|
9
|
+
import { INTROSPECT_VALUE_SOURCE } from '../../editors/introspect-value'
|
|
9
10
|
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
11
12
|
// TJS Runtime for iframe — loaded from a built file, not a hand-maintained stub.
|
|
@@ -48,6 +49,32 @@ export interface IframeDocOptions {
|
|
|
48
49
|
autoCallTjsFunction?: boolean
|
|
49
50
|
/** Whether parent is in dark mode — sets color-scheme on iframe */
|
|
50
51
|
darkMode?: boolean
|
|
52
|
+
/**
|
|
53
|
+
* Install the introspection bridge: after the user's code runs, a message
|
|
54
|
+
* listener answers `{type:'tjs-introspect', id, path}` by evaluating `path` in
|
|
55
|
+
* module scope and returning the value's real members. Used by the hidden
|
|
56
|
+
* autocomplete sandbox (IntrospectionBridge), not the visible preview.
|
|
57
|
+
*/
|
|
58
|
+
introspectionBridge?: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Script (injected at module top-level, after the user's code) that answers
|
|
63
|
+
* member-introspection queries from the parent. `eval(path)` is a DIRECT eval in
|
|
64
|
+
* the module's lexical scope, so it sees the user's top-level `const`/`let`
|
|
65
|
+
* bindings (e.g. `todoApp.items`). Runs only in the hidden sandbox.
|
|
66
|
+
*/
|
|
67
|
+
function introspectionBridgeScript(): string {
|
|
68
|
+
return `
|
|
69
|
+
const __introspectValue = ${INTROSPECT_VALUE_SOURCE};
|
|
70
|
+
window.addEventListener('message', (e) => {
|
|
71
|
+
const d = e.data;
|
|
72
|
+
if (!d || d.type !== 'tjs-introspect') return;
|
|
73
|
+
let members = [];
|
|
74
|
+
try { members = __introspectValue(eval(d.path)); } catch (_e) {}
|
|
75
|
+
parent.postMessage({ type: 'tjs-introspect-result', id: d.id, members }, '*');
|
|
76
|
+
});
|
|
77
|
+
parent.postMessage({ type: 'tjs-bridge-ready' }, '*');`
|
|
51
78
|
}
|
|
52
79
|
|
|
53
80
|
/**
|
|
@@ -68,8 +95,34 @@ export function buildIframeDoc(options: IframeDocOptions): string {
|
|
|
68
95
|
parentBindings = false,
|
|
69
96
|
autoCallTjsFunction = false,
|
|
70
97
|
darkMode = false,
|
|
98
|
+
introspectionBridge = false,
|
|
71
99
|
} = options
|
|
72
100
|
|
|
101
|
+
// Introspection sandbox: run user code at MODULE TOP LEVEL (no try-wrap) so
|
|
102
|
+
// top-level `const`/`let` are module-scoped and the bridge's direct `eval` can
|
|
103
|
+
// see them. The listener is installed first; it's invoked later (at query
|
|
104
|
+
// time), by when the module — and `todoApp` — has fully initialised. Ready is
|
|
105
|
+
// posted up front so globals/imports stay introspectable even if user code
|
|
106
|
+
// throws partway. No try/catch precisely because a block would re-scope the
|
|
107
|
+
// declarations away from `eval`.
|
|
108
|
+
if (introspectionBridge) {
|
|
109
|
+
return `<!DOCTYPE html>
|
|
110
|
+
<html>
|
|
111
|
+
<head>
|
|
112
|
+
<style>:root { color-scheme: ${darkMode ? 'dark' : 'light dark'} }</style>
|
|
113
|
+
${importMapScript}
|
|
114
|
+
</head>
|
|
115
|
+
<body>
|
|
116
|
+
${runtimeScriptTag}
|
|
117
|
+
<script type="module">
|
|
118
|
+
${importStatements.join('\n ')}
|
|
119
|
+
${introspectionBridgeScript()}
|
|
120
|
+
${jsCode}
|
|
121
|
+
</script>
|
|
122
|
+
</body>
|
|
123
|
+
</html>`
|
|
124
|
+
}
|
|
125
|
+
|
|
73
126
|
const colorScheme = darkMode ? 'dark' : 'light dark'
|
|
74
127
|
|
|
75
128
|
const parentBindingsScript = parentBindings
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
buildAutocompleteContext,
|
|
44
44
|
type AutocompleteContext,
|
|
45
45
|
} from './autocomplete-context'
|
|
46
|
+
import { IntrospectionBridge } from './introspection-bridge'
|
|
46
47
|
|
|
47
48
|
// Available modules for autocomplete introspection
|
|
48
49
|
const AVAILABLE_MODULES: Record<string, any> = {
|
|
@@ -166,6 +167,8 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
|
|
|
166
167
|
private autocompleteContext: AutocompleteContext | null = null
|
|
167
168
|
private contextUpdateTimer: ReturnType<typeof setTimeout> | null = null
|
|
168
169
|
private contextBuildPromise: Promise<void> | null = null
|
|
170
|
+
// Hidden sandbox for runtime-truth member completion (todoApp.items, …)
|
|
171
|
+
private introspectionBridge: IntrospectionBridge | null = null
|
|
169
172
|
|
|
170
173
|
/**
|
|
171
174
|
* Get metadata for autocomplete - returns all discovered functions
|
|
@@ -185,6 +188,15 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
|
|
|
185
188
|
return this.autocompleteContext?.bindings
|
|
186
189
|
}
|
|
187
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Member completion from the introspection bridge — the user's REAL executed
|
|
193
|
+
* scope (`todoApp.items` → the live array). Returns undefined if the sandbox
|
|
194
|
+
* isn't ready, so the provider falls back to static completions.
|
|
195
|
+
*/
|
|
196
|
+
private getMembers = (path: string) => {
|
|
197
|
+
return this.introspectionBridge?.members(path) ?? Promise.resolve(undefined)
|
|
198
|
+
}
|
|
199
|
+
|
|
188
200
|
/**
|
|
189
201
|
* Update autocomplete context (debounced)
|
|
190
202
|
* Called when editor content changes.
|
|
@@ -198,6 +210,14 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
|
|
|
198
210
|
const doBuild = async () => {
|
|
199
211
|
const source = this.parts.tjsEditor.value
|
|
200
212
|
|
|
213
|
+
// Refresh the introspection sandbox (runtime-truth member completion).
|
|
214
|
+
// Independent of import-binding resolution below; failures are swallowed
|
|
215
|
+
// inside the bridge (keeps the last good sandbox).
|
|
216
|
+
if (!this.introspectionBridge) {
|
|
217
|
+
this.introspectionBridge = new IntrospectionBridge()
|
|
218
|
+
}
|
|
219
|
+
void this.introspectionBridge.refresh(source)
|
|
220
|
+
|
|
201
221
|
try {
|
|
202
222
|
const newContext = await buildAutocompleteContext(source)
|
|
203
223
|
// Only update if we got bindings (keep last good context otherwise)
|
|
@@ -649,6 +669,7 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
|
|
|
649
669
|
this.parts.tjsEditor.autocomplete = {
|
|
650
670
|
getMetadata: this.getMetadataForAutocomplete,
|
|
651
671
|
getLiveBindings: this.getLiveBindings,
|
|
672
|
+
getMembers: this.getMembers,
|
|
652
673
|
}
|
|
653
674
|
|
|
654
675
|
// Build initial autocomplete context immediately
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CSS torture set — predicates for the exact cases where TS structural
|
|
3
|
+
* types and JSON Schema cave to `string`/`any`:
|
|
4
|
+
* - open value grammars: var(), calc(), !important
|
|
5
|
+
* - order-flexible shorthands: the `animation` family (tokenize + classify)
|
|
6
|
+
* - open recursive structure: nested selectors / keyframes (recurse)
|
|
7
|
+
*
|
|
8
|
+
* Written in named-composition style — no inline arrows, no IO, no async —
|
|
9
|
+
* so it reads as "synchronous AJS" and verifies predicate-safe. `||` composition
|
|
10
|
+
* and `.every(namedPredicate)` are natural here because predicates run as native
|
|
11
|
+
* JS (the verified fast path), not through the sandboxed VM interpreter.
|
|
12
|
+
*/
|
|
13
|
+
export declare const CSS_PREDICATE_SOURCE: string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface PredicateDiagnostic {
|
|
2
|
+
predicate: string;
|
|
3
|
+
message: string;
|
|
4
|
+
line: number;
|
|
5
|
+
column: number;
|
|
6
|
+
}
|
|
7
|
+
export interface VerifyResult {
|
|
8
|
+
safe: boolean;
|
|
9
|
+
predicates: string[];
|
|
10
|
+
diagnostics: PredicateDiagnostic[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Build the effectful-name set from a VM atom registry: every atom tagged
|
|
14
|
+
* `effects: 'io'` (plus the non-atom globals). This is how the predicate
|
|
15
|
+
* verifier consumes the atom-effects classification.
|
|
16
|
+
*/
|
|
17
|
+
export declare function effectfulFrom(atoms: Record<string, {
|
|
18
|
+
op?: string;
|
|
19
|
+
effects?: 'pure' | 'io';
|
|
20
|
+
}>): Set<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Verify every top-level function in `source` is predicate-safe. Because safety
|
|
23
|
+
* is a closure property, the whole cluster is safe iff each function is — so the
|
|
24
|
+
* transitive check is automatic: a call to another function in the set is fine,
|
|
25
|
+
* a call to anything effectful/unknown is not.
|
|
26
|
+
*/
|
|
27
|
+
export declare function verifyPredicates(source: string, opts?: {
|
|
28
|
+
effectful?: Set<string>;
|
|
29
|
+
}): VerifyResult;
|
|
30
|
+
/** Format diagnostics for a thrown error / log. */
|
|
31
|
+
export declare function formatDiagnostics(d: PredicateDiagnostic[]): string;
|
|
32
|
+
/**
|
|
33
|
+
* Verify, then compile the predicate cluster to native synchronous JS functions.
|
|
34
|
+
* Throws (with located diagnostics) at "definition time" if not predicate-safe —
|
|
35
|
+
* so an IO-using "predicate" never even compiles.
|
|
36
|
+
*/
|
|
37
|
+
export declare function compilePredicates(source: string, exports: string[], opts?: {
|
|
38
|
+
effectful?: Set<string>;
|
|
39
|
+
}): Record<string, (...args: any[]) => any>;
|