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.
Files changed (40) hide show
  1. package/demo/docs.json +32 -26
  2. package/demo/src/examples.ts +23 -83
  3. package/demo/src/playground-shared.ts +666 -0
  4. package/demo/src/tjs-playground.ts +65 -550
  5. package/demo/src/ts-examples.ts +5 -4
  6. package/demo/src/ts-playground.ts +50 -414
  7. package/dist/index.js +143 -160
  8. package/dist/index.js.map +12 -12
  9. package/dist/src/lang/emitters/js.d.ts +34 -2
  10. package/dist/src/lang/index.d.ts +1 -1
  11. package/dist/src/lang/types.d.ts +1 -1
  12. package/dist/src/types/Type.d.ts +3 -1
  13. package/dist/tjs-full.js +143 -160
  14. package/dist/tjs-full.js.map +12 -12
  15. package/dist/tjs-transpiler.js +122 -55
  16. package/dist/tjs-transpiler.js.map +9 -8
  17. package/dist/tjs-vm.js +14 -14
  18. package/dist/tjs-vm.js.map +5 -5
  19. package/docs/docs.json +792 -0
  20. package/docs/index.js +2652 -2835
  21. package/docs/index.js.map +11 -10
  22. package/editors/codemirror/ajs-language.ts +27 -1
  23. package/editors/codemirror/autocomplete.test.ts +3 -3
  24. package/package.json +1 -1
  25. package/src/lang/codegen.test.ts +11 -11
  26. package/src/lang/emitters/from-ts.ts +1 -1
  27. package/src/lang/emitters/js.ts +228 -4
  28. package/src/lang/index.ts +0 -3
  29. package/src/lang/inference.ts +40 -8
  30. package/src/lang/lang.test.ts +192 -35
  31. package/src/lang/roundtrip.test.ts +155 -0
  32. package/src/lang/runtime.ts +7 -0
  33. package/src/lang/types.ts +2 -0
  34. package/src/lang/typescript-syntax.test.ts +6 -4
  35. package/src/lang/wasm.test.ts +20 -0
  36. package/src/lang/wasm.ts +143 -0
  37. package/src/types/Type.test.ts +64 -0
  38. package/src/types/Type.ts +22 -1
  39. package/src/use-cases/transpiler-integration.test.ts +10 -10
  40. package/src/vm/atoms/batteries.ts +2 -0
package/src/lang/wasm.ts CHANGED
@@ -227,6 +227,16 @@ const Op = {
227
227
  i32_extend16_s: 0xc1,
228
228
  } as const
229
229
 
230
+ /** Reverse lookup: opcode byte -> instruction name */
231
+ const OpName: Record<number, string> = Object.fromEntries(
232
+ Object.entries(Op).map(([name, code]) => [code, name.replace(/_/g, '.')])
233
+ )
234
+
235
+ /** Emit WAT instruction to context */
236
+ function wat(ctx: CompileContext, instruction: string): void {
237
+ ctx.wat.push(' '.repeat(ctx.watIndent) + instruction)
238
+ }
239
+
230
240
  // ============================================================================
231
241
  // LEB128 Encoding
232
242
  // ============================================================================
@@ -286,6 +296,129 @@ function encodeVector(items: number[][]): number[] {
286
296
  return [...encodeULEB128(items.length), ...items.flat()]
287
297
  }
288
298
 
299
+ // ============================================================================
300
+ // Disassembly (for debugging)
301
+ // ============================================================================
302
+
303
+ /** Decode ULEB128 from bytes, return [value, bytesConsumed] */
304
+ function decodeULEB128(bytes: number[], offset: number): [number, number] {
305
+ let result = 0
306
+ let shift = 0
307
+ let i = offset
308
+ while (i < bytes.length) {
309
+ const byte = bytes[i]
310
+ result |= (byte & 0x7f) << shift
311
+ i++
312
+ if ((byte & 0x80) === 0) break
313
+ shift += 7
314
+ }
315
+ return [result, i - offset]
316
+ }
317
+
318
+ /** Decode f64 from 8 bytes */
319
+ function decodeF64(bytes: number[], offset: number): number {
320
+ const buffer = new ArrayBuffer(8)
321
+ const view = new Uint8Array(buffer)
322
+ for (let i = 0; i < 8; i++) view[i] = bytes[offset + i]
323
+ return new Float64Array(buffer)[0]
324
+ }
325
+
326
+ /** Disassemble function body bytes to WAT-like text */
327
+ function disassemble(
328
+ code: number[],
329
+ params: TypedParam[],
330
+ localTypes: WasmValueType[]
331
+ ): string {
332
+ const lines: string[] = []
333
+ let indent = 1
334
+ const ind = () => ' '.repeat(indent)
335
+
336
+ // Function signature
337
+ const paramStr = params
338
+ .map((p, i) => `(param $${p.name} ${p.type})`)
339
+ .join(' ')
340
+ const localStr = localTypes
341
+ .map((t, i) => `(local $L${params.length + i} ${t})`)
342
+ .join(' ')
343
+ lines.push(`(func (export "compute") ${paramStr} (result f64)`)
344
+ if (localStr) lines.push(` ${localStr}`)
345
+
346
+ let i = 0
347
+ while (i < code.length) {
348
+ const op = code[i]
349
+ const name = OpName[op] || `unknown(0x${op.toString(16)})`
350
+ i++
351
+
352
+ // Handle instructions with immediates
353
+ if (op === Op.local_get || op === Op.local_set || op === Op.local_tee) {
354
+ const [idx, len] = decodeULEB128(code, i)
355
+ i += len
356
+ const paramName =
357
+ idx < params.length ? `$${params[idx].name}` : `$L${idx}`
358
+ lines.push(`${ind()}${name} ${paramName}`)
359
+ } else if (op === Op.br || op === Op.br_if) {
360
+ const [depth, len] = decodeULEB128(code, i)
361
+ i += len
362
+ lines.push(`${ind()}${name} ${depth}`)
363
+ } else if (op === Op.i32_const) {
364
+ const [val, len] = decodeULEB128(code, i)
365
+ i += len
366
+ lines.push(`${ind()}i32.const ${val}`)
367
+ } else if (op === Op.f64_const) {
368
+ const val = decodeF64(code, i)
369
+ i += 8
370
+ lines.push(`${ind()}f64.const ${val}`)
371
+ } else if (op === Op.block || op === Op.loop) {
372
+ const blockType = code[i]
373
+ i++
374
+ lines.push(
375
+ `${ind()}${name}${
376
+ blockType === Type.void
377
+ ? ''
378
+ : ` (result ${blockType === Type.f64 ? 'f64' : 'i32'})`
379
+ }`
380
+ )
381
+ indent++
382
+ } else if (op === Op.if) {
383
+ const blockType = code[i]
384
+ i++
385
+ lines.push(
386
+ `${ind()}if${
387
+ blockType === Type.void
388
+ ? ''
389
+ : ` (result ${blockType === Type.f64 ? 'f64' : 'i32'})`
390
+ }`
391
+ )
392
+ indent++
393
+ } else if (op === Op.else) {
394
+ indent--
395
+ lines.push(`${ind()}else`)
396
+ indent++
397
+ } else if (op === Op.end) {
398
+ indent = Math.max(1, indent - 1)
399
+ lines.push(`${ind()}end`)
400
+ } else if (
401
+ op === Op.f64_load ||
402
+ op === Op.f64_store ||
403
+ op === Op.f32_load ||
404
+ op === Op.f32_store ||
405
+ op === Op.i32_load ||
406
+ op === Op.i32_store
407
+ ) {
408
+ const [align, len1] = decodeULEB128(code, i)
409
+ i += len1
410
+ const [offset, len2] = decodeULEB128(code, i)
411
+ i += len2
412
+ lines.push(`${ind()}${name}${offset ? ` offset=${offset}` : ''}`)
413
+ } else {
414
+ lines.push(`${ind()}${name}`)
415
+ }
416
+ }
417
+
418
+ lines.push(')')
419
+ return lines.join('\n')
420
+ }
421
+
289
422
  // ============================================================================
290
423
  // Type System
291
424
  // ============================================================================
@@ -391,6 +524,10 @@ interface CompileContext {
391
524
  needsMemory: boolean
392
525
  /** Whether the function has a return statement */
393
526
  hasReturn: boolean
527
+ /** WAT text representation lines (for debugging) */
528
+ wat: string[]
529
+ /** Current indentation level for WAT */
530
+ watIndent: number
394
531
  }
395
532
 
396
533
  function createContext(params: TypedParam[]): CompileContext {
@@ -405,6 +542,8 @@ function createContext(params: TypedParam[]): CompileContext {
405
542
  needsMathImports: new Set(),
406
543
  needsMemory: false,
407
544
  hasReturn: false,
545
+ wat: [],
546
+ watIndent: 1,
408
547
  }
409
548
 
410
549
  // Add params to locals map
@@ -1613,11 +1752,15 @@ export function compileToWasm(block: WasmBlock): WasmCompileResult {
1613
1752
  ctx.hasReturn
1614
1753
  )
1615
1754
 
1755
+ // Generate WAT disassembly for debugging
1756
+ const watText = disassemble(code, params, ctx.localTypes)
1757
+
1616
1758
  return {
1617
1759
  bytes: new Uint8Array(moduleBytes),
1618
1760
  warnings: ctx.warnings,
1619
1761
  success: true,
1620
1762
  needsMemory: ctx.needsMemory,
1763
+ wat: watText,
1621
1764
  }
1622
1765
  } catch (e: any) {
1623
1766
  return {
@@ -137,6 +137,70 @@ describe('Type()', () => {
137
137
  expect(Email.check(Email.example)).toBe(true)
138
138
  })
139
139
  })
140
+
141
+ describe('Schema examples support', () => {
142
+ it('extracts examples from schema metadata', () => {
143
+ const Username = Type(
144
+ 'username',
145
+ s.string.meta({ examples: ['alice', 'bob', 'charlie'] })
146
+ )
147
+
148
+ expect(Username.examples).toEqual(['alice', 'bob', 'charlie'])
149
+ })
150
+
151
+ it('sets example to first schema example when no explicit example given', () => {
152
+ const Username = Type(
153
+ 'username',
154
+ s.string.meta({ examples: ['alice', 'bob'] })
155
+ )
156
+
157
+ expect(Username.example).toBe('alice')
158
+ })
159
+
160
+ it('preserves explicit example over schema examples', () => {
161
+ const Username = Type(
162
+ 'username',
163
+ s.string.meta({ examples: ['alice', 'bob'] }),
164
+ 'explicit_user'
165
+ )
166
+
167
+ expect(Username.example).toBe('explicit_user')
168
+ expect(Username.examples).toEqual(['alice', 'bob'])
169
+ })
170
+
171
+ it('has no examples when schema lacks metadata', () => {
172
+ const Name = Type('name', s.string)
173
+
174
+ expect(Name.examples).toBeUndefined()
175
+ })
176
+
177
+ it('has no examples for predicate-based types', () => {
178
+ const Even = Type(
179
+ 'even number',
180
+ (n) => typeof n === 'number' && n % 2 === 0
181
+ )
182
+
183
+ expect(Even.examples).toBeUndefined()
184
+ })
185
+
186
+ it('has no examples for simple form', () => {
187
+ const Name = Type('name', 'Alice')
188
+
189
+ expect(Name.examples).toBeUndefined()
190
+ expect(Name.example).toBe('Alice')
191
+ })
192
+
193
+ it('validates using schema even with examples', () => {
194
+ const ShortString = Type(
195
+ 'short string',
196
+ s.string.max(5).meta({ examples: ['hi', 'hey'] })
197
+ )
198
+
199
+ expect(ShortString.check('hi')).toBe(true)
200
+ expect(ShortString.check('toolong')).toBe(false)
201
+ expect(ShortString.examples).toEqual(['hi', 'hey'])
202
+ })
203
+ })
140
204
  })
141
205
 
142
206
  describe('Built-in Types', () => {
package/src/types/Type.ts CHANGED
@@ -40,8 +40,10 @@ export interface RuntimeType<T = unknown> {
40
40
  readonly schema?: Schema
41
41
  /** The predicate function (if predicate-based) */
42
42
  readonly predicate?: (value: unknown) => boolean
43
- /** Example value (for documentation and implicit testing) */
43
+ /** Example value (for documentation and signature testing) */
44
44
  readonly example?: T
45
+ /** Multiple example values (from schema metadata, for autocomplete hints) */
46
+ readonly examples?: T[]
45
47
  /** Default value (for instantiation) */
46
48
  readonly default?: T
47
49
  /** Brand for type identification */
@@ -150,6 +152,24 @@ export function Type<T = unknown>(
150
152
  description = schemaToDescription(schema)
151
153
  }
152
154
 
155
+ // Extract examples from schema metadata (if any)
156
+ let examples: T[] | undefined
157
+ if (schema) {
158
+ const jsonSchema = (schema as any)?.schema ?? schema
159
+ if (
160
+ jsonSchema &&
161
+ typeof jsonSchema === 'object' &&
162
+ Array.isArray((jsonSchema as any).examples)
163
+ ) {
164
+ examples = (jsonSchema as any).examples as T[]
165
+ }
166
+ }
167
+
168
+ // If no explicit example was provided, use first schema example for autocomplete
169
+ if (example === undefined && examples && examples.length > 0) {
170
+ example = examples[0]
171
+ }
172
+
153
173
  // Build the check function
154
174
  const check = (value: unknown): value is T => {
155
175
  if (predicate) {
@@ -167,6 +187,7 @@ export function Type<T = unknown>(
167
187
  schema,
168
188
  predicate,
169
189
  example,
190
+ examples,
170
191
  default: defaultValue,
171
192
  __runtimeType: true as const,
172
193
  }
@@ -134,7 +134,7 @@ describe('Transpiler Integration', () => {
134
134
  expect(signature.parameters.query.type.kind).toBe('string')
135
135
  expect(signature.parameters.query.required).toBe(true)
136
136
  expect(signature.parameters.query.description).toBe('Search terms')
137
- expect(signature.parameters.limit.type.kind).toBe('number')
137
+ expect(signature.parameters.limit.type.kind).toBe('integer')
138
138
  expect(signature.parameters.limit.required).toBe(false)
139
139
  expect(signature.parameters.limit.default).toBe(10)
140
140
  expect(signature.parameters.limit.description).toBe('Max results')
@@ -153,7 +153,7 @@ describe('Transpiler Integration', () => {
153
153
  expect(signature.parameters.user.type.kind).toBe('object')
154
154
  expect(signature.parameters.user.type.shape?.name.kind).toBe('string')
155
155
  expect(signature.parameters.user.type.shape?.email.kind).toBe('string')
156
- expect(signature.parameters.user.type.shape?.age.kind).toBe('number')
156
+ expect(signature.parameters.user.type.shape?.age.kind).toBe('integer')
157
157
  expect(signature.parameters.user.required).toBe(true)
158
158
 
159
159
  expect(signature.parameters.options.type.kind).toBe('object')
@@ -383,16 +383,16 @@ describe('Transpiler Integration', () => {
383
383
  let answer = ''
384
384
  let valid = false
385
385
  let tries = 0
386
-
386
+
387
387
  while (!valid && tries < 3) {
388
388
  answer = llmPredict({ prompt: question })
389
389
  tries = tries + 1
390
-
390
+
391
391
  if (answer == 'A' || answer == 'B' || answer == 'C' || answer == 'D') {
392
392
  valid = true
393
393
  }
394
394
  }
395
-
395
+
396
396
  return { answer, tries, valid }
397
397
  }
398
398
  `)
@@ -772,9 +772,9 @@ describe('Transpiler Integration', () => {
772
772
  const ast = ajs(`
773
773
  function test() {
774
774
  let d = Date('2024-06-15T10:30:00Z')
775
- return {
776
- year: d.year,
777
- month: d.month,
775
+ return {
776
+ year: d.year,
777
+ month: d.month,
778
778
  day: d.day,
779
779
  hours: d.hours,
780
780
  minutes: d.minutes
@@ -826,7 +826,7 @@ describe('Transpiler Integration', () => {
826
826
  const ast = ajs(`
827
827
  function test() {
828
828
  let d = Date('2024-06-15T14:30:45Z')
829
- return {
829
+ return {
830
830
  iso: d.format('ISO'),
831
831
  date: d.format('date'),
832
832
  custom: d.format('YYYY-MM-DD')
@@ -847,7 +847,7 @@ describe('Transpiler Integration', () => {
847
847
  function test() {
848
848
  let a = Date('2024-01-15')
849
849
  let b = Date('2024-01-20')
850
- return {
850
+ return {
851
851
  aBeforeB: a.isBefore(b),
852
852
  aAfterB: a.isAfter(b)
853
853
  }
@@ -145,6 +145,7 @@ export const llmPredictBattery = defineAtom(
145
145
  responseFormat: s.any.optional,
146
146
  }),
147
147
  s.object({
148
+ role: s.string.optional,
148
149
  content: s.string.optional,
149
150
  tool_calls: s.array(s.any).optional,
150
151
  }),
@@ -189,6 +190,7 @@ export const llmVision = defineAtom(
189
190
  responseFormat: s.any.optional,
190
191
  }),
191
192
  s.object({
193
+ role: s.string.optional,
192
194
  content: s.string.optional,
193
195
  tool_calls: s.array(s.any).optional,
194
196
  }),