tjs-lang 0.5.2 → 0.5.3
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 +5 -5
- package/demo/src/demo-nav.ts +10 -3
- package/package.json +1 -1
- package/src/lang/codegen.test.ts +10 -2
- package/src/lang/emitters/from-ts.ts +6 -1
- package/src/lang/eval.ts +41 -2
- package/src/runtime.test.ts +144 -13
- package/src/vm/runtime.ts +19 -0
package/demo/docs.json
CHANGED
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
"group": "docs",
|
|
152
152
|
"order": 0,
|
|
153
153
|
"navTitle": "Documentation",
|
|
154
|
-
"text": "<!--{\"section\": \"tjs\", \"group\": \"docs\", \"order\": 0, \"navTitle\": \"Documentation\"}-->\n\n# TJS: Typed JavaScript\n\n_Types as Examples. Zero Build. Runtime Metadata._\n\n---\n\n## What is TJS?\n\nTJS is a typed superset of JavaScript where **types are concrete values**, not abstract annotations.\n\n```typescript\n// TypeScript: abstract type annotation\nfunction greet(name: string): string\n\n// TJS: concrete example value\nfunction greet(name: 'World') -> '' { return `Hello, ${name}!` }\n```\n\nThe example `'World'` tells TJS that `name` is a string. The example `''` tells TJS the return type is a string. Types are inferred from the examples you provide.\n\nTJS transpiles to JavaScript with embedded `__tjs` metadata, enabling runtime type checking, autocomplete from live objects, and documentation generation.\n\n---\n\n## The Compiler\n\nTJS compiles in the browser. No webpack, no node_modules, no build server.\n\n```typescript\nimport { tjs } from 'tjs-lang'\n\nconst code = tjs`\n function add(a: 0, b: 0) -> 0 {\n return a + b\n }\n`\n\n// Returns transpiled JavaScript with __tjs metadata\n```\n\nYou can also use the CLI:\n\n```bash\nbun src/cli/tjs.ts check file.tjs # Parse and type check\nbun src/cli/tjs.ts run file.tjs # Transpile and execute\nbun src/cli/tjs.ts emit file.tjs # Output transpiled JS\nbun src/cli/tjs.ts types file.tjs # Output type metadata\n```\n\n---\n\n## Syntax\n\n### Parameter Types (Colon Syntax)\n\n> **Not TypeScript.** TJS colon syntax looks like TypeScript but has different\n> semantics. The value after `:` is a **concrete example**, not a type name.\n> Write `name: 'Alice'` (example value), not `name: string` (type name).\n> TJS infers the type from the example: `'Alice'` → string, `0` → integer,\n> `true` → boolean.\n\nRequired parameters use colon syntax with an example value:\n\n```typescript\nfunction greet(name: 'Alice') {} // name is required, type: string\nfunction calculate(value: 0) {} // value is required, type: integer\nfunction measure(rate: 0.0) {} // rate is required, type: number (float)\nfunction count(n: +0) {} // n is required, type: non-negative integer\nfunction toggle(flag: true) {} // flag is required, type: boolean\n```\n\n### Numeric Types\n\nTJS distinguishes three numeric types using valid JavaScript syntax:\n\n```typescript\nfunction process(\n rate: 3.14, // number (float) -- has a decimal point\n count: 42, // integer -- whole number, no decimal\n index: +0 // non-negative integer -- prefixed with +\n) {}\n```\n\n| You Write | Type Inferred | Runtime Validation |\n| --------- | ---------------------- | ------------------------------- |\n| `3.14` | `number` (float) | `typeof x === 'number'` |\n| `0.0` | `number` (float) | `typeof x === 'number'` |\n| `42` | `integer` | `Number.isInteger(x)` |\n| `0` | `integer` | `Number.isInteger(x)` |\n| `+20` | `non-negative integer` | `Number.isInteger(x) && x >= 0` |\n| `+0` | `non-negative integer` | `Number.isInteger(x) && x >= 0` |\n| `-5` | `integer` | `Number.isInteger(x)` |\n| `-3.5` | `number` (float) | `typeof x === 'number'` |\n\nAll of these are valid JavaScript expressions. TJS reads the syntax more\ncarefully to give you finer-grained type checking than JS or TypeScript\nprovide natively.\n\n### Optional Parameters (Default Values)\n\nOptional parameters use `=` with a default value:\n\n```typescript\nfunction greet(name = 'World') {} // name is optional, defaults to 'World'\nfunction calculate(value = 0) {} // value is optional, defaults to 0 (integer)\n```\n\n### TypeScript-Style Optional\n\nYou can also use `?:` syntax:\n\n```typescript\nfunction greet(name?: '') {} // same as name = ''\n```\n\n### Object Parameters\n\nObject shapes are defined by example:\n\n```typescript\nfunction createUser(user: { name: ''; age: 0 }) {}\n// user must be an object with string name and number age\n```\n\n### Nullable Types\n\nUse `||` for union with null:\n\n```typescript\nfunction find(id: 0 || null) { } // number or null\n```\n\n### Return Types (Arrow Syntax)\n\nReturn types use `->`:\n\n```typescript\nfunction add(a: 0, b: 0) -> 0 {\n return a + b\n}\n\nfunction getUser(id: 0) -> { name: '', age: 0 } {\n return { name: 'Alice', age: 30 }\n}\n```\n\n### Array Types\n\nArrays use bracket syntax with an example element:\n\n```typescript\nfunction sum(numbers: [0]) -> 0 { // array of numbers\n return numbers.reduce((a, b) => a + b, 0)\n}\n\nfunction names(users: [{ name: '' }]) { // array of objects\n return users.map(u => u.name)\n}\n```\n\n---\n\n## Safety Markers\n\n### Unsafe Functions\n\nSkip validation for hot paths:\n\n```typescript\nfunction fastAdd(! a: 0, b: 0) { return a + b }\n```\n\nThe `!` marker after the function name skips input validation.\n\n### Safe Functions\n\nExplicit validation (for emphasis):\n\n```typescript\nfunction safeAdd(? a: 0, b: 0) { return a + b }\n```\n\n### Unsafe Blocks\n\nSkip validation for a block of code:\n\n```typescript\nunsafe {\n fastPath(data)\n anotherHotFunction(moreData)\n}\n```\n\n### Module Safety Directive\n\nSet the default validation level for an entire file:\n\n```typescript\nsafety none // No validation (metadata only)\nsafety inputs // Validate function inputs (default)\nsafety all // Validate everything (debug mode)\n```\n\n---\n\n## Type System\n\n### Type()\n\nDefine named types with predicates:\n\n```typescript\n// Simple type from example\nType Name 'Alice'\n\n// Type with description\nType User {\n description: 'a user object'\n example: { name: '', age: 0 }\n}\n\n// Type with predicate\nType PositiveNumber {\n description: 'a positive number'\n example: 1\n predicate(x) { return x > 0 }\n}\n```\n\nTypes can be used in function signatures:\n\n```typescript\nfunction greet(name: Name) -> '' {\n return `Hello, ${name}!`\n}\n```\n\n### Generic()\n\nRuntime-checkable generics:\n\n```typescript\nGeneric Box<T> {\n description: 'a boxed value'\n predicate(x, T) {\n return typeof x === 'object' && x !== null && 'value' in x && T(x.value)\n }\n}\n\n// With default type parameter\nGeneric Container<T, U = ''> {\n description: 'container with label'\n predicate(obj, T, U) {\n return T(obj.item) && U(obj.label)\n }\n}\n```\n\n### Union()\n\nDiscriminated unions:\n\n```typescript\nconst Shape = Union('kind', {\n circle: { radius: 0 },\n rectangle: { width: 0, height: 0 }\n})\n\nfunction area(shape: Shape) -> 0 {\n if (shape.kind === 'circle') {\n return Math.PI * shape.radius ** 2\n }\n return shape.width * shape.height\n}\n```\n\n### Enum()\n\nString or numeric enums:\n\n```typescript\nconst Status = Enum(['pending', 'active', 'completed'])\nconst Priority = Enum({ low: 1, medium: 2, high: 3 })\n\nfunction setStatus(status: Status) {}\n```\n\n---\n\n## Structural Equality: Is / IsNot\n\nJavaScript's `==` is broken (type coercion). TJS provides structural equality:\n\n```typescript\n// Structural comparison - no coercion\n[1, 2] Is [1, 2] // true\n5 Is \"5\" // false (different types)\n{ a: 1 } Is { a: 1 } // true\n\n// Arrays compared element-by-element\n[1, [2, 3]] Is [1, [2, 3]] // true\n\n// Negation\n5 IsNot \"5\" // true\n```\n\n### Custom Equality\n\nObjects can define custom equality in two ways:\n\n**1. `[tjsEquals]` symbol protocol** (preferred for Proxies and advanced use):\n\n```typescript\nimport { tjsEquals } from 'tjs-lang/lang'\n\n// A proxy that delegates equality to its target\nconst target = { x: 1, y: 2 }\nconst proxy = new Proxy({\n [tjsEquals](other) { return target Is other }\n}, {})\n\nproxy == { x: 1, y: 2 } // true — delegates to target\n```\n\n**2. `.Equals` method** (simple, works on any object or class):\n\n```typescript\nclass Point {\n constructor(x: 0, y: 0) { this.x = x; this.y = y }\n Equals(other) { return this.x === other.x && this.y === other.y }\n}\n\nPoint(1, 2) Is Point(1, 2) // true (uses .Equals)\n```\n\n**Priority:** `[tjsEquals]` symbol > `.Equals` method > structural comparison.\n\nThe symbol is `Symbol.for('tjs.equals')`, so it works across realms. Access it\nvia `import { tjsEquals } from 'tjs-lang/lang'` or `__tjs.tjsEquals` at runtime.\n\n---\n\n## Classes\n\n### Callable Without `new`\n\nTJS classes are callable without the `new` keyword:\n\n```typescript\nclass User {\n constructor(name: '') {\n this.name = name\n }\n}\n\n// Both work identically:\nconst u1 = User('Alice') // TJS way - clean\nconst u2 = new User('Alice') // Also works (linter warns)\n```\n\n### Private Fields\n\nUse `#` for private fields:\n\n```typescript\nclass Counter {\n #count = 0\n\n increment() {\n this.#count++\n }\n get value() {\n return this.#count\n }\n}\n```\n\nWhen converting from TypeScript, `private foo` becomes `#foo`.\n\n### Getters and Setters\n\nAsymmetric types are captured:\n\n```typescript\nclass Timestamp {\n #value\n\n constructor(initial: '' | 0 | null) {\n this.#value = initial === null ? new Date() : new Date(initial)\n }\n\n set value(v: '' | 0 | null) {\n this.#value = v === null ? new Date() : new Date(v)\n }\n\n get value() {\n return this.#value\n }\n}\n\nconst ts = Timestamp('2024-01-15')\nts.value = 0 // SET accepts: string | number | null\nts.value // GET returns: Date\n```\n\n---\n\n## Polymorphic Functions\n\nMultiple function declarations with the same name are automatically merged into a dispatcher that routes by argument count and type:\n\n```typescript\nfunction describe(value: 0) {\n return 'number: ' + value\n}\nfunction describe(value: '') {\n return 'string: ' + value\n}\nfunction describe(value: { name: '' }) {\n return 'object: ' + value.name\n}\n\ndescribe(42) // 'number: 42'\ndescribe('hello') // 'string: hello'\ndescribe({ name: 'world' }) // 'object: world'\ndescribe(true) // MonadicError: no matching overload\n```\n\n### Dispatch Order\n\n1. **Arity** first (number of arguments)\n2. **Type specificity** within same arity: `integer` > `number` > `any`; objects before primitives\n3. **Declaration order** as tiebreaker\n\n### Polymorphic Constructors\n\nClasses can have multiple constructor signatures. The first becomes the real JS constructor; additional variants become factory functions:\n\n```typescript\nTjsClass\n\nclass Point {\n constructor(x: 0.0, y: 0.0) {\n this.x = x\n this.y = y\n }\n constructor(coords: { x: 0.0; y: 0.0 }) {\n this.x = coords.x\n this.y = coords.y\n }\n}\n\nPoint(3, 4) // variant 1: two numbers\nPoint({ x: 10, y: 20 }) // variant 2: object\n```\n\nAll variants produce correct `instanceof` results.\n\n### Compile-Time Validation\n\nTJS catches these errors at transpile time:\n\n- **Ambiguous signatures**: Two variants with identical types at every position\n- **Rest parameters**: `...args` not supported in polymorphic functions\n- **Mixed async/sync**: All variants must agree\n\n---\n\n## Local Class Extensions\n\nAdd methods to built-in types without polluting prototypes:\n\n```typescript\nextend String {\n capitalize() {\n return this[0].toUpperCase() + this.slice(1)\n }\n words() {\n return this.split(/\\s+/)\n }\n}\n\n'hello world'.capitalize() // 'Hello world'\n'foo bar baz'.words() // ['foo', 'bar', 'baz']\n```\n\n### How It Works\n\nFor known-type receivers (literals, typed variables), calls are rewritten at transpile time to `.call()` — zero runtime overhead:\n\n```javascript\n// TJS source:\n'hello'.capitalize()\n\n// Generated JS:\n__ext_String.capitalize.call('hello')\n```\n\nFor unknown types, a runtime registry (`registerExtension` / `resolveExtension`) provides fallback dispatch.\n\n### Supported Types\n\nExtensions work on any type: `String`, `Number`, `Array`, `Boolean`, custom classes, and DOM classes like `HTMLElement`. Multiple `extend` blocks for the same type merge left-to-right (later declarations can override earlier methods).\n\n### Rules\n\n- Arrow functions are **not allowed** in extend blocks (they don't bind `this`)\n- Extensions are **file-local** — they don't leak across modules\n- Prototypes are **never modified** — `String.prototype.capitalize` remains `undefined`\n\n---\n\n## Runtime Features\n\n### `__tjs` Metadata\n\nEvery TJS function carries its type information:\n\n```typescript\nfunction createUser(input: { name: '', age: 0 }) -> { id: 0 } {\n return { id: 123 }\n}\n\nconsole.log(createUser.__tjs)\n// {\n// params: {\n// input: { type: { kind: 'object', shape: { name: 'string', age: 'number' } } }\n// },\n// returns: { kind: 'object', shape: { id: 'number' } }\n// }\n```\n\nThis enables:\n\n- Autocomplete from live objects\n- Runtime type validation\n- Automatic documentation generation\n\n### Monadic Errors\n\nType validation failures return `MonadicError` instances (extends `Error`),\nnot thrown exceptions:\n\n```typescript\nimport { isMonadicError } from 'tjs-lang/lang'\n\nconst result = createUser({ name: 123 }) // wrong type\n// MonadicError: Expected string for 'createUser.name', got number\n\nif (isMonadicError(result)) {\n console.log(result.message) // \"Expected string for 'createUser.name', got number\"\n console.log(result.path) // \"createUser.name\"\n console.log(result.expected) // \"string\"\n console.log(result.actual) // \"number\"\n}\n```\n\nNo try/catch gambling. The host survives invalid inputs.\n\nFor general-purpose error values (not type errors), use the `error()` helper\nwhich returns plain `{ $error: true, message }` objects checkable with `isError()`.\n\n### Inline Tests\n\nTests live next to code:\n\n```typescript\nfunction double(x: 0) -> 0 { return x * 2 }\n\ntest('doubles numbers') {\n expect(double(5)).toBe(10)\n expect(double(-3)).toBe(-6)\n}\n```\n\nTests are extracted at compile time and can be:\n\n- Run during transpilation\n- Stripped in production builds\n- Used for documentation generation\n\n### WASM Blocks\n\nDrop into WebAssembly for compute-heavy code:\n\n```typescript\nfunction vectorDot(a: [0], b: [0]) -> 0 {\n let sum = 0\n wasm {\n for (let i = 0; i < a.length; i++) {\n sum = sum + a[i] * b[i]\n }\n }\n return sum\n}\n```\n\nVariables are captured automatically. Falls back to JS if WASM unavailable.\n\n#### SIMD Intrinsics (f32x4)\n\nFor compute-heavy workloads, use f32x4 SIMD intrinsics to process 4 float32 values per instruction:\n\n```typescript\nconst scale = wasm (arr: Float32Array, len: 0, factor: 0.0) -> 0 {\n let s = f32x4_splat(factor)\n for (let i = 0; i < len; i += 4) {\n let off = i * 4\n let v = f32x4_load(arr, off)\n f32x4_store(arr, off, f32x4_mul(v, s))\n }\n} fallback {\n for (let i = 0; i < len; i++) arr[i] *= factor\n}\n```\n\nAvailable intrinsics:\n\n| Intrinsic | Description |\n|-----------|-------------|\n| `f32x4_load(ptr, byteOffset)` | Load 4 floats from memory into v128 |\n| `f32x4_store(ptr, byteOffset, vec)` | Store v128 as 4 floats to memory |\n| `f32x4_splat(scalar)` | Fill all 4 lanes with a scalar value |\n| `f32x4_extract_lane(vec, N)` | Extract float from lane 0-3 |\n| `f32x4_replace_lane(vec, N, val)` | Replace one lane, return new v128 |\n| `f32x4_add(a, b)` | Lane-wise addition |\n| `f32x4_sub(a, b)` | Lane-wise subtraction |\n| `f32x4_mul(a, b)` | Lane-wise multiplication |\n| `f32x4_div(a, b)` | Lane-wise division |\n| `f32x4_neg(v)` | Negate all lanes |\n| `f32x4_sqrt(v)` | Square root of all lanes |\n\nThis mirrors C/C++ SIMD intrinsics (`_mm_add_ps`, etc.) — explicit, predictable, no auto-vectorization magic.\n\n#### Zero-Copy Arrays: `wasmBuffer()`\n\nBy default, typed arrays passed to WASM blocks are copied into WASM memory before the call and copied back out after. For large arrays called frequently, this overhead can negate WASM's speed advantage.\n\n`wasmBuffer(Constructor, length)` allocates typed arrays directly in WASM linear memory. These arrays work like normal typed arrays from JavaScript, but when passed to a `wasm {}` block, they're zero-copy — the data is already there.\n\n```typescript\n// Allocate particle positions in WASM memory\nconst starX = wasmBuffer(Float32Array, 50000)\nconst starY = wasmBuffer(Float32Array, 50000)\n\n// Use from JS like normal arrays\nfor (let i = 0; i < 50000; i++) {\n starX[i] = (Math.random() - 0.5) * 2000\n starY[i] = (Math.random() - 0.5) * 2000\n}\n\n// Zero-copy SIMD processing\nfunction moveParticles(! xs: Float32Array, ys: Float32Array, len: 0, dx: 0.0, dy: 0.0) {\n wasm {\n let vdx = f32x4_splat(dx)\n let vdy = f32x4_splat(dy)\n for (let i = 0; i < len; i += 4) {\n let off = i * 4\n f32x4_store(xs, off, f32x4_add(f32x4_load(xs, off), vdx))\n f32x4_store(ys, off, f32x4_add(f32x4_load(ys, off), vdy))\n }\n } fallback {\n for (let i = 0; i < len; i++) { xs[i] += dx; ys[i] += dy }\n }\n}\n\n// After WASM runs, JS sees the mutations immediately\nmoveParticles(starX, starY, 50000, 1.0, 0.5)\nconsole.log(starX[0]) // updated in place, no copy\n```\n\nKey points:\n- Supported constructors: `Float32Array`, `Float64Array`, `Int32Array`, `Uint8Array`\n- Uses a bump allocator — allocations persist for program lifetime\n- All WASM blocks in a file share one 64MB memory\n- Regular typed arrays still work (copy in/out as before)\n- Use `!` (unsafe) on hot-path functions to skip runtime type checks\n\n---\n\n## Module System\n\n### Imports\n\nTJS supports URL imports:\n\n```typescript\nimport { Button } from 'https://cdn.example.com/ui-kit.tjs'\nimport { validate } from './utils/validation.tjs'\n```\n\nModules are:\n\n- Fetched on demand\n- Transpiled in the browser\n- Cached independently (IndexedDB + service worker)\n\n### CDN Integration\n\nExternal packages from esm.sh with pinned versions:\n\n```typescript\nimport lodash from 'https://esm.sh/lodash@4.17.21'\n```\n\n---\n\n## TypeScript Compatibility\n\n### TS → TJS Converter\n\nConvert existing TypeScript:\n\n```bash\nbun src/cli/tjs.ts convert file.ts\n```\n\n```typescript\n// TypeScript\nfunction greet(name: string, age?: number): string { ... }\n\n// Converts to TJS\nfunction greet(name: '', age = 0) -> '' { ... }\n```\n\n### What Gets Converted\n\n| TypeScript | TJS |\n| -------------------------- | ------------------- |\n| `name: string` | `name: ''` |\n| `age: number` | `age: 0.0` |\n| `flag: boolean` | `flag: true` |\n| `items: string[]` | `items: ['']` |\n| `age?: number` | `age = 0.0` |\n| `private foo` | `#foo` |\n| `interface User` | `Type User` |\n| `type Status = 'a' \\| 'b'` | `Union(['a', 'b'])` |\n| `enum Color` | `Enum(...)` |\n\n---\n\n## Performance\n\n| Mode | Overhead | Use Case |\n| --------------- | --------- | ------------------------------- |\n| `safety none` | **1.0x** | Metadata only, no validation |\n| `safety inputs` | **~1.5x** | Production (single-arg objects) |\n| `(!) unsafe` | **1.0x** | Hot paths |\n| `wasm {}` | **<1.0x** | Compute-heavy code |\n\n### Why 1.5x, Not 25x\n\nMost validators interpret schemas at runtime (~25x overhead). TJS generates inline checks at transpile time:\n\n```typescript\n// Generated (JIT-friendly)\nif (\n typeof input !== 'object' ||\n input === null ||\n typeof input.name !== 'string' ||\n typeof input.age !== 'number'\n) {\n return { $error: true, message: 'Invalid input', path: 'fn.input' }\n}\n```\n\nNo schema interpretation. No object iteration. The JIT inlines these completely.\n\n---\n\n## Bare Assignments\n\nUppercase identifiers automatically get `const`:\n\n```typescript\nFoo = Type('test', 'example') // becomes: const Foo = Type(...)\nMyConfig = { debug: true } // becomes: const MyConfig = { ... }\n```\n\n---\n\n## Limitations\n\n### What TJS Doesn't Do\n\n- **No gradual typing** - types are all-or-nothing per function\n- **No complex type inference** - you provide examples, not constraints\n- **No declaration files** - types live in the code, not `.d.ts`\n- **No type-level computation** - no conditional types, mapped types, etc.\n\n### What TJS Intentionally Avoids\n\n- Build steps\n- External type checkers\n- Complex tooling configuration\n- Separation of types from runtime\n\n---\n\n## Learn More\n\n- [AJS Documentation](DOCS-AJS.md) — The agent runtime\n- [Builder's Manifesto](MANIFESTO-BUILDER.md) — Why TJS is fun\n- [Enterprise Guide](MANIFESTO-ENTERPRISE.md) — Why TJS is safe\n- [Technical Context](CONTEXT.md) — Architecture deep dive\n"
|
|
154
|
+
"text": "<!--{\"section\": \"tjs\", \"group\": \"docs\", \"order\": 0, \"navTitle\": \"Documentation\"}-->\n\n# TJS: Typed JavaScript\n\n_Types as Examples. Zero Build. Runtime Metadata._\n\n---\n\n## What is TJS?\n\nTJS is a typed superset of JavaScript where **types are concrete values**, not abstract annotations.\n\n```typescript\n// TypeScript: abstract type annotation\nfunction greet(name: string): string\n\n// TJS: concrete example value\nfunction greet(name: 'World') -> '' { return `Hello, ${name}!` }\n```\n\nThe example `'World'` tells TJS that `name` is a string. The example `''` tells TJS the return type is a string. Types are inferred from the examples you provide.\n\nTJS transpiles to JavaScript with embedded `__tjs` metadata, enabling runtime type checking, autocomplete from live objects, and documentation generation.\n\n---\n\n## The Compiler\n\nTJS compiles in the browser. No webpack, no node_modules, no build server.\n\n```typescript\nimport { tjs } from 'tjs-lang'\n\nconst code = tjs`\n function add(a: 0, b: 0) -> 0 {\n return a + b\n }\n`\n\n// Returns transpiled JavaScript with __tjs metadata\n```\n\nYou can also use the CLI:\n\n```bash\nbun src/cli/tjs.ts check file.tjs # Parse and type check\nbun src/cli/tjs.ts run file.tjs # Transpile and execute\nbun src/cli/tjs.ts emit file.tjs # Output transpiled JS\nbun src/cli/tjs.ts types file.tjs # Output type metadata\n```\n\n---\n\n## Syntax\n\n### Parameter Types (Colon Syntax)\n\n> **Not TypeScript.** TJS colon syntax looks like TypeScript but has different\n> semantics. The value after `:` is a **concrete example**, not a type name.\n> Write `name: 'Alice'` (example value), not `name: string` (type name).\n> TJS infers the type from the example: `'Alice'` → string, `0` → integer,\n> `true` → boolean.\n\nRequired parameters use colon syntax with an example value:\n\n```typescript\nfunction greet(name: 'Alice') {} // name is required, type: string\nfunction calculate(value: 0) {} // value is required, type: integer\nfunction measure(rate: 0.0) {} // rate is required, type: number (float)\nfunction count(n: +0) {} // n is required, type: non-negative integer\nfunction toggle(flag: true) {} // flag is required, type: boolean\n```\n\n### Numeric Types\n\nTJS distinguishes three numeric types using valid JavaScript syntax:\n\n```typescript\nfunction process(\n rate: 3.14, // number (float) -- has a decimal point\n count: 42, // integer -- whole number, no decimal\n index: +0 // non-negative integer -- prefixed with +\n) {}\n```\n\n| You Write | Type Inferred | Runtime Validation |\n| --------- | ---------------------- | ------------------------------- |\n| `3.14` | `number` (float) | `typeof x === 'number'` |\n| `0.0` | `number` (float) | `typeof x === 'number'` |\n| `42` | `integer` | `Number.isInteger(x)` |\n| `0` | `integer` | `Number.isInteger(x)` |\n| `+20` | `non-negative integer` | `Number.isInteger(x) && x >= 0` |\n| `+0` | `non-negative integer` | `Number.isInteger(x) && x >= 0` |\n| `-5` | `integer` | `Number.isInteger(x)` |\n| `-3.5` | `number` (float) | `typeof x === 'number'` |\n\nAll of these are valid JavaScript expressions. TJS reads the syntax more\ncarefully to give you finer-grained type checking than JS or TypeScript\nprovide natively.\n\n### Optional Parameters (Default Values)\n\nOptional parameters use `=` with a default value:\n\n```typescript\nfunction greet(name = 'World') {} // name is optional, defaults to 'World'\nfunction calculate(value = 0) {} // value is optional, defaults to 0 (integer)\n```\n\n### TypeScript-Style Optional\n\nYou can also use `?:` syntax:\n\n```typescript\nfunction greet(name?: '') {} // same as name = ''\n```\n\n### Object Parameters\n\nObject shapes are defined by example:\n\n```typescript\nfunction createUser(user: { name: ''; age: 0 }) {}\n// user must be an object with string name and number age\n```\n\n### Nullable Types\n\nUse `||` for union with null:\n\n```typescript\nfunction find(id: 0 || null) { } // number or null\n```\n\n### Return Types (Arrow Syntax)\n\nReturn types use `->`:\n\n```typescript\nfunction add(a: 0, b: 0) -> 0 {\n return a + b\n}\n\nfunction getUser(id: 0) -> { name: '', age: 0 } {\n return { name: 'Alice', age: 30 }\n}\n```\n\n### Array Types\n\nArrays use bracket syntax with an example element:\n\n```typescript\nfunction sum(numbers: [0]) -> 0 { // array of numbers\n return numbers.reduce((a, b) => a + b, 0)\n}\n\nfunction names(users: [{ name: '' }]) { // array of objects\n return users.map(u => u.name)\n}\n```\n\n---\n\n## Safety Markers\n\n### Unsafe Functions\n\nSkip validation for hot paths:\n\n```typescript\nfunction fastAdd(! a: 0, b: 0) { return a + b }\n```\n\nThe `!` marker after the function name skips input validation.\n\n### Safe Functions\n\nExplicit validation (for emphasis):\n\n```typescript\nfunction safeAdd(? a: 0, b: 0) { return a + b }\n```\n\n### Unsafe Blocks\n\nSkip validation for a block of code:\n\n```typescript\nunsafe {\n fastPath(data)\n anotherHotFunction(moreData)\n}\n```\n\n### Module Safety Directive\n\nSet the default validation level for an entire file:\n\n```typescript\nsafety none // No validation (metadata only)\nsafety inputs // Validate function inputs (default)\nsafety all // Validate everything (debug mode)\n```\n\n---\n\n## Type System\n\n### Type()\n\nDefine named types with predicates:\n\n```typescript\n// Simple type from example\nType Name 'Alice'\n\n// Type with description\nType User {\n description: 'a user object'\n example: { name: '', age: 0 }\n}\n\n// Type with predicate\nType PositiveNumber {\n description: 'a positive number'\n example: 1\n predicate(x) { return x > 0 }\n}\n```\n\nTypes can be used in function signatures:\n\n```typescript\nfunction greet(name: Name) -> '' {\n return `Hello, ${name}!`\n}\n```\n\n### Generic()\n\nRuntime-checkable generics:\n\n```typescript\nGeneric Box<T> {\n description: 'a boxed value'\n predicate(x, T) {\n return typeof x === 'object' && x !== null && 'value' in x && T(x.value)\n }\n}\n\n// With default type parameter\nGeneric Container<T, U = ''> {\n description: 'container with label'\n predicate(obj, T, U) {\n return T(obj.item) && U(obj.label)\n }\n}\n```\n\n### Union()\n\nDiscriminated unions:\n\n```typescript\nconst Shape = Union('kind', {\n circle: { radius: 0 },\n rectangle: { width: 0, height: 0 }\n})\n\nfunction area(shape: Shape) -> 0 {\n if (shape.kind === 'circle') {\n return Math.PI * shape.radius ** 2\n }\n return shape.width * shape.height\n}\n```\n\n### Enum()\n\nString or numeric enums:\n\n```typescript\nconst Status = Enum(['pending', 'active', 'completed'])\nconst Priority = Enum({ low: 1, medium: 2, high: 3 })\n\nfunction setStatus(status: Status) {}\n```\n\n---\n\n## Structural Equality: Is / IsNot\n\nJavaScript's `==` is broken (type coercion). TJS provides structural equality:\n\n```typescript\n// Structural comparison - no coercion\n[1, 2] Is [1, 2] // true\n5 Is \"5\" // false (different types)\n{ a: 1 } Is { a: 1 } // true\n\n// Arrays compared element-by-element\n[1, [2, 3]] Is [1, [2, 3]] // true\n\n// Negation\n5 IsNot \"5\" // true\n```\n\n### Custom Equality\n\nObjects can define custom equality in two ways:\n\n**1. `[tjsEquals]` symbol protocol** (preferred for Proxies and advanced use):\n\n```typescript\nimport { tjsEquals } from 'tjs-lang/lang'\n\n// A proxy that delegates equality to its target\nconst target = { x: 1, y: 2 }\nconst proxy = new Proxy({\n [tjsEquals](other) { return target Is other }\n}, {})\n\nproxy == { x: 1, y: 2 } // true — delegates to target\n```\n\n**2. `.Equals` method** (simple, works on any object or class):\n\n```typescript\nclass Point {\n constructor(x: 0, y: 0) { this.x = x; this.y = y }\n Equals(other) { return this.x === other.x && this.y === other.y }\n}\n\nPoint(1, 2) Is Point(1, 2) // true (uses .Equals)\n```\n\n**Priority:** `[tjsEquals]` symbol > `.Equals` method > structural comparison.\n\nThe symbol is `Symbol.for('tjs.equals')`, so it works across realms. Access it\nvia `import { tjsEquals } from 'tjs-lang/lang'` or `__tjs.tjsEquals` at runtime.\n\n---\n\n## Classes\n\n### Callable Without `new`\n\nTJS classes are callable without the `new` keyword:\n\n```typescript\nclass User {\n constructor(name: '') {\n this.name = name\n }\n}\n\n// Both work identically:\nconst u1 = User('Alice') // TJS way - clean\nconst u2 = new User('Alice') // Also works (linter warns)\n```\n\n### Private Fields\n\nUse `#` for private fields:\n\n```typescript\nclass Counter {\n #count = 0\n\n increment() {\n this.#count++\n }\n get value() {\n return this.#count\n }\n}\n```\n\nWhen converting from TypeScript, `private foo` becomes `#foo`.\n\n### Getters and Setters\n\nAsymmetric types are captured:\n\n```typescript\nclass Timestamp {\n #value\n\n constructor(initial: '' | 0 | null) {\n this.#value = initial === null ? new Date() : new Date(initial)\n }\n\n set value(v: '' | 0 | null) {\n this.#value = v === null ? new Date() : new Date(v)\n }\n\n get value() {\n return this.#value\n }\n}\n\nconst ts = Timestamp('2024-01-15')\nts.value = 0 // SET accepts: string | number | null\nts.value // GET returns: Date\n```\n\n---\n\n## Polymorphic Functions\n\nMultiple function declarations with the same name are automatically merged into a dispatcher that routes by argument count and type:\n\n```typescript\nfunction describe(value: 0) {\n return 'number: ' + value\n}\nfunction describe(value: '') {\n return 'string: ' + value\n}\nfunction describe(value: { name: '' }) {\n return 'object: ' + value.name\n}\n\ndescribe(42) // 'number: 42'\ndescribe('hello') // 'string: hello'\ndescribe({ name: 'world' }) // 'object: world'\ndescribe(true) // MonadicError: no matching overload\n```\n\n### Dispatch Order\n\n1. **Arity** first (number of arguments)\n2. **Type specificity** within same arity: `integer` > `number` > `any`; objects before primitives\n3. **Declaration order** as tiebreaker\n\n### Polymorphic Constructors\n\nClasses can have multiple constructor signatures. The first becomes the real JS constructor; additional variants become factory functions:\n\n```typescript\nTjsClass\n\nclass Point {\n constructor(x: 0.0, y: 0.0) {\n this.x = x\n this.y = y\n }\n constructor(coords: { x: 0.0; y: 0.0 }) {\n this.x = coords.x\n this.y = coords.y\n }\n}\n\nPoint(3, 4) // variant 1: two numbers\nPoint({ x: 10, y: 20 }) // variant 2: object\n```\n\nAll variants produce correct `instanceof` results.\n\n### Compile-Time Validation\n\nTJS catches these errors at transpile time:\n\n- **Ambiguous signatures**: Two variants with identical types at every position\n- **Rest parameters**: `...args` not supported in polymorphic functions\n- **Mixed async/sync**: All variants must agree\n\n---\n\n## Local Class Extensions\n\nAdd methods to built-in types without polluting prototypes:\n\n```typescript\nextend String {\n capitalize() {\n return this[0].toUpperCase() + this.slice(1)\n }\n words() {\n return this.split(/\\s+/)\n }\n}\n\n'hello world'.capitalize() // 'Hello world'\n'foo bar baz'.words() // ['foo', 'bar', 'baz']\n```\n\n### How It Works\n\nFor known-type receivers (literals, typed variables), calls are rewritten at transpile time to `.call()` — zero runtime overhead:\n\n```javascript\n// TJS source:\n'hello'.capitalize()\n\n// Generated JS:\n__ext_String.capitalize.call('hello')\n```\n\nFor unknown types, a runtime registry (`registerExtension` / `resolveExtension`) provides fallback dispatch.\n\n### Supported Types\n\nExtensions work on any type: `String`, `Number`, `Array`, `Boolean`, custom classes, and DOM classes like `HTMLElement`. Multiple `extend` blocks for the same type merge left-to-right (later declarations can override earlier methods).\n\n### Rules\n\n- Arrow functions are **not allowed** in extend blocks (they don't bind `this`)\n- Extensions are **file-local** — they don't leak across modules\n- Prototypes are **never modified** — `String.prototype.capitalize` remains `undefined`\n\n---\n\n## Runtime Features\n\n### `__tjs` Metadata\n\nEvery TJS function carries its type information:\n\n```typescript\nfunction createUser(input: { name: '', age: 0 }) -> { id: 0 } {\n return { id: 123 }\n}\n\nconsole.log(createUser.__tjs)\n// {\n// params: {\n// input: { type: { kind: 'object', shape: { name: 'string', age: 'number' } } }\n// },\n// returns: { kind: 'object', shape: { id: 'number' } }\n// }\n```\n\nThis enables:\n\n- Autocomplete from live objects\n- Runtime type validation\n- Automatic documentation generation\n\n### Monadic Errors\n\nType validation failures return `MonadicError` instances (extends `Error`),\nnot thrown exceptions:\n\n```typescript\nimport { isMonadicError } from 'tjs-lang/lang'\n\nconst result = createUser({ name: 123 }) // wrong type\n// MonadicError: Expected string for 'createUser.name', got number\n\nif (isMonadicError(result)) {\n console.log(result.message) // \"Expected string for 'createUser.name', got number\"\n console.log(result.path) // \"createUser.name\"\n console.log(result.expected) // \"string\"\n console.log(result.actual) // \"number\"\n}\n```\n\nNo try/catch gambling. The host survives invalid inputs.\n\nFor general-purpose error values (not type errors), use the `error()` helper\nwhich returns plain `{ $error: true, message }` objects checkable with `isError()`.\n\n### Inline Tests\n\nTests live next to code:\n\n```typescript\nfunction double(x: 0) -> 0 { return x * 2 }\n\ntest('doubles numbers') {\n expect(double(5)).toBe(10)\n expect(double(-3)).toBe(-6)\n}\n```\n\nTests are extracted at compile time and can be:\n\n- Run during transpilation\n- Stripped in production builds\n- Used for documentation generation\n\n### WASM Blocks\n\nDrop into WebAssembly for compute-heavy code:\n\n```typescript\nfunction vectorDot(a: [0], b: [0]) -> 0 {\n let sum = 0\n wasm {\n for (let i = 0; i < a.length; i++) {\n sum = sum + a[i] * b[i]\n }\n }\n return sum\n}\n```\n\nVariables are captured automatically. Falls back to JS if WASM unavailable.\n\n#### SIMD Intrinsics (f32x4)\n\nFor compute-heavy workloads, use f32x4 SIMD intrinsics to process 4 float32 values per instruction:\n\n```typescript\nconst scale = wasm (arr: Float32Array, len: 0, factor: 0.0) -> 0 {\n let s = f32x4_splat(factor)\n for (let i = 0; i < len; i += 4) {\n let off = i * 4\n let v = f32x4_load(arr, off)\n f32x4_store(arr, off, f32x4_mul(v, s))\n }\n} fallback {\n for (let i = 0; i < len; i++) arr[i] *= factor\n}\n```\n\nAvailable intrinsics:\n\n| Intrinsic | Description |\n|-----------|-------------|\n| `f32x4_load(ptr, byteOffset)` | Load 4 floats from memory into v128 |\n| `f32x4_store(ptr, byteOffset, vec)` | Store v128 as 4 floats to memory |\n| `f32x4_splat(scalar)` | Fill all 4 lanes with a scalar value |\n| `f32x4_extract_lane(vec, N)` | Extract float from lane 0-3 |\n| `f32x4_replace_lane(vec, N, val)` | Replace one lane, return new v128 |\n| `f32x4_add(a, b)` | Lane-wise addition |\n| `f32x4_sub(a, b)` | Lane-wise subtraction |\n| `f32x4_mul(a, b)` | Lane-wise multiplication |\n| `f32x4_div(a, b)` | Lane-wise division |\n| `f32x4_neg(v)` | Negate all lanes |\n| `f32x4_sqrt(v)` | Square root of all lanes |\n\nThis mirrors C/C++ SIMD intrinsics (`_mm_add_ps`, etc.) — explicit, predictable, no auto-vectorization magic.\n\n#### Zero-Copy Arrays: `wasmBuffer()`\n\nBy default, typed arrays passed to WASM blocks are copied into WASM memory before the call and copied back out after. For large arrays called frequently, this overhead can negate WASM's speed advantage.\n\n`wasmBuffer(Constructor, length)` allocates typed arrays directly in WASM linear memory. These arrays work like normal typed arrays from JavaScript, but when passed to a `wasm {}` block, they're zero-copy — the data is already there.\n\n```typescript\n// Allocate particle positions in WASM memory\nconst starX = wasmBuffer(Float32Array, 50000)\nconst starY = wasmBuffer(Float32Array, 50000)\n\n// Use from JS like normal arrays\nfor (let i = 0; i < 50000; i++) {\n starX[i] = (Math.random() - 0.5) * 2000\n starY[i] = (Math.random() - 0.5) * 2000\n}\n\n// Zero-copy SIMD processing\nfunction moveParticles(! xs: Float32Array, ys: Float32Array, len: 0, dx: 0.0, dy: 0.0) {\n wasm {\n let vdx = f32x4_splat(dx)\n let vdy = f32x4_splat(dy)\n for (let i = 0; i < len; i += 4) {\n let off = i * 4\n f32x4_store(xs, off, f32x4_add(f32x4_load(xs, off), vdx))\n f32x4_store(ys, off, f32x4_add(f32x4_load(ys, off), vdy))\n }\n } fallback {\n for (let i = 0; i < len; i++) { xs[i] += dx; ys[i] += dy }\n }\n}\n\n// After WASM runs, JS sees the mutations immediately\nmoveParticles(starX, starY, 50000, 1.0, 0.5)\nconsole.log(starX[0]) // updated in place, no copy\n```\n\nKey points:\n- Supported constructors: `Float32Array`, `Float64Array`, `Int32Array`, `Uint8Array`\n- Uses a bump allocator — allocations persist for program lifetime\n- All WASM blocks in a file share one 64MB memory\n- Regular typed arrays still work (copy in/out as before)\n- Use `!` (unsafe) on hot-path functions to skip runtime type checks\n\n---\n\n## Module System\n\n### Imports\n\nTJS supports URL imports:\n\n```typescript\nimport { Button } from 'https://cdn.example.com/ui-kit.tjs'\nimport { validate } from './utils/validation.tjs'\n```\n\nModules are:\n\n- Fetched on demand\n- Transpiled in the browser\n- Cached independently (IndexedDB + service worker)\n\n### CDN Integration\n\nExternal packages from esm.sh with pinned versions:\n\n```typescript\nimport lodash from 'https://esm.sh/lodash@4.17.21'\n```\n\n---\n\n## TypeScript Compatibility\n\n### TS → TJS Converter\n\nConvert existing TypeScript:\n\n```bash\nbun src/cli/tjs.ts convert file.ts\n```\n\n```typescript\n// TypeScript\nfunction greet(name: string, age?: number): string { ... }\n\n// Converts to TJS\nfunction greet(name: '', age = 0) -> '' { ... }\n```\n\n### What Gets Converted\n\n| TypeScript | TJS |\n| -------------------------- | ------------------- |\n| `name: string` | `name: ''` |\n| `age: number` | `age: 0.0` |\n| `flag: boolean` | `flag: false` |\n| `items: string[]` | `items: ['']` |\n| `age?: number` | `age = 0.0` |\n| `private foo` | `#foo` |\n| `interface User` | `Type User` |\n| `type Status = 'a' \\| 'b'` | `Union(['a', 'b'])` |\n| `enum Color` | `Enum(...)` |\n\n> **Lossy boolean conversion:** TypeScript `x?: boolean` becomes TJS `x = false`.\n> This collapses \"not passed\" (`undefined`) and \"passed as `false`\" into the same\n> default value. Code that distinguishes the three states (`true` / `false` /\n> `undefined`) may break. A future version may emit `x: false || null` for\n> optional booleans to preserve the `undefined` state.\n\n---\n\n## Performance\n\n| Mode | Overhead | Use Case |\n| --------------- | --------- | ------------------------------- |\n| `safety none` | **1.0x** | Metadata only, no validation |\n| `safety inputs` | **~1.5x** | Production (single-arg objects) |\n| `(!) unsafe` | **1.0x** | Hot paths |\n| `wasm {}` | **<1.0x** | Compute-heavy code |\n\n### Why 1.5x, Not 25x\n\nMost validators interpret schemas at runtime (~25x overhead). TJS generates inline checks at transpile time:\n\n```typescript\n// Generated (JIT-friendly)\nif (\n typeof input !== 'object' ||\n input === null ||\n typeof input.name !== 'string' ||\n typeof input.age !== 'number'\n) {\n return { $error: true, message: 'Invalid input', path: 'fn.input' }\n}\n```\n\nNo schema interpretation. No object iteration. The JIT inlines these completely.\n\n---\n\n## Bare Assignments\n\nUppercase identifiers automatically get `const`:\n\n```typescript\nFoo = Type('test', 'example') // becomes: const Foo = Type(...)\nMyConfig = { debug: true } // becomes: const MyConfig = { ... }\n```\n\n---\n\n## Limitations\n\n### What TJS Doesn't Do\n\n- **No gradual typing** - types are all-or-nothing per function\n- **No complex type inference** - you provide examples, not constraints\n- **No declaration files** - types live in the code, not `.d.ts`\n- **No type-level computation** - no conditional types, mapped types, etc.\n\n### What TJS Intentionally Avoids\n\n- Build steps\n- External type checkers\n- Complex tooling configuration\n- Separation of types from runtime\n\n---\n\n## Learn More\n\n- [AJS Documentation](DOCS-AJS.md) — The agent runtime\n- [Builder's Manifesto](MANIFESTO-BUILDER.md) — Why TJS is fun\n- [Enterprise Guide](MANIFESTO-ENTERPRISE.md) — Why TJS is safe\n- [Technical Context](CONTEXT.md) — Architecture deep dive\n"
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
157
|
"title": "Starfield",
|
|
@@ -394,7 +394,7 @@
|
|
|
394
394
|
"group": "advanced",
|
|
395
395
|
"order": 19,
|
|
396
396
|
"requiresApi": true,
|
|
397
|
-
"code": "function solveWithCode({ problem = 'Calculate the 10th Fibonacci number' }) {\n // System prompt with AsyncJS rules and example\n let systemContext =\n 'You write AsyncJS code. AsyncJS is a JavaScript subset.\\n\\nRULES:\\n- NO: async, await, new, class, this, var, for loops\\n- Use let for variables, while for loops\\n
|
|
397
|
+
"code": "function solveWithCode({ problem = 'Calculate the 10th Fibonacci number' }) {\n // System prompt with AsyncJS rules and example\n let systemContext =\n 'You write AsyncJS code. AsyncJS is a JavaScript subset.\\n\\nRULES:\\n- Functions take a destructured object param: function foo({ a, b })\\n- MUST return an object. WRONG: return 42. RIGHT: return { result: 42 }\\n- NO: async, await, new, class, this, var, for loops\\n- Use let for variables, while for loops\\n\\nEXAMPLE (factorial):\\nfunction solve() {\\n let result = 1\\n let i = 5\\n while (i > 1) {\\n result = result * i\\n i = i - 1\\n }\\n return { result }\\n}\\n\\nReturn ONLY the function code, nothing else.'\n\n let prompt =\n systemContext + '\\n\\nWrite a function called \"solve\" that: ' + problem\n\n let response = llmPredict({ prompt })\n\n // Clean up code - remove markdown fences, fix escapes, extract function\n let code = response\n code = code.replace(/",
|
|
398
398
|
"language": "ajs",
|
|
399
399
|
"description": "LLM writes and runs code to solve a problem (requires llm capability)"
|
|
400
400
|
},
|
|
@@ -407,7 +407,7 @@
|
|
|
407
407
|
"group": "advanced",
|
|
408
408
|
"order": 20,
|
|
409
409
|
"requiresApi": true,
|
|
410
|
-
"code": "function generateCode({ task = 'Calculate the factorial of n' }) {\n // System prompt with AsyncJS rules and complete example\n let systemContext =\n 'You write AsyncJS code. AsyncJS is a subset of JavaScript.\\n\\nRULES:\\n- Types by example: fn(n: 5) means required number param with example value 5\\n- NO: async, await, new, class, this, var, for, generator functions (function*)\\n- Use let for variables, while for loops\\n
|
|
410
|
+
"code": "function generateCode({ task = 'Calculate the factorial of n' }) {\n // System prompt with AsyncJS rules and complete example\n let systemContext =\n 'You write AsyncJS code. AsyncJS is a subset of JavaScript.\\n\\nRULES:\\n- Functions take a destructured object param: function foo({ a, b })\\n- MUST return an object. WRONG: return 42. RIGHT: return { result: 42 }\\n- Types by example: fn({ n: 5 }) means required number param with example value 5\\n- NO: async, await, new, class, this, var, for, generator functions (function*)\\n- Use let for variables, while for loops\\n\\nEXAMPLE - calculating sum of 1 to n:\\nfunction sumTo({ n: 10 }) {\\n let sum = 0\\n let i = 1\\n while (i <= n) {\\n sum = sum + i\\n i = i + 1\\n }\\n return { result: sum }\\n}'\n\n let schema = Schema.response('generated_code', {\n code: '',\n description: '',\n })\n\n let prompt =\n systemContext +\n '\\n\\nWrite an AsyncJS function for: ' +\n task +\n '\\n\\nReturn ONLY valid AsyncJS code in the code field. Must start with \"function\" and use while loops (not for loops).'\n\n let response = llmPredict({ prompt, options: { responseFormat: schema } })\n let result = JSON.parse(response)\n\n // Clean up any markdown fences and fix escaped newlines\n let code = result.code\n code = code.replace(/",
|
|
411
411
|
"language": "ajs",
|
|
412
412
|
"description": "LLM writes AsyncJS code from a description (requires llm capability)"
|
|
413
413
|
},
|
|
@@ -539,7 +539,7 @@
|
|
|
539
539
|
"group": "docs",
|
|
540
540
|
"order": 0,
|
|
541
541
|
"navTitle": "Documentation",
|
|
542
|
-
"text": "<!--{\"section\": \"ajs\", \"group\": \"docs\", \"order\": 0, \"navTitle\": \"Documentation\"}-->\n\n# AJS: The Agent Language\n\n_Code as Data. Safe. Async. Sandboxed._\n\n---\n\n## What is AJS?\n\nAJS (AsyncJS) is a JavaScript subset that compiles to a **JSON AST**. It's designed for untrusted code—user scripts, LLM-generated agents, remote logic.\n\n```javascript\nfunction searchAndSummarize({ query }) {\n let results = httpFetch({ url: `https://api.example.com/search?q=${query}` })\n let summary = llmPredict({ prompt: `Summarize: ${JSON.stringify(results)}` })\n return { query, summary }\n}\n```\n\nThis compiles to JSON that can be:\n\n- Stored in a database\n- Sent over the network\n- Executed in a sandboxed VM\n- Audited before running\n\n---\n\n## The VM\n\nAJS runs in a gas-limited, isolated VM with strict resource controls.\n\n```typescript\nimport { ajs, AgentVM } from 'tjs-lang'\n\nconst agent = ajs`\n function process({ url }) {\n let data = httpFetch({ url })\n return { fetched: data }\n }\n`\n\nconst vm = new AgentVM()\nconst result = await vm.run(\n agent,\n { url: 'https://api.example.com' },\n {\n fuel: 1000, // CPU budget\n timeoutMs: 5000, // Wall-clock limit\n }\n)\n```\n\n### Fuel Metering\n\nEvery operation costs fuel:\n\n| Operation | Cost |\n| ------------------------ | ---- |\n| Expression evaluation | 0.01 |\n| Variable set/get | 0.1 |\n| Control flow (if, while) | 0.5 |\n| HTTP fetch | 10 |\n| LLM predict | 100 |\n\nWhen fuel runs out, execution stops safely:\n\n```typescript\nif (result.fuelExhausted) {\n // Agent tried to run forever - stopped safely\n}\n```\n\n### Timeout Enforcement\n\nFuel protects against CPU abuse. Timeouts protect against I/O abuse:\n\n```typescript\nawait vm.run(agent, args, {\n fuel: 1000,\n timeoutMs: 5000, // Hard 5-second limit\n})\n```\n\nSlow network calls can't hang your servers.\n\n### Capability Injection\n\nThe VM starts with **zero capabilities**. You grant what each agent needs:\n\n```typescript\nconst capabilities = {\n fetch: createFetchCapability({\n allowedHosts: ['api.example.com'],\n }),\n store: createReadOnlyStore(),\n // No llm - this agent can't call AI\n}\n\nawait vm.run(agent, args, { capabilities })\n```\n\n---\n\n## Syntax\n\nAJS is a JavaScript subset. Familiar syntax, restricted features.\n\n### What's Allowed\n\n```javascript\n// Functions\nfunction process({ input }) {\n return { output: input * 2 }\n}\n\n// Variables\nlet x = 10\nconst y = 'hello'\n\n// Conditionals\nif (x > 5) {\n return 'big'\n} else {\n return 'small'\n}\n\n// Loops\nfor (let i = 0; i < 10; i++) {\n total = total + i\n}\n\nfor (let item of items) {\n results.push(item.name)\n}\n\nwhile (count > 0) {\n count = count - 1\n}\n\n// Try/catch\ntry {\n riskyOperation()\n} catch (e) {\n return { error: e.message }\n}\n\n// Template literals\nlet message = `Hello, ${name}!`\n\n// Object/array literals\nlet obj = { a: 1, b: 2 }\nlet arr = [1, 2, 3]\n\n// Destructuring\nlet { name, age } = user\nlet [first, second] = items\n\n// Spread\nlet merged = { ...defaults, ...overrides }\nlet combined = [...arr1, ...arr2]\n\n// Ternary\nlet result = x > 0 ? 'positive' : 'non-positive'\n\n// Logical operators\nlet value = a && b\nlet fallback = a || defaultValue\nlet nullish = a ?? defaultValue\n```\n\n### What's Forbidden\n\n| Feature | Why Forbidden |\n| -------------------------- | ------------------------------------------------- |\n| `class` | Too complex for LLMs, enables prototype pollution |\n| `new` | Arbitrary object construction |\n| `this` | Implicit context, hard to sandbox |\n| Closures | State escapes the sandbox |\n| `async`/`await` | VM handles async internally |\n| `eval`, `Function` | Code injection |\n| `__proto__`, `constructor` | Prototype pollution |\n| `import`/`export` | Module system handled by host |\n\nAJS is intentionally simple—simple enough for 4B parameter LLMs to generate correctly.\n\n### Differences from JavaScript\n\nAJS expressions differ from standard JavaScript in a few important ways:\n\n**Null-safe member access.** All member access uses optional chaining internally. Accessing a property on `null` or `undefined` returns `undefined` instead of throwing `TypeError`:\n\n```javascript\nlet x = null\nlet y = x.foo.bar // undefined (no error)\n```\n\nThis is a deliberate safety choice — agents shouldn't crash on missing data.\n\n**No computed member access with variables.** You can use literal indices (`items[0]`, `obj[\"key\"]`) but not variable indices (`items[i]`). This is rejected at transpile time:\n\n```javascript\n// Works\nlet first = items[0]\nlet name = user['name']\n\n// Fails: \"Computed member access with variables not yet supported\"\nlet item = items[i]\n```\n\nWorkaround: use array atoms like `map`, `reduce`, or `for...of` loops instead of index-based access.\n\n**Structural equality.** `==` and `!=` perform deep structural comparison, not reference or coerced equality. No type coercion: `'1' == 1` is `false`. Use `===` and `!==` for identity (reference) checks:\n\n```javascript\n[1, 2] == [1, 2] // true (structural)\n[1, 2] === [1, 2] // false (different objects)\n{ a: 1 } == { a: 1 } // true (structural)\n'1' == 1 // false (no coercion, unlike JS)\nnull == undefined // true (nullish equality preserved)\n```\n\nObjects with a `.Equals` method or `[Symbol.for('tjs.equals')]` handler get custom comparison behavior.\n\n---\n\n## Atoms\n\nAtoms are the built-in operations. Each atom has a defined cost, input schema, and output schema.\n\n### Flow Control\n\n| Atom | Description |\n| -------- | ------------------------------ |\n| `seq` | Execute operations in sequence |\n| `if` | Conditional branching |\n| `while` | Loop with condition |\n| `return` | Return a value |\n| `try` | Error handling |\n\n### State Management\n\n| Atom | Description |\n| ------------ | -------------------------- |\n| `varSet` | Set a variable |\n| `varGet` | Get a variable |\n| `varsLet` | Batch variable declaration |\n| `varsImport` | Import from arguments |\n| `varsExport` | Export as result |\n| `scope` | Create a local scope |\n\n### I/O\n\n| Atom | Description |\n| ----------- | ------------------------------------------- |\n| `httpFetch` | HTTP requests (requires `fetch` capability) |\n\n### Storage (Core)\n\n| Atom | Description |\n| ------------- | ------------------------ |\n| `storeGet` | Get from key-value store |\n| `storeSet` | Set in key-value store |\n| `storeSearch` | Vector similarity search |\n\n### Storage (Battery)\n\n| Atom | Description |\n| ----------------------- | ------------------------------------- |\n| `storeVectorize` | Generate embeddings from text |\n| `storeCreateCollection` | Create a vector store collection |\n| `storeVectorAdd` | Add a document to a vector collection |\n\n### AI (Core)\n\n| Atom | Description |\n| ------------ | ------------------------------------------ |\n| `llmPredict` | Simple LLM inference (`prompt` → `string`) |\n| `agentRun` | Run a sub-agent |\n\n### AI (Battery)\n\n| Atom | Description |\n| ------------------- | ---------------------------------------------- |\n| `llmPredictBattery` | Chat completion (system/user → message object) |\n| `llmVision` | Analyze images using a vision-capable model |\n\n### Procedures\n\n| Atom | Description |\n| ------------------------ | ------------------------------ |\n| `storeProcedure` | Store an AST as callable token |\n| `releaseProcedure` | Delete a stored procedure |\n| `clearExpiredProcedures` | Clean up expired tokens |\n\n### Utilities\n\n| Atom | Description |\n| --------- | ------------------------ |\n| `random` | Random number generation |\n| `uuid` | Generate UUIDs |\n| `hash` | Compute hashes |\n| `memoize` | In-memory memoization |\n| `cache` | Persistent caching |\n\n---\n\n## Battery Atoms Reference\n\nBattery atoms provide LLM, embedding, and vector store capabilities. They\nrequire a separate import and capability setup.\n\n### Setup\n\n```javascript\nimport { AgentVM } from 'tjs-lang'\nimport { batteryAtoms, getBatteries } from 'tjs-lang'\n\nconst vm = new AgentVM(batteryAtoms)\nconst batteries = await getBatteries() // auto-detects LM Studio models\n\nconst { result } = await vm.run(agent, args, {\n fuel: 1000,\n capabilities: batteries,\n})\n```\n\nThe `getBatteries()` function auto-detects LM Studio and returns:\n\n```javascript\n{\n vector: { embed }, // embedding function (undefined if no LM Studio)\n store: { ... }, // key-value + vector store (always present)\n llmBattery: { predict, embed }, // LLM chat + embeddings (null if no LM Studio)\n models: { ... }, // detected model info (null if no LM Studio)\n}\n```\n\n**Important:** `vector` and `llmBattery` will be `undefined`/`null` if LM Studio\nisn't running or the connection is made over HTTPS (local LLM calls are blocked\nfrom HTTPS contexts for security). Always check for availability or handle\nthe atom's \"missing capability\" error.\n\n### Capability Keys\n\nBattery atoms look up capabilities by specific keys that differ from the base\n`Capabilities` interface:\n\n| Capability key | Used by atoms | Contains |\n| -------------- | -------------------------------------------------------- | ------------------------------- |\n| `llmBattery` | `llmPredictBattery`, `llmVision` | `{ predict, embed }` (full LLM) |\n| `vector` | `storeVectorize` | `{ embed }` only |\n| `store` | `storeSearch`, `storeCreateCollection`, `storeVectorAdd` | KV + vector store operations |\n| `llm` | `llmPredict` (core atom) | `{ predict }` (simple) |\n| `fetch` | `httpFetch` (core atom) | fetch function |\n\nThe split exists because `storeVectorize` only needs the embedding function,\nwhile `llmPredictBattery` needs the full chat API. If you're providing your own\ncapabilities (not using `getBatteries()`), wire the keys accordingly.\n\n### `llmPredict` vs `llmPredictBattery`\n\nThere are two LLM atoms with different interfaces:\n\n| Atom | Input | Output | Capability |\n| ------------------- | ----------------------- | -------------- | ------------------------- |\n| `llmPredict` | `{ prompt }` | `string` | `capabilities.llm` |\n| `llmPredictBattery` | `{ system, user, ... }` | message object | `capabilities.llmBattery` |\n\nUse `llmPredict` for simple prompts. Use `llmPredictBattery` when you need\nsystem prompts, tool calling, or structured output.\n\n### `llmPredictBattery`\n\nChat completion with system prompt, tool calling, and structured output support.\n\n**Input:**\n\n| Field | Type | Required | Description |\n| ---------------- | -------- | -------- | --------------------------------------------- |\n| `system` | `string` | No | System prompt (defaults to helpful assistant) |\n| `user` | `string` | Yes | User message |\n| `tools` | `any[]` | No | Tool definitions (OpenAI format) |\n| `responseFormat` | `any` | No | Structured output format |\n\n**Output:** OpenAI chat message object:\n\n```javascript\n{\n role: 'assistant',\n content: 'The answer is 42.', // null when using tool calls\n tool_calls: [...] // present when tools are invoked\n}\n```\n\n**Example:**\n\n```javascript\nlet response = llmPredictBattery({\n system: 'You are a helpful assistant.',\n user: 'What is the capital of France?',\n})\n// response.content === 'Paris is the capital of France.'\n```\n\n**Cost:** 100 fuel\n\n### `llmVision`\n\nAnalyze images using a vision-capable model.\n\n**Input:**\n\n| Field | Type | Required | Description |\n| ---------------- | ---------- | -------- | ----------------------------------------------- |\n| `system` | `string` | No | System prompt |\n| `prompt` | `string` | Yes | Text prompt describing what to analyze |\n| `images` | `string[]` | Yes | URLs or data URIs (`data:image/...;base64,...`) |\n| `responseFormat` | `any` | No | Structured output format |\n\n**Output:** Same as `llmPredictBattery` (message object with `role`, `content`, `tool_calls`).\n\n**Example:**\n\n```javascript\nlet analysis = llmVision({\n prompt: 'Describe what you see in this image.',\n images: ['https://example.com/photo.jpg'],\n})\n// analysis.content === 'The image shows a sunset over the ocean...'\n```\n\n**Cost:** 150 fuel | **Timeout:** 120 seconds\n\n### `storeVectorize`\n\nGenerate embeddings from text using the vector battery.\n\n**Input:**\n\n| Field | Type | Required | Description |\n| ------- | -------- | -------- | ---------------------- |\n| `text` | `string` | Yes | Text to embed |\n| `model` | `string` | No | Embedding model to use |\n\n**Output:** `number[]` — the embedding vector.\n\n**Example:**\n\n```javascript\nlet embedding = storeVectorize({ text: 'TJS is a typed JavaScript' })\n// embedding === [0.023, -0.412, 0.891, ...]\n```\n\n**Cost:** 20 fuel | **Capability:** `vector`\n\n### `storeCreateCollection`\n\nCreate a vector store collection for similarity search.\n\n**Input:**\n\n| Field | Type | Required | Description |\n| ------------ | -------- | -------- | -------------------------------- |\n| `collection` | `string` | Yes | Collection name |\n| `dimension` | `number` | No | Vector dimension (auto-detected) |\n\n**Output:** None.\n\n**Cost:** 5 fuel | **Capability:** `store`\n\n### `storeVectorAdd`\n\nAdd a document to a vector store collection. The document is automatically\nembedded and indexed.\n\n**Input:**\n\n| Field | Type | Required | Description |\n| ------------ | -------- | -------- | ----------------- |\n| `collection` | `string` | Yes | Collection name |\n| `doc` | `any` | Yes | Document to store |\n\n**Output:** None.\n\n**Example:**\n\n```javascript\nstoreVectorAdd({\n collection: 'articles',\n doc: { title: 'Intro to TJS', content: 'TJS is...', embedding: [...] }\n})\n```\n\n**Cost:** 5 fuel | **Capability:** `store`\n\n### `storeSearch`\n\nSearch a vector store collection by similarity.\n\n**Input:**\n\n| Field | Type | Required | Description |\n| ------------- | ---------- | -------- | ------------------------------ |\n| `collection` | `string` | Yes | Collection name |\n| `queryVector` | `number[]` | Yes | Query embedding vector |\n| `k` | `number` | No | Number of results (default: 5) |\n| `filter` | `object` | No | Metadata filter |\n\n**Output:** `any[]` — array of matching documents, sorted by similarity.\n\n**Example:**\n\n```javascript\nlet query = storeVectorize({ text: 'How does type checking work?' })\nlet results = storeSearch({\n collection: 'articles',\n queryVector: query,\n k: 3,\n})\n// results === [{ title: 'Type System', content: '...' }, ...]\n```\n\n**Cost:** 5 + k fuel (dynamic) | **Capability:** `store`\n\n---\n\n## Expression Builtins\n\nAJS expressions have access to safe built-in objects:\n\n### Math\n\nAll standard math functions:\n\n```javascript\nMath.abs(-5) // 5\nMath.floor(3.7) // 3\nMath.sqrt(16) // 4\nMath.sin(Math.PI) // ~0\nMath.random() // 0-1\nMath.max(1, 2, 3) // 3\nMath.min(1, 2, 3) // 1\n```\n\n### JSON\n\nParse and stringify:\n\n```javascript\nJSON.parse('{\"a\": 1}') // { a: 1 }\nJSON.stringify({ a: 1 }) // '{\"a\": 1}'\n```\n\n### Array\n\nStatic methods:\n\n```javascript\nArray.isArray([1, 2]) // true\nArray.from('abc') // ['a', 'b', 'c']\nArray.of(1, 2, 3) // [1, 2, 3]\n```\n\n### Object\n\nStatic methods:\n\n```javascript\nObject.keys({ a: 1 }) // ['a']\nObject.values({ a: 1 }) // [1]\nObject.entries({ a: 1 }) // [['a', 1]]\nObject.fromEntries([['a', 1]]) // { a: 1 }\nObject.assign({}, a, b) // merged object\n```\n\n### String\n\nStatic methods:\n\n```javascript\nString.fromCharCode(65) // 'A'\nString.fromCodePoint(128512) // emoji\n```\n\n### Number\n\nConstants and checks:\n\n```javascript\nNumber.MAX_VALUE\nNumber.isNaN(NaN) // true\nNumber.isFinite(100) // true\nNumber.parseInt('42') // 42\nNumber.parseFloat('3.14') // 3.14\n```\n\n### Set Operations\n\nSet-like operations:\n\n```javascript\nSet.add([1, 2], 3) // [1, 2, 3]\nSet.remove([1, 2, 3], 2) // [1, 3]\nSet.union([1, 2], [2, 3]) // [1, 2, 3]\nSet.intersection([1, 2], [2, 3]) // [2]\nSet.diff([1, 2, 3], [2]) // [1, 3]\n```\n\n### Date\n\nDate factory with arithmetic:\n\n```javascript\nDate.now() // timestamp\nDate.create('2024-01-15') // Date object\nDate.add(date, 1, 'day') // new Date\nDate.format(date, 'YYYY-MM-DD')\n```\n\n### Schema\n\nBuild JSON schemas for structured LLM outputs:\n\n```javascript\n// From example\nlet schema = Schema.response('person', { name: '', age: 0 })\n\n// With constraints\nlet schema = Schema.response(\n 'user',\n Schema.object({\n email: Schema.string.email,\n age: Schema.number.int.min(0).max(150).optional,\n role: Schema.enum(['admin', 'user', 'guest']),\n })\n)\n```\n\n---\n\n## JSON AST Format\n\nAJS compiles to a JSON AST. Here's what it looks like:\n\n### Sequence\n\n```json\n{\n \"$seq\": [\n { \"$op\": \"varSet\", \"key\": \"x\", \"value\": 10 },\n { \"$op\": \"varSet\", \"key\": \"y\", \"value\": 20 },\n {\n \"$op\": \"return\",\n \"value\": { \"$expr\": \"binary\", \"op\": \"+\", \"left\": \"x\", \"right\": \"y\" }\n }\n ]\n}\n```\n\n### Expressions\n\n```json\n// Literal\n{ \"$expr\": \"literal\", \"value\": 42 }\n\n// Identifier\n{ \"$expr\": \"ident\", \"name\": \"varName\" }\n\n// Binary operation\n{ \"$expr\": \"binary\", \"op\": \"+\", \"left\": {...}, \"right\": {...} }\n\n// Member access\n{ \"$expr\": \"member\", \"object\": {...}, \"property\": \"foo\" }\n\n// Template literal\n{ \"$expr\": \"template\", \"tmpl\": \"Hello, ${name}!\" }\n```\n\n### Conditionals\n\n```json\n{\n \"$op\": \"if\",\n \"cond\": { \"$expr\": \"binary\", \"op\": \">\", \"left\": \"x\", \"right\": 0 },\n \"then\": { \"$seq\": [...] },\n \"else\": { \"$seq\": [...] }\n}\n```\n\n### Loops\n\n```json\n{\n \"$op\": \"while\",\n \"cond\": { \"$expr\": \"binary\", \"op\": \">\", \"left\": \"count\", \"right\": 0 },\n \"body\": { \"$seq\": [...] }\n}\n```\n\n---\n\n## Security Model\n\n### Zero Capabilities by Default\n\nThe VM can't do anything unless you allow it:\n\n```typescript\n// This agent can only compute - no I/O\nawait vm.run(agent, args, { capabilities: {} })\n\n// This agent can fetch from one domain\nawait vm.run(agent, args, {\n capabilities: {\n fetch: createFetchCapability({ allowedHosts: ['api.example.com'] }),\n },\n})\n```\n\n### Forbidden Properties\n\nThese property names are blocked to prevent prototype pollution:\n\n- `__proto__`\n- `constructor`\n- `prototype`\n\n### SSRF Protection\n\nThe `httpFetch` atom can be configured with:\n\n- Allowlisted hosts only\n- Blocked private IP ranges\n- Request signing requirements\n\n### ReDoS Protection\n\nSuspicious regex patterns are rejected before execution.\n\n### Execution Tracing\n\nEvery agent run can produce an audit trail:\n\n```typescript\nconst { result, trace } = await vm.run(agent, args, { trace: true })\n\n// trace: [\n// { op: 'varSet', key: 'x', fuelBefore: 1000, fuelAfter: 999.9 },\n// { op: 'httpFetch', url: '...', fuelBefore: 999.9, fuelAfter: 989.9 },\n// ...\n// ]\n```\n\n---\n\n## Use Cases\n\n### AI Agents\n\n```javascript\nfunction researchAgent({ topic }) {\n let searchResults = httpFetch({\n url: `https://api.search.com?q=${topic}`,\n })\n\n let summary = llmPredict({\n system: 'You are a research assistant.',\n user: `Summarize these results about ${topic}: ${searchResults}`,\n })\n\n return { topic, summary }\n}\n```\n\n### Rule Engines\n\n```javascript\nfunction applyDiscounts({ cart, userTier }) {\n let discount = 0\n\n if (userTier === 'gold') {\n discount = 0.2\n } else if (userTier === 'silver') {\n discount = 0.1\n }\n\n if (cart.total > 100) {\n discount = discount + 0.05\n }\n\n return {\n originalTotal: cart.total,\n discount: discount,\n finalTotal: cart.total * (1 - discount),\n }\n}\n```\n\n### Smart Configuration\n\n```javascript\nfunction routeRequest({ request, config }) {\n for (let rule of config.rules) {\n if (request.path.startsWith(rule.prefix)) {\n return { backend: rule.backend, timeout: rule.timeout }\n }\n }\n return { backend: config.defaultBackend, timeout: 30000 }\n}\n```\n\n### Remote Jobs\n\n```javascript\nfunction processDataBatch({ items, transform }) {\n let results = []\n for (let item of items) {\n let processed = applyTransform(item, transform)\n results.push(processed)\n }\n return { processed: results.length, results }\n}\n```\n\n---\n\n## Custom Atoms\n\nExtend the runtime with your own operations:\n\n```typescript\nimport { defineAtom, AgentVM, s } from 'tjs-lang'\n\nconst myScraper = defineAtom(\n 'scrape', // OpCode\n s.object({ url: s.string }), // Input Schema\n s.string, // Output Schema\n async ({ url }, ctx) => {\n const res = await ctx.capabilities.fetch(url)\n return await res.text()\n },\n { cost: 5 } // Fuel cost\n)\n\nconst myVM = new AgentVM({ scrape: myScraper })\n```\n\nAtoms must:\n\n- Be non-blocking (no synchronous CPU-heavy work)\n- Respect `ctx.signal` for cancellation\n- Access I/O only via `ctx.capabilities`\n\n---\n\n## Builder API\n\nFor programmatic AST construction:\n\n```typescript\nimport { Agent, s } from 'tjs-lang'\n\nconst agent = Agent.take(s.object({ price: s.number, taxRate: s.number }))\n .varSet({ key: 'total', value: Agent.expr('price * (1 + taxRate)') })\n .return(s.object({ total: s.number }))\n\nconst ast = agent.toJSON() // JSON-serializable AST\n```\n\nThe builder is lower-level but gives full control over AST construction.\n\n---\n\n## Limitations\n\n### What AJS Doesn't Do\n\n- **No closures** - functions can't capture outer scope\n- **No classes** - use plain objects\n- **No async/await syntax** - the VM handles async internally\n- **No modules** - logic is self-contained\n- **No direct DOM access** - everything goes through capabilities\n- **No computed member access with variables** - `items[i]` is rejected; use `items[0]` (literal) or `for...of` loops\n\n### What AJS Intentionally Avoids\n\n- Complex language features that enable escape from the sandbox\n- Syntax that LLMs frequently hallucinate incorrectly\n- Patterns that make code hard to audit\n\n---\n\n## Performance\n\n- **100 agents in ~6ms** (torture test benchmark)\n- **~0.01 fuel per expression**\n- **Proportional memory charging** prevents runaway allocations\n\nAJS is interpreted (JSON AST), so it's slower than native JS. But:\n\n- Execution is predictable and bounded\n- I/O dominates most agent workloads\n- Tracing is free (built into the VM)\n\nFor compute-heavy operations in your platform code, use TJS with `wasm {}` blocks.\n\n---\n\n## Learn More\n\n- [TJS Documentation](DOCS-TJS.md) — The host language\n- [Builder's Manifesto](MANIFESTO-BUILDER.md) — Why AJS is fun\n- [Enterprise Guide](MANIFESTO-ENTERPRISE.md) — Why AJS is safe\n- [Technical Context](CONTEXT.md) — Architecture deep dive\n"
|
|
542
|
+
"text": "<!--{\"section\": \"ajs\", \"group\": \"docs\", \"order\": 0, \"navTitle\": \"Documentation\"}-->\n\n# AJS: The Agent Language\n\n_Code as Data. Safe. Async. Sandboxed._\n\n---\n\n## What is AJS?\n\nAJS (AsyncJS) is a JavaScript subset that compiles to a **JSON AST**. It's designed for untrusted code—user scripts, LLM-generated agents, remote logic.\n\n```javascript\nfunction searchAndSummarize({ query }) {\n let results = httpFetch({ url: `https://api.example.com/search?q=${query}` })\n let summary = llmPredict({ prompt: `Summarize: ${JSON.stringify(results)}` })\n return { query, summary }\n}\n```\n\nThis compiles to JSON that can be:\n\n- Stored in a database\n- Sent over the network\n- Executed in a sandboxed VM\n- Audited before running\n\n---\n\n## The VM\n\nAJS runs in a gas-limited, isolated VM with strict resource controls.\n\n```typescript\nimport { ajs, AgentVM } from 'tjs-lang'\n\nconst agent = ajs`\n function process({ url }) {\n let data = httpFetch({ url })\n return { fetched: data }\n }\n`\n\nconst vm = new AgentVM()\nconst result = await vm.run(\n agent,\n { url: 'https://api.example.com' },\n {\n fuel: 1000, // CPU budget\n timeoutMs: 5000, // Wall-clock limit\n }\n)\n```\n\n### Fuel Metering\n\nEvery operation costs fuel:\n\n| Operation | Cost |\n| ------------------------ | ---- |\n| Expression evaluation | 0.01 |\n| Variable set/get | 0.1 |\n| Control flow (if, while) | 0.5 |\n| HTTP fetch | 10 |\n| LLM predict | 100 |\n\nWhen fuel runs out, execution stops safely:\n\n```typescript\nif (result.fuelExhausted) {\n // Agent tried to run forever - stopped safely\n}\n```\n\n### Timeout Enforcement\n\nFuel protects against CPU abuse. Timeouts protect against I/O abuse:\n\n```typescript\nawait vm.run(agent, args, {\n fuel: 1000,\n timeoutMs: 5000, // Hard 5-second limit\n})\n```\n\nSlow network calls can't hang your servers.\n\n### Capability Injection\n\nThe VM starts with **zero capabilities**. You grant what each agent needs:\n\n```typescript\nconst capabilities = {\n fetch: createFetchCapability({\n allowedHosts: ['api.example.com'],\n }),\n store: createReadOnlyStore(),\n // No llm - this agent can't call AI\n}\n\nawait vm.run(agent, args, { capabilities })\n```\n\n---\n\n## Input/Output Contract\n\nAJS agents are composable — one agent's output feeds into another's input. To ensure this works reliably:\n\n- **Functions take a single destructured object parameter:** `function process({ input })`\n- **Functions must return a plain object:** `return { result }`, `return { summary, count }`\n- **Non-object returns produce an AgentError:** `return 42` or `return 'hello'` will fail\n- **Bare `return` is allowed** for void functions (no output)\n\n```javascript\n// CORRECT — object in, object out\nfunction add({ a, b }) {\n return { sum: a + b }\n}\n\n// WRONG — non-object returns are errors\nfunction add({ a, b }) {\n return a + b // AgentError: must return an object\n}\n```\n\n---\n\n## Syntax\n\nAJS is a JavaScript subset. Familiar syntax, restricted features.\n\n### What's Allowed\n\n```javascript\n// Functions\nfunction process({ input }) {\n return { output: input * 2 }\n}\n\n// Variables\nlet x = 10\nconst y = 'hello'\n\n// Conditionals\nif (x > 5) {\n return { size: 'big' }\n} else {\n return { size: 'small' }\n}\n\n// Loops\nfor (let i = 0; i < 10; i++) {\n total = total + i\n}\n\nfor (let item of items) {\n results.push(item.name)\n}\n\nwhile (count > 0) {\n count = count - 1\n}\n\n// Try/catch\ntry {\n riskyOperation()\n} catch (e) {\n return { error: e.message }\n}\n\n// Template literals\nlet message = `Hello, ${name}!`\n\n// Object/array literals\nlet obj = { a: 1, b: 2 }\nlet arr = [1, 2, 3]\n\n// Destructuring\nlet { name, age } = user\nlet [first, second] = items\n\n// Spread\nlet merged = { ...defaults, ...overrides }\nlet combined = [...arr1, ...arr2]\n\n// Ternary\nlet result = x > 0 ? 'positive' : 'non-positive'\n\n// Logical operators\nlet value = a && b\nlet fallback = a || defaultValue\nlet nullish = a ?? defaultValue\n```\n\n### What's Forbidden\n\n| Feature | Why Forbidden |\n| -------------------------- | ------------------------------------------------- |\n| `class` | Too complex for LLMs, enables prototype pollution |\n| `new` | Arbitrary object construction |\n| `this` | Implicit context, hard to sandbox |\n| Closures | State escapes the sandbox |\n| `async`/`await` | VM handles async internally |\n| `eval`, `Function` | Code injection |\n| `__proto__`, `constructor` | Prototype pollution |\n| `import`/`export` | Module system handled by host |\n\nAJS is intentionally simple—simple enough for 4B parameter LLMs to generate correctly.\n\n### Differences from JavaScript\n\nAJS expressions differ from standard JavaScript in a few important ways:\n\n**Null-safe member access.** All member access uses optional chaining internally. Accessing a property on `null` or `undefined` returns `undefined` instead of throwing `TypeError`:\n\n```javascript\nlet x = null\nlet y = x.foo.bar // undefined (no error)\n```\n\nThis is a deliberate safety choice — agents shouldn't crash on missing data.\n\n**No computed member access with variables.** You can use literal indices (`items[0]`, `obj[\"key\"]`) but not variable indices (`items[i]`). This is rejected at transpile time:\n\n```javascript\n// Works\nlet first = items[0]\nlet name = user['name']\n\n// Fails: \"Computed member access with variables not yet supported\"\nlet item = items[i]\n```\n\nWorkaround: use array atoms like `map`, `reduce`, or `for...of` loops instead of index-based access.\n\n**Structural equality.** `==` and `!=` perform deep structural comparison, not reference or coerced equality. No type coercion: `'1' == 1` is `false`. Use `===` and `!==` for identity (reference) checks:\n\n```javascript\n[1, 2] == [1, 2] // true (structural)\n[1, 2] === [1, 2] // false (different objects)\n{ a: 1 } == { a: 1 } // true (structural)\n'1' == 1 // false (no coercion, unlike JS)\nnull == undefined // true (nullish equality preserved)\n```\n\nObjects with a `.Equals` method or `[Symbol.for('tjs.equals')]` handler get custom comparison behavior.\n\n---\n\n## Atoms\n\nAtoms are the built-in operations. Each atom has a defined cost, input schema, and output schema.\n\n### Flow Control\n\n| Atom | Description |\n| -------- | ------------------------------ |\n| `seq` | Execute operations in sequence |\n| `if` | Conditional branching |\n| `while` | Loop with condition |\n| `return` | Return a value |\n| `try` | Error handling |\n\n### State Management\n\n| Atom | Description |\n| ------------ | -------------------------- |\n| `varSet` | Set a variable |\n| `varGet` | Get a variable |\n| `varsLet` | Batch variable declaration |\n| `varsImport` | Import from arguments |\n| `varsExport` | Export as result |\n| `scope` | Create a local scope |\n\n### I/O\n\n| Atom | Description |\n| ----------- | ------------------------------------------- |\n| `httpFetch` | HTTP requests (requires `fetch` capability) |\n\n### Storage (Core)\n\n| Atom | Description |\n| ------------- | ------------------------ |\n| `storeGet` | Get from key-value store |\n| `storeSet` | Set in key-value store |\n| `storeSearch` | Vector similarity search |\n\n### Storage (Battery)\n\n| Atom | Description |\n| ----------------------- | ------------------------------------- |\n| `storeVectorize` | Generate embeddings from text |\n| `storeCreateCollection` | Create a vector store collection |\n| `storeVectorAdd` | Add a document to a vector collection |\n\n### AI (Core)\n\n| Atom | Description |\n| ------------ | ------------------------------------------ |\n| `llmPredict` | Simple LLM inference (`prompt` → `string`) |\n| `agentRun` | Run a sub-agent |\n\n### AI (Battery)\n\n| Atom | Description |\n| ------------------- | ---------------------------------------------- |\n| `llmPredictBattery` | Chat completion (system/user → message object) |\n| `llmVision` | Analyze images using a vision-capable model |\n\n### Procedures\n\n| Atom | Description |\n| ------------------------ | ------------------------------ |\n| `storeProcedure` | Store an AST as callable token |\n| `releaseProcedure` | Delete a stored procedure |\n| `clearExpiredProcedures` | Clean up expired tokens |\n\n### Utilities\n\n| Atom | Description |\n| --------- | ------------------------ |\n| `random` | Random number generation |\n| `uuid` | Generate UUIDs |\n| `hash` | Compute hashes |\n| `memoize` | In-memory memoization |\n| `cache` | Persistent caching |\n\n---\n\n## Battery Atoms Reference\n\nBattery atoms provide LLM, embedding, and vector store capabilities. They\nrequire a separate import and capability setup.\n\n### Setup\n\n```javascript\nimport { AgentVM } from 'tjs-lang'\nimport { batteryAtoms, getBatteries } from 'tjs-lang'\n\nconst vm = new AgentVM(batteryAtoms)\nconst batteries = await getBatteries() // auto-detects LM Studio models\n\nconst { result } = await vm.run(agent, args, {\n fuel: 1000,\n capabilities: batteries,\n})\n```\n\nThe `getBatteries()` function auto-detects LM Studio and returns:\n\n```javascript\n{\n vector: { embed }, // embedding function (undefined if no LM Studio)\n store: { ... }, // key-value + vector store (always present)\n llmBattery: { predict, embed }, // LLM chat + embeddings (null if no LM Studio)\n models: { ... }, // detected model info (null if no LM Studio)\n}\n```\n\n**Important:** `vector` and `llmBattery` will be `undefined`/`null` if LM Studio\nisn't running or the connection is made over HTTPS (local LLM calls are blocked\nfrom HTTPS contexts for security). Always check for availability or handle\nthe atom's \"missing capability\" error.\n\n### Capability Keys\n\nBattery atoms look up capabilities by specific keys that differ from the base\n`Capabilities` interface:\n\n| Capability key | Used by atoms | Contains |\n| -------------- | -------------------------------------------------------- | ------------------------------- |\n| `llmBattery` | `llmPredictBattery`, `llmVision` | `{ predict, embed }` (full LLM) |\n| `vector` | `storeVectorize` | `{ embed }` only |\n| `store` | `storeSearch`, `storeCreateCollection`, `storeVectorAdd` | KV + vector store operations |\n| `llm` | `llmPredict` (core atom) | `{ predict }` (simple) |\n| `fetch` | `httpFetch` (core atom) | fetch function |\n\nThe split exists because `storeVectorize` only needs the embedding function,\nwhile `llmPredictBattery` needs the full chat API. If you're providing your own\ncapabilities (not using `getBatteries()`), wire the keys accordingly.\n\n### `llmPredict` vs `llmPredictBattery`\n\nThere are two LLM atoms with different interfaces:\n\n| Atom | Input | Output | Capability |\n| ------------------- | ----------------------- | -------------- | ------------------------- |\n| `llmPredict` | `{ prompt }` | `string` | `capabilities.llm` |\n| `llmPredictBattery` | `{ system, user, ... }` | message object | `capabilities.llmBattery` |\n\nUse `llmPredict` for simple prompts. Use `llmPredictBattery` when you need\nsystem prompts, tool calling, or structured output.\n\n### `llmPredictBattery`\n\nChat completion with system prompt, tool calling, and structured output support.\n\n**Input:**\n\n| Field | Type | Required | Description |\n| ---------------- | -------- | -------- | --------------------------------------------- |\n| `system` | `string` | No | System prompt (defaults to helpful assistant) |\n| `user` | `string` | Yes | User message |\n| `tools` | `any[]` | No | Tool definitions (OpenAI format) |\n| `responseFormat` | `any` | No | Structured output format |\n\n**Output:** OpenAI chat message object:\n\n```javascript\n{\n role: 'assistant',\n content: 'The answer is 42.', // null when using tool calls\n tool_calls: [...] // present when tools are invoked\n}\n```\n\n**Example:**\n\n```javascript\nlet response = llmPredictBattery({\n system: 'You are a helpful assistant.',\n user: 'What is the capital of France?',\n})\n// response.content === 'Paris is the capital of France.'\n```\n\n**Cost:** 100 fuel\n\n### `llmVision`\n\nAnalyze images using a vision-capable model.\n\n**Input:**\n\n| Field | Type | Required | Description |\n| ---------------- | ---------- | -------- | ----------------------------------------------- |\n| `system` | `string` | No | System prompt |\n| `prompt` | `string` | Yes | Text prompt describing what to analyze |\n| `images` | `string[]` | Yes | URLs or data URIs (`data:image/...;base64,...`) |\n| `responseFormat` | `any` | No | Structured output format |\n\n**Output:** Same as `llmPredictBattery` (message object with `role`, `content`, `tool_calls`).\n\n**Example:**\n\n```javascript\nlet analysis = llmVision({\n prompt: 'Describe what you see in this image.',\n images: ['https://example.com/photo.jpg'],\n})\n// analysis.content === 'The image shows a sunset over the ocean...'\n```\n\n**Cost:** 150 fuel | **Timeout:** 120 seconds\n\n### `storeVectorize`\n\nGenerate embeddings from text using the vector battery.\n\n**Input:**\n\n| Field | Type | Required | Description |\n| ------- | -------- | -------- | ---------------------- |\n| `text` | `string` | Yes | Text to embed |\n| `model` | `string` | No | Embedding model to use |\n\n**Output:** `number[]` — the embedding vector.\n\n**Example:**\n\n```javascript\nlet embedding = storeVectorize({ text: 'TJS is a typed JavaScript' })\n// embedding === [0.023, -0.412, 0.891, ...]\n```\n\n**Cost:** 20 fuel | **Capability:** `vector`\n\n### `storeCreateCollection`\n\nCreate a vector store collection for similarity search.\n\n**Input:**\n\n| Field | Type | Required | Description |\n| ------------ | -------- | -------- | -------------------------------- |\n| `collection` | `string` | Yes | Collection name |\n| `dimension` | `number` | No | Vector dimension (auto-detected) |\n\n**Output:** None.\n\n**Cost:** 5 fuel | **Capability:** `store`\n\n### `storeVectorAdd`\n\nAdd a document to a vector store collection. The document is automatically\nembedded and indexed.\n\n**Input:**\n\n| Field | Type | Required | Description |\n| ------------ | -------- | -------- | ----------------- |\n| `collection` | `string` | Yes | Collection name |\n| `doc` | `any` | Yes | Document to store |\n\n**Output:** None.\n\n**Example:**\n\n```javascript\nstoreVectorAdd({\n collection: 'articles',\n doc: { title: 'Intro to TJS', content: 'TJS is...', embedding: [...] }\n})\n```\n\n**Cost:** 5 fuel | **Capability:** `store`\n\n### `storeSearch`\n\nSearch a vector store collection by similarity.\n\n**Input:**\n\n| Field | Type | Required | Description |\n| ------------- | ---------- | -------- | ------------------------------ |\n| `collection` | `string` | Yes | Collection name |\n| `queryVector` | `number[]` | Yes | Query embedding vector |\n| `k` | `number` | No | Number of results (default: 5) |\n| `filter` | `object` | No | Metadata filter |\n\n**Output:** `any[]` — array of matching documents, sorted by similarity.\n\n**Example:**\n\n```javascript\nlet query = storeVectorize({ text: 'How does type checking work?' })\nlet results = storeSearch({\n collection: 'articles',\n queryVector: query,\n k: 3,\n})\n// results === [{ title: 'Type System', content: '...' }, ...]\n```\n\n**Cost:** 5 + k fuel (dynamic) | **Capability:** `store`\n\n---\n\n## Expression Builtins\n\nAJS expressions have access to safe built-in objects:\n\n### Math\n\nAll standard math functions:\n\n```javascript\nMath.abs(-5) // 5\nMath.floor(3.7) // 3\nMath.sqrt(16) // 4\nMath.sin(Math.PI) // ~0\nMath.random() // 0-1\nMath.max(1, 2, 3) // 3\nMath.min(1, 2, 3) // 1\n```\n\n### JSON\n\nParse and stringify:\n\n```javascript\nJSON.parse('{\"a\": 1}') // { a: 1 }\nJSON.stringify({ a: 1 }) // '{\"a\": 1}'\n```\n\n### Array\n\nStatic methods:\n\n```javascript\nArray.isArray([1, 2]) // true\nArray.from('abc') // ['a', 'b', 'c']\nArray.of(1, 2, 3) // [1, 2, 3]\n```\n\n### Object\n\nStatic methods:\n\n```javascript\nObject.keys({ a: 1 }) // ['a']\nObject.values({ a: 1 }) // [1]\nObject.entries({ a: 1 }) // [['a', 1]]\nObject.fromEntries([['a', 1]]) // { a: 1 }\nObject.assign({}, a, b) // merged object\n```\n\n### String\n\nStatic methods:\n\n```javascript\nString.fromCharCode(65) // 'A'\nString.fromCodePoint(128512) // emoji\n```\n\n### Number\n\nConstants and checks:\n\n```javascript\nNumber.MAX_VALUE\nNumber.isNaN(NaN) // true\nNumber.isFinite(100) // true\nNumber.parseInt('42') // 42\nNumber.parseFloat('3.14') // 3.14\n```\n\n### Set Operations\n\nSet-like operations:\n\n```javascript\nSet.add([1, 2], 3) // [1, 2, 3]\nSet.remove([1, 2, 3], 2) // [1, 3]\nSet.union([1, 2], [2, 3]) // [1, 2, 3]\nSet.intersection([1, 2], [2, 3]) // [2]\nSet.diff([1, 2, 3], [2]) // [1, 3]\n```\n\n### Date\n\nDate factory with arithmetic:\n\n```javascript\nDate.now() // timestamp\nDate.create('2024-01-15') // Date object\nDate.add(date, 1, 'day') // new Date\nDate.format(date, 'YYYY-MM-DD')\n```\n\n### Schema\n\nBuild JSON schemas for structured LLM outputs:\n\n```javascript\n// From example\nlet schema = Schema.response('person', { name: '', age: 0 })\n\n// With constraints\nlet schema = Schema.response(\n 'user',\n Schema.object({\n email: Schema.string.email,\n age: Schema.number.int.min(0).max(150).optional,\n role: Schema.enum(['admin', 'user', 'guest']),\n })\n)\n```\n\n---\n\n## JSON AST Format\n\nAJS compiles to a JSON AST. Here's what it looks like:\n\n### Sequence\n\n```json\n{\n \"$seq\": [\n { \"$op\": \"varSet\", \"key\": \"x\", \"value\": 10 },\n { \"$op\": \"varSet\", \"key\": \"y\", \"value\": 20 },\n {\n \"$op\": \"return\",\n \"value\": { \"$expr\": \"binary\", \"op\": \"+\", \"left\": \"x\", \"right\": \"y\" }\n }\n ]\n}\n```\n\n### Expressions\n\n```json\n// Literal\n{ \"$expr\": \"literal\", \"value\": 42 }\n\n// Identifier\n{ \"$expr\": \"ident\", \"name\": \"varName\" }\n\n// Binary operation\n{ \"$expr\": \"binary\", \"op\": \"+\", \"left\": {...}, \"right\": {...} }\n\n// Member access\n{ \"$expr\": \"member\", \"object\": {...}, \"property\": \"foo\" }\n\n// Template literal\n{ \"$expr\": \"template\", \"tmpl\": \"Hello, ${name}!\" }\n```\n\n### Conditionals\n\n```json\n{\n \"$op\": \"if\",\n \"cond\": { \"$expr\": \"binary\", \"op\": \">\", \"left\": \"x\", \"right\": 0 },\n \"then\": { \"$seq\": [...] },\n \"else\": { \"$seq\": [...] }\n}\n```\n\n### Loops\n\n```json\n{\n \"$op\": \"while\",\n \"cond\": { \"$expr\": \"binary\", \"op\": \">\", \"left\": \"count\", \"right\": 0 },\n \"body\": { \"$seq\": [...] }\n}\n```\n\n---\n\n## Security Model\n\n### Zero Capabilities by Default\n\nThe VM can't do anything unless you allow it:\n\n```typescript\n// This agent can only compute - no I/O\nawait vm.run(agent, args, { capabilities: {} })\n\n// This agent can fetch from one domain\nawait vm.run(agent, args, {\n capabilities: {\n fetch: createFetchCapability({ allowedHosts: ['api.example.com'] }),\n },\n})\n```\n\n### Forbidden Properties\n\nThese property names are blocked to prevent prototype pollution:\n\n- `__proto__`\n- `constructor`\n- `prototype`\n\n### SSRF Protection\n\nThe `httpFetch` atom can be configured with:\n\n- Allowlisted hosts only\n- Blocked private IP ranges\n- Request signing requirements\n\n### ReDoS Protection\n\nSuspicious regex patterns are rejected before execution.\n\n### Execution Tracing\n\nEvery agent run can produce an audit trail:\n\n```typescript\nconst { result, trace } = await vm.run(agent, args, { trace: true })\n\n// trace: [\n// { op: 'varSet', key: 'x', fuelBefore: 1000, fuelAfter: 999.9 },\n// { op: 'httpFetch', url: '...', fuelBefore: 999.9, fuelAfter: 989.9 },\n// ...\n// ]\n```\n\n---\n\n## Use Cases\n\n### AI Agents\n\n```javascript\nfunction researchAgent({ topic }) {\n let searchResults = httpFetch({\n url: `https://api.search.com?q=${topic}`,\n })\n\n let summary = llmPredict({\n system: 'You are a research assistant.',\n user: `Summarize these results about ${topic}: ${searchResults}`,\n })\n\n return { topic, summary }\n}\n```\n\n### Rule Engines\n\n```javascript\nfunction applyDiscounts({ cart, userTier }) {\n let discount = 0\n\n if (userTier === 'gold') {\n discount = 0.2\n } else if (userTier === 'silver') {\n discount = 0.1\n }\n\n if (cart.total > 100) {\n discount = discount + 0.05\n }\n\n return {\n originalTotal: cart.total,\n discount: discount,\n finalTotal: cart.total * (1 - discount),\n }\n}\n```\n\n### Smart Configuration\n\n```javascript\nfunction routeRequest({ request, config }) {\n for (let rule of config.rules) {\n if (request.path.startsWith(rule.prefix)) {\n return { backend: rule.backend, timeout: rule.timeout }\n }\n }\n return { backend: config.defaultBackend, timeout: 30000 }\n}\n```\n\n### Remote Jobs\n\n```javascript\nfunction processDataBatch({ items, transform }) {\n let results = []\n for (let item of items) {\n let processed = applyTransform(item, transform)\n results.push(processed)\n }\n return { processed: results.length, results }\n}\n```\n\n---\n\n## Custom Atoms\n\nExtend the runtime with your own operations:\n\n```typescript\nimport { defineAtom, AgentVM, s } from 'tjs-lang'\n\nconst myScraper = defineAtom(\n 'scrape', // OpCode\n s.object({ url: s.string }), // Input Schema\n s.string, // Output Schema\n async ({ url }, ctx) => {\n const res = await ctx.capabilities.fetch(url)\n return await res.text()\n },\n { cost: 5 } // Fuel cost\n)\n\nconst myVM = new AgentVM({ scrape: myScraper })\n```\n\nAtoms must:\n\n- Be non-blocking (no synchronous CPU-heavy work)\n- Respect `ctx.signal` for cancellation\n- Access I/O only via `ctx.capabilities`\n\n---\n\n## Builder API\n\nFor programmatic AST construction:\n\n```typescript\nimport { Agent, s } from 'tjs-lang'\n\nconst agent = Agent.take(s.object({ price: s.number, taxRate: s.number }))\n .varSet({ key: 'total', value: Agent.expr('price * (1 + taxRate)') })\n .return(s.object({ total: s.number }))\n\nconst ast = agent.toJSON() // JSON-serializable AST\n```\n\nThe builder is lower-level but gives full control over AST construction.\n\n---\n\n## Limitations\n\n### What AJS Doesn't Do\n\n- **No closures** - functions can't capture outer scope\n- **No classes** - use plain objects\n- **No async/await syntax** - the VM handles async internally\n- **No modules** - logic is self-contained\n- **No direct DOM access** - everything goes through capabilities\n- **No computed member access with variables** - `items[i]` is rejected; use `items[0]` (literal) or `for...of` loops\n\n### What AJS Intentionally Avoids\n\n- Complex language features that enable escape from the sandbox\n- Syntax that LLMs frequently hallucinate incorrectly\n- Patterns that make code hard to audit\n\n---\n\n## Performance\n\n- **100 agents in ~6ms** (torture test benchmark)\n- **~0.01 fuel per expression**\n- **Proportional memory charging** prevents runaway allocations\n\nAJS is interpreted (JSON AST), so it's slower than native JS. But:\n\n- Execution is predictable and bounded\n- I/O dominates most agent workloads\n- Tracing is free (built into the VM)\n\nFor compute-heavy operations in your platform code, use TJS with `wasm {}` blocks.\n\n---\n\n## Learn More\n\n- [TJS Documentation](DOCS-TJS.md) — The host language\n- [Builder's Manifesto](MANIFESTO-BUILDER.md) — Why AJS is fun\n- [Enterprise Guide](MANIFESTO-ENTERPRISE.md) — Why AJS is safe\n- [Technical Context](CONTEXT.md) — Architecture deep dive\n"
|
|
543
543
|
},
|
|
544
544
|
{
|
|
545
545
|
"title": "tjs-lang Technical Context",
|
|
@@ -631,7 +631,7 @@
|
|
|
631
631
|
"title": "AJS LLM System Prompt",
|
|
632
632
|
"filename": "ajs-llm-prompt.md",
|
|
633
633
|
"path": "guides/ajs-llm-prompt.md",
|
|
634
|
-
"text": "# AJS LLM System Prompt\n\n> **Maintenance Note:** This prompt must be updated when [ajs.md](./ajs.md) changes.\n> Key areas to sync: type syntax, built-ins (Set/Date), control flow, and forbidden constructs.\n\nUse this system prompt when asking an LLM to generate AJS code.\n\n---\n\n## System Prompt\n\n````\nYou are an expert code generator for **AJS**, a specialized subset of JavaScript for AI Agents.\nAJS looks like JavaScript but has strict differences. You must adhere to these rules:\n\n### 1. SYNTAX & TYPES\n- **Types by Example:** Do NOT use TypeScript types. Use \"Example Types\" where the value implies the type.\n - WRONG: `function search(query: string, limit?: number)`\n - RIGHT: `function search(query: 'search term', limit = 10)`\n - `name: 'value'` means REQUIRED string. `count: 5` means REQUIRED number. `name = 'value'` means OPTIONAL.\n- **Number Parameters:** Use ACTUAL NUMBER LITERALS, never strings or type names.\n - WRONG: `function add(a: 'number', b: 'number')` - 'number' is a STRING!\n - WRONG: `function add(a: '5', b: '10')` - these are STRINGS in quotes!\n - RIGHT: `function add(a: 0, b: 0)` - bare numbers, no quotes\n - RIGHT: `function factorial(n: 5)` - bare number literal\n- **No Return Type Annotations:** Do NOT add return types after the parameter list.\n - WRONG: `function foo(x: 0): number { ... }`\n - WRONG: `function foo(x: 0) -> number { ... }`\n - RIGHT: `function foo(x: 0) { ... }`\n- **No Classes:** Do NOT use `class`, `new`, `this`, or `prototype`.\n- **No Async/Await:** Do NOT use `async` or `await`. All functions are implicitly asynchronous.\n - WRONG: `let x = await fetch(...)`\n - RIGHT: `let x = httpFetch({ url: '...' })`\n\n###
|
|
634
|
+
"text": "# AJS LLM System Prompt\n\n> **Maintenance Note:** This prompt must be updated when [ajs.md](./ajs.md) changes.\n> Key areas to sync: type syntax, built-ins (Set/Date), control flow, and forbidden constructs.\n\nUse this system prompt when asking an LLM to generate AJS code.\n\n---\n\n## System Prompt\n\n````\nYou are an expert code generator for **AJS**, a specialized subset of JavaScript for AI Agents.\nAJS looks like JavaScript but has strict differences. You must adhere to these rules:\n\n### 1. INPUT/OUTPUT CONTRACT\n- Functions take a SINGLE **destructured object** parameter: `function foo({ a, b })`\n - WRONG: `function foo(a, b)` — positional params not supported\n - RIGHT: `function foo({ a, b })` — destructured object\n- Functions MUST return a **plain object**: `return { result }`, `return { summary, count }`\n - WRONG: `return 42`, `return 'hello'`, `return [1, 2]`\n - RIGHT: `return { result: 42 }`, `return { message: 'hello' }`, `return { items: [1, 2] }`\n - Bare `return` (no value) is allowed for void functions\n- Non-object returns produce a runtime error (AgentError)\n\n### 2. SYNTAX & TYPES\n- **Types by Example:** Do NOT use TypeScript types. Use \"Example Types\" where the value implies the type.\n - WRONG: `function search(query: string, limit?: number)`\n - RIGHT: `function search(query: 'search term', limit = 10)`\n - `name: 'value'` means REQUIRED string. `count: 5` means REQUIRED number. `name = 'value'` means OPTIONAL.\n- **Number Parameters:** Use ACTUAL NUMBER LITERALS, never strings or type names.\n - WRONG: `function add(a: 'number', b: 'number')` - 'number' is a STRING!\n - WRONG: `function add(a: '5', b: '10')` - these are STRINGS in quotes!\n - RIGHT: `function add(a: 0, b: 0)` - bare numbers, no quotes\n - RIGHT: `function factorial(n: 5)` - bare number literal\n- **No Return Type Annotations:** Do NOT add return types after the parameter list.\n - WRONG: `function foo(x: 0): number { ... }`\n - WRONG: `function foo(x: 0) -> number { ... }`\n - RIGHT: `function foo(x: 0) { ... }`\n- **No Classes:** Do NOT use `class`, `new`, `this`, or `prototype`.\n- **No Async/Await:** Do NOT use `async` or `await`. All functions are implicitly asynchronous.\n - WRONG: `let x = await fetch(...)`\n - RIGHT: `let x = httpFetch({ url: '...' })`\n\n### 3. BUILT-INS & FACTORIES\n- **No `new` Keyword:** Never use `new`. Use factory functions.\n - WRONG: `new Date()`, `new Set()`, `new Array()`\n - RIGHT: `Date()`, `Set([1,2])`, `['a','b']`\n- **Date Objects:** `Date()` returns an **immutable** object.\n - Months are 1-indexed (1=Jan, not 0=Jan).\n - Methods like `.add({ days: 5 })` return a NEW Date object.\n - Access components: `.year`, `.month`, `.day`, `.hours`, `.minutes`, `.seconds`\n - Format: `.format('date')`, `.format('iso')`, `.format('YYYY-MM-DD')`\n- **Set Objects:** `Set([items])` returns an object with:\n - Mutable: `.add(x)`, `.remove(x)`, `.clear()`\n - Immutable algebra: `.union(other)`, `.intersection(other)`, `.diff(other)` - return NEW Sets\n - Query: `.has(x)`, `.size`, `.toArray()`\n- **Optional Chaining:** Use `?.` for safe property access: `obj?.nested?.value`\n- **Schema Filtering:** `filter(data, schema)` strips extra properties:\n - `filter({ a: 1, b: 2, extra: 3 }, { a: 0, b: 0 })` returns `{ a: 1, b: 2 }`\n - Useful for sanitizing LLM outputs or API responses\n\n### 4. ATOMS VS. BUILT-INS\n- **Atoms (External Tools):** ALWAYS accept a single object argument.\n - Pattern: `atomName({ param: value })`\n - Examples: `search({ query: topic })`, `llmPredict({ system: '...', user: '...' })`\n - **template atom:** `template({ tmpl: 'Hello, {{name}}!', vars: { name } })` - for string interpolation\n - IMPORTANT: Use SINGLE QUOTES for tmpl, NOT backticks! Backticks cause parse errors.\n - WRONG: `template({ tmpl: \\`{{name}}\\`, vars: { name } })`\n - RIGHT: `template({ tmpl: '{{name}}', vars: { name } })`\n- **Built-ins (Math, JSON, String, Array):** Use standard JS syntax.\n - `Math.max(1, 2)`, `JSON.parse(str)`, `str.split(',')`, `arr.map(x => x * 2)`\n\n### 5. ERROR HANDLING\n- Errors propagate automatically (Monadic flow). If one step fails, subsequent steps are skipped.\n- Only use `try/catch` if you need to recover from a failure and continue.\n\n### 6. FORBIDDEN CONSTRUCTS\nThese will cause transpile errors:\n- `async`, `await` - not needed, all calls are implicitly async\n- `new` - use factory functions instead\n- `class`, `this` - use plain functions and objects\n- `var` - use `let` instead\n- `import`, `require` - atoms must be registered with the VM\n- `console.log` - use trace capabilities if needed\n\n### EXAMPLES\n\n**Example 1: Search Agent**\n```javascript\nfunction researchAgent(topic: 'quantum computing') {\n let searchResults = search({ query: topic, limit: 5 })\n if (searchResults?.length == 0) {\n return { error: 'No results found' }\n }\n let summary = summarize({ text: JSON.stringify(searchResults), length: 'short' })\n return { summary }\n}\n```\n\n**Example 2: Factorial with while loop (number parameter)**\n```javascript\nfunction factorial(n: 5) {\n let result = 1\n let i = n\n while (i > 1) {\n result = result * i\n i = i - 1\n }\n return { result }\n}\n```\n\n**Example 3: Math with multiple number parameters**\n```javascript\nfunction calculateVolume(width: 2, height: 3, depth: 4) {\n let volume = width * height * depth\n return { volume }\n}\n```\nNote: width, height, depth are BARE NUMBERS (2, 3, 4), NOT strings like '2' or 'number'!\n\n**Example 4: Greeting with template atom**\n```javascript\nfunction greet(name: 'World', greeting = 'Hello') {\n let message = template({ tmpl: '{{greeting}}, {{name}}!', vars: { greeting, name } })\n return { message }\n}\n```\n````\n\n```\n\n---\n\n## Self-Correction Loop\n\nWhen testing with local LLMs, implement error feedback:\n\n1. Run the LLM with this prompt\n2. If output contains `async`, `await`, `new`, `class`, or `this`, feed back:\n > \"Error: You used '[keyword]'. AJS forbids '[keyword]'. [Alternative].\"\n3. The model typically fixes it on the second attempt\n\nExample corrections:\n- `new Date()` → \"Use `Date()` factory function instead\"\n- `await fetch()` → \"Remove `await`, use `httpFetch({ url })` - all calls are implicitly async\"\n- `class Agent` → \"Use plain functions, AJS is purely functional\"\n\n---\n\n## Compact Version (for context-limited models)\n\n```\n\nYou generate AJS code. Rules:\n\n1. Functions take a destructured object param and MUST return objects: `function foo({ a, b }) { return { result: a + b } }`\n - WRONG: `return 42` or `return 'hello'` — non-object returns are errors\n2. Types by example: `fn({ name: 'string', count: 10 })` - string in quotes, numbers BARE (no quotes!)\n - WRONG: `fn({ x: 'number' })` or `fn({ x: '5' })` - these are STRINGS\n - RIGHT: `fn({ x: 0 })` or `fn({ x: 5 })` - bare number literals\n3. NO: async/await, new, class, this, var, import, return type annotations\n4. Atoms use object args: `search({ query: x })`. Built-ins normal: `Math.max(1,2)`\n5. Factories: `Date()`, `Set([1,2])` - no `new` keyword\n6. Date is immutable, months 1-12. Set has .add/.remove (mutable) and .union/.diff (immutable)\n7. Use `?.` for optional chaining: `obj?.prop?.value`\n8. Use `filter(data, schema)` to strip extra properties from objects\n\n```\n\n```\n"
|
|
635
635
|
},
|
|
636
636
|
{
|
|
637
637
|
"title": "AJS Patterns",
|
package/demo/src/demo-nav.ts
CHANGED
|
@@ -90,7 +90,7 @@ export class DemoNav extends Component {
|
|
|
90
90
|
private mdViewer: MarkdownViewer | null = null
|
|
91
91
|
|
|
92
92
|
// Track current selection for highlighting
|
|
93
|
-
private _currentView: 'home' | 'ajs' | 'tjs' = 'home'
|
|
93
|
+
private _currentView: 'home' | 'ajs' | 'tjs' | 'ts' = 'home'
|
|
94
94
|
private _currentExample: string | null = null
|
|
95
95
|
|
|
96
96
|
// Computed example arrays from docs
|
|
@@ -113,13 +113,15 @@ export class DemoNav extends Component {
|
|
|
113
113
|
return this._currentView
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
set currentView(value: 'home' | 'ajs' | 'tjs') {
|
|
116
|
+
set currentView(value: 'home' | 'ajs' | 'tjs' | 'ts') {
|
|
117
117
|
this._currentView = value
|
|
118
118
|
// Auto-open the appropriate section
|
|
119
119
|
if (value === 'ajs') {
|
|
120
120
|
this.openSection = 'ajs-demos'
|
|
121
121
|
} else if (value === 'tjs') {
|
|
122
122
|
this.openSection = 'tjs-demos'
|
|
123
|
+
} else if (value === 'ts') {
|
|
124
|
+
this.openSection = 'ts-demos'
|
|
123
125
|
}
|
|
124
126
|
this.rebuildNav()
|
|
125
127
|
// Update indicator after rebuild (DOM now exists)
|
|
@@ -164,11 +166,16 @@ export class DemoNav extends Component {
|
|
|
164
166
|
} else if (view === 'tjs') {
|
|
165
167
|
this._currentView = 'tjs'
|
|
166
168
|
this.openSection = 'tjs-demos'
|
|
169
|
+
} else if (view === 'ts') {
|
|
170
|
+
this._currentView = 'ts'
|
|
171
|
+
this.openSection = 'ts-demos'
|
|
167
172
|
} else if (view === 'home') {
|
|
168
173
|
this._currentView = 'home'
|
|
169
174
|
} else if (
|
|
170
175
|
section &&
|
|
171
|
-
['ajs-demos', 'tjs-demos', 'ajs-docs', 'tjs-docs'].includes(
|
|
176
|
+
['ajs-demos', 'tjs-demos', 'ts-demos', 'ajs-docs', 'tjs-docs'].includes(
|
|
177
|
+
section
|
|
178
|
+
)
|
|
172
179
|
) {
|
|
173
180
|
this.openSection = section
|
|
174
181
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tjs-lang",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
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",
|
package/src/lang/codegen.test.ts
CHANGED
|
@@ -47,7 +47,15 @@ describe('TS → TJS conversion quality', () => {
|
|
|
47
47
|
const ts = `function toggle(flag: boolean): boolean { return !flag }`
|
|
48
48
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
49
49
|
|
|
50
|
-
expect(code).toContain('flag:
|
|
50
|
+
expect(code).toContain('flag: false')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('converts optional boolean param to = false', () => {
|
|
54
|
+
const ts = `function greet(name: string, excited?: boolean): string { return excited ? name + '!' : name }`
|
|
55
|
+
const { code } = fromTS(ts, { emitTJS: true })
|
|
56
|
+
|
|
57
|
+
expect(code).toContain('excited = false')
|
|
58
|
+
expect(code).not.toContain('excited = true')
|
|
51
59
|
})
|
|
52
60
|
|
|
53
61
|
it('converts array param correctly', () => {
|
|
@@ -101,7 +109,7 @@ describe('TS → TJS conversion quality', () => {
|
|
|
101
109
|
const ts = `function isValid(): boolean { return true }`
|
|
102
110
|
const { code } = fromTS(ts, { emitTJS: true })
|
|
103
111
|
|
|
104
|
-
expect(code).toContain('-!
|
|
112
|
+
expect(code).toContain('-! false')
|
|
105
113
|
})
|
|
106
114
|
|
|
107
115
|
it('converts object return type to -! syntax', () => {
|
|
@@ -139,7 +139,12 @@ function typeToExample(
|
|
|
139
139
|
case ts.SyntaxKind.NumberKeyword:
|
|
140
140
|
return '0.0'
|
|
141
141
|
case ts.SyntaxKind.BooleanKeyword:
|
|
142
|
-
|
|
142
|
+
// REVISIT: TS `x?: boolean` becomes TJS `x = false`, which collapses
|
|
143
|
+
// "not passed" (undefined) and "passed as false" into the same value.
|
|
144
|
+
// Code that distinguishes the three states (true/false/undefined) will
|
|
145
|
+
// break. Consider emitting `x: false || null` for optional booleans
|
|
146
|
+
// to preserve the undefined state.
|
|
147
|
+
return 'false'
|
|
143
148
|
case ts.SyntaxKind.NullKeyword:
|
|
144
149
|
return 'null'
|
|
145
150
|
case ts.SyntaxKind.UndefinedKeyword:
|
package/src/lang/eval.ts
CHANGED
|
@@ -14,6 +14,27 @@ import { transpile } from './core'
|
|
|
14
14
|
let _vm: AgentVM<Record<string, never>> | null = null
|
|
15
15
|
const getVM = () => (_vm ??= new AgentVM())
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Walk an AST and wrap return values in { __result: value } objects.
|
|
19
|
+
* This lets Eval/SafeFunction return arbitrary values through the VM,
|
|
20
|
+
* which enforces strict object returns for agent composability.
|
|
21
|
+
*/
|
|
22
|
+
function wrapReturnValues(node: any): void {
|
|
23
|
+
if (!node || typeof node !== 'object') return
|
|
24
|
+
if (Array.isArray(node)) {
|
|
25
|
+
for (const child of node) wrapReturnValues(child)
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
if (node.op === 'return' && 'value' in node) {
|
|
29
|
+
node.value = { __result: node.value }
|
|
30
|
+
}
|
|
31
|
+
// Recurse into steps (seq), branches (if/else), etc.
|
|
32
|
+
if (node.steps) wrapReturnValues(node.steps)
|
|
33
|
+
if (node.then) wrapReturnValues(node.then)
|
|
34
|
+
if (node.else) wrapReturnValues(node.else)
|
|
35
|
+
if (node.body) wrapReturnValues(node.body)
|
|
36
|
+
}
|
|
37
|
+
|
|
17
38
|
/** Capabilities that can be injected into SafeFunction/Eval */
|
|
18
39
|
export interface SafeCapabilities {
|
|
19
40
|
/** Fetch function for HTTP requests */
|
|
@@ -65,14 +86,24 @@ export async function Eval(options: EvalOptions): Promise<{
|
|
|
65
86
|
try {
|
|
66
87
|
const { ast } = transpile(wrappedCode)
|
|
67
88
|
|
|
89
|
+
// Box return values in objects for VM strict-return compliance.
|
|
90
|
+
// Walk AST and wrap each { op: 'return', value } into
|
|
91
|
+
// { op: 'return', value: { __result: originalValue } }
|
|
92
|
+
wrapReturnValues(ast)
|
|
93
|
+
|
|
68
94
|
const vmResult = await vm.run(ast, context, {
|
|
69
95
|
fuel,
|
|
70
96
|
timeoutMs,
|
|
71
97
|
capabilities,
|
|
72
98
|
})
|
|
73
99
|
|
|
100
|
+
// Unwrap the boxed result
|
|
101
|
+
const raw = vmResult.result
|
|
102
|
+
const result =
|
|
103
|
+
raw && typeof raw === 'object' && '__result' in raw ? raw.__result : raw
|
|
104
|
+
|
|
74
105
|
return {
|
|
75
|
-
result
|
|
106
|
+
result,
|
|
76
107
|
fuelUsed: vmResult.fuelUsed,
|
|
77
108
|
error: vmResult.error
|
|
78
109
|
? { message: vmResult.error.message || String(vmResult.error) }
|
|
@@ -128,6 +159,9 @@ export async function SafeFunction(options: SafeFunctionOptions): Promise<
|
|
|
128
159
|
// Pre-compile the AST (done once at creation time)
|
|
129
160
|
const { ast } = transpile(source)
|
|
130
161
|
|
|
162
|
+
// Box return values for VM strict-return compliance
|
|
163
|
+
wrapReturnValues(ast)
|
|
164
|
+
|
|
131
165
|
// Return a function that runs the pre-compiled AST
|
|
132
166
|
return async (...args: unknown[]) => {
|
|
133
167
|
const context: Record<string, unknown> = {}
|
|
@@ -142,8 +176,13 @@ export async function SafeFunction(options: SafeFunctionOptions): Promise<
|
|
|
142
176
|
capabilities,
|
|
143
177
|
})
|
|
144
178
|
|
|
179
|
+
// Unwrap the boxed result
|
|
180
|
+
const raw = vmResult.result
|
|
181
|
+
const result =
|
|
182
|
+
raw && typeof raw === 'object' && '__result' in raw ? raw.__result : raw
|
|
183
|
+
|
|
145
184
|
return {
|
|
146
|
-
result
|
|
185
|
+
result,
|
|
147
186
|
fuelUsed: vmResult.fuelUsed,
|
|
148
187
|
error: vmResult.error
|
|
149
188
|
? { message: vmResult.error.message || String(vmResult.error) }
|
package/src/runtime.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'bun:test'
|
|
2
|
-
import { defineAtom } from './runtime'
|
|
2
|
+
import { defineAtom, isAgentError } from './runtime'
|
|
3
3
|
import { AgentVM } from './vm'
|
|
4
4
|
import { s } from 'tosijs-schema'
|
|
5
5
|
import { ajs } from './transpiler'
|
|
@@ -611,11 +611,17 @@ describe('Edge Cases', () => {
|
|
|
611
611
|
right: { $expr: 'literal', value: [1, 2, 3] },
|
|
612
612
|
},
|
|
613
613
|
},
|
|
614
|
-
{
|
|
614
|
+
{
|
|
615
|
+
op: 'return',
|
|
616
|
+
schema: {
|
|
617
|
+
type: 'object',
|
|
618
|
+
properties: { res: { type: 'boolean' } },
|
|
619
|
+
},
|
|
620
|
+
},
|
|
615
621
|
],
|
|
616
622
|
} as any
|
|
617
623
|
const result = await vm.run(ast, {})
|
|
618
|
-
expect(result.result).toBe(true)
|
|
624
|
+
expect(result.result.res).toBe(true)
|
|
619
625
|
})
|
|
620
626
|
|
|
621
627
|
it('== compares objects structurally', async () => {
|
|
@@ -632,11 +638,17 @@ describe('Edge Cases', () => {
|
|
|
632
638
|
right: { $expr: 'literal', value: { a: 1, b: 2 } },
|
|
633
639
|
},
|
|
634
640
|
},
|
|
635
|
-
{
|
|
641
|
+
{
|
|
642
|
+
op: 'return',
|
|
643
|
+
schema: {
|
|
644
|
+
type: 'object',
|
|
645
|
+
properties: { res: { type: 'boolean' } },
|
|
646
|
+
},
|
|
647
|
+
},
|
|
636
648
|
],
|
|
637
649
|
} as any
|
|
638
650
|
const result = await vm.run(ast, {})
|
|
639
|
-
expect(result.result).toBe(true)
|
|
651
|
+
expect(result.result.res).toBe(true)
|
|
640
652
|
})
|
|
641
653
|
|
|
642
654
|
it('== does not coerce types', async () => {
|
|
@@ -653,11 +665,17 @@ describe('Edge Cases', () => {
|
|
|
653
665
|
right: { $expr: 'literal', value: 1 },
|
|
654
666
|
},
|
|
655
667
|
},
|
|
656
|
-
{
|
|
668
|
+
{
|
|
669
|
+
op: 'return',
|
|
670
|
+
schema: {
|
|
671
|
+
type: 'object',
|
|
672
|
+
properties: { res: { type: 'boolean' } },
|
|
673
|
+
},
|
|
674
|
+
},
|
|
657
675
|
],
|
|
658
676
|
} as any
|
|
659
677
|
const result = await vm.run(ast, {})
|
|
660
|
-
expect(result.result).toBe(false) // no coercion
|
|
678
|
+
expect(result.result.res).toBe(false) // no coercion
|
|
661
679
|
})
|
|
662
680
|
|
|
663
681
|
it('!= returns true for structurally different objects', async () => {
|
|
@@ -674,11 +692,17 @@ describe('Edge Cases', () => {
|
|
|
674
692
|
right: { $expr: 'literal', value: { a: 2 } },
|
|
675
693
|
},
|
|
676
694
|
},
|
|
677
|
-
{
|
|
695
|
+
{
|
|
696
|
+
op: 'return',
|
|
697
|
+
schema: {
|
|
698
|
+
type: 'object',
|
|
699
|
+
properties: { res: { type: 'boolean' } },
|
|
700
|
+
},
|
|
701
|
+
},
|
|
678
702
|
],
|
|
679
703
|
} as any
|
|
680
704
|
const result = await vm.run(ast, {})
|
|
681
|
-
expect(result.result).toBe(true)
|
|
705
|
+
expect(result.result.res).toBe(true)
|
|
682
706
|
})
|
|
683
707
|
|
|
684
708
|
it('null == undefined is true (nullish equality)', async () => {
|
|
@@ -695,11 +719,17 @@ describe('Edge Cases', () => {
|
|
|
695
719
|
right: { $expr: 'ident', name: 'missing' },
|
|
696
720
|
},
|
|
697
721
|
},
|
|
698
|
-
{
|
|
722
|
+
{
|
|
723
|
+
op: 'return',
|
|
724
|
+
schema: {
|
|
725
|
+
type: 'object',
|
|
726
|
+
properties: { res: { type: 'boolean' } },
|
|
727
|
+
},
|
|
728
|
+
},
|
|
699
729
|
],
|
|
700
730
|
} as any
|
|
701
731
|
const result = await vm.run(ast, {})
|
|
702
|
-
expect(result.result).toBe(true) // null == undefined
|
|
732
|
+
expect(result.result.res).toBe(true) // null == undefined
|
|
703
733
|
})
|
|
704
734
|
|
|
705
735
|
it('=== still uses identity comparison', async () => {
|
|
@@ -716,11 +746,112 @@ describe('Edge Cases', () => {
|
|
|
716
746
|
right: { $expr: 'literal', value: [1, 2] },
|
|
717
747
|
},
|
|
718
748
|
},
|
|
719
|
-
{
|
|
749
|
+
{
|
|
750
|
+
op: 'return',
|
|
751
|
+
schema: {
|
|
752
|
+
type: 'object',
|
|
753
|
+
properties: { res: { type: 'boolean' } },
|
|
754
|
+
},
|
|
755
|
+
},
|
|
720
756
|
],
|
|
721
757
|
} as any
|
|
722
758
|
const result = await vm.run(ast, {})
|
|
723
|
-
expect(result.result).toBe(false) // different references
|
|
759
|
+
expect(result.result.res).toBe(false) // different references
|
|
760
|
+
})
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
describe('strict object returns', () => {
|
|
764
|
+
it('should allow object returns', async () => {
|
|
765
|
+
const ast = ajs(`function test() { return { value: 42 } }`)
|
|
766
|
+
const result = await vm.run(ast, {})
|
|
767
|
+
expect(result.result).toEqual({ value: 42 })
|
|
768
|
+
expect(result.error).toBeUndefined()
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
it('should allow empty object returns', async () => {
|
|
772
|
+
const ast = ajs(`function test() { return {} }`)
|
|
773
|
+
const result = await vm.run(ast, {})
|
|
774
|
+
expect(result.result).toEqual({})
|
|
775
|
+
expect(result.error).toBeUndefined()
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
it('should allow bare return (no value)', async () => {
|
|
779
|
+
const ast = {
|
|
780
|
+
op: 'seq',
|
|
781
|
+
steps: [{ op: 'return', value: undefined }],
|
|
782
|
+
} as any
|
|
783
|
+
const result = await vm.run(ast, {})
|
|
784
|
+
expect(result.error).toBeUndefined()
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
it('should allow null return', async () => {
|
|
788
|
+
const ast = {
|
|
789
|
+
op: 'seq',
|
|
790
|
+
steps: [{ op: 'return', value: null }],
|
|
791
|
+
} as any
|
|
792
|
+
const result = await vm.run(ast, {})
|
|
793
|
+
expect(result.error).toBeUndefined()
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
it('should reject number return', async () => {
|
|
797
|
+
const ast = {
|
|
798
|
+
op: 'seq',
|
|
799
|
+
steps: [{ op: 'return', value: { $expr: 'literal', value: 42 } }],
|
|
800
|
+
} as any
|
|
801
|
+
const result = await vm.run(ast, {})
|
|
802
|
+
expect(isAgentError(result.result)).toBe(true)
|
|
803
|
+
expect(result.error?.message).toContain('must return an object')
|
|
804
|
+
expect(result.error?.message).toContain('number')
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
it('should reject string return', async () => {
|
|
808
|
+
const ast = {
|
|
809
|
+
op: 'seq',
|
|
810
|
+
steps: [{ op: 'return', value: { $expr: 'literal', value: 'hello' } }],
|
|
811
|
+
} as any
|
|
812
|
+
const result = await vm.run(ast, {})
|
|
813
|
+
expect(isAgentError(result.result)).toBe(true)
|
|
814
|
+
expect(result.error?.message).toContain('must return an object')
|
|
815
|
+
expect(result.error?.message).toContain('string')
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
it('should reject array return', async () => {
|
|
819
|
+
const ast = {
|
|
820
|
+
op: 'seq',
|
|
821
|
+
steps: [
|
|
822
|
+
{ op: 'return', value: { $expr: 'literal', value: [1, 2, 3] } },
|
|
823
|
+
],
|
|
824
|
+
} as any
|
|
825
|
+
const result = await vm.run(ast, {})
|
|
826
|
+
expect(isAgentError(result.result)).toBe(true)
|
|
827
|
+
expect(result.error?.message).toContain('must return an object')
|
|
828
|
+
expect(result.error?.message).toContain('array')
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
it('should reject boolean return', async () => {
|
|
832
|
+
const ast = {
|
|
833
|
+
op: 'seq',
|
|
834
|
+
steps: [{ op: 'return', value: { $expr: 'literal', value: true } }],
|
|
835
|
+
} as any
|
|
836
|
+
const result = await vm.run(ast, {})
|
|
837
|
+
expect(isAgentError(result.result)).toBe(true)
|
|
838
|
+
expect(result.error?.message).toContain('must return an object')
|
|
839
|
+
expect(result.error?.message).toContain('boolean')
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
it('should allow AgentError to propagate through return', async () => {
|
|
843
|
+
// Errors are objects but need special handling — must not be blocked
|
|
844
|
+
const ast = ajs(`
|
|
845
|
+
function test({ x }) {
|
|
846
|
+
if (x < 0) {
|
|
847
|
+
return { error: 'negative' }
|
|
848
|
+
}
|
|
849
|
+
return { value: x }
|
|
850
|
+
}
|
|
851
|
+
`)
|
|
852
|
+
const result = await vm.run(ast, { x: -1 })
|
|
853
|
+
expect(result.result).toEqual({ error: 'negative' })
|
|
854
|
+
expect(result.error).toBeUndefined()
|
|
724
855
|
})
|
|
725
856
|
})
|
|
726
857
|
})
|
package/src/vm/runtime.ts
CHANGED
|
@@ -1554,6 +1554,25 @@ export const ret = defineAtom(
|
|
|
1554
1554
|
// New style: return has explicit value
|
|
1555
1555
|
if ('value' in step) {
|
|
1556
1556
|
const res = resolveValue(step.value, ctx)
|
|
1557
|
+
|
|
1558
|
+
// Enforce object returns — agents must return objects for composability
|
|
1559
|
+
if (
|
|
1560
|
+
res !== undefined &&
|
|
1561
|
+
res !== null &&
|
|
1562
|
+
!isAgentError(res) &&
|
|
1563
|
+
(typeof res !== 'object' || Array.isArray(res))
|
|
1564
|
+
) {
|
|
1565
|
+
const err = new AgentError(
|
|
1566
|
+
`Agent must return an object, got ${
|
|
1567
|
+
Array.isArray(res) ? 'array' : typeof res
|
|
1568
|
+
}`,
|
|
1569
|
+
'return'
|
|
1570
|
+
)
|
|
1571
|
+
ctx.error = err
|
|
1572
|
+
ctx.output = err
|
|
1573
|
+
return err
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1557
1576
|
ctx.output = res
|
|
1558
1577
|
return res
|
|
1559
1578
|
}
|