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
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 {
|
package/src/types/Type.test.ts
CHANGED
|
@@ -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
|
|
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('
|
|
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('
|
|
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
|
}),
|