tjs-lang 0.7.8 → 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.
@@ -423,14 +423,14 @@ console.log('(2 + 3) * 4 =', calc.add(2).add(3).multiply(4).getResult())
423
423
  description:
424
424
  'Complete example showing the TS -> TJS -> JS value proposition',
425
425
  group: 'advanced',
426
- code: `/**
427
- * THE FULL PICTURE
428
- *
429
- * TypeScript promises type safety.
430
- * TJS delivers it at RUNTIME.
431
- *
432
- * This is what "TS keeps its promise" means.
433
- */
426
+ code: `/*#
427
+ ## The Full Picture
428
+
429
+ TypeScript promises type safety.
430
+ TJS delivers it at RUNTIME.
431
+
432
+ This is what "TS keeps its promise" means.
433
+ */
434
434
 
435
435
  // Define your types with standard TypeScript syntax
436
436
  interface Product {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tjs-lang",
3
- "version": "0.7.8",
3
+ "version": "0.8.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",
@@ -53,6 +53,10 @@
53
53
  "types": "./dist/src/batteries/index.d.ts",
54
54
  "default": "./dist/tjs-batteries.js"
55
55
  },
56
+ "./linalg": {
57
+ "bun": "./src/linalg/index.tjs",
58
+ "default": "./dist/tjs-linalg.js"
59
+ },
56
60
  "./src": "./src/index.ts",
57
61
  "./editors/monaco": "./editors/monaco/ajs-monarch.js",
58
62
  "./editors/codemirror": "./editors/codemirror/ajs-language.js",
@@ -131,8 +135,8 @@
131
135
  "functions:build": "cd functions && npm run build",
132
136
  "functions:deploy": "cd functions && npm run deploy",
133
137
  "functions:serve": "cd functions && npm run serve",
134
- "deploy:hosting": "firebase deploy --only hosting",
135
- "deploy": "npm run build:demo && npm run functions:deploy && firebase deploy --only hosting",
138
+ "deploy:hosting": "bun run build:demo && firebase deploy --only hosting",
139
+ "deploy": "bun run build:demo && bun run functions:deploy && firebase deploy --only hosting",
136
140
  "start": "bun run build:demo && bun run dev"
137
141
  },
138
142
  "dependencies": {
@@ -157,6 +157,154 @@ function second(x: 0): 0 { return x }
157
157
  })
158
158
  })
159
159
 
160
+ describe('JSDoc-style doc blocks', () => {
161
+ it('extracts /** */ blocks and strips leading asterisks', () => {
162
+ const source = `
163
+ /**
164
+ * # Title
165
+ *
166
+ * Body line 1
167
+ * Body line 2
168
+ */
169
+ `
170
+ const result = generateDocs(source)
171
+
172
+ expect(result.items).toHaveLength(1)
173
+ const doc = result.items[0] as any
174
+ expect(doc.type).toBe('doc')
175
+ expect(doc.content).toBe('# Title\n\nBody line 1\nBody line 2')
176
+ })
177
+
178
+ it('handles single-line JSDoc', () => {
179
+ const source = `/** A short note. */`
180
+ const result = generateDocs(source)
181
+
182
+ const doc = result.items[0] as any
183
+ expect(doc.type).toBe('doc')
184
+ expect(doc.content).toBe('A short note.')
185
+ })
186
+
187
+ it('preserves markdown lists and tables', () => {
188
+ const source = `
189
+ /**
190
+ * ## Options
191
+ *
192
+ * | Flag | Meaning |
193
+ * |------|---------|
194
+ * | \`-v\` | verbose |
195
+ *
196
+ * - first
197
+ * - second
198
+ */
199
+ `
200
+ const result = generateDocs(source)
201
+
202
+ const doc = result.items[0] as any
203
+ expect(doc.content).toContain('## Options')
204
+ expect(doc.content).toContain('| Flag | Meaning |')
205
+ expect(doc.content).toContain('- first')
206
+ })
207
+
208
+ it('leaves @param / @returns as plain markdown', () => {
209
+ const source = `
210
+ /**
211
+ * Square the input.
212
+ *
213
+ * @param x - the input
214
+ * @returns the squared value
215
+ */
216
+ function square(x: 0): 0 { return x * x }
217
+ `
218
+ const result = generateDocs(source)
219
+
220
+ const doc = result.items[0] as any
221
+ expect(doc.content).toContain('@param x - the input')
222
+ expect(doc.content).toContain('@returns the squared value')
223
+ })
224
+
225
+ it('skips JSDoc inside function bodies', () => {
226
+ const source = `
227
+ function outer() {
228
+ /**
229
+ * Should not be extracted — inside a body.
230
+ */
231
+ return 1
232
+ }
233
+ `
234
+ const result = generateDocs(source)
235
+
236
+ // Only the function itself, no doc item
237
+ const docs = result.items.filter((i) => i.type === 'doc')
238
+ expect(docs).toHaveLength(0)
239
+ })
240
+
241
+ it('skips empty JSDoc blocks', () => {
242
+ const source = `
243
+ /**
244
+ *
245
+ */
246
+ function f() {}
247
+ `
248
+ const result = generateDocs(source)
249
+
250
+ const docs = result.items.filter((i) => i.type === 'doc')
251
+ expect(docs).toHaveLength(0)
252
+ })
253
+
254
+ it('does not treat /* ... */ as a doc comment', () => {
255
+ const source = `
256
+ /* just a regular block comment */
257
+ function f() {}
258
+ `
259
+ const result = generateDocs(source)
260
+
261
+ const docs = result.items.filter((i) => i.type === 'doc')
262
+ expect(docs).toHaveLength(0)
263
+ })
264
+
265
+ it('interleaves JSDoc with functions in document order', () => {
266
+ const source = `
267
+ /**
268
+ * # First
269
+ */
270
+ function first(x: 0): 0 { return x }
271
+
272
+ /**
273
+ * # Second
274
+ */
275
+ function second(x: 0): 0 { return x }
276
+ `
277
+ const result = generateDocs(source)
278
+
279
+ expect(result.items).toHaveLength(4)
280
+ expect(result.items[0].type).toBe('doc')
281
+ expect((result.items[0] as any).content).toContain('# First')
282
+ expect(result.items[1].type).toBe('function')
283
+ expect(result.items[2].type).toBe('doc')
284
+ expect((result.items[2] as any).content).toContain('# Second')
285
+ expect(result.items[3].type).toBe('function')
286
+ })
287
+
288
+ it('coexists with /*# blocks in the same file', () => {
289
+ const source = `
290
+ /*#
291
+ ## TJS-native
292
+ */
293
+
294
+ /**
295
+ * ## JSDoc-native
296
+ */
297
+ function f(x: 0): 0 { return x }
298
+ `
299
+ const result = generateDocs(source)
300
+
301
+ const docs = result.items.filter((i) => i.type === 'doc') as any[]
302
+ expect(docs).toHaveLength(2)
303
+ expect(docs[0].content).toContain('TJS-native')
304
+ expect(docs[1].content).toContain('JSDoc-native')
305
+ })
306
+ })
307
+
160
308
  describe('markdown output', () => {
161
309
  it('renders doc blocks as plain markdown', () => {
162
310
  const source = `
package/src/lang/docs.ts CHANGED
@@ -120,6 +120,34 @@ export type DocItem =
120
120
  members: string[] // constructor / method signatures, no bodies
121
121
  }
122
122
 
123
+ // Dedent a block of text by the smallest leading-whitespace indent
124
+ // found across non-empty lines.
125
+ function dedent(content: string): string {
126
+ const lines = content.split('\n')
127
+ const minIndent = lines
128
+ .filter((line) => line.trim().length > 0)
129
+ .reduce((min, line) => {
130
+ const indent = line.match(/^(\s*)/)?.[1].length || 0
131
+ return Math.min(min, indent)
132
+ }, Infinity)
133
+ if (minIndent === 0 || minIndent === Infinity) return content
134
+ return lines.map((line) => line.slice(minIndent)).join('\n')
135
+ }
136
+
137
+ // Strip the leading ` * ` from each line of a JSDoc comment body.
138
+ // The first line (between `/**` and the first newline) is left alone —
139
+ // content can appear there directly. Empty `*`-only lines become blank.
140
+ function stripJSDocAsterisks(content: string): string {
141
+ const lines = content.split('\n')
142
+ return lines
143
+ .map((line, i) => {
144
+ if (i === 0) return line
145
+ // Remove optional leading whitespace, then `*`, then optional single space
146
+ return line.replace(/^[ \t]*\*[ \t]?/, '')
147
+ })
148
+ .join('\n')
149
+ }
150
+
123
151
  /**
124
152
  * Generate documentation from TJS source
125
153
  *
@@ -137,8 +165,12 @@ export function generateDocs(source: string): DocResult {
137
165
  // shown in `/*# ... */` doc blocks as real declarations.
138
166
  const isInComment = computeInComment(source)
139
167
 
140
- // Find all doc blocks, functions, and classes; sort by position
168
+ // Find all doc blocks, functions, and classes; sort by position.
169
+ // Two doc-block flavors are recognized:
170
+ // /*# ... */ — TJS native: content is markdown verbatim
171
+ // /** ... */ — JSDoc: each line's leading ` * ` is stripped, then markdown
141
172
  const docPattern = /\/\*#([\s\S]*?)\*\//g
173
+ const jsdocPattern = /\/\*\*([\s\S]*?)\*\//g
142
174
  // Match the START of a function declaration. Params (which can contain
143
175
  // nested parens like `fn = (x) => x`) are captured by balanced-paren
144
176
  // scanning below, NOT by this regex.
@@ -155,24 +187,26 @@ export function generateDocs(source: string): DocResult {
155
187
  continue
156
188
  }
157
189
 
158
- // Dedent content
159
- let content = match[1]
160
- const lines = content.split('\n')
161
- const minIndent = lines
162
- .filter((line) => line.trim().length > 0)
163
- .reduce((min, line) => {
164
- const indent = line.match(/^(\s*)/)?.[1].length || 0
165
- return Math.min(min, indent)
166
- }, Infinity)
167
-
168
- if (minIndent > 0 && minIndent < Infinity) {
169
- content = lines.map((line) => line.slice(minIndent)).join('\n')
170
- }
190
+ const content = dedent(match[1]).trim()
191
+ if (!content) continue
192
+
193
+ matches.push({
194
+ type: 'doc',
195
+ index: match.index,
196
+ data: content,
197
+ })
198
+ }
199
+
200
+ while ((match = jsdocPattern.exec(source)) !== null) {
201
+ if (braceDepthAt[match.index] !== 0) continue
202
+
203
+ const content = dedent(stripJSDocAsterisks(match[1])).trim()
204
+ if (!content) continue
171
205
 
172
206
  matches.push({
173
207
  type: 'doc',
174
208
  index: match.index,
175
- data: content.trim(),
209
+ data: content,
176
210
  })
177
211
  }
178
212
 
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * TJS WASM Bootstrap Generation
3
3
  *
4
- * Compiles inline WASM blocks and generates JavaScript bootstrap code.
4
+ * Compiles the file's WASM blocks into a single WebAssembly.Module with
5
+ * one exported function per block, then emits JavaScript that compiles
6
+ * and instantiates the module once at startup. This is the foundation
7
+ * for cross-file wasm composition (see wasm-library-plan.md, Phase 3) —
8
+ * once everything's in one module, intra-module calls cost nothing.
5
9
  */
6
10
 
7
- import { compileToWasm } from '../wasm'
11
+ import { compileBlocksToModule } from '../wasm'
8
12
  import type { WasmBlock } from '../parser'
9
13
 
10
14
  export function generateWasmBootstrap(blocks: WasmBlock[]): {
@@ -16,75 +20,61 @@ export function generateWasmBootstrap(blocks: WasmBlock[]): {
16
20
  byteLength?: number
17
21
  }[]
18
22
  } {
19
- const results: {
20
- id: string
21
- success: boolean
22
- error?: string
23
- byteLength?: number
24
- }[] = []
25
- const compiledBlocks: {
26
- id: string
27
- base64: string
28
- captures: string[]
29
- needsMemory: boolean
30
- wat: string
31
- }[] = []
23
+ const compiled = compileBlocksToModule(blocks)
32
24
 
33
- for (const block of blocks) {
34
- const result = compileToWasm(block)
35
- if (result.success) {
36
- // Convert bytes to base64 for embedding
37
- const base64 = btoa(String.fromCharCode(...result.bytes))
38
- compiledBlocks.push({
39
- id: block.id,
40
- base64,
41
- captures: block.captures,
42
- needsMemory: result.needsMemory ?? false,
43
- wat: result.wat ?? '',
44
- })
45
- results.push({
46
- id: block.id,
47
- success: true,
48
- byteLength: result.bytes.length,
49
- })
50
- } else {
51
- results.push({
52
- id: block.id,
53
- success: false,
54
- error: result.error,
55
- })
25
+ // Map per-block status to the public result shape (preserves input order)
26
+ const exportById = new Map(compiled.exports.map((e) => [e.id, e]))
27
+ const results = compiled.results.map((r) => {
28
+ if (!r.success) {
29
+ return { id: r.id, success: false, error: r.error }
56
30
  }
57
- }
31
+ const exp = exportById.get(r.id)!
32
+ return {
33
+ id: r.id,
34
+ success: true,
35
+ byteLength: compiled.bytes.length,
36
+ // Per-export byte length isn't meaningful in the consolidated module;
37
+ // we report the total module size for every successful block.
38
+ _exportName: exp.exportName,
39
+ }
40
+ })
58
41
 
59
- if (compiledBlocks.length === 0) {
42
+ if (compiled.exports.length === 0) {
60
43
  return { code: '', results }
61
44
  }
62
45
 
63
- // Generate WAT comments for each block
64
- const watComments = compiledBlocks
65
- .map((b) => {
66
- const watLines = b.wat.split('\n').map((line) => ` * ${line}`)
67
- return `/**\n * WASM: ${b.id}\n${watLines.join('\n')}\n */`
46
+ // WAT comment block one section per included function
47
+ const watComments = compiled.exports
48
+ .map((e) => {
49
+ const watLines = e.wat.split('\n').map((line) => ` * ${line}`)
50
+ return `/**\n * WASM: ${e.id} (export: ${e.exportName})\n${watLines.join(
51
+ '\n'
52
+ )}\n */`
68
53
  })
69
54
  .join('\n')
70
55
 
71
- // Generate self-contained bootstrap code
72
- // This runs immediately and sets up globalThis.__tjs_wasm_N functions
73
- const blockData = compiledBlocks
56
+ // Per-export metadata embedded in the bootstrap.
57
+ // id = original block id (becomes globalThis[id])
58
+ // n = export name in the composed module (instance.exports[n])
59
+ // c = capture annotations for type-aware wrapping
60
+ // m = whether this export uses memory (must be true if any other
61
+ // export uses it, since memory is shared at module level)
62
+ const exportData = compiled.exports
74
63
  .map(
75
- (b) =>
76
- `{id:${JSON.stringify(b.id)},b64:${JSON.stringify(
77
- b.base64
78
- )},c:${JSON.stringify(b.captures)},m:${b.needsMemory}}`
64
+ (e) =>
65
+ `{id:${JSON.stringify(e.id)},n:${JSON.stringify(
66
+ e.exportName
67
+ )},c:${JSON.stringify(e.captures)},m:${e.needsMemory}}`
79
68
  )
80
69
  .join(',')
81
70
 
82
- // Check if any block needs memory (shared across all blocks)
83
- const anyNeedsMemory = compiledBlocks.some((b) => b.needsMemory)
71
+ const moduleBase64 = btoa(String.fromCharCode(...compiled.bytes))
72
+ const anyNeedsMemory = compiled.needsMemory
84
73
 
85
74
  const code = `${watComments}
86
75
  ;(async()=>{
87
- const __wasmBlocks=[${blockData}];
76
+ const __wasmExports=[${exportData}];
77
+ const __wasmModuleB64=${JSON.stringify(moduleBase64)};
88
78
  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};
89
79
  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}};
90
80
  ${
@@ -94,30 +84,32 @@ let __woff=0;
94
84
  globalThis.wasmBuffer=function(Ctor,len){const bytes=len*Ctor.BYTES_PER_ELEMENT;const align=Math.max(Ctor.BYTES_PER_ELEMENT,16);__woff=(__woff+align-1)&~(align-1);const arr=new Ctor(__wasmMem.buffer,__woff,len);__woff+=bytes;return arr};`
95
85
  : ''
96
86
  }
97
- for(const{id,b64,c,m}of __wasmBlocks){
98
- const bytes=__b64ToBytes(b64);
87
+ const __wasmInst=await WebAssembly.instantiate(await WebAssembly.compile(__b64ToBytes(__wasmModuleB64)),${
88
+ anyNeedsMemory ? '{env:{memory:__wasmMem}}' : '{}'
89
+ });
90
+ for(const{id,n,c,m}of __wasmExports){
91
+ const compute=__wasmInst.exports[n];
99
92
  const params=c.map(__parseType);
100
93
  const hasArrays=params.some(p=>p.a);
101
- const mem=m?__wasmMem:null;
102
- const imp=mem?{env:{memory:mem}}:{};
103
- const inst=await WebAssembly.instantiate(await WebAssembly.compile(bytes),imp);
104
- const compute=inst.exports.compute;
105
94
  if(!hasArrays){globalThis[id]=compute;continue}
106
95
  globalThis[id]=function(...args){
107
- const mv=new Uint8Array(mem.buffer);let off=__woff;const ptrs=[];
96
+ const mv=new Uint8Array(__wasmMem.buffer);let off=__woff;const ptrs=[];
108
97
  for(let i=0;i<params.length;i++){const p=params[i],a=args[i];
109
98
  if(p.a&&a?.buffer){
110
- if(a.buffer===mem.buffer){ptrs.push(a.byteOffset)}
99
+ if(a.buffer===__wasmMem.buffer){ptrs.push(a.byteOffset)}
111
100
  else{const ab=new Uint8Array(a.buffer,a.byteOffset,a.byteLength);off=(off+15)&~15;mv.set(ab,off);ptrs.push(off);off+=ab.length}
112
101
  } else ptrs.push(a)}
113
102
  const r=compute(...ptrs);off=__woff;
114
103
  for(let i=0;i<params.length;i++){const p=params[i],a=args[i];
115
104
  if(p.a&&a?.buffer){
116
- if(a.buffer===mem.buffer) continue;
105
+ if(a.buffer===__wasmMem.buffer) continue;
117
106
  const ab=new Uint8Array(a.buffer,a.byteOffset,a.byteLength);off=(off+15)&~15;ab.set(mv.slice(off,off+ab.length));off+=ab.length}}
118
107
  return r};
119
108
  }})();
120
109
  `.trim()
121
110
 
122
- return { code, results }
111
+ // Strip the temporary _exportName field before returning to caller.
112
+ const publicResults = results.map(({ _exportName: _, ...rest }) => rest)
113
+
114
+ return { code, results: publicResults }
123
115
  }
@@ -101,6 +101,15 @@ export interface TJSTranspileOptions {
101
101
  * Used when tests depend on imported modules.
102
102
  */
103
103
  resolvedImports?: Record<string, string>
104
+ /**
105
+ * Optional ModuleLoader for cross-file `wasm function` composition (Phase 3
106
+ * of the wasm-library plan). When provided, `import { dot } from
107
+ * 'tjs-lang/linalg'`-style imports are resolved at transpile time and any
108
+ * matching `wasm function` declarations are composed into this file's
109
+ * consolidated WebAssembly.Module. When omitted, imports are preserved
110
+ * verbatim (default behavior — runtime resolves them).
111
+ */
112
+ moduleLoader?: any
104
113
  }
105
114
 
106
115
  /** Result of running tests at transpile time */
@@ -631,13 +640,19 @@ export function transpileToJS(
631
640
  } = parse(cleanSource, {
632
641
  filename,
633
642
  colonShorthand: true,
643
+ moduleLoader: options.moduleLoader,
634
644
  })
635
645
 
636
646
  // Find ALL functions in the program
637
647
  const functions = findAllFunctions(program)
638
648
 
639
649
  // Preprocess source (handles TJS syntax transformations)
640
- const preprocessed = preprocess(cleanSource)
650
+ // Pass through the moduleLoader so Phase 3 cross-file wasm composition
651
+ // sees imported `wasm function` declarations.
652
+ const preprocessed = preprocess(cleanSource, {
653
+ moduleLoader: options.moduleLoader,
654
+ filename,
655
+ })
641
656
 
642
657
  // Apply the same source-level equality transforms to extracted test/mock
643
658
  // bodies so they observe the module's TJS semantics (e.g. structural ==).
@@ -1524,9 +1524,10 @@ function double(x: 0, y: 0) {
1524
1524
  expect(result.wasmCompiled?.[0].success).toBe(true)
1525
1525
  expect(result.wasmCompiled?.[0].byteLength).toBeGreaterThan(0)
1526
1526
 
1527
- // Output should contain base64-encoded WASM
1528
- expect(result.code).toContain('__wasmBlocks')
1529
- expect(result.code).toContain('b64:')
1527
+ // Output should contain base64-encoded WASM. After Phase 0.5 consolidation
1528
+ // the emitter ships one module per file and a per-export metadata table.
1529
+ expect(result.code).toContain('__wasmExports')
1530
+ expect(result.code).toContain('__wasmModuleB64')
1530
1531
 
1531
1532
  // Execute with async function to allow WASM instantiation
1532
1533
  const fn = new Function(
package/src/lang/index.ts CHANGED
@@ -74,6 +74,15 @@ export {
74
74
  type FunctionTypeInfo,
75
75
  type ParamTypeInfo,
76
76
  } from './docs'
77
+ export {
78
+ ModuleLoader,
79
+ inMemoryFileSystem,
80
+ type ModuleLoaderOptions,
81
+ type FileSystem,
82
+ type LoadedModule,
83
+ type ImportEntry,
84
+ type ExportEntry,
85
+ } from './module-loader'
77
86
  export {
78
87
  extractTests,
79
88
  assertFunction,