tjs-lang 0.2.7 → 0.3.0
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/demo/docs.json +32 -26
- package/demo/src/examples.ts +23 -83
- package/demo/src/playground-shared.ts +666 -0
- package/demo/src/tjs-playground.ts +65 -550
- package/demo/src/ts-examples.ts +5 -4
- package/demo/src/ts-playground.ts +50 -414
- package/dist/index.js +143 -160
- package/dist/index.js.map +12 -12
- package/dist/src/lang/emitters/js.d.ts +34 -2
- package/dist/src/lang/index.d.ts +1 -1
- package/dist/src/lang/types.d.ts +1 -1
- package/dist/src/types/Type.d.ts +3 -1
- package/dist/tjs-full.js +143 -160
- package/dist/tjs-full.js.map +12 -12
- package/dist/tjs-transpiler.js +122 -55
- package/dist/tjs-transpiler.js.map +9 -8
- package/dist/tjs-vm.js +14 -14
- package/dist/tjs-vm.js.map +5 -5
- package/docs/docs.json +792 -0
- package/docs/index.js +2652 -2835
- package/docs/index.js.map +11 -10
- package/editors/codemirror/ajs-language.ts +27 -1
- package/editors/codemirror/autocomplete.test.ts +3 -3
- package/package.json +1 -1
- package/src/lang/codegen.test.ts +11 -11
- package/src/lang/emitters/from-ts.ts +1 -1
- package/src/lang/emitters/js.ts +228 -4
- package/src/lang/index.ts +0 -3
- package/src/lang/inference.ts +40 -8
- package/src/lang/lang.test.ts +192 -35
- package/src/lang/roundtrip.test.ts +155 -0
- package/src/lang/runtime.ts +7 -0
- package/src/lang/types.ts +2 -0
- package/src/lang/typescript-syntax.test.ts +6 -4
- package/src/lang/wasm.test.ts +20 -0
- package/src/lang/wasm.ts +143 -0
- package/src/types/Type.test.ts +64 -0
- package/src/types/Type.ts +22 -1
- package/src/use-cases/transpiler-integration.test.ts +10 -10
- package/src/vm/atoms/batteries.ts +2 -0
|
@@ -1353,6 +1353,17 @@ function getPlaceholderForParam(name: string, info: any): string {
|
|
|
1353
1353
|
return String(ex)
|
|
1354
1354
|
}
|
|
1355
1355
|
|
|
1356
|
+
// Then check for examples from schema metadata (use first as placeholder)
|
|
1357
|
+
const examples = info.type?.examples || info.examples
|
|
1358
|
+
if (Array.isArray(examples) && examples.length > 0) {
|
|
1359
|
+
const ex = examples[0]
|
|
1360
|
+
if (typeof ex === 'string') return `'${ex}'`
|
|
1361
|
+
if (typeof ex === 'number' || typeof ex === 'boolean') return String(ex)
|
|
1362
|
+
if (Array.isArray(ex)) return JSON.stringify(ex)
|
|
1363
|
+
if (typeof ex === 'object') return JSON.stringify(ex)
|
|
1364
|
+
return String(ex)
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1356
1367
|
// Then check for explicit default value
|
|
1357
1368
|
if (info.default !== undefined && info.default !== null) {
|
|
1358
1369
|
const def = info.default
|
|
@@ -1481,12 +1492,27 @@ function tjsCompletionSource(config: AutocompleteConfig = {}) {
|
|
|
1481
1492
|
// Handle both { type: 'string' } and { kind: 'string' } formats
|
|
1482
1493
|
const returnType =
|
|
1483
1494
|
meta.returns?.type || meta.returns?.kind || 'void'
|
|
1495
|
+
|
|
1496
|
+
// Build info string with description and parameter examples
|
|
1497
|
+
let infoText = meta.description || ''
|
|
1498
|
+
for (const [pName, pInfo] of paramEntries as [string, any][]) {
|
|
1499
|
+
const pExamples = pInfo.type?.examples || pInfo.examples
|
|
1500
|
+
if (Array.isArray(pExamples) && pExamples.length > 0) {
|
|
1501
|
+
const formatted = pExamples
|
|
1502
|
+
.map((ex: any) =>
|
|
1503
|
+
typeof ex === 'string' ? `'${ex}'` : String(ex)
|
|
1504
|
+
)
|
|
1505
|
+
.join(', ')
|
|
1506
|
+
infoText += `${infoText ? '\n' : ''}${pName}: e.g. ${formatted}`
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1484
1510
|
options.push(
|
|
1485
1511
|
snippetCompletion(`${name}(${snippetParams})`, {
|
|
1486
1512
|
label: name,
|
|
1487
1513
|
type: 'function',
|
|
1488
1514
|
detail: `(${paramList}) -> ${returnType}`,
|
|
1489
|
-
info:
|
|
1515
|
+
info: infoText || undefined,
|
|
1490
1516
|
boost: 2, // Boost user-defined functions above globals
|
|
1491
1517
|
})
|
|
1492
1518
|
)
|
|
@@ -432,8 +432,8 @@ const after = 2
|
|
|
432
432
|
|
|
433
433
|
expect(metadata).toBeDefined()
|
|
434
434
|
// metadata is now keyed by function name
|
|
435
|
-
expect(metadata?.add?.params?.a?.type?.kind).toBe('
|
|
436
|
-
expect(metadata?.add?.params?.b?.type?.kind).toBe('
|
|
435
|
+
expect(metadata?.add?.params?.a?.type?.kind).toBe('integer')
|
|
436
|
+
expect(metadata?.add?.params?.b?.type?.kind).toBe('integer')
|
|
437
437
|
})
|
|
438
438
|
|
|
439
439
|
it('extracts example-based types', () => {
|
|
@@ -445,7 +445,7 @@ const after = 2
|
|
|
445
445
|
expect(metadata).toBeDefined()
|
|
446
446
|
// metadata is now keyed by function name
|
|
447
447
|
expect(metadata?.greet?.params?.name?.type?.kind).toBe('string')
|
|
448
|
-
expect(metadata?.greet?.params?.times?.type?.kind).toBe('
|
|
448
|
+
expect(metadata?.greet?.params?.times?.type?.kind).toBe('integer')
|
|
449
449
|
expect(metadata?.greet?.params?.times?.default).toBe(1)
|
|
450
450
|
})
|
|
451
451
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tjs-lang",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Type-safe JavaScript dialect with runtime validation, sandboxed VM execution, and AI agent orchestration. Transpiles TypeScript to validated JS with fuel-metered execution for untrusted code.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
package/src/lang/codegen.test.ts
CHANGED
|
@@ -54,7 +54,7 @@ describe('TS → TJS conversion quality', () => {
|
|
|
54
54
|
const ts = `function sum(nums: number[]): number { return nums.reduce((a, b) => a + b, 0) }`
|
|
55
55
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
56
56
|
|
|
57
|
-
expect(code).toContain('nums: [0]')
|
|
57
|
+
expect(code).toContain('nums: [0.0]')
|
|
58
58
|
})
|
|
59
59
|
|
|
60
60
|
it('converts object param correctly', () => {
|
|
@@ -62,7 +62,7 @@ describe('TS → TJS conversion quality', () => {
|
|
|
62
62
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
63
63
|
|
|
64
64
|
expect(code).toContain("name: ''")
|
|
65
|
-
expect(code).toContain('age: 0')
|
|
65
|
+
expect(code).toContain('age: 0.0')
|
|
66
66
|
})
|
|
67
67
|
|
|
68
68
|
it('handles multiple params in order', () => {
|
|
@@ -70,7 +70,7 @@ describe('TS → TJS conversion quality', () => {
|
|
|
70
70
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
71
71
|
|
|
72
72
|
// Should have both params with colon syntax
|
|
73
|
-
expect(code).toMatch(/add\(a: 0, b: 0\)/)
|
|
73
|
+
expect(code).toMatch(/add\(a: 0\.0, b: 0\.0\)/)
|
|
74
74
|
})
|
|
75
75
|
|
|
76
76
|
it('handles mixed required and optional params', () => {
|
|
@@ -228,7 +228,7 @@ class Calculator {
|
|
|
228
228
|
`
|
|
229
229
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
230
230
|
|
|
231
|
-
expect(code).toContain('add(a: 0, b: 0) -! 0')
|
|
231
|
+
expect(code).toContain('add(a: 0.0, b: 0.0) -! 0.0')
|
|
232
232
|
})
|
|
233
233
|
|
|
234
234
|
it('converts getters and setters', () => {
|
|
@@ -269,7 +269,7 @@ class MathUtils {
|
|
|
269
269
|
`
|
|
270
270
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
271
271
|
|
|
272
|
-
expect(code).toContain('static double(x: 0) -! 0')
|
|
272
|
+
expect(code).toContain('static double(x: 0.0) -! 0.0')
|
|
273
273
|
})
|
|
274
274
|
|
|
275
275
|
it('converts async methods', () => {
|
|
@@ -396,7 +396,7 @@ describe('TJS → JS transpilation quality', () => {
|
|
|
396
396
|
|
|
397
397
|
expect(code).toContain('__tjs')
|
|
398
398
|
// types is now keyed by function name
|
|
399
|
-
expect(types?.double?.returns?.kind).toBe('
|
|
399
|
+
expect(types?.double?.returns?.kind).toBe('integer')
|
|
400
400
|
})
|
|
401
401
|
|
|
402
402
|
it('marks required params correctly', () => {
|
|
@@ -561,8 +561,8 @@ function greet(name: string): string {
|
|
|
561
561
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
562
562
|
|
|
563
563
|
// All functions should be present (TS transpiler uses -! to skip signature tests)
|
|
564
|
-
expect(code).toContain('function add(a: 0, b: 0) -! 0')
|
|
565
|
-
expect(code).toContain('function multiply(a: 0, b: 0) -! 0')
|
|
564
|
+
expect(code).toContain('function add(a: 0.0, b: 0.0) -! 0.0')
|
|
565
|
+
expect(code).toContain('function multiply(a: 0.0, b: 0.0) -! 0.0')
|
|
566
566
|
expect(code).toContain("function greet(name: '') -! ''")
|
|
567
567
|
|
|
568
568
|
// Should be valid TJS (no TypeScript syntax remaining)
|
|
@@ -738,7 +738,7 @@ function greet(name: '', excited = false) -! '' {
|
|
|
738
738
|
// add function
|
|
739
739
|
expect(types?.add?.params?.a?.required).toBe(true)
|
|
740
740
|
expect(types?.add?.params?.b?.required).toBe(true)
|
|
741
|
-
expect(types?.add?.returns?.kind).toBe('
|
|
741
|
+
expect(types?.add?.returns?.kind).toBe('integer')
|
|
742
742
|
|
|
743
743
|
// greet function
|
|
744
744
|
expect(types?.greet?.params?.name?.required).toBe(true)
|
|
@@ -1146,7 +1146,7 @@ function processUser(user: { name: string; age: number }): string {
|
|
|
1146
1146
|
// TS → TJS
|
|
1147
1147
|
const { code: tjsCode } = fromTS(ts, { emitTJS: true })
|
|
1148
1148
|
expect(tjsCode).toContain("name: ''")
|
|
1149
|
-
expect(tjsCode).toContain('age: 0')
|
|
1149
|
+
expect(tjsCode).toContain('age: 0.0')
|
|
1150
1150
|
|
|
1151
1151
|
// TJS → JS (already has -! from TS transpiler)
|
|
1152
1152
|
const { code: jsCode, types } = tjs(tjsCode)
|
|
@@ -1386,7 +1386,7 @@ function getData(id: 0) -! { value: 0 } {
|
|
|
1386
1386
|
expect(result.value).toBeUndefined()
|
|
1387
1387
|
|
|
1388
1388
|
// But error properties are accessible
|
|
1389
|
-
expect(result.message).toContain('Expected
|
|
1389
|
+
expect(result.message).toContain('Expected integer')
|
|
1390
1390
|
})
|
|
1391
1391
|
})
|
|
1392
1392
|
|
package/src/lang/emitters/js.ts
CHANGED
|
@@ -17,6 +17,33 @@
|
|
|
17
17
|
* params: { name: { type: 'string', required: true, example: 'world' } },
|
|
18
18
|
* returns: { type: 'string' }
|
|
19
19
|
* }
|
|
20
|
+
*
|
|
21
|
+
* TODO: Self-contained output (no runtime dependency)
|
|
22
|
+
* =====================================================
|
|
23
|
+
* Currently, transpiled code references `globalThis.__tjs` for:
|
|
24
|
+
* - __tjs.pushStack() / popStack() - debug stack traces
|
|
25
|
+
* - __tjs.typeError() - monadic error creation
|
|
26
|
+
* - __tjs.Is() / IsNot() - structural equality (when == / != used)
|
|
27
|
+
*
|
|
28
|
+
* This requires either:
|
|
29
|
+
* 1. The runtime to be installed via installRuntime()
|
|
30
|
+
* 2. A stub to be provided (e.g., playground's inline stub)
|
|
31
|
+
*
|
|
32
|
+
* The ideal is that TJS produces completely independent code that only needs
|
|
33
|
+
* things it semantically needs (like fetch for HTTP calls). The runtime
|
|
34
|
+
* functions above are ~30 lines and could be inlined when used:
|
|
35
|
+
*
|
|
36
|
+
* - typeError: Create a simple Error with extra properties
|
|
37
|
+
* - pushStack/popStack: Could be no-ops in production, or inline array ops
|
|
38
|
+
* - Is/IsNot: ~20 lines for deep structural equality
|
|
39
|
+
*
|
|
40
|
+
* Options to explore:
|
|
41
|
+
* 1. Inline minimal runtime when needed (adds ~1KB unminified per output)
|
|
42
|
+
* 2. Add transpile option: { standalone: true } to emit self-contained code
|
|
43
|
+
* 3. Tree-shake: only inline the specific functions actually referenced
|
|
44
|
+
*
|
|
45
|
+
* See also: demo/src/tjs-playground.ts which has a manual __tjs stub that
|
|
46
|
+
* must stay in sync with the runtime - a symptom of this leaky abstraction.
|
|
20
47
|
*/
|
|
21
48
|
|
|
22
49
|
import type { FunctionDeclaration, Program } from 'acorn'
|
|
@@ -25,6 +52,7 @@ import { parse, extractTDoc, preprocess } from '../parser'
|
|
|
25
52
|
import type { TypeDescriptor, ParameterDescriptor } from '../types'
|
|
26
53
|
import { inferTypeFromValue, parseParameter } from '../inference'
|
|
27
54
|
import { extractTests } from '../tests'
|
|
55
|
+
import { compileToWasm } from '../wasm'
|
|
28
56
|
|
|
29
57
|
export interface TJSTranspileOptions {
|
|
30
58
|
/** Filename for error messages */
|
|
@@ -86,8 +114,13 @@ export interface TJSTranspileResult {
|
|
|
86
114
|
testCount?: number
|
|
87
115
|
/** Test results (when runTests is true or 'only') */
|
|
88
116
|
testResults?: TestResult[]
|
|
89
|
-
/** WASM
|
|
90
|
-
|
|
117
|
+
/** WASM compilation results (for debugging/inspection) */
|
|
118
|
+
wasmCompiled?: {
|
|
119
|
+
id: string
|
|
120
|
+
success: boolean
|
|
121
|
+
error?: string
|
|
122
|
+
byteLength?: number
|
|
123
|
+
}[]
|
|
91
124
|
}
|
|
92
125
|
|
|
93
126
|
export interface TJSTypeInfo {
|
|
@@ -583,6 +616,19 @@ export function transpileToJS(
|
|
|
583
616
|
}
|
|
584
617
|
}
|
|
585
618
|
|
|
619
|
+
// Compile WASM blocks at transpile time and embed in output
|
|
620
|
+
let wasmCompiled:
|
|
621
|
+
| { id: string; success: boolean; error?: string; byteLength?: number }[]
|
|
622
|
+
| undefined
|
|
623
|
+
if (preprocessed.wasmBlocks.length > 0) {
|
|
624
|
+
wasmCompiled = []
|
|
625
|
+
const wasmBootstrap = generateWasmBootstrap(preprocessed.wasmBlocks)
|
|
626
|
+
if (wasmBootstrap.code) {
|
|
627
|
+
code = wasmBootstrap.code + '\n' + code
|
|
628
|
+
}
|
|
629
|
+
wasmCompiled = wasmBootstrap.results
|
|
630
|
+
}
|
|
631
|
+
|
|
586
632
|
return {
|
|
587
633
|
code,
|
|
588
634
|
types: allTypes,
|
|
@@ -591,8 +637,7 @@ export function transpileToJS(
|
|
|
591
637
|
testRunner: tests.length > 0 ? testRunner : undefined,
|
|
592
638
|
testCount: tests.length > 0 ? tests.length : undefined,
|
|
593
639
|
testResults,
|
|
594
|
-
|
|
595
|
-
preprocessed.wasmBlocks.length > 0 ? preprocessed.wasmBlocks : undefined,
|
|
640
|
+
wasmCompiled,
|
|
596
641
|
}
|
|
597
642
|
}
|
|
598
643
|
|
|
@@ -823,6 +868,10 @@ function generateTypeCheckExpr(
|
|
|
823
868
|
return `typeof ${fieldPath} !== 'string'`
|
|
824
869
|
case 'number':
|
|
825
870
|
return `typeof ${fieldPath} !== 'number'`
|
|
871
|
+
case 'integer':
|
|
872
|
+
return `(typeof ${fieldPath} !== 'number' || !Number.isInteger(${fieldPath}))`
|
|
873
|
+
case 'non-negative-integer':
|
|
874
|
+
return `(typeof ${fieldPath} !== 'number' || !Number.isInteger(${fieldPath}) || ${fieldPath} < 0)`
|
|
826
875
|
case 'boolean':
|
|
827
876
|
return `typeof ${fieldPath} !== 'boolean'`
|
|
828
877
|
case 'null':
|
|
@@ -1448,6 +1497,41 @@ function runAllTests(
|
|
|
1448
1497
|
if (!__deepEqual(actual, expected)) {
|
|
1449
1498
|
throw new Error('Expected ' + __format(expected) + ' but got ' + __format(actual))
|
|
1450
1499
|
}
|
|
1500
|
+
},
|
|
1501
|
+
toContain(item) {
|
|
1502
|
+
if (!Array.isArray(actual) || !actual.some(function(v) { return __deepEqual(v, item) })) {
|
|
1503
|
+
throw new Error('Expected ' + __format(actual) + ' to contain ' + __format(item))
|
|
1504
|
+
}
|
|
1505
|
+
},
|
|
1506
|
+
toBeTruthy() {
|
|
1507
|
+
if (!actual) {
|
|
1508
|
+
throw new Error('Expected ' + __format(actual) + ' to be truthy')
|
|
1509
|
+
}
|
|
1510
|
+
},
|
|
1511
|
+
toBeFalsy() {
|
|
1512
|
+
if (actual) {
|
|
1513
|
+
throw new Error('Expected ' + __format(actual) + ' to be falsy')
|
|
1514
|
+
}
|
|
1515
|
+
},
|
|
1516
|
+
toBeNull() {
|
|
1517
|
+
if (actual !== null) {
|
|
1518
|
+
throw new Error('Expected null but got ' + __format(actual))
|
|
1519
|
+
}
|
|
1520
|
+
},
|
|
1521
|
+
toBeUndefined() {
|
|
1522
|
+
if (actual !== undefined) {
|
|
1523
|
+
throw new Error('Expected undefined but got ' + __format(actual))
|
|
1524
|
+
}
|
|
1525
|
+
},
|
|
1526
|
+
toBeGreaterThan(n) {
|
|
1527
|
+
if (!(actual > n)) {
|
|
1528
|
+
throw new Error('Expected ' + __format(actual) + ' to be greater than ' + n)
|
|
1529
|
+
}
|
|
1530
|
+
},
|
|
1531
|
+
toBeLessThan(n) {
|
|
1532
|
+
if (!(actual < n)) {
|
|
1533
|
+
throw new Error('Expected ' + __format(actual) + ' to be less than ' + n)
|
|
1534
|
+
}
|
|
1451
1535
|
}
|
|
1452
1536
|
}
|
|
1453
1537
|
}
|
|
@@ -1589,6 +1673,41 @@ function runTestBlocks(
|
|
|
1589
1673
|
if (!__deepEqual(actual, expected)) {
|
|
1590
1674
|
throw new Error('Expected ' + __format(expected) + ' but got ' + __format(actual))
|
|
1591
1675
|
}
|
|
1676
|
+
},
|
|
1677
|
+
toContain(item) {
|
|
1678
|
+
if (!Array.isArray(actual) || !actual.some(function(v) { return __deepEqual(v, item) })) {
|
|
1679
|
+
throw new Error('Expected ' + __format(actual) + ' to contain ' + __format(item))
|
|
1680
|
+
}
|
|
1681
|
+
},
|
|
1682
|
+
toBeTruthy() {
|
|
1683
|
+
if (!actual) {
|
|
1684
|
+
throw new Error('Expected ' + __format(actual) + ' to be truthy')
|
|
1685
|
+
}
|
|
1686
|
+
},
|
|
1687
|
+
toBeFalsy() {
|
|
1688
|
+
if (actual) {
|
|
1689
|
+
throw new Error('Expected ' + __format(actual) + ' to be falsy')
|
|
1690
|
+
}
|
|
1691
|
+
},
|
|
1692
|
+
toBeNull() {
|
|
1693
|
+
if (actual !== null) {
|
|
1694
|
+
throw new Error('Expected null but got ' + __format(actual))
|
|
1695
|
+
}
|
|
1696
|
+
},
|
|
1697
|
+
toBeUndefined() {
|
|
1698
|
+
if (actual !== undefined) {
|
|
1699
|
+
throw new Error('Expected undefined but got ' + __format(actual))
|
|
1700
|
+
}
|
|
1701
|
+
},
|
|
1702
|
+
toBeGreaterThan(n) {
|
|
1703
|
+
if (!(actual > n)) {
|
|
1704
|
+
throw new Error('Expected ' + __format(actual) + ' to be greater than ' + n)
|
|
1705
|
+
}
|
|
1706
|
+
},
|
|
1707
|
+
toBeLessThan(n) {
|
|
1708
|
+
if (!(actual < n)) {
|
|
1709
|
+
throw new Error('Expected ' + __format(actual) + ' to be less than ' + n)
|
|
1710
|
+
}
|
|
1592
1711
|
}
|
|
1593
1712
|
}
|
|
1594
1713
|
}
|
|
@@ -1897,3 +2016,108 @@ function runSignatureTest(
|
|
|
1897
2016
|
}
|
|
1898
2017
|
}
|
|
1899
2018
|
}
|
|
2019
|
+
|
|
2020
|
+
/**
|
|
2021
|
+
* Compile WASM blocks and generate bootstrap code that embeds the compiled bytes
|
|
2022
|
+
* and instantiates them on load.
|
|
2023
|
+
*/
|
|
2024
|
+
function generateWasmBootstrap(blocks: import('../parser').WasmBlock[]): {
|
|
2025
|
+
code: string
|
|
2026
|
+
results: {
|
|
2027
|
+
id: string
|
|
2028
|
+
success: boolean
|
|
2029
|
+
error?: string
|
|
2030
|
+
byteLength?: number
|
|
2031
|
+
}[]
|
|
2032
|
+
} {
|
|
2033
|
+
const results: {
|
|
2034
|
+
id: string
|
|
2035
|
+
success: boolean
|
|
2036
|
+
error?: string
|
|
2037
|
+
byteLength?: number
|
|
2038
|
+
}[] = []
|
|
2039
|
+
const compiledBlocks: {
|
|
2040
|
+
id: string
|
|
2041
|
+
base64: string
|
|
2042
|
+
captures: string[]
|
|
2043
|
+
needsMemory: boolean
|
|
2044
|
+
wat: string
|
|
2045
|
+
}[] = []
|
|
2046
|
+
|
|
2047
|
+
for (const block of blocks) {
|
|
2048
|
+
const result = compileToWasm(block)
|
|
2049
|
+
if (result.success) {
|
|
2050
|
+
// Convert bytes to base64 for embedding
|
|
2051
|
+
const base64 = btoa(String.fromCharCode(...result.bytes))
|
|
2052
|
+
compiledBlocks.push({
|
|
2053
|
+
id: block.id,
|
|
2054
|
+
base64,
|
|
2055
|
+
captures: block.captures,
|
|
2056
|
+
needsMemory: result.needsMemory ?? false,
|
|
2057
|
+
wat: result.wat ?? '',
|
|
2058
|
+
})
|
|
2059
|
+
results.push({
|
|
2060
|
+
id: block.id,
|
|
2061
|
+
success: true,
|
|
2062
|
+
byteLength: result.bytes.length,
|
|
2063
|
+
})
|
|
2064
|
+
} else {
|
|
2065
|
+
results.push({
|
|
2066
|
+
id: block.id,
|
|
2067
|
+
success: false,
|
|
2068
|
+
error: result.error,
|
|
2069
|
+
})
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
if (compiledBlocks.length === 0) {
|
|
2074
|
+
return { code: '', results }
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// Generate WAT comments for each block
|
|
2078
|
+
const watComments = compiledBlocks
|
|
2079
|
+
.map((b) => {
|
|
2080
|
+
const watLines = b.wat.split('\n').map((line) => ` * ${line}`)
|
|
2081
|
+
return `/**\n * WASM: ${b.id}\n${watLines.join('\n')}\n */`
|
|
2082
|
+
})
|
|
2083
|
+
.join('\n')
|
|
2084
|
+
|
|
2085
|
+
// Generate self-contained bootstrap code
|
|
2086
|
+
// This runs immediately and sets up globalThis.__tjs_wasm_N functions
|
|
2087
|
+
const blockData = compiledBlocks
|
|
2088
|
+
.map(
|
|
2089
|
+
(b) =>
|
|
2090
|
+
`{id:${JSON.stringify(b.id)},b64:${JSON.stringify(
|
|
2091
|
+
b.base64
|
|
2092
|
+
)},c:${JSON.stringify(b.captures)},m:${b.needsMemory}}`
|
|
2093
|
+
)
|
|
2094
|
+
.join(',')
|
|
2095
|
+
|
|
2096
|
+
const code = `${watComments}
|
|
2097
|
+
;(async()=>{
|
|
2098
|
+
const __wasmBlocks=[${blockData}];
|
|
2099
|
+
const __b64ToBytes=s=>{const b=atob(s),a=new Uint8Array(b.length);for(let i=0;i<b.length;i++)a[i]=b.charCodeAt(i);return a};
|
|
2100
|
+
const __parseType=c=>{const m=c.match(/^(\\w+)\\s*:\\s*(\\w+)$/);if(!m)return{n:c,t:'f64',a:false};const[,n,ts]=m;const at={Float32Array:'f32',Float64Array:'f64',Int32Array:'i32',Uint8Array:'i32'};if(at[ts])return{n,t:'i32',a:true,at:ts};return{n,t:'f64',a:false}};
|
|
2101
|
+
for(const{id,b64,c,m}of __wasmBlocks){
|
|
2102
|
+
const bytes=__b64ToBytes(b64);
|
|
2103
|
+
const params=c.map(__parseType);
|
|
2104
|
+
const hasArrays=params.some(p=>p.a);
|
|
2105
|
+
let mem;if(m)mem=new WebAssembly.Memory({initial:256});
|
|
2106
|
+
const imp=mem?{env:{memory:mem}}:{};
|
|
2107
|
+
const inst=await WebAssembly.instantiate(await WebAssembly.compile(bytes),imp);
|
|
2108
|
+
const compute=inst.exports.compute;
|
|
2109
|
+
if(!hasArrays){globalThis[id]=compute;continue}
|
|
2110
|
+
globalThis[id]=function(...args){
|
|
2111
|
+
const mv=new Uint8Array(mem.buffer);let off=0;const ptrs=[];
|
|
2112
|
+
for(let i=0;i<params.length;i++){const p=params[i],a=args[i];
|
|
2113
|
+
if(p.a&&a?.buffer){const ab=new Uint8Array(a.buffer,a.byteOffset,a.byteLength);mv.set(ab,off);ptrs.push(off);off+=ab.length;off=(off+7)&~7}
|
|
2114
|
+
else ptrs.push(a)}
|
|
2115
|
+
const r=compute(...ptrs);off=0;
|
|
2116
|
+
for(let i=0;i<params.length;i++){const p=params[i],a=args[i];
|
|
2117
|
+
if(p.a&&a?.buffer){const ab=new Uint8Array(a.buffer,a.byteOffset,a.byteLength);ab.set(mv.slice(off,off+ab.length));off+=ab.length;off=(off+7)&~7}}
|
|
2118
|
+
return r};
|
|
2119
|
+
}})();
|
|
2120
|
+
`.trim()
|
|
2121
|
+
|
|
2122
|
+
return { code, results }
|
|
2123
|
+
}
|
package/src/lang/index.ts
CHANGED
|
@@ -90,10 +90,7 @@ export {
|
|
|
90
90
|
instantiateWasm,
|
|
91
91
|
registerWasmBlock,
|
|
92
92
|
compileWasmBlocks,
|
|
93
|
-
compileWasmBlocksForIframe,
|
|
94
|
-
generateWasmInstantiationCode,
|
|
95
93
|
type WasmCompileResult,
|
|
96
|
-
type CompiledWasmData,
|
|
97
94
|
} from './wasm'
|
|
98
95
|
export type { WasmBlock } from './parser'
|
|
99
96
|
export {
|
package/src/lang/inference.ts
CHANGED
|
@@ -29,7 +29,13 @@ export function inferTypeFromValue(node: Expression): TypeDescriptor {
|
|
|
29
29
|
return { kind: 'string' }
|
|
30
30
|
}
|
|
31
31
|
if (typeof value === 'number') {
|
|
32
|
-
|
|
32
|
+
// Distinguish float vs integer by checking if source contains '.'
|
|
33
|
+
// 2.0 -> number (float), 42 -> integer
|
|
34
|
+
const raw = (node as any).raw as string | undefined
|
|
35
|
+
if (raw && raw.includes('.')) {
|
|
36
|
+
return { kind: 'number' }
|
|
37
|
+
}
|
|
38
|
+
return { kind: 'integer' }
|
|
33
39
|
}
|
|
34
40
|
if (typeof value === 'boolean') {
|
|
35
41
|
return { kind: 'boolean' }
|
|
@@ -110,14 +116,26 @@ export function inferTypeFromValue(node: Expression): TypeDescriptor {
|
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
case 'UnaryExpression': {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
) {
|
|
118
|
-
const value =
|
|
119
|
+
const op = (node as any).operator
|
|
120
|
+
const arg = (node as any).argument
|
|
121
|
+
|
|
122
|
+
// +N means non-negative integer (e.g., +1, +3)
|
|
123
|
+
if (op === '+' && arg.type === 'Literal') {
|
|
124
|
+
const value = arg.value
|
|
119
125
|
if (typeof value === 'number') {
|
|
120
|
-
return { kind: '
|
|
126
|
+
return { kind: 'non-negative-integer' }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// -N means integer or float depending on source
|
|
131
|
+
if (op === '-' && arg.type === 'Literal') {
|
|
132
|
+
const value = arg.value
|
|
133
|
+
if (typeof value === 'number') {
|
|
134
|
+
const raw = arg.raw as string | undefined
|
|
135
|
+
if (raw && raw.includes('.')) {
|
|
136
|
+
return { kind: 'number' }
|
|
137
|
+
}
|
|
138
|
+
return { kind: 'integer' }
|
|
121
139
|
}
|
|
122
140
|
}
|
|
123
141
|
return { kind: 'any' }
|
|
@@ -258,6 +276,10 @@ export function extractLiteralValue(node: Expression): any {
|
|
|
258
276
|
const arg = extractLiteralValue((node as any).argument)
|
|
259
277
|
return typeof arg === 'number' ? -arg : undefined
|
|
260
278
|
}
|
|
279
|
+
if ((node as any).operator === '+') {
|
|
280
|
+
const arg = extractLiteralValue((node as any).argument)
|
|
281
|
+
return typeof arg === 'number' ? +arg : undefined
|
|
282
|
+
}
|
|
261
283
|
return undefined
|
|
262
284
|
|
|
263
285
|
case 'LogicalExpression': {
|
|
@@ -310,6 +332,12 @@ export function typeToString(type: TypeDescriptor): string {
|
|
|
310
332
|
return type.nullable ? 'string | null' : 'string'
|
|
311
333
|
case 'number':
|
|
312
334
|
return type.nullable ? 'number | null' : 'number'
|
|
335
|
+
case 'integer':
|
|
336
|
+
return type.nullable ? 'integer | null' : 'integer'
|
|
337
|
+
case 'non-negative-integer':
|
|
338
|
+
return type.nullable
|
|
339
|
+
? 'non-negative integer | null'
|
|
340
|
+
: 'non-negative integer'
|
|
313
341
|
case 'boolean':
|
|
314
342
|
return type.nullable ? 'boolean | null' : 'boolean'
|
|
315
343
|
case 'null':
|
|
@@ -354,6 +382,10 @@ export function checkType(value: any, type: TypeDescriptor): boolean {
|
|
|
354
382
|
return typeof value === 'string'
|
|
355
383
|
case 'number':
|
|
356
384
|
return typeof value === 'number'
|
|
385
|
+
case 'integer':
|
|
386
|
+
return typeof value === 'number' && Number.isInteger(value)
|
|
387
|
+
case 'non-negative-integer':
|
|
388
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0
|
|
357
389
|
case 'boolean':
|
|
358
390
|
return typeof value === 'boolean'
|
|
359
391
|
case 'array':
|