tjs-lang 0.7.7 → 0.8.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/CLAUDE.md +99 -33
- package/bin/docs.js +4 -1
- package/demo/docs.json +104 -22
- package/demo/src/examples.test.ts +1 -0
- package/demo/src/imports.test.ts +16 -4
- package/demo/src/imports.ts +60 -15
- package/demo/src/playground-shared.ts +9 -8
- package/demo/src/tfs-worker.js +205 -147
- package/demo/src/tjs-playground.ts +34 -10
- package/demo/src/ts-examples.ts +8 -8
- package/demo/src/ts-playground.ts +24 -8
- package/dist/index.js +118 -101
- package/dist/index.js.map +4 -4
- package/dist/src/lang/bool-coercion.d.ts +50 -0
- package/dist/src/lang/docs.d.ts +31 -6
- package/dist/src/lang/linter.d.ts +8 -0
- package/dist/src/lang/parser-transforms.d.ts +18 -0
- package/dist/src/lang/parser-types.d.ts +2 -0
- package/dist/src/lang/parser.d.ts +3 -0
- package/dist/src/lang/runtime.d.ts +34 -0
- package/dist/src/lang/types.d.ts +9 -1
- package/dist/src/rbac/index.d.ts +1 -1
- package/dist/src/vm/runtime.d.ts +1 -1
- package/dist/tjs-eval.js +38 -36
- package/dist/tjs-eval.js.map +4 -4
- package/dist/tjs-from-ts.js +20 -20
- package/dist/tjs-from-ts.js.map +3 -3
- package/dist/tjs-lang.js +85 -83
- package/dist/tjs-lang.js.map +4 -4
- package/dist/tjs-vm.js +47 -45
- package/dist/tjs-vm.js.map +4 -4
- package/llms.txt +79 -0
- package/package.json +9 -4
- package/src/cli/commands/convert.test.ts +16 -21
- package/src/lang/bool-coercion.test.ts +203 -0
- package/src/lang/bool-coercion.ts +314 -0
- package/src/lang/codegen.test.ts +137 -0
- package/src/lang/docs.test.ts +476 -1
- package/src/lang/docs.ts +471 -37
- package/src/lang/emitters/ast.ts +11 -12
- package/src/lang/emitters/dts.test.ts +41 -0
- package/src/lang/emitters/dts.ts +9 -0
- package/src/lang/emitters/js-tests.ts +9 -4
- package/src/lang/emitters/js-wasm.ts +57 -65
- package/src/lang/emitters/js.ts +198 -3
- package/src/lang/features.test.ts +4 -3
- package/src/lang/index.ts +9 -0
- package/src/lang/inference.ts +54 -0
- package/src/lang/linter.test.ts +104 -1
- package/src/lang/linter.ts +124 -1
- package/src/lang/module-loader.test.ts +318 -0
- package/src/lang/module-loader.ts +419 -0
- package/src/lang/parser-params.ts +31 -0
- package/src/lang/parser-transforms.ts +640 -0
- package/src/lang/parser-types.ts +35 -0
- package/src/lang/parser.test.ts +73 -1
- package/src/lang/parser.ts +77 -3
- package/src/lang/runtime.ts +98 -0
- package/src/lang/types.ts +6 -0
- package/src/lang/wasm.test.ts +1293 -2
- package/src/lang/wasm.ts +470 -87
- package/src/linalg/index.tjs +119 -0
- package/src/linalg/linalg.test.ts +294 -0
- package/src/linalg/vector-search.bench.test.ts +395 -0
- package/src/rbac/index.ts +2 -2
- package/src/rbac/rules.tjs.d.ts +9 -0
- package/src/vm/atoms/batteries.ts +2 -2
- package/src/vm/runtime.ts +10 -3
- package/dist/src/rbac/rules.d.ts +0 -184
- package/src/rbac/rules.js +0 -338
package/src/lang/parser-types.ts
CHANGED
|
@@ -15,6 +15,17 @@ export interface ParseOptions {
|
|
|
15
15
|
* When true, skips == to Is() transformation since the VM handles == correctly.
|
|
16
16
|
*/
|
|
17
17
|
vmTarget?: boolean
|
|
18
|
+
/**
|
|
19
|
+
* Optional ModuleLoader for cross-file `wasm function` composition (Phase 3).
|
|
20
|
+
* When provided, imports are resolved at transpile time and matching wasm
|
|
21
|
+
* functions are composed into the consumer's WebAssembly.Module. When
|
|
22
|
+
* omitted, imports are preserved verbatim (the default — runtime resolves
|
|
23
|
+
* them as before).
|
|
24
|
+
*
|
|
25
|
+
* Type is left as `any` here to avoid a circular import with module-loader.ts;
|
|
26
|
+
* callers should pass a `ModuleLoader` instance.
|
|
27
|
+
*/
|
|
28
|
+
moduleLoader?: any
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
/**
|
|
@@ -37,6 +48,21 @@ export interface ParseOptions {
|
|
|
37
48
|
export interface WasmBlock {
|
|
38
49
|
/** Unique ID for this block */
|
|
39
50
|
id: string
|
|
51
|
+
/**
|
|
52
|
+
* Declared function name (only set for top-level `wasm function NAME(...)`
|
|
53
|
+
* declarations — Phase 1+). Used by Phase 3 cross-file composition to
|
|
54
|
+
* match an imported symbol against a wasm function declaration. Inline
|
|
55
|
+
* `wasm {}` blocks have no name and don't participate in composition.
|
|
56
|
+
*/
|
|
57
|
+
name?: string
|
|
58
|
+
/**
|
|
59
|
+
* Declared return-type annotation, e.g. `'f64'`. Only set for top-level
|
|
60
|
+
* `wasm function NAME(...): RetType` declarations; presence/absence is
|
|
61
|
+
* used to determine `hasReturn` BEFORE the body is compiled, so the
|
|
62
|
+
* function index map can be built up-front for wasm-to-wasm calls.
|
|
63
|
+
* Inline blocks have no declared return type.
|
|
64
|
+
*/
|
|
65
|
+
returnType?: string
|
|
40
66
|
/** The body (JS subset that compiles to WASM, also used as fallback) */
|
|
41
67
|
body: string
|
|
42
68
|
/** Explicit fallback body (only if different from body) */
|
|
@@ -83,6 +109,13 @@ export interface PreprocessOptions {
|
|
|
83
109
|
* Default: false (transform == to Is() for TJS code running in regular JS)
|
|
84
110
|
*/
|
|
85
111
|
vmTarget?: boolean
|
|
112
|
+
/**
|
|
113
|
+
* Optional ModuleLoader for cross-file `wasm function` composition (Phase 3).
|
|
114
|
+
* See ParseOptions.moduleLoader for details.
|
|
115
|
+
*/
|
|
116
|
+
moduleLoader?: any
|
|
117
|
+
/** Path of the file being preprocessed (used as importer context). */
|
|
118
|
+
filename?: string
|
|
86
119
|
}
|
|
87
120
|
|
|
88
121
|
/**
|
|
@@ -144,6 +177,8 @@ export interface TjsModes {
|
|
|
144
177
|
tjsSafeEval: boolean
|
|
145
178
|
/** TjsNoVar: var declarations are syntax errors */
|
|
146
179
|
tjsNoVar: boolean
|
|
180
|
+
/** TjsSafeAssign: let declarations need an initializer or `: example` annotation; literal undefined/null/void 0 assigned to typed lets is flagged */
|
|
181
|
+
tjsSafeAssign: boolean
|
|
147
182
|
}
|
|
148
183
|
|
|
149
184
|
/**
|
package/src/lang/parser.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'bun:test'
|
|
2
2
|
import { transpile, ajs, tjs } from './index'
|
|
3
|
-
import { preprocess } from './parser'
|
|
3
|
+
import { preprocess, parse } from './parser'
|
|
4
4
|
import { createRuntime, isMonadicError } from './runtime'
|
|
5
5
|
|
|
6
6
|
describe('Transpiler', () => {
|
|
@@ -1067,4 +1067,76 @@ test 'always fails' { throw new Error('intentional') }
|
|
|
1067
1067
|
expect(varSetStep.value.op).toBe('??')
|
|
1068
1068
|
})
|
|
1069
1069
|
})
|
|
1070
|
+
|
|
1071
|
+
describe('stray `=>` on function declarations', () => {
|
|
1072
|
+
it('errors clearly on `function f(...): T => { body }`', () => {
|
|
1073
|
+
// A common mistake — writing arrow-function syntax on a regular
|
|
1074
|
+
// function declaration. Without the dedicated check, the `=>` falls
|
|
1075
|
+
// through to Acorn which complains at a misleading position.
|
|
1076
|
+
expect(() =>
|
|
1077
|
+
tjs(`function f(s: '', n = 0): 0 => {
|
|
1078
|
+
return s.length + n
|
|
1079
|
+
}`)
|
|
1080
|
+
).toThrow(/Unexpected '=>' after function declaration/)
|
|
1081
|
+
})
|
|
1082
|
+
|
|
1083
|
+
it('errors with column pointing at the `=>` (not earlier in the line)', () => {
|
|
1084
|
+
let caught: any
|
|
1085
|
+
try {
|
|
1086
|
+
tjs(`function f(): 0 => { return 0 }`)
|
|
1087
|
+
} catch (e) {
|
|
1088
|
+
caught = e
|
|
1089
|
+
}
|
|
1090
|
+
expect(caught).toBeDefined()
|
|
1091
|
+
// The `=>` is at column 17 (1-indexed): `function f(): 0 ` is 16 chars
|
|
1092
|
+
expect(caught.column).toBe(16)
|
|
1093
|
+
})
|
|
1094
|
+
|
|
1095
|
+
it('does not error on regular function declarations', () => {
|
|
1096
|
+
expect(() =>
|
|
1097
|
+
tjs(`function f(s: ''): 0 { return s.length }`)
|
|
1098
|
+
).not.toThrow()
|
|
1099
|
+
})
|
|
1100
|
+
|
|
1101
|
+
it('does not error on real arrow function expressions', () => {
|
|
1102
|
+
expect(() => tjs(`const f = (s = '') => s.length`)).not.toThrow()
|
|
1103
|
+
})
|
|
1104
|
+
})
|
|
1105
|
+
|
|
1106
|
+
describe('let type annotations (TjsSafeAssign)', () => {
|
|
1107
|
+
it("strips `: <example>` from `let x: ''` and records annotation", () => {
|
|
1108
|
+
const r = parse(`let x: ''`)
|
|
1109
|
+
expect(r.letAnnotations.get('x')).toBe("''")
|
|
1110
|
+
})
|
|
1111
|
+
|
|
1112
|
+
it('strips `: <example>` from `let x: 0 = 5` and keeps the initializer', () => {
|
|
1113
|
+
const r = parse(`let x: 0 = 5`)
|
|
1114
|
+
expect(r.letAnnotations.get('x')).toBe('0')
|
|
1115
|
+
const decl = r.ast.body[0] as any
|
|
1116
|
+
expect(decl.type).toBe('VariableDeclaration')
|
|
1117
|
+
expect(decl.declarations[0].init).not.toBeNull()
|
|
1118
|
+
})
|
|
1119
|
+
|
|
1120
|
+
it('handles object-literal example with nested braces', () => {
|
|
1121
|
+
const r = parse(
|
|
1122
|
+
`function f() { let result: { ok: false }; return result }`
|
|
1123
|
+
)
|
|
1124
|
+
expect(r.letAnnotations.get('result')).toBe('{ ok: false }')
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
it('does not strip `:` inside string literals', () => {
|
|
1128
|
+
const r = parse(`let s = 'a:b:c'`)
|
|
1129
|
+
expect(r.letAnnotations.size).toBe(0)
|
|
1130
|
+
})
|
|
1131
|
+
|
|
1132
|
+
it('TjsSafeAssign mode is on by default in native TJS', () => {
|
|
1133
|
+
const r = parse(`let x = 0`)
|
|
1134
|
+
expect(r.tjsModes.tjsSafeAssign).toBe(true)
|
|
1135
|
+
})
|
|
1136
|
+
|
|
1137
|
+
it('TjsCompat directive disables TjsSafeAssign', () => {
|
|
1138
|
+
const r = parse(`TjsCompat\nlet x = 0`)
|
|
1139
|
+
expect(r.tjsModes.tjsSafeAssign).toBe(false)
|
|
1140
|
+
})
|
|
1141
|
+
})
|
|
1070
1142
|
})
|
package/src/lang/parser.ts
CHANGED
|
@@ -31,6 +31,8 @@ import { transformParenExpressions } from './parser-params'
|
|
|
31
31
|
import {
|
|
32
32
|
transformTryWithoutCatch,
|
|
33
33
|
extractWasmBlocks,
|
|
34
|
+
extractWasmFunctions,
|
|
35
|
+
composeImportedWasmFunctions,
|
|
34
36
|
transformIsOperators,
|
|
35
37
|
insertAsiProtection,
|
|
36
38
|
transformEqualityToStructural,
|
|
@@ -51,6 +53,7 @@ import {
|
|
|
51
53
|
transformConstBang,
|
|
52
54
|
transformBangAccess,
|
|
53
55
|
transformExtensionCalls,
|
|
56
|
+
transformLetTypeAnnotations,
|
|
54
57
|
} from './parser-transforms'
|
|
55
58
|
|
|
56
59
|
// Re-export transformExtensionCalls for js.ts
|
|
@@ -120,6 +123,7 @@ export function preprocess(
|
|
|
120
123
|
testErrors: string[]
|
|
121
124
|
polymorphicNames: Set<string>
|
|
122
125
|
extensions: Map<string, Set<string>>
|
|
126
|
+
letAnnotations: Map<string, string>
|
|
123
127
|
} {
|
|
124
128
|
const originalSource = source
|
|
125
129
|
let moduleSafety: 'none' | 'inputs' | 'all' | undefined
|
|
@@ -143,6 +147,7 @@ export function preprocess(
|
|
|
143
147
|
tjsStandard: false,
|
|
144
148
|
tjsSafeEval: false,
|
|
145
149
|
tjsNoVar: false,
|
|
150
|
+
tjsSafeAssign: false,
|
|
146
151
|
}
|
|
147
152
|
: {
|
|
148
153
|
tjsEquals: true,
|
|
@@ -152,6 +157,7 @@ export function preprocess(
|
|
|
152
157
|
tjsStandard: true,
|
|
153
158
|
tjsSafeEval: false, // opt-in only (adds import)
|
|
154
159
|
tjsNoVar: true,
|
|
160
|
+
tjsSafeAssign: true,
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
// Safety: native TJS defaults to 'inputs' (runtime default),
|
|
@@ -180,7 +186,7 @@ export function preprocess(
|
|
|
180
186
|
// TjsCompat disables all TJS modes (useful for native TJS opting out)
|
|
181
187
|
// Individual modes: TjsEquals, TjsClass, TjsDate, TjsNoeval, TjsStandard, TjsSafeEval
|
|
182
188
|
const directivePattern =
|
|
183
|
-
/^(\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*)\s*(TjsStrict|TjsCompat|TjsEquals|TjsClass|TjsDate|TjsNoeval|TjsNoVar|TjsStandard|TjsSafeEval)\b/
|
|
189
|
+
/^(\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*)\s*(TjsStrict|TjsCompat|TjsEquals|TjsClass|TjsDate|TjsNoeval|TjsNoVar|TjsStandard|TjsSafeEval|TjsSafeAssign)\b/
|
|
184
190
|
|
|
185
191
|
let match
|
|
186
192
|
while ((match = source.match(directivePattern))) {
|
|
@@ -194,6 +200,7 @@ export function preprocess(
|
|
|
194
200
|
tjsModes.tjsNoeval = true
|
|
195
201
|
tjsModes.tjsNoVar = true
|
|
196
202
|
tjsModes.tjsStandard = true
|
|
203
|
+
tjsModes.tjsSafeAssign = true
|
|
197
204
|
} else if (directive === 'TjsCompat') {
|
|
198
205
|
// Disable all TJS modes (JS-compatible)
|
|
199
206
|
tjsModes.tjsEquals = false
|
|
@@ -203,6 +210,7 @@ export function preprocess(
|
|
|
203
210
|
tjsModes.tjsNoVar = false
|
|
204
211
|
tjsModes.tjsStandard = false
|
|
205
212
|
tjsModes.tjsSafeEval = false
|
|
213
|
+
tjsModes.tjsSafeAssign = false
|
|
206
214
|
} else if (directive === 'TjsEquals') {
|
|
207
215
|
tjsModes.tjsEquals = true
|
|
208
216
|
} else if (directive === 'TjsClass') {
|
|
@@ -217,6 +225,8 @@ export function preprocess(
|
|
|
217
225
|
tjsModes.tjsStandard = true
|
|
218
226
|
} else if (directive === 'TjsSafeEval') {
|
|
219
227
|
tjsModes.tjsSafeEval = true
|
|
228
|
+
} else if (directive === 'TjsSafeAssign') {
|
|
229
|
+
tjsModes.tjsSafeAssign = true
|
|
220
230
|
}
|
|
221
231
|
|
|
222
232
|
// Remove the directive from source
|
|
@@ -247,6 +257,21 @@ export function preprocess(
|
|
|
247
257
|
// Must happen before acorn parsing since !. is not valid JS
|
|
248
258
|
source = transformBangAccess(source)
|
|
249
259
|
|
|
260
|
+
// Transform `let x: <example>` declarations: strip annotation and record
|
|
261
|
+
// varName -> example. Must happen before paren transforms so the colon
|
|
262
|
+
// is not confused with TS-style annotations on params/returns.
|
|
263
|
+
const letAnnoResult = transformLetTypeAnnotations(source)
|
|
264
|
+
source = letAnnoResult.source
|
|
265
|
+
const letAnnotations = letAnnoResult.annotations
|
|
266
|
+
|
|
267
|
+
// Extract `wasm function NAME(...) { ... }` declarations EARLY, before
|
|
268
|
+
// any source-level transforms that would mangle wasm-body text. In
|
|
269
|
+
// particular, the equality transforms below rewrite `==` to `Eq()` and
|
|
270
|
+
// `Is`/`IsNot` to function calls — wasm bodies use literal operators
|
|
271
|
+
// and shouldn't be affected.
|
|
272
|
+
const wasmFunctions = extractWasmFunctions(source)
|
|
273
|
+
source = wasmFunctions.source
|
|
274
|
+
|
|
250
275
|
// Transform Is/IsNot infix operators to function calls
|
|
251
276
|
// a Is b -> Is(a, b)
|
|
252
277
|
// a IsNot b -> IsNot(a, b)
|
|
@@ -275,6 +300,18 @@ export function preprocess(
|
|
|
275
300
|
// Foo = ... -> const Foo = ...
|
|
276
301
|
source = transformBareAssignments(source)
|
|
277
302
|
|
|
303
|
+
// Phase 3: cross-file wasm-function composition. When a ModuleLoader is
|
|
304
|
+
// supplied, resolve `import { ... } from '<spec>'` statements at transpile
|
|
305
|
+
// time. Any imported names that correspond to `wasm function` declarations
|
|
306
|
+
// in the source module get pulled into the consumer's wasm module, with
|
|
307
|
+
// the import statement rewritten to a local JS wrapper. No loader supplied
|
|
308
|
+
// = no behavior change (imports stay verbatim, runtime resolves them).
|
|
309
|
+
const importedWasm = composeImportedWasmFunctions(source, {
|
|
310
|
+
loader: options.moduleLoader,
|
|
311
|
+
importerPath: options.filename,
|
|
312
|
+
})
|
|
313
|
+
source = importedWasm.source
|
|
314
|
+
|
|
278
315
|
// Unified paren expression transformer
|
|
279
316
|
// Handles: function params, arrow params, return types, safe/unsafe markers
|
|
280
317
|
// Model: open paren can be ( or (? or (!, close can be ) or )-> or )-? or )-!
|
|
@@ -309,9 +346,24 @@ export function preprocess(
|
|
|
309
346
|
source = polyResult.source
|
|
310
347
|
|
|
311
348
|
// Extract WASM blocks: wasm(args) { ... } fallback { ... }
|
|
349
|
+
// `wasm function` declarations are already extracted earlier in the pipeline
|
|
350
|
+
// (see above, before transformParenExpressions). This finds the remaining
|
|
351
|
+
// inline `wasm { ... }` blocks inside regular tjs functions.
|
|
312
352
|
const wasmBlocks = extractWasmBlocks(source)
|
|
313
353
|
source = wasmBlocks.source
|
|
314
354
|
|
|
355
|
+
// Combine all flavors of wasm blocks for the downstream emitter.
|
|
356
|
+
// They're indistinguishable from the compiler's perspective — all have
|
|
357
|
+
// an id, body, captures, and need the same module composition treatment.
|
|
358
|
+
// - wasmFunctions: top-level `wasm function NAME(...)` decls in this file
|
|
359
|
+
// - importedWasm: cross-file `wasm function`s pulled in via Phase 3
|
|
360
|
+
// - wasmBlocks: inline `wasm { ... }` blocks nested in tjs functions
|
|
361
|
+
const allWasmBlocks = [
|
|
362
|
+
...wasmFunctions.blocks,
|
|
363
|
+
...importedWasm.blocks,
|
|
364
|
+
...wasmBlocks.blocks,
|
|
365
|
+
]
|
|
366
|
+
|
|
315
367
|
// Extract and run test blocks: test 'desc'? { body }
|
|
316
368
|
// Tests run at transpile time and are stripped from output
|
|
317
369
|
const testResult = extractAndRunTests(source, options.dangerouslySkipTests)
|
|
@@ -366,11 +418,12 @@ export function preprocess(
|
|
|
366
418
|
requiredParams,
|
|
367
419
|
unsafeFunctions,
|
|
368
420
|
safeFunctions,
|
|
369
|
-
wasmBlocks:
|
|
421
|
+
wasmBlocks: allWasmBlocks,
|
|
370
422
|
tests: testResult.tests,
|
|
371
423
|
testErrors: testResult.errors,
|
|
372
424
|
polymorphicNames: polyResult.polymorphicNames,
|
|
373
425
|
extensions: extResult.extensions,
|
|
426
|
+
letAnnotations,
|
|
374
427
|
}
|
|
375
428
|
}
|
|
376
429
|
|
|
@@ -392,6 +445,8 @@ export function parse(
|
|
|
392
445
|
wasmBlocks: WasmBlock[]
|
|
393
446
|
tests: TestBlock[]
|
|
394
447
|
testErrors: string[]
|
|
448
|
+
letAnnotations: Map<string, string>
|
|
449
|
+
tjsModes: TjsModes
|
|
395
450
|
} {
|
|
396
451
|
const {
|
|
397
452
|
filename = '<source>',
|
|
@@ -412,8 +467,14 @@ export function parse(
|
|
|
412
467
|
wasmBlocks,
|
|
413
468
|
tests,
|
|
414
469
|
testErrors,
|
|
470
|
+
letAnnotations,
|
|
471
|
+
tjsModes,
|
|
415
472
|
} = colonShorthand
|
|
416
|
-
? preprocess(source, {
|
|
473
|
+
? preprocess(source, {
|
|
474
|
+
vmTarget,
|
|
475
|
+
moduleLoader: options.moduleLoader,
|
|
476
|
+
filename: options.filename,
|
|
477
|
+
})
|
|
417
478
|
: {
|
|
418
479
|
source,
|
|
419
480
|
returnType: undefined,
|
|
@@ -426,6 +487,17 @@ export function parse(
|
|
|
426
487
|
wasmBlocks: [] as WasmBlock[],
|
|
427
488
|
tests: [] as TestBlock[],
|
|
428
489
|
testErrors: [] as string[],
|
|
490
|
+
letAnnotations: new Map<string, string>(),
|
|
491
|
+
tjsModes: {
|
|
492
|
+
tjsEquals: false,
|
|
493
|
+
tjsClass: false,
|
|
494
|
+
tjsDate: false,
|
|
495
|
+
tjsNoeval: false,
|
|
496
|
+
tjsStandard: false,
|
|
497
|
+
tjsSafeEval: false,
|
|
498
|
+
tjsNoVar: false,
|
|
499
|
+
tjsSafeAssign: false,
|
|
500
|
+
} as TjsModes,
|
|
429
501
|
}
|
|
430
502
|
|
|
431
503
|
try {
|
|
@@ -448,6 +520,8 @@ export function parse(
|
|
|
448
520
|
wasmBlocks,
|
|
449
521
|
tests,
|
|
450
522
|
testErrors,
|
|
523
|
+
letAnnotations,
|
|
524
|
+
tjsModes,
|
|
451
525
|
}
|
|
452
526
|
} catch (e: any) {
|
|
453
527
|
// Convert Acorn error to our error type
|
package/src/lang/runtime.ts
CHANGED
|
@@ -605,6 +605,26 @@ export function TypeOf(value: unknown): string {
|
|
|
605
605
|
return typeof value
|
|
606
606
|
}
|
|
607
607
|
|
|
608
|
+
/**
|
|
609
|
+
* Honest boolean coercion. Like `Boolean(x)` but unwraps boxed primitives
|
|
610
|
+
* first, fixing the JS footgun `Boolean(new Boolean(false)) === true`.
|
|
611
|
+
*
|
|
612
|
+
* Under TjsStandard, the source rewriter wraps every truthiness context
|
|
613
|
+
* (if/while/for/do-while conditions, `!`, `&&`, `||`, ternary, and
|
|
614
|
+
* top-level `Boolean(x)` calls) with this function so a boxed `false`
|
|
615
|
+
* actually behaves as `false`.
|
|
616
|
+
*/
|
|
617
|
+
export function toBool(value: unknown): boolean {
|
|
618
|
+
if (
|
|
619
|
+
value instanceof Boolean ||
|
|
620
|
+
value instanceof Number ||
|
|
621
|
+
value instanceof String
|
|
622
|
+
) {
|
|
623
|
+
return Boolean((value as any).valueOf())
|
|
624
|
+
}
|
|
625
|
+
return Boolean(value)
|
|
626
|
+
}
|
|
627
|
+
|
|
608
628
|
export function Eq(a: unknown, b: unknown): boolean {
|
|
609
629
|
// Unwrap boxed primitives
|
|
610
630
|
if (a instanceof String || a instanceof Number || a instanceof Boolean) {
|
|
@@ -844,6 +864,78 @@ type TypeSpec =
|
|
|
844
864
|
| string
|
|
845
865
|
| { check: (v: unknown) => boolean | string; description: string }
|
|
846
866
|
|
|
867
|
+
/**
|
|
868
|
+
* Check that a passed-in function's declared shape matches the expected
|
|
869
|
+
* shape. Returns the function unchanged on a match, or a MonadicError on
|
|
870
|
+
* mismatch. Untyped functions (no `__tjs` metadata — anonymous arrows
|
|
871
|
+
* like `x => false`) pass through unchanged on the assumption that the
|
|
872
|
+
* caller knows what they're doing; they accept any args and return
|
|
873
|
+
* whatever they return.
|
|
874
|
+
*
|
|
875
|
+
* This is a ONE-SHOT check at pass time, NOT a per-call wrapper. The TJS
|
|
876
|
+
* design call: a wrong-shape callback is ONE error at the boundary, not
|
|
877
|
+
* N errors when the receiving function invokes the callback N times.
|
|
878
|
+
*
|
|
879
|
+
* Compatibility rules (deliberately permissive — strict subtyping is a
|
|
880
|
+
* separate, larger feature):
|
|
881
|
+
* - For each expected param: the actual function may declare fewer
|
|
882
|
+
* params (extras simply not used). If both declare a kind, they
|
|
883
|
+
* must match exactly. Either side being `any` always matches.
|
|
884
|
+
* - For the return type: same exact-match rule when both are known.
|
|
885
|
+
*/
|
|
886
|
+
export function checkFnShape(
|
|
887
|
+
fn: unknown,
|
|
888
|
+
expectedParamKinds: string[],
|
|
889
|
+
expectedReturnKind: string,
|
|
890
|
+
path: string
|
|
891
|
+
): unknown {
|
|
892
|
+
if (typeof fn !== 'function') return fn // outer "is callable" check already ran
|
|
893
|
+
const meta = (fn as any).__tjs
|
|
894
|
+
if (!meta || !meta.params) return fn // untyped — let it run
|
|
895
|
+
|
|
896
|
+
const actualEntries = Object.entries(meta.params) as Array<
|
|
897
|
+
[string, { type?: { kind?: string } }]
|
|
898
|
+
>
|
|
899
|
+
for (let i = 0; i < expectedParamKinds.length; i++) {
|
|
900
|
+
const expectedKind = expectedParamKinds[i]
|
|
901
|
+
if (expectedKind === 'any') continue
|
|
902
|
+
const actual = actualEntries[i]
|
|
903
|
+
if (!actual) continue // function takes fewer params, OK
|
|
904
|
+
const actualKind = actual[1]?.type?.kind
|
|
905
|
+
if (!actualKind || actualKind === 'any') continue
|
|
906
|
+
if (actualKind !== expectedKind) {
|
|
907
|
+
return new MonadicError(
|
|
908
|
+
`Expected (...arg${i}: ${expectedKind}, ...) for '${path}', ` +
|
|
909
|
+
`but callback declares arg${i} as ${actualKind}`,
|
|
910
|
+
`${path}(arg${i})`,
|
|
911
|
+
expectedKind,
|
|
912
|
+
actualKind
|
|
913
|
+
)
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (expectedReturnKind !== 'any' && meta.returns) {
|
|
918
|
+
// Metadata's `returns` is `{ type: TypeDescriptor, defaults?: ... }`,
|
|
919
|
+
// but defensively also accept a bare TypeDescriptor.
|
|
920
|
+
const actualReturnKind = meta.returns.type?.kind ?? meta.returns.kind
|
|
921
|
+
if (
|
|
922
|
+
actualReturnKind &&
|
|
923
|
+
actualReturnKind !== 'any' &&
|
|
924
|
+
actualReturnKind !== expectedReturnKind
|
|
925
|
+
) {
|
|
926
|
+
return new MonadicError(
|
|
927
|
+
`Expected callback returning ${expectedReturnKind} for '${path}', ` +
|
|
928
|
+
`but callback returns ${actualReturnKind}`,
|
|
929
|
+
`${path}(return)`,
|
|
930
|
+
expectedReturnKind,
|
|
931
|
+
actualReturnKind
|
|
932
|
+
)
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
return fn
|
|
937
|
+
}
|
|
938
|
+
|
|
847
939
|
/** Parameter metadata with optional location */
|
|
848
940
|
interface ParamMeta {
|
|
849
941
|
type: TypeSpec
|
|
@@ -1478,6 +1570,7 @@ export function createRuntime() {
|
|
|
1478
1570
|
checkType,
|
|
1479
1571
|
validateArgs,
|
|
1480
1572
|
wrap,
|
|
1573
|
+
checkFnShape,
|
|
1481
1574
|
wrapClass,
|
|
1482
1575
|
compareVersions,
|
|
1483
1576
|
versionsCompatible,
|
|
@@ -1528,6 +1621,8 @@ export function createRuntime() {
|
|
|
1528
1621
|
NotEq,
|
|
1529
1622
|
// Honest typeof (typeof with TjsEquals)
|
|
1530
1623
|
TypeOf,
|
|
1624
|
+
// Honest truthiness (unwraps boxed primitives)
|
|
1625
|
+
toBool,
|
|
1531
1626
|
tjsEquals,
|
|
1532
1627
|
// Extensions
|
|
1533
1628
|
registerExtension: instanceRegisterExtension,
|
|
@@ -1559,6 +1654,7 @@ export const runtime = {
|
|
|
1559
1654
|
checkType,
|
|
1560
1655
|
validateArgs,
|
|
1561
1656
|
wrap,
|
|
1657
|
+
checkFnShape,
|
|
1562
1658
|
wrapClass,
|
|
1563
1659
|
compareVersions,
|
|
1564
1660
|
versionsCompatible,
|
|
@@ -1612,6 +1708,8 @@ export const runtime = {
|
|
|
1612
1708
|
NotEq,
|
|
1613
1709
|
// Honest typeof (used by typeof with TjsEquals)
|
|
1614
1710
|
TypeOf,
|
|
1711
|
+
// Honest truthiness (used in TjsStandard for boxed-primitive coercion)
|
|
1712
|
+
toBool,
|
|
1615
1713
|
}
|
|
1616
1714
|
|
|
1617
1715
|
/**
|
package/src/lang/types.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface TypeDescriptor {
|
|
|
22
22
|
| 'array'
|
|
23
23
|
| 'object'
|
|
24
24
|
| 'union'
|
|
25
|
+
| 'function'
|
|
25
26
|
| 'any'
|
|
26
27
|
nullable?: boolean
|
|
27
28
|
/** For arrays: the element type */
|
|
@@ -32,6 +33,11 @@ export interface TypeDescriptor {
|
|
|
32
33
|
members?: TypeDescriptor[]
|
|
33
34
|
/** For destructured parameters: full parameter descriptors */
|
|
34
35
|
destructuredParams?: Record<string, ParameterDescriptor>
|
|
36
|
+
/** For functions: declared parameters with names and inferred types */
|
|
37
|
+
params?: Array<{ name: string; type: TypeDescriptor }>
|
|
38
|
+
/** For functions: inferred return type. Concise arrow bodies infer from
|
|
39
|
+
* the expression; block bodies and complex expressions stay `any`. */
|
|
40
|
+
returns?: TypeDescriptor
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
/** Describes a function parameter */
|