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
@@ -1353,6 +1353,17 @@ function getPlaceholderForParam(name: string, info: any): string {
1353
1353
  return String(ex)
1354
1354
  }
1355
1355
 
1356
+ // Then check for examples from schema metadata (use first as placeholder)
1357
+ const examples = info.type?.examples || info.examples
1358
+ if (Array.isArray(examples) && examples.length > 0) {
1359
+ const ex = examples[0]
1360
+ if (typeof ex === 'string') return `'${ex}'`
1361
+ if (typeof ex === 'number' || typeof ex === 'boolean') return String(ex)
1362
+ if (Array.isArray(ex)) return JSON.stringify(ex)
1363
+ if (typeof ex === 'object') return JSON.stringify(ex)
1364
+ return String(ex)
1365
+ }
1366
+
1356
1367
  // Then check for explicit default value
1357
1368
  if (info.default !== undefined && info.default !== null) {
1358
1369
  const def = info.default
@@ -1481,12 +1492,27 @@ function tjsCompletionSource(config: AutocompleteConfig = {}) {
1481
1492
  // Handle both { type: 'string' } and { kind: 'string' } formats
1482
1493
  const returnType =
1483
1494
  meta.returns?.type || meta.returns?.kind || 'void'
1495
+
1496
+ // Build info string with description and parameter examples
1497
+ let infoText = meta.description || ''
1498
+ for (const [pName, pInfo] of paramEntries as [string, any][]) {
1499
+ const pExamples = pInfo.type?.examples || pInfo.examples
1500
+ if (Array.isArray(pExamples) && pExamples.length > 0) {
1501
+ const formatted = pExamples
1502
+ .map((ex: any) =>
1503
+ typeof ex === 'string' ? `'${ex}'` : String(ex)
1504
+ )
1505
+ .join(', ')
1506
+ infoText += `${infoText ? '\n' : ''}${pName}: e.g. ${formatted}`
1507
+ }
1508
+ }
1509
+
1484
1510
  options.push(
1485
1511
  snippetCompletion(`${name}(${snippetParams})`, {
1486
1512
  label: name,
1487
1513
  type: 'function',
1488
1514
  detail: `(${paramList}) -> ${returnType}`,
1489
- info: meta.description,
1515
+ info: infoText || undefined,
1490
1516
  boost: 2, // Boost user-defined functions above globals
1491
1517
  })
1492
1518
  )
@@ -432,8 +432,8 @@ const after = 2
432
432
 
433
433
  expect(metadata).toBeDefined()
434
434
  // metadata is now keyed by function name
435
- expect(metadata?.add?.params?.a?.type?.kind).toBe('number')
436
- expect(metadata?.add?.params?.b?.type?.kind).toBe('number')
435
+ expect(metadata?.add?.params?.a?.type?.kind).toBe('integer')
436
+ expect(metadata?.add?.params?.b?.type?.kind).toBe('integer')
437
437
  })
438
438
 
439
439
  it('extracts example-based types', () => {
@@ -445,7 +445,7 @@ const after = 2
445
445
  expect(metadata).toBeDefined()
446
446
  // metadata is now keyed by function name
447
447
  expect(metadata?.greet?.params?.name?.type?.kind).toBe('string')
448
- expect(metadata?.greet?.params?.times?.type?.kind).toBe('number')
448
+ expect(metadata?.greet?.params?.times?.type?.kind).toBe('integer')
449
449
  expect(metadata?.greet?.params?.times?.default).toBe(1)
450
450
  })
451
451
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tjs-lang",
3
- "version": "0.2.7",
3
+ "version": "0.3.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",
@@ -54,7 +54,7 @@ describe('TS → TJS conversion quality', () => {
54
54
  const ts = `function sum(nums: number[]): number { return nums.reduce((a, b) => a + b, 0) }`
55
55
  const { code } = fromTS(ts, { emitTJS: true })
56
56
 
57
- expect(code).toContain('nums: [0]')
57
+ expect(code).toContain('nums: [0.0]')
58
58
  })
59
59
 
60
60
  it('converts object param correctly', () => {
@@ -62,7 +62,7 @@ describe('TS → TJS conversion quality', () => {
62
62
  const { code } = fromTS(ts, { emitTJS: true })
63
63
 
64
64
  expect(code).toContain("name: ''")
65
- expect(code).toContain('age: 0')
65
+ expect(code).toContain('age: 0.0')
66
66
  })
67
67
 
68
68
  it('handles multiple params in order', () => {
@@ -70,7 +70,7 @@ describe('TS → TJS conversion quality', () => {
70
70
  const { code } = fromTS(ts, { emitTJS: true })
71
71
 
72
72
  // Should have both params with colon syntax
73
- expect(code).toMatch(/add\(a: 0, b: 0\)/)
73
+ expect(code).toMatch(/add\(a: 0\.0, b: 0\.0\)/)
74
74
  })
75
75
 
76
76
  it('handles mixed required and optional params', () => {
@@ -228,7 +228,7 @@ class Calculator {
228
228
  `
229
229
  const { code } = fromTS(ts, { emitTJS: true })
230
230
 
231
- expect(code).toContain('add(a: 0, b: 0) -! 0')
231
+ expect(code).toContain('add(a: 0.0, b: 0.0) -! 0.0')
232
232
  })
233
233
 
234
234
  it('converts getters and setters', () => {
@@ -269,7 +269,7 @@ class MathUtils {
269
269
  `
270
270
  const { code } = fromTS(ts, { emitTJS: true })
271
271
 
272
- expect(code).toContain('static double(x: 0) -! 0')
272
+ expect(code).toContain('static double(x: 0.0) -! 0.0')
273
273
  })
274
274
 
275
275
  it('converts async methods', () => {
@@ -396,7 +396,7 @@ describe('TJS → JS transpilation quality', () => {
396
396
 
397
397
  expect(code).toContain('__tjs')
398
398
  // types is now keyed by function name
399
- expect(types?.double?.returns?.kind).toBe('number')
399
+ expect(types?.double?.returns?.kind).toBe('integer')
400
400
  })
401
401
 
402
402
  it('marks required params correctly', () => {
@@ -561,8 +561,8 @@ function greet(name: string): string {
561
561
  const { code } = fromTS(ts, { emitTJS: true })
562
562
 
563
563
  // All functions should be present (TS transpiler uses -! to skip signature tests)
564
- expect(code).toContain('function add(a: 0, b: 0) -! 0')
565
- expect(code).toContain('function multiply(a: 0, b: 0) -! 0')
564
+ expect(code).toContain('function add(a: 0.0, b: 0.0) -! 0.0')
565
+ expect(code).toContain('function multiply(a: 0.0, b: 0.0) -! 0.0')
566
566
  expect(code).toContain("function greet(name: '') -! ''")
567
567
 
568
568
  // Should be valid TJS (no TypeScript syntax remaining)
@@ -738,7 +738,7 @@ function greet(name: '', excited = false) -! '' {
738
738
  // add function
739
739
  expect(types?.add?.params?.a?.required).toBe(true)
740
740
  expect(types?.add?.params?.b?.required).toBe(true)
741
- expect(types?.add?.returns?.kind).toBe('number')
741
+ expect(types?.add?.returns?.kind).toBe('integer')
742
742
 
743
743
  // greet function
744
744
  expect(types?.greet?.params?.name?.required).toBe(true)
@@ -1146,7 +1146,7 @@ function processUser(user: { name: string; age: number }): string {
1146
1146
  // TS → TJS
1147
1147
  const { code: tjsCode } = fromTS(ts, { emitTJS: true })
1148
1148
  expect(tjsCode).toContain("name: ''")
1149
- expect(tjsCode).toContain('age: 0')
1149
+ expect(tjsCode).toContain('age: 0.0')
1150
1150
 
1151
1151
  // TJS → JS (already has -! from TS transpiler)
1152
1152
  const { code: jsCode, types } = tjs(tjsCode)
@@ -1386,7 +1386,7 @@ function getData(id: 0) -! { value: 0 } {
1386
1386
  expect(result.value).toBeUndefined()
1387
1387
 
1388
1388
  // But error properties are accessible
1389
- expect(result.message).toContain('Expected number')
1389
+ expect(result.message).toContain('Expected integer')
1390
1390
  })
1391
1391
  })
1392
1392
 
@@ -137,7 +137,7 @@ function typeToExample(
137
137
  case ts.SyntaxKind.StringKeyword:
138
138
  return "''"
139
139
  case ts.SyntaxKind.NumberKeyword:
140
- return '0'
140
+ return '0.0'
141
141
  case ts.SyntaxKind.BooleanKeyword:
142
142
  return 'true'
143
143
  case ts.SyntaxKind.NullKeyword:
@@ -17,6 +17,33 @@
17
17
  * params: { name: { type: 'string', required: true, example: 'world' } },
18
18
  * returns: { type: 'string' }
19
19
  * }
20
+ *
21
+ * TODO: Self-contained output (no runtime dependency)
22
+ * =====================================================
23
+ * Currently, transpiled code references `globalThis.__tjs` for:
24
+ * - __tjs.pushStack() / popStack() - debug stack traces
25
+ * - __tjs.typeError() - monadic error creation
26
+ * - __tjs.Is() / IsNot() - structural equality (when == / != used)
27
+ *
28
+ * This requires either:
29
+ * 1. The runtime to be installed via installRuntime()
30
+ * 2. A stub to be provided (e.g., playground's inline stub)
31
+ *
32
+ * The ideal is that TJS produces completely independent code that only needs
33
+ * things it semantically needs (like fetch for HTTP calls). The runtime
34
+ * functions above are ~30 lines and could be inlined when used:
35
+ *
36
+ * - typeError: Create a simple Error with extra properties
37
+ * - pushStack/popStack: Could be no-ops in production, or inline array ops
38
+ * - Is/IsNot: ~20 lines for deep structural equality
39
+ *
40
+ * Options to explore:
41
+ * 1. Inline minimal runtime when needed (adds ~1KB unminified per output)
42
+ * 2. Add transpile option: { standalone: true } to emit self-contained code
43
+ * 3. Tree-shake: only inline the specific functions actually referenced
44
+ *
45
+ * See also: demo/src/tjs-playground.ts which has a manual __tjs stub that
46
+ * must stay in sync with the runtime - a symptom of this leaky abstraction.
20
47
  */
21
48
 
22
49
  import type { FunctionDeclaration, Program } from 'acorn'
@@ -25,6 +52,7 @@ import { parse, extractTDoc, preprocess } from '../parser'
25
52
  import type { TypeDescriptor, ParameterDescriptor } from '../types'
26
53
  import { inferTypeFromValue, parseParameter } from '../inference'
27
54
  import { extractTests } from '../tests'
55
+ import { compileToWasm } from '../wasm'
28
56
 
29
57
  export interface TJSTranspileOptions {
30
58
  /** Filename for error messages */
@@ -86,8 +114,13 @@ export interface TJSTranspileResult {
86
114
  testCount?: number
87
115
  /** Test results (when runTests is true or 'only') */
88
116
  testResults?: TestResult[]
89
- /** WASM blocks extracted from source (need to be compiled before execution) */
90
- wasmBlocks?: import('../parser').WasmBlock[]
117
+ /** WASM compilation results (for debugging/inspection) */
118
+ wasmCompiled?: {
119
+ id: string
120
+ success: boolean
121
+ error?: string
122
+ byteLength?: number
123
+ }[]
91
124
  }
92
125
 
93
126
  export interface TJSTypeInfo {
@@ -583,6 +616,19 @@ export function transpileToJS(
583
616
  }
584
617
  }
585
618
 
619
+ // Compile WASM blocks at transpile time and embed in output
620
+ let wasmCompiled:
621
+ | { id: string; success: boolean; error?: string; byteLength?: number }[]
622
+ | undefined
623
+ if (preprocessed.wasmBlocks.length > 0) {
624
+ wasmCompiled = []
625
+ const wasmBootstrap = generateWasmBootstrap(preprocessed.wasmBlocks)
626
+ if (wasmBootstrap.code) {
627
+ code = wasmBootstrap.code + '\n' + code
628
+ }
629
+ wasmCompiled = wasmBootstrap.results
630
+ }
631
+
586
632
  return {
587
633
  code,
588
634
  types: allTypes,
@@ -591,8 +637,7 @@ export function transpileToJS(
591
637
  testRunner: tests.length > 0 ? testRunner : undefined,
592
638
  testCount: tests.length > 0 ? tests.length : undefined,
593
639
  testResults,
594
- wasmBlocks:
595
- preprocessed.wasmBlocks.length > 0 ? preprocessed.wasmBlocks : undefined,
640
+ wasmCompiled,
596
641
  }
597
642
  }
598
643
 
@@ -823,6 +868,10 @@ function generateTypeCheckExpr(
823
868
  return `typeof ${fieldPath} !== 'string'`
824
869
  case 'number':
825
870
  return `typeof ${fieldPath} !== 'number'`
871
+ case 'integer':
872
+ return `(typeof ${fieldPath} !== 'number' || !Number.isInteger(${fieldPath}))`
873
+ case 'non-negative-integer':
874
+ return `(typeof ${fieldPath} !== 'number' || !Number.isInteger(${fieldPath}) || ${fieldPath} < 0)`
826
875
  case 'boolean':
827
876
  return `typeof ${fieldPath} !== 'boolean'`
828
877
  case 'null':
@@ -1448,6 +1497,41 @@ function runAllTests(
1448
1497
  if (!__deepEqual(actual, expected)) {
1449
1498
  throw new Error('Expected ' + __format(expected) + ' but got ' + __format(actual))
1450
1499
  }
1500
+ },
1501
+ toContain(item) {
1502
+ if (!Array.isArray(actual) || !actual.some(function(v) { return __deepEqual(v, item) })) {
1503
+ throw new Error('Expected ' + __format(actual) + ' to contain ' + __format(item))
1504
+ }
1505
+ },
1506
+ toBeTruthy() {
1507
+ if (!actual) {
1508
+ throw new Error('Expected ' + __format(actual) + ' to be truthy')
1509
+ }
1510
+ },
1511
+ toBeFalsy() {
1512
+ if (actual) {
1513
+ throw new Error('Expected ' + __format(actual) + ' to be falsy')
1514
+ }
1515
+ },
1516
+ toBeNull() {
1517
+ if (actual !== null) {
1518
+ throw new Error('Expected null but got ' + __format(actual))
1519
+ }
1520
+ },
1521
+ toBeUndefined() {
1522
+ if (actual !== undefined) {
1523
+ throw new Error('Expected undefined but got ' + __format(actual))
1524
+ }
1525
+ },
1526
+ toBeGreaterThan(n) {
1527
+ if (!(actual > n)) {
1528
+ throw new Error('Expected ' + __format(actual) + ' to be greater than ' + n)
1529
+ }
1530
+ },
1531
+ toBeLessThan(n) {
1532
+ if (!(actual < n)) {
1533
+ throw new Error('Expected ' + __format(actual) + ' to be less than ' + n)
1534
+ }
1451
1535
  }
1452
1536
  }
1453
1537
  }
@@ -1589,6 +1673,41 @@ function runTestBlocks(
1589
1673
  if (!__deepEqual(actual, expected)) {
1590
1674
  throw new Error('Expected ' + __format(expected) + ' but got ' + __format(actual))
1591
1675
  }
1676
+ },
1677
+ toContain(item) {
1678
+ if (!Array.isArray(actual) || !actual.some(function(v) { return __deepEqual(v, item) })) {
1679
+ throw new Error('Expected ' + __format(actual) + ' to contain ' + __format(item))
1680
+ }
1681
+ },
1682
+ toBeTruthy() {
1683
+ if (!actual) {
1684
+ throw new Error('Expected ' + __format(actual) + ' to be truthy')
1685
+ }
1686
+ },
1687
+ toBeFalsy() {
1688
+ if (actual) {
1689
+ throw new Error('Expected ' + __format(actual) + ' to be falsy')
1690
+ }
1691
+ },
1692
+ toBeNull() {
1693
+ if (actual !== null) {
1694
+ throw new Error('Expected null but got ' + __format(actual))
1695
+ }
1696
+ },
1697
+ toBeUndefined() {
1698
+ if (actual !== undefined) {
1699
+ throw new Error('Expected undefined but got ' + __format(actual))
1700
+ }
1701
+ },
1702
+ toBeGreaterThan(n) {
1703
+ if (!(actual > n)) {
1704
+ throw new Error('Expected ' + __format(actual) + ' to be greater than ' + n)
1705
+ }
1706
+ },
1707
+ toBeLessThan(n) {
1708
+ if (!(actual < n)) {
1709
+ throw new Error('Expected ' + __format(actual) + ' to be less than ' + n)
1710
+ }
1592
1711
  }
1593
1712
  }
1594
1713
  }
@@ -1897,3 +2016,108 @@ function runSignatureTest(
1897
2016
  }
1898
2017
  }
1899
2018
  }
2019
+
2020
+ /**
2021
+ * Compile WASM blocks and generate bootstrap code that embeds the compiled bytes
2022
+ * and instantiates them on load.
2023
+ */
2024
+ function generateWasmBootstrap(blocks: import('../parser').WasmBlock[]): {
2025
+ code: string
2026
+ results: {
2027
+ id: string
2028
+ success: boolean
2029
+ error?: string
2030
+ byteLength?: number
2031
+ }[]
2032
+ } {
2033
+ const results: {
2034
+ id: string
2035
+ success: boolean
2036
+ error?: string
2037
+ byteLength?: number
2038
+ }[] = []
2039
+ const compiledBlocks: {
2040
+ id: string
2041
+ base64: string
2042
+ captures: string[]
2043
+ needsMemory: boolean
2044
+ wat: string
2045
+ }[] = []
2046
+
2047
+ for (const block of blocks) {
2048
+ const result = compileToWasm(block)
2049
+ if (result.success) {
2050
+ // Convert bytes to base64 for embedding
2051
+ const base64 = btoa(String.fromCharCode(...result.bytes))
2052
+ compiledBlocks.push({
2053
+ id: block.id,
2054
+ base64,
2055
+ captures: block.captures,
2056
+ needsMemory: result.needsMemory ?? false,
2057
+ wat: result.wat ?? '',
2058
+ })
2059
+ results.push({
2060
+ id: block.id,
2061
+ success: true,
2062
+ byteLength: result.bytes.length,
2063
+ })
2064
+ } else {
2065
+ results.push({
2066
+ id: block.id,
2067
+ success: false,
2068
+ error: result.error,
2069
+ })
2070
+ }
2071
+ }
2072
+
2073
+ if (compiledBlocks.length === 0) {
2074
+ return { code: '', results }
2075
+ }
2076
+
2077
+ // Generate WAT comments for each block
2078
+ const watComments = compiledBlocks
2079
+ .map((b) => {
2080
+ const watLines = b.wat.split('\n').map((line) => ` * ${line}`)
2081
+ return `/**\n * WASM: ${b.id}\n${watLines.join('\n')}\n */`
2082
+ })
2083
+ .join('\n')
2084
+
2085
+ // Generate self-contained bootstrap code
2086
+ // This runs immediately and sets up globalThis.__tjs_wasm_N functions
2087
+ const blockData = compiledBlocks
2088
+ .map(
2089
+ (b) =>
2090
+ `{id:${JSON.stringify(b.id)},b64:${JSON.stringify(
2091
+ b.base64
2092
+ )},c:${JSON.stringify(b.captures)},m:${b.needsMemory}}`
2093
+ )
2094
+ .join(',')
2095
+
2096
+ const code = `${watComments}
2097
+ ;(async()=>{
2098
+ const __wasmBlocks=[${blockData}];
2099
+ 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};
2100
+ 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}};
2101
+ for(const{id,b64,c,m}of __wasmBlocks){
2102
+ const bytes=__b64ToBytes(b64);
2103
+ const params=c.map(__parseType);
2104
+ const hasArrays=params.some(p=>p.a);
2105
+ let mem;if(m)mem=new WebAssembly.Memory({initial:256});
2106
+ const imp=mem?{env:{memory:mem}}:{};
2107
+ const inst=await WebAssembly.instantiate(await WebAssembly.compile(bytes),imp);
2108
+ const compute=inst.exports.compute;
2109
+ if(!hasArrays){globalThis[id]=compute;continue}
2110
+ globalThis[id]=function(...args){
2111
+ const mv=new Uint8Array(mem.buffer);let off=0;const ptrs=[];
2112
+ for(let i=0;i<params.length;i++){const p=params[i],a=args[i];
2113
+ if(p.a&&a?.buffer){const ab=new Uint8Array(a.buffer,a.byteOffset,a.byteLength);mv.set(ab,off);ptrs.push(off);off+=ab.length;off=(off+7)&~7}
2114
+ else ptrs.push(a)}
2115
+ const r=compute(...ptrs);off=0;
2116
+ for(let i=0;i<params.length;i++){const p=params[i],a=args[i];
2117
+ if(p.a&&a?.buffer){const ab=new Uint8Array(a.buffer,a.byteOffset,a.byteLength);ab.set(mv.slice(off,off+ab.length));off+=ab.length;off=(off+7)&~7}}
2118
+ return r};
2119
+ }})();
2120
+ `.trim()
2121
+
2122
+ return { code, results }
2123
+ }
package/src/lang/index.ts CHANGED
@@ -90,10 +90,7 @@ export {
90
90
  instantiateWasm,
91
91
  registerWasmBlock,
92
92
  compileWasmBlocks,
93
- compileWasmBlocksForIframe,
94
- generateWasmInstantiationCode,
95
93
  type WasmCompileResult,
96
- type CompiledWasmData,
97
94
  } from './wasm'
98
95
  export type { WasmBlock } from './parser'
99
96
  export {
@@ -29,7 +29,13 @@ export function inferTypeFromValue(node: Expression): TypeDescriptor {
29
29
  return { kind: 'string' }
30
30
  }
31
31
  if (typeof value === 'number') {
32
- return { kind: 'number' }
32
+ // Distinguish float vs integer by checking if source contains '.'
33
+ // 2.0 -> number (float), 42 -> integer
34
+ const raw = (node as any).raw as string | undefined
35
+ if (raw && raw.includes('.')) {
36
+ return { kind: 'number' }
37
+ }
38
+ return { kind: 'integer' }
33
39
  }
34
40
  if (typeof value === 'boolean') {
35
41
  return { kind: 'boolean' }
@@ -110,14 +116,26 @@ export function inferTypeFromValue(node: Expression): TypeDescriptor {
110
116
  }
111
117
 
112
118
  case 'UnaryExpression': {
113
- // Handle negative numbers: -1
114
- if (
115
- (node as any).operator === '-' &&
116
- (node as any).argument.type === 'Literal'
117
- ) {
118
- const value = (node as any).argument.value
119
+ const op = (node as any).operator
120
+ const arg = (node as any).argument
121
+
122
+ // +N means non-negative integer (e.g., +1, +3)
123
+ if (op === '+' && arg.type === 'Literal') {
124
+ const value = arg.value
119
125
  if (typeof value === 'number') {
120
- return { kind: 'number' }
126
+ return { kind: 'non-negative-integer' }
127
+ }
128
+ }
129
+
130
+ // -N means integer or float depending on source
131
+ if (op === '-' && arg.type === 'Literal') {
132
+ const value = arg.value
133
+ if (typeof value === 'number') {
134
+ const raw = arg.raw as string | undefined
135
+ if (raw && raw.includes('.')) {
136
+ return { kind: 'number' }
137
+ }
138
+ return { kind: 'integer' }
121
139
  }
122
140
  }
123
141
  return { kind: 'any' }
@@ -258,6 +276,10 @@ export function extractLiteralValue(node: Expression): any {
258
276
  const arg = extractLiteralValue((node as any).argument)
259
277
  return typeof arg === 'number' ? -arg : undefined
260
278
  }
279
+ if ((node as any).operator === '+') {
280
+ const arg = extractLiteralValue((node as any).argument)
281
+ return typeof arg === 'number' ? +arg : undefined
282
+ }
261
283
  return undefined
262
284
 
263
285
  case 'LogicalExpression': {
@@ -310,6 +332,12 @@ export function typeToString(type: TypeDescriptor): string {
310
332
  return type.nullable ? 'string | null' : 'string'
311
333
  case 'number':
312
334
  return type.nullable ? 'number | null' : 'number'
335
+ case 'integer':
336
+ return type.nullable ? 'integer | null' : 'integer'
337
+ case 'non-negative-integer':
338
+ return type.nullable
339
+ ? 'non-negative integer | null'
340
+ : 'non-negative integer'
313
341
  case 'boolean':
314
342
  return type.nullable ? 'boolean | null' : 'boolean'
315
343
  case 'null':
@@ -354,6 +382,10 @@ export function checkType(value: any, type: TypeDescriptor): boolean {
354
382
  return typeof value === 'string'
355
383
  case 'number':
356
384
  return typeof value === 'number'
385
+ case 'integer':
386
+ return typeof value === 'number' && Number.isInteger(value)
387
+ case 'non-negative-integer':
388
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0
357
389
  case 'boolean':
358
390
  return typeof value === 'boolean'
359
391
  case 'array':