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.
Files changed (70) hide show
  1. package/CLAUDE.md +99 -33
  2. package/bin/docs.js +4 -1
  3. package/demo/docs.json +104 -22
  4. package/demo/src/examples.test.ts +1 -0
  5. package/demo/src/imports.test.ts +16 -4
  6. package/demo/src/imports.ts +60 -15
  7. package/demo/src/playground-shared.ts +9 -8
  8. package/demo/src/tfs-worker.js +205 -147
  9. package/demo/src/tjs-playground.ts +34 -10
  10. package/demo/src/ts-examples.ts +8 -8
  11. package/demo/src/ts-playground.ts +24 -8
  12. package/dist/index.js +118 -101
  13. package/dist/index.js.map +4 -4
  14. package/dist/src/lang/bool-coercion.d.ts +50 -0
  15. package/dist/src/lang/docs.d.ts +31 -6
  16. package/dist/src/lang/linter.d.ts +8 -0
  17. package/dist/src/lang/parser-transforms.d.ts +18 -0
  18. package/dist/src/lang/parser-types.d.ts +2 -0
  19. package/dist/src/lang/parser.d.ts +3 -0
  20. package/dist/src/lang/runtime.d.ts +34 -0
  21. package/dist/src/lang/types.d.ts +9 -1
  22. package/dist/src/rbac/index.d.ts +1 -1
  23. package/dist/src/vm/runtime.d.ts +1 -1
  24. package/dist/tjs-eval.js +38 -36
  25. package/dist/tjs-eval.js.map +4 -4
  26. package/dist/tjs-from-ts.js +20 -20
  27. package/dist/tjs-from-ts.js.map +3 -3
  28. package/dist/tjs-lang.js +85 -83
  29. package/dist/tjs-lang.js.map +4 -4
  30. package/dist/tjs-vm.js +47 -45
  31. package/dist/tjs-vm.js.map +4 -4
  32. package/llms.txt +79 -0
  33. package/package.json +9 -4
  34. package/src/cli/commands/convert.test.ts +16 -21
  35. package/src/lang/bool-coercion.test.ts +203 -0
  36. package/src/lang/bool-coercion.ts +314 -0
  37. package/src/lang/codegen.test.ts +137 -0
  38. package/src/lang/docs.test.ts +476 -1
  39. package/src/lang/docs.ts +471 -37
  40. package/src/lang/emitters/ast.ts +11 -12
  41. package/src/lang/emitters/dts.test.ts +41 -0
  42. package/src/lang/emitters/dts.ts +9 -0
  43. package/src/lang/emitters/js-tests.ts +9 -4
  44. package/src/lang/emitters/js-wasm.ts +57 -65
  45. package/src/lang/emitters/js.ts +198 -3
  46. package/src/lang/features.test.ts +4 -3
  47. package/src/lang/index.ts +9 -0
  48. package/src/lang/inference.ts +54 -0
  49. package/src/lang/linter.test.ts +104 -1
  50. package/src/lang/linter.ts +124 -1
  51. package/src/lang/module-loader.test.ts +318 -0
  52. package/src/lang/module-loader.ts +419 -0
  53. package/src/lang/parser-params.ts +31 -0
  54. package/src/lang/parser-transforms.ts +640 -0
  55. package/src/lang/parser-types.ts +35 -0
  56. package/src/lang/parser.test.ts +73 -1
  57. package/src/lang/parser.ts +77 -3
  58. package/src/lang/runtime.ts +98 -0
  59. package/src/lang/types.ts +6 -0
  60. package/src/lang/wasm.test.ts +1293 -2
  61. package/src/lang/wasm.ts +470 -87
  62. package/src/linalg/index.tjs +119 -0
  63. package/src/linalg/linalg.test.ts +294 -0
  64. package/src/linalg/vector-search.bench.test.ts +395 -0
  65. package/src/rbac/index.ts +2 -2
  66. package/src/rbac/rules.tjs.d.ts +9 -0
  67. package/src/vm/atoms/batteries.ts +2 -2
  68. package/src/vm/runtime.ts +10 -3
  69. package/dist/src/rbac/rules.d.ts +0 -184
  70. package/src/rbac/rules.js +0 -338
@@ -0,0 +1,419 @@
1
+ /**
2
+ * Transpile-time module loader.
3
+ *
4
+ * Until now the transpiler has preserved import statements verbatim — runtime
5
+ * resolvers (the bun plugin, the playground service worker, the browser ESM
6
+ * loader) handle them on demand. That's the right default for regular JS
7
+ * interop, but Phase 3 of the wasm-library plan needs *static* visibility into
8
+ * imported sources: given `import { dot } from 'tjs-lang/linalg'`, we have to
9
+ * read linalg's source at transpile time so cross-file `wasm function`
10
+ * declarations can be composed into the consumer's wasm module.
11
+ *
12
+ * This loader is the foundation for that work. It's also useful as a building
13
+ * block for any future cross-file static analysis (type flow, dead code, etc.)
14
+ * — hence the name "module loader" rather than "wasm import resolver."
15
+ *
16
+ * Usage:
17
+ * const loader = new ModuleLoader({ baseDir: '/path/to/project' })
18
+ * const mod = loader.load('./math.tjs', '/path/to/project/app.tjs')
19
+ * if (mod) { console.log(mod.exports) }
20
+ *
21
+ * Resolution rules (in order):
22
+ * - URL specifiers (http://, https://, data:): not loadable, returns null
23
+ * - Relative paths (./foo, ../bar): resolved against importer's dir
24
+ * - Absolute paths (/foo/bar): used as-is
25
+ * - Bare specifiers (foo, foo/bar): walk up importer looking for
26
+ * node_modules/<spec>; also
27
+ * try bareSpecifierRoots
28
+ *
29
+ * For each candidate, we try extensions in order: `.tjs`, `.ts`, `.js`.
30
+ * (Index files: `<dir>/index.<ext>`.)
31
+ *
32
+ * The loader does NOT mutate transpiler behavior. It's an additive capability;
33
+ * Phase 3 (cross-file wasm composition) is the first caller.
34
+ */
35
+
36
+ import { existsSync, readFileSync } from 'node:fs'
37
+ import { dirname, isAbsolute, resolve as pathResolve, sep } from 'node:path'
38
+ import { parse as parseTjs } from './parser'
39
+
40
+ const SUPPORTED_EXTENSIONS = ['.tjs', '.ts', '.js']
41
+
42
+ /** Pluggable filesystem hook. Default uses node:fs. */
43
+ export interface FileSystem {
44
+ /** Return source text for an absolute path, or null if it doesn't exist. */
45
+ readFile(path: string): string | null
46
+ /** Return true if the path exists and is a file. */
47
+ exists(path: string): boolean
48
+ }
49
+
50
+ const defaultFileSystem: FileSystem = {
51
+ readFile(path) {
52
+ try {
53
+ return readFileSync(path, 'utf8')
54
+ } catch {
55
+ return null
56
+ }
57
+ },
58
+ exists(path) {
59
+ try {
60
+ return existsSync(path)
61
+ } catch {
62
+ return false
63
+ }
64
+ },
65
+ }
66
+
67
+ export interface ModuleLoaderOptions {
68
+ /** Filesystem hook (default: node:fs based) */
69
+ fs?: FileSystem
70
+ /** Where to resolve bare specifiers from when no importer is given. */
71
+ baseDir?: string
72
+ /**
73
+ * Extra roots tried for bare specifiers BEFORE the standard node_modules
74
+ * walk. Useful for monorepos or tests that want to point at a virtual
75
+ * package directory. Each root is treated as if it were a `node_modules/`.
76
+ */
77
+ bareSpecifierRoots?: string[]
78
+ /** Cache size cap. 0 disables caching. (default 256) */
79
+ cacheLimit?: number
80
+ }
81
+
82
+ /** A single import / re-export specifier extracted from the AST */
83
+ export interface ImportEntry {
84
+ /** Original module specifier (e.g. './math.tjs', 'tjs-lang/linalg') */
85
+ specifier: string
86
+ /** Local name in the importing module */
87
+ local: string
88
+ /** Imported name in the source module ('default' for default imports) */
89
+ imported: string
90
+ /** True if this came from `import * as X` */
91
+ namespace: boolean
92
+ }
93
+
94
+ /** A top-level export from a module */
95
+ export interface ExportEntry {
96
+ /** Exported name (or 'default' for default exports) */
97
+ name: string
98
+ /** Kind of declaration being exported */
99
+ kind: 'function' | 'class' | 'variable' | 're-export' | 'unknown'
100
+ /** For re-exports, the source specifier */
101
+ fromSpecifier?: string
102
+ }
103
+
104
+ export interface LoadedModule {
105
+ /** Resolved absolute path */
106
+ path: string
107
+ /** Original source text */
108
+ source: string
109
+ /** AST + tjs preprocessing output (lazy — only computed once per module) */
110
+ parseResult: ReturnType<typeof parseTjs>
111
+ /** Imports declared by this module */
112
+ imports: ImportEntry[]
113
+ /** Top-level exports */
114
+ exports: ExportEntry[]
115
+ }
116
+
117
+ export class ModuleLoader {
118
+ private cache = new Map<string, LoadedModule>()
119
+ private fs: FileSystem
120
+ private baseDir: string
121
+ private bareSpecifierRoots: string[]
122
+ private cacheLimit: number
123
+
124
+ constructor(options: ModuleLoaderOptions = {}) {
125
+ this.fs = options.fs ?? defaultFileSystem
126
+ this.baseDir = options.baseDir ?? process.cwd()
127
+ this.bareSpecifierRoots = options.bareSpecifierRoots ?? []
128
+ this.cacheLimit = options.cacheLimit ?? 256
129
+ }
130
+
131
+ /**
132
+ * Resolve a specifier to an absolute path. Returns null when the specifier
133
+ * is not loadable as a local TJS/TS/JS file (URLs, missing files, unknown
134
+ * bare specifiers all return null — the caller falls back to verbatim
135
+ * import preservation).
136
+ */
137
+ resolve(specifier: string, importerPath?: string): string | null {
138
+ // Reject URL-style and data: specifiers — runtime resolvers handle these
139
+ if (
140
+ specifier.startsWith('http://') ||
141
+ specifier.startsWith('https://') ||
142
+ specifier.startsWith('data:') ||
143
+ specifier.startsWith('file://')
144
+ ) {
145
+ return null
146
+ }
147
+
148
+ if (specifier.startsWith('./') || specifier.startsWith('../')) {
149
+ return this.tryExtensions(
150
+ pathResolve(
151
+ importerPath ? dirname(importerPath) : this.baseDir,
152
+ specifier
153
+ )
154
+ )
155
+ }
156
+
157
+ if (isAbsolute(specifier)) {
158
+ return this.tryExtensions(specifier)
159
+ }
160
+
161
+ return this.resolveBare(specifier, importerPath)
162
+ }
163
+
164
+ /**
165
+ * Load a module by specifier. Returns null if not resolvable (caller treats
166
+ * this as "leave the import statement alone").
167
+ *
168
+ * Repeated calls with the same specifier+importer combination hit the cache.
169
+ */
170
+ load(specifier: string, importerPath?: string): LoadedModule | null {
171
+ const path = this.resolve(specifier, importerPath)
172
+ if (!path) return null
173
+
174
+ const cached = this.cache.get(path)
175
+ if (cached) return cached
176
+
177
+ const source = this.fs.readFile(path)
178
+ if (source === null) return null
179
+
180
+ let parseResult: ReturnType<typeof parseTjs>
181
+ try {
182
+ parseResult = parseTjs(source, { filename: path })
183
+ } catch {
184
+ // Parse failure: don't cache, don't claim to have loaded it.
185
+ // Caller falls back to verbatim import preservation.
186
+ return null
187
+ }
188
+
189
+ const imports = collectImports(parseResult.ast)
190
+ const exports = collectExports(parseResult.ast)
191
+
192
+ const loaded: LoadedModule = {
193
+ path,
194
+ source,
195
+ parseResult,
196
+ imports,
197
+ exports,
198
+ }
199
+
200
+ if (this.cacheLimit > 0) {
201
+ // Naive eviction: drop the first inserted entry when over the limit.
202
+ // Modules are small and the cache is short-lived per transpile session.
203
+ if (this.cache.size >= this.cacheLimit) {
204
+ const firstKey = this.cache.keys().next().value
205
+ if (firstKey !== undefined) this.cache.delete(firstKey)
206
+ }
207
+ this.cache.set(path, loaded)
208
+ }
209
+
210
+ return loaded
211
+ }
212
+
213
+ /** Drop all cached modules. */
214
+ clearCache(): void {
215
+ this.cache.clear()
216
+ }
217
+
218
+ /** Try each supported extension; return the first existing path or null. */
219
+ private tryExtensions(basePath: string): string | null {
220
+ // If the path already has one of our extensions, try it directly first.
221
+ if (SUPPORTED_EXTENSIONS.some((ext) => basePath.endsWith(ext))) {
222
+ return this.fs.exists(basePath) ? basePath : null
223
+ }
224
+ for (const ext of SUPPORTED_EXTENSIONS) {
225
+ const withExt = basePath + ext
226
+ if (this.fs.exists(withExt)) return withExt
227
+ }
228
+ // Try as directory (look for index.<ext>)
229
+ for (const ext of SUPPORTED_EXTENSIONS) {
230
+ const indexPath = pathResolve(basePath, 'index' + ext)
231
+ if (this.fs.exists(indexPath)) return indexPath
232
+ }
233
+ return null
234
+ }
235
+
236
+ /**
237
+ * Resolve a bare specifier (e.g. `tjs-lang/linalg`, `lodash`) by walking
238
+ * up the importer's directory tree looking for `node_modules/<spec>`.
239
+ * Also checks each configured `bareSpecifierRoots` entry first.
240
+ */
241
+ private resolveBare(specifier: string, importerPath?: string): string | null {
242
+ // Try configured roots first (test fixtures, monorepo packages, etc.)
243
+ for (const root of this.bareSpecifierRoots) {
244
+ const candidate = this.tryExtensions(pathResolve(root, specifier))
245
+ if (candidate) return candidate
246
+ }
247
+
248
+ // Walk up from the importer (or baseDir) looking for node_modules
249
+ let dir = importerPath ? dirname(importerPath) : this.baseDir
250
+ // Ensure absolute to avoid an infinite loop on relative inputs
251
+ dir = pathResolve(dir)
252
+ while (true) {
253
+ const nodeModulesCandidate = this.tryExtensions(
254
+ pathResolve(dir, 'node_modules', specifier)
255
+ )
256
+ if (nodeModulesCandidate) return nodeModulesCandidate
257
+
258
+ // Also try resolving via package.json's "main" or "exports" — for now,
259
+ // we're conservative: the standard tjs library layout uses a
260
+ // `src/index.tjs` entry point, which tryExtensions will find via the
261
+ // "directory with index.<ext>" branch above. Full package.json
262
+ // exports-field resolution can come later if needed.
263
+
264
+ const parent = dirname(dir)
265
+ if (parent === dir) break // hit filesystem root
266
+ dir = parent
267
+ }
268
+
269
+ return null
270
+ }
271
+ }
272
+
273
+ // ============================================================================
274
+ // AST inspection helpers
275
+ // ============================================================================
276
+
277
+ /**
278
+ * Extract import declarations from a Program AST.
279
+ * Acorn types: `ImportDeclaration` with `specifiers` (array of
280
+ * ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier).
281
+ */
282
+ function collectImports(ast: any): ImportEntry[] {
283
+ const imports: ImportEntry[] = []
284
+ if (!ast || !Array.isArray(ast.body)) return imports
285
+
286
+ for (const node of ast.body) {
287
+ if (node.type !== 'ImportDeclaration') continue
288
+ const specifier = node.source?.value
289
+ if (typeof specifier !== 'string') continue
290
+
291
+ for (const spec of node.specifiers ?? []) {
292
+ const local = spec.local?.name
293
+ if (typeof local !== 'string') continue
294
+
295
+ switch (spec.type) {
296
+ case 'ImportSpecifier':
297
+ imports.push({
298
+ specifier,
299
+ local,
300
+ imported: spec.imported?.name ?? local,
301
+ namespace: false,
302
+ })
303
+ break
304
+ case 'ImportDefaultSpecifier':
305
+ imports.push({
306
+ specifier,
307
+ local,
308
+ imported: 'default',
309
+ namespace: false,
310
+ })
311
+ break
312
+ case 'ImportNamespaceSpecifier':
313
+ imports.push({
314
+ specifier,
315
+ local,
316
+ imported: '*',
317
+ namespace: true,
318
+ })
319
+ break
320
+ }
321
+ }
322
+ }
323
+
324
+ return imports
325
+ }
326
+
327
+ /**
328
+ * Extract top-level exports from a Program AST.
329
+ *
330
+ * Covers:
331
+ * - `export function foo() {}`
332
+ * - `export class Foo {}`
333
+ * - `export const x = ...`, `export let`, `export var`
334
+ * - `export { a, b as c }`
335
+ * - `export { a } from './other'`
336
+ * - `export * from './other'`
337
+ * - `export default ...`
338
+ *
339
+ * Does NOT yet recognize `wasm function` — Phase 1 will introduce that
340
+ * declaration kind and a follow-up will surface it here as kind: 'wasm-function'.
341
+ */
342
+ function collectExports(ast: any): ExportEntry[] {
343
+ const exports: ExportEntry[] = []
344
+ if (!ast || !Array.isArray(ast.body)) return exports
345
+
346
+ for (const node of ast.body) {
347
+ if (node.type === 'ExportNamedDeclaration') {
348
+ // export function foo() {} | export class Foo {} | export const x = ...
349
+ if (node.declaration) {
350
+ const decl = node.declaration
351
+ if (decl.type === 'FunctionDeclaration' && decl.id?.name) {
352
+ exports.push({ name: decl.id.name, kind: 'function' })
353
+ } else if (decl.type === 'ClassDeclaration' && decl.id?.name) {
354
+ exports.push({ name: decl.id.name, kind: 'class' })
355
+ } else if (decl.type === 'VariableDeclaration') {
356
+ for (const v of decl.declarations) {
357
+ if (v.id?.type === 'Identifier' && v.id.name) {
358
+ exports.push({ name: v.id.name, kind: 'variable' })
359
+ }
360
+ }
361
+ }
362
+ }
363
+ // export { a, b as c } [from './other']
364
+ if (Array.isArray(node.specifiers)) {
365
+ for (const spec of node.specifiers) {
366
+ const exportedName = spec.exported?.name
367
+ if (typeof exportedName !== 'string') continue
368
+ exports.push({
369
+ name: exportedName,
370
+ kind: node.source ? 're-export' : 'unknown',
371
+ fromSpecifier: node.source?.value,
372
+ })
373
+ }
374
+ }
375
+ } else if (node.type === 'ExportDefaultDeclaration') {
376
+ const decl = node.declaration
377
+ const kind: ExportEntry['kind'] =
378
+ decl?.type === 'FunctionDeclaration' ||
379
+ decl?.type === 'FunctionExpression' ||
380
+ decl?.type === 'ArrowFunctionExpression'
381
+ ? 'function'
382
+ : decl?.type === 'ClassDeclaration' ||
383
+ decl?.type === 'ClassExpression'
384
+ ? 'class'
385
+ : 'unknown'
386
+ exports.push({ name: 'default', kind })
387
+ } else if (node.type === 'ExportAllDeclaration') {
388
+ // export * from './other'
389
+ const fromSpecifier = node.source?.value
390
+ if (typeof fromSpecifier === 'string') {
391
+ exports.push({
392
+ name: '*',
393
+ kind: 're-export',
394
+ fromSpecifier,
395
+ })
396
+ }
397
+ }
398
+ }
399
+
400
+ return exports
401
+ }
402
+
403
+ /**
404
+ * Helper for tests / advanced use: build a FileSystem from a plain
405
+ * `Map<string, string>`. Keys must be absolute paths.
406
+ */
407
+ export function inMemoryFileSystem(
408
+ files: Map<string, string> | Record<string, string>
409
+ ): FileSystem {
410
+ const map =
411
+ files instanceof Map ? files : new Map(Object.entries(files))
412
+ // Normalize separators so path.resolve()'s output matches map keys
413
+ const normalize = (p: string) => p.split('/').join(sep)
414
+ const lookup = (p: string) => map.get(p) ?? map.get(normalize(p)) ?? null
415
+ return {
416
+ readFile: (p) => lookup(p),
417
+ exists: (p) => lookup(p) !== null,
418
+ }
419
+ }
@@ -12,6 +12,7 @@ import type {
12
12
  ContextFrame,
13
13
  TjsModes,
14
14
  } from './parser-types'
15
+ import { locAt } from './parser-transforms'
15
16
 
16
17
  export function transformParenExpressions(
17
18
  source: string,
@@ -331,6 +332,23 @@ export function transformParenExpressions(
331
332
  i = typeResult.endPos
332
333
  }
333
334
  }
335
+
336
+ // Catch a common mistake: writing `=> {` after a function declaration's
337
+ // return type (or after `)`), as if it were an arrow function. Without
338
+ // this check, the `=>` would pass through to Acorn, which complains
339
+ // with a generic "Unexpected token" at a misleading position.
340
+ let arrowCheck = i
341
+ while (arrowCheck < source.length && /\s/.test(source[arrowCheck]))
342
+ arrowCheck++
343
+ if (source[arrowCheck] === '=' && source[arrowCheck + 1] === '>') {
344
+ throw new SyntaxError(
345
+ "Unexpected '=>' after function declaration. " +
346
+ 'Function declarations use `function name(params) { body }`, ' +
347
+ 'not arrow syntax. Remove the `=>`.',
348
+ locAt(ctx.originalSource, arrowCheck),
349
+ ctx.originalSource
350
+ )
351
+ }
334
352
  continue
335
353
  }
336
354
 
@@ -410,6 +428,19 @@ export function transformParenExpressions(
410
428
  }
411
429
  }
412
430
 
431
+ // Same `=>` check for class methods.
432
+ let k = i
433
+ while (k < source.length && /\s/.test(source[k])) k++
434
+ if (source[k] === '=' && source[k + 1] === '>') {
435
+ throw new SyntaxError(
436
+ "Unexpected '=>' after method declaration. " +
437
+ 'Methods use `name(params) { body }`, not arrow syntax. ' +
438
+ 'Remove the `=>`.',
439
+ locAt(ctx.originalSource, k),
440
+ ctx.originalSource
441
+ )
442
+ }
443
+
413
444
  continue
414
445
  }
415
446