tjs-lang 0.5.1 → 0.5.2

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 CHANGED
@@ -67,9 +67,9 @@
67
67
  "type": "example",
68
68
  "group": "basics",
69
69
  "order": 1,
70
- "code": "/*#\nThe classic first function in any language.\n\nDemonstrates:\n- Type annotations via examples (name: 'World')\n- Return type example (-> 'Hello, World') - tests the signature!\n- Inline tests with test blocks\n- Markdown documentation via /*# comments\n*/\ntest 'greet says hello' {\n expect(greet('TJS')).toBe('Hello, TJS!')\n}\n\nfunction greet(name: 'World') -> 'Hello, World!' {\n return `Hello, ${name}!`\n}",
70
+ "code": "/*#\n## Types by Example\n\nIn TJS, the example value after `:` IS the type:\n\n| Syntax | Type |\n|--------|------|\n| `name: 'Alice'` | string (required) |\n| `count: 42` | integer |\n| `rate: 3.14` | number (float) |\n| `index: +0` | non-negative integer |\n| `name = 'default'` | string (optional, defaults to 'default') |\n| `data: { x: 0, y: 0 }` | object with shape |\n*/\n\nfunction greet(name: 'World') -> 'Hello, World!' {\n return `Hello, ${name}!`\n}\n\n// Numeric type narrowing — all valid JS syntax\nfunction clampIndex(index: +0, max: +0) -> +0 {\n return Math.min(index, max)\n}\n\nfunction mix(a: 0.0, b: 0.0, t: 0.0) -> 0.0 {\n return a + (b - a) * t\n}\n\ntest 'greet says hello' {\n expect(greet('TJS')).toBe('Hello, TJS!')\n}\n\ntest 'type errors are values, not exceptions' {\n const err = greet(42)\n expect(err instanceof Error).toBe(true)\n}\n\ntest 'numeric types are precise' {\n expect(clampIndex(5, 10)).toBe(5)\n // negative fails non-negative integer check\n expect(clampIndex(-1, 10) instanceof Error).toBe(true)\n // float fails integer check\n expect(clampIndex(3.5, 10) instanceof Error).toBe(true)\n}\n\ntest 'floats accept any number' {\n expect(mix(0, 100, 0.5)).toBe(50)\n}",
71
71
  "language": "tjs",
72
- "description": "Simple typed greeting function with docs and tests"
72
+ "description": "Types-by-example: the value IS the type annotation"
73
73
  },
74
74
  {
75
75
  "title": "Required vs Optional",
@@ -173,7 +173,7 @@
173
173
  "type": "example",
174
174
  "group": "featured",
175
175
  "order": 0,
176
- "code": "// ═══════════════════════════════════════════════════════════\n// 1. SAFETY DIRECTIVE & TJS MODES\n// These must appear before any other code.\n// ═══════════════════════════════════════════════════════════\n\nsafety inputs\n\nTjsEquals\nTjsClass\n\n/*#\n# TJS Grammar Reference\n\nA runnable reference for TJS syntax. Each section demonstrates a\nfeature with a test proving it works.\n\n## Quick Index\n| Feature | Section |\n|---------|---------|\n| Safety & modes | `safety`, `TjsEquals`, `TjsClass` |\n| Parameters | Colon `:`, optional `=`, destructured `{}` |\n| Numeric narrowing | `42` int, `3.14` float, `+0` non-negative |\n| Return types | `->`, `-?`, `-!` |\n| Safety markers | `(! ...)` unsafe, `(? ...)` safe |\n| Type/Generic/Enum/Union | See above (requires full runtime) |\n| Bare assignments | `Uppercase = ...` |\n| Classes | Callable without `new` |\n| Polymorphic functions | Same name, different signatures |\n| Polymorphic constructors | Multiple `constructor()` |\n| Local extensions | `extend String { ... }` |\n| Equality | `==` structural, `===` identity, `Is`/`IsNot` |\n| Try without catch | Monadic error conversion |\n| Inline tests | Test blocks |\n| TDoc comments | Slash-star-hash markdown blocks |\n*/\n\n// ═══════════════════════════════════════════════════════════\n// 2. PARAMETER SYNTAX\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Parameters\n\nColon `:` = required (example value infers type).\nEquals `=` = optional (default value).\nQuestion mark `?:` = optional (TS-style).\n*/\n\n// Required params (colon shorthand)\nfunction greet(name: 'Alice') -> 'Hello, Alice' {\n return 'Hello, ' + name\n}\n\n// Optional params (equals = default)\nfunction greetOpt(name = 'World') -> 'Hello, World' {\n return 'Hello, ' + name\n}\n\n// Destructured object params (colon = required, equals = optional)\nfunction createUser({ name: 'Anon', role = 'user' }) -> { name: '', role: '' } {\n return { name, role }\n}\n\n// Numeric type narrowing: 42 = integer, 3.14 = float, +0 = non-negative int\nfunction calc(count: 42, rate: 3.14, index: +0) -> 0.0 {\n return (count + index) * rate\n}\n\ntest 'parameter syntax' {\n expect(greet('Bob')).toBe('Hello, Bob')\n expect(greetOpt()).toBe('Hello, World')\n expect(createUser({ name: 'Eve' })).toEqual({ name: 'Eve', role: 'user' })\n expect(calc(10, 1.5, 2)).toBe(18)\n}\n\n// ═══════════════════════════════════════════════════════════\n// 3. RETURN TYPES\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Return Types\n\n`->` signature test at transpile time.\n`-?` signature test + runtime output validation.\n`-!` skip signature test entirely.\n*/\n\n// -> : transpile-time check (double(5) must equal 10)\nfunction double(x: 5) -> 10 {\n return x * 2\n}\n\n// -! : skip test (useful when return shape varies)\nfunction safeDivide(a: 10, b: 2) -! 5 {\n if (b === 0) return { error: 'div by zero' }\n return a / b\n}\n\ntest 'return types' {\n expect(double(7)).toBe(14)\n expect(safeDivide(10, 0)).toEqual({ error: 'div by zero' })\n}\n\n// ═══════════════════════════════════════════════════════════\n// 4. SAFETY MARKERS\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Safety Markers\n\n`!` = unsafe (skip input validation). Fast path for trusted callers.\n`?` = safe (force validation even inside `unsafe` blocks).\n*/\n\nfunction fastAdd(! a: 0, b: 0) -> 0 {\n return a + b\n}\n\nfunction safeAdd(? a: 0, b: 0) -> 0 {\n return a + b\n}\n\ntest 'safety markers' {\n expect(fastAdd(3, 4)).toBe(7)\n expect(safeAdd(3, 4)).toBe(7)\n}\n\n// ═══════════════════════════════════════════════════════════\n// 5. BARE ASSIGNMENTS\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Bare Assignments\n\nUppercase identifiers auto-get `const`.\n*/\n\nGreeting = 'Hello'\nMaxRetries = 3\n\ntest 'bare assignments' {\n expect(Greeting).toBe('Hello')\n expect(MaxRetries).toBe(3)\n}\n\n// ═══════════════════════════════════════════════════════════\n// 7. CLASSES (callable without new)\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Classes\n\nWith `TjsClass` enabled, classes are callable without `new`.\n*/\n\nclass Point {\n constructor(x: 0.0, y: 0.0) {\n this.x = x\n this.y = y\n }\n\n magnitude() {\n return Math.sqrt(this.x * this.x + this.y * this.y)\n }\n}\n\ntest 'classes callable without new' {\n const p = Point(3, 4)\n expect(p instanceof Point).toBe(true)\n expect(p.magnitude()).toBe(5)\n}\n\n// ═══════════════════════════════════════════════════════════\n// 8. POLYMORPHIC FUNCTIONS\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Polymorphic Functions\n\nSame name, different signatures. Dispatched by arity/type.\nSee the **Polymorphic Functions** example for more.\n*/\n\nfunction describe(value: 0) {\n return 'number: ' + value\n}\n\nfunction describe(first: '', last: '') {\n return first + ' ' + last\n}\n\ntest 'polymorphic dispatch by arity' {\n expect(describe(42)).toBe('number: 42')\n expect(describe('Jane', 'Doe')).toBe('Jane Doe')\n}\n\n// ═══════════════════════════════════════════════════════════\n// 9. POLYMORPHIC CONSTRUCTORS\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Polymorphic Constructors\n\nMultiple `constructor()` declarations in a class.\nSee the **Polymorphic Constructors** example for more.\n*/\n\nclass Vec2 {\n constructor(x: 0.0, y: 0.0) {\n this.x = x\n this.y = y\n }\n\n constructor(obj: { x: 0.0, y: 0.0 }) {\n this.x = obj.x\n this.y = obj.y\n }\n}\n\ntest 'polymorphic constructors' {\n const a = Vec2(1, 2)\n const b = Vec2({ x: 1, y: 2 })\n expect(a.x).toBe(b.x)\n expect(a.y).toBe(b.y)\n}\n\n// ═══════════════════════════════════════════════════════════\n// 10. LOCAL EXTENSIONS\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Local Extensions\n\nAdd methods to built-in types without prototype pollution.\nRewritten to `.call()` at transpile time for known types.\nSee the **Local Extensions** example for a runnable demo.\n\n extend String {\n capitalize() { return this[0].toUpperCase() + this.slice(1) }\n }\n\n extend Array {\n last() { return this[this.length - 1] }\n }\n\n 'hello'.capitalize() // 'Hello'\n [1, 2, 3].last() // 3\n*/\n\n// ═══════════════════════════════════════════════════════════\n// 11. EQUALITY OPERATORS\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Equality\n\nWith `TjsEquals` enabled (at the top of this file):\n- `==` / `!=` use structural comparison (deep value equality)\n- `===` / `!==` are identity checks (same reference)\n- `Is` / `IsNot` are explicit structural operators (any mode)\n\n const a = { x: 1, y: [2, 3] }\n const b = { x: 1, y: [2, 3] }\n a == b // true (structural: same shape)\n a === b // false (identity: different objects)\n a Is b // true (explicit structural)\n a IsNot b // false\n*/\n\n// ═══════════════════════════════════════════════════════════\n// 12. TRY WITHOUT CATCH\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Try Without Catch\n\nA bare `try` block auto-converts exceptions to monadic errors.\n*/\n\nfunction parseJSON(s: '{\"a\":1}') -! { a: 1 } {\n try {\n return JSON.parse(s)\n }\n}\n\ntest 'try without catch' {\n expect(parseJSON('{\"ok\":true}')).toEqual({ ok: true })\n const bad = parseJSON('not json')\n expect(bad.$error).toBe(true)\n}\n\n// ═══════════════════════════════════════════════════════════\n// 13. INLINE TESTS\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Inline Tests\n\nTest blocks run at transpile time and are stripped from\noutput. They have full access to the module scope, so you\ncan test private functions without exporting them.\n*/\n\nfunction _private(x: 0) -> 0 {\n return x * x\n}\n\ntest 'inline tests can reach private functions' {\n expect(_private(5)).toBe(25)\n}\n\n// ═══════════════════════════════════════════════════════════\n// 14. MODULE EXPORTS\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Module Exports\n\nStandard ES module syntax works. Functions and values\ncan be exported for use by other modules.\n*/\n\nexport function publicHelper(x: 0) -> 0 {\n return x + 1\n}\n\n// ═══════════════════════════════════════════════════════════\n// 15. TDOC COMMENTS\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## TDoc Comments\n\nThese comment blocks (opened with slash-star-hash) contain\nmarkdown that becomes rich documentation in the playground\nand API docs. Every such block you've seen above is a TDoc.\n*/\n\n// ═══════════════════════════════════════════════════════════\n// OUTPUT\n// ═══════════════════════════════════════════════════════════\n\nconsole.log('TJS Grammar Reference — all tests passed!')\nconsole.log('Features demonstrated:', [\n 'safety directive', 'TJS modes', 'colon params', 'optional params',\n 'destructured params', 'numeric narrowing',\n 'return types (-> -? -!)', 'safety markers (! ?)',\n 'Type', 'Generic', 'Enum', 'Union', 'bare assignments',\n 'classes', 'polymorphic functions', 'polymorphic constructors',\n 'local extensions', 'structural equality', 'Is/IsNot',\n 'try without catch', 'inline tests', 'module exports', 'TDoc'\n].join(', '))",
176
+ "code": "// ═══════════════════════════════════════════════════════════\n// 1. SAFETY DIRECTIVE & TJS MODES\n// These must appear before any other code.\n// ═══════════════════════════════════════════════════════════\n\nsafety inputs\n\nTjsEquals\nTjsClass\n\n/*#\n# TJS Grammar Reference\n\nA runnable reference for TJS syntax. Each section demonstrates a\nfeature with a test proving it works.\n\n## Quick Index\n| Feature | Section |\n|---------|---------|\n| Safety & modes | `safety`, `TjsEquals`, `TjsClass` |\n| Parameters | Colon `:`, optional `=`, destructured `{}` |\n| Numeric narrowing | `42` int, `3.14` float, `+0` non-negative |\n| Return types | `->`, `-?`, `-!` |\n| Safety markers | `(! ...)` unsafe, `(? ...)` safe |\n| Type/Generic/Enum/Union | See above (requires full runtime) |\n| Bare assignments | `Uppercase = ...` |\n| Classes | Callable without `new` |\n| Polymorphic functions | Same name, different signatures |\n| Polymorphic constructors | Multiple `constructor()` |\n| Local extensions | `extend String { ... }` |\n| Equality | `==` structural, `===` identity, `Is`/`IsNot` |\n| Try without catch | Monadic error conversion |\n| Inline tests | Test blocks |\n| TDoc comments | Slash-star-hash markdown blocks |\n*/\n\n// ═══════════════════════════════════════════════════════════\n// 2. PARAMETER SYNTAX\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Parameters\n\nColon `:` = required (example value infers type).\nEquals `=` = optional (default value).\nQuestion mark `?:` = optional (TS-style).\n*/\n\n// Required params (colon shorthand)\nfunction greet(name: 'Alice') -> 'Hello, Alice' {\n return 'Hello, ' + name\n}\n\n// Optional params (equals = default)\nfunction greetOpt(name = 'World') -> 'Hello, World' {\n return 'Hello, ' + name\n}\n\n// Destructured object params (colon = required, equals = optional)\nfunction createUser({ name: 'Anon', role = 'user' }) -> { name: '', role: '' } {\n return { name, role }\n}\n\n// Numeric type narrowing: 42 = integer, 3.14 = float, +0 = non-negative int\nfunction calc(count: 42, rate: 3.14, index: +0) -> 0.0 {\n return (count + index) * rate\n}\n\ntest 'parameter syntax' {\n expect(greet('Bob')).toBe('Hello, Bob')\n expect(greetOpt()).toBe('Hello, World')\n expect(createUser({ name: 'Eve' })).toEqual({ name: 'Eve', role: 'user' })\n expect(calc(10, 1.5, 2)).toBe(18)\n}\n\n// ═══════════════════════════════════════════════════════════\n// 3. RETURN TYPES\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Return Types\n\n`->` signature test at transpile time.\n`-?` signature test + runtime output validation.\n`-!` skip signature test entirely.\n*/\n\n// -> : transpile-time check (double(5) must equal 10)\nfunction double(x: 5) -> 10 {\n return x * 2\n}\n\n// -! : skip test (useful when return shape varies)\nfunction safeDivide(a: 10, b: 2) -! 5 {\n if (b === 0) return { error: 'div by zero' }\n return a / b\n}\n\ntest 'return types' {\n expect(double(7)).toBe(14)\n expect(safeDivide(10, 0)).toEqual({ error: 'div by zero' })\n}\n\n// ═══════════════════════════════════════════════════════════\n// 4. SAFETY MARKERS\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Safety Markers\n\n`!` = unsafe (skip input validation). Fast path for trusted callers.\n`?` = safe (force validation even inside `unsafe` blocks).\n*/\n\nfunction fastAdd(! a: 0, b: 0) -> 0 {\n return a + b\n}\n\nfunction safeAdd(? a: 0, b: 0) -> 0 {\n return a + b\n}\n\ntest 'safety markers' {\n expect(fastAdd(3, 4)).toBe(7)\n expect(safeAdd(3, 4)).toBe(7)\n}\n\n// ═══════════════════════════════════════════════════════════\n// 5. BARE ASSIGNMENTS\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Bare Assignments\n\nUppercase identifiers auto-get `const`.\n*/\n\nGreeting = 'Hello'\nMaxRetries = 3\n\ntest 'bare assignments' {\n expect(Greeting).toBe('Hello')\n expect(MaxRetries).toBe(3)\n}\n\n// ═══════════════════════════════════════════════════════════\n// 7. CLASSES (callable without new)\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Classes\n\nWith `TjsClass` enabled, classes are callable without `new`.\n*/\n\nclass Point {\n constructor(x: 0.0, y: 0.0) {\n this.x = x\n this.y = y\n }\n\n magnitude() {\n return Math.sqrt(this.x * this.x + this.y * this.y)\n }\n}\n\ntest 'classes callable without new' {\n const p = Point(3, 4)\n expect(p instanceof Point).toBe(true)\n expect(p.magnitude()).toBe(5)\n}\n\n// ═══════════════════════════════════════════════════════════\n// 8. POLYMORPHIC FUNCTIONS\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Polymorphic Functions\n\nSame name, different signatures. Dispatched by arity/type.\nSee the **Polymorphic Functions** example for more.\n*/\n\nfunction describe(value: 0) {\n return 'number: ' + value\n}\n\nfunction describe(first: '', last: '') {\n return first + ' ' + last\n}\n\ntest 'polymorphic dispatch by arity' {\n expect(describe(42)).toBe('number: 42')\n expect(describe('Jane', 'Doe')).toBe('Jane Doe')\n}\n\n// ═══════════════════════════════════════════════════════════\n// 9. POLYMORPHIC CONSTRUCTORS\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Polymorphic Constructors\n\nMultiple `constructor()` declarations in a class.\nSee the **Polymorphic Constructors** example for more.\n*/\n\nclass Vec2 {\n constructor(x: 0.0, y: 0.0) {\n this.x = x\n this.y = y\n }\n\n constructor(obj: { x: 0.0, y: 0.0 }) {\n this.x = obj.x\n this.y = obj.y\n }\n}\n\ntest 'polymorphic constructors' {\n const a = Vec2(1, 2)\n const b = Vec2({ x: 1, y: 2 })\n expect(a.x).toBe(b.x)\n expect(a.y).toBe(b.y)\n}\n\n// ═══════════════════════════════════════════════════════════\n// 10. LOCAL EXTENSIONS\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Local Extensions\n\nAdd methods to built-in types without prototype pollution.\nRewritten to `.call()` at transpile time for known types.\nSee the **Local Extensions** example for a runnable demo.\n\n extend String {\n capitalize() { return this[0].toUpperCase() + this.slice(1) }\n }\n\n extend Array {\n last() { return this[this.length - 1] }\n }\n\n 'hello'.capitalize() // 'Hello'\n [1, 2, 3].last() // 3\n*/\n\n// ═══════════════════════════════════════════════════════════\n// 11. EQUALITY OPERATORS\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Equality\n\nWith `TjsEquals` enabled (at the top of this file):\n- `==` / `!=` use structural comparison (deep value equality)\n- `===` / `!==` are identity checks (same reference)\n- `Is` / `IsNot` are explicit structural operators (any mode)\n\n const a = { x: 1, y: [2, 3] }\n const b = { x: 1, y: [2, 3] }\n a == b // true (structural: same shape)\n a === b // false (identity: different objects)\n a Is b // true (explicit structural)\n a IsNot b // false\n*/\n\n// ═══════════════════════════════════════════════════════════\n// 12. TRY WITHOUT CATCH\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Try Without Catch\n\nA bare `try` block auto-converts exceptions to monadic errors.\n*/\n\nfunction parseJSON(s: '{\"a\":1}') -! { a: 1 } {\n try {\n return JSON.parse(s)\n }\n}\n\ntest 'try without catch' {\n expect(parseJSON('{\"ok\":true}')).toEqual({ ok: true })\n const bad = parseJSON('not json')\n expect(bad instanceof Error).toBe(true)\n}\n\n// ═══════════════════════════════════════════════════════════\n// 13. INLINE TESTS\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Inline Tests\n\nTest blocks run at transpile time and are stripped from\noutput. They have full access to the module scope, so you\ncan test private functions without exporting them.\n*/\n\nfunction _private(x: 0) -> 0 {\n return x * x\n}\n\ntest 'inline tests can reach private functions' {\n expect(_private(5)).toBe(25)\n}\n\n// ═══════════════════════════════════════════════════════════\n// 14. MODULE EXPORTS\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## Module Exports\n\nStandard ES module syntax works. Functions and values\ncan be exported for use by other modules.\n*/\n\nexport function publicHelper(x: 0) -> 0 {\n return x + 1\n}\n\n// ═══════════════════════════════════════════════════════════\n// 15. TDOC COMMENTS\n// ═══════════════════════════════════════════════════════════\n\n/*#\n## TDoc Comments\n\nThese comment blocks (opened with slash-star-hash) contain\nmarkdown that becomes rich documentation in the playground\nand API docs. Every such block you've seen above is a TDoc.\n*/\n\n// ═══════════════════════════════════════════════════════════\n// OUTPUT\n// ═══════════════════════════════════════════════════════════\n\nconsole.log('TJS Grammar Reference — all tests passed!')\nconsole.log('Features demonstrated:', [\n 'safety directive', 'TJS modes', 'colon params', 'optional params',\n 'destructured params', 'numeric narrowing',\n 'return types (-> -? -!)', 'safety markers (! ?)',\n 'Type', 'Generic', 'Enum', 'Union', 'bare assignments',\n 'classes', 'polymorphic functions', 'polymorphic constructors',\n 'local extensions', 'structural equality', 'Is/IsNot',\n 'try without catch', 'inline tests', 'module exports', 'TDoc'\n].join(', '))",
177
177
  "language": "tjs",
178
178
  "description": "Comprehensive reference covering all major TJS syntax features. **Type declarations** (require full `tjs-lang` runtime — shown here for reference): Type Name = 'Alice' Type Age 'a non-negative age' { example: 25 predicate(x) { return typeof x === 'number' && x >= 0 } } Generic Pair<A, B> { description: 'a pair of values' predicate(obj, A, B) { ... } } Enum Direction 'cardinal direction' { North, East, South, West } Enum Color 'CSS color' { Red = 'red', Green = 'green', Blue = 'blue' } Union Status 'task status' 'pending' | 'active' | 'done' All other features are exercised in the runnable code below:"
179
179
  },
@@ -209,7 +209,7 @@
209
209
  "type": "example",
210
210
  "group": "fullstack",
211
211
  "order": 13,
212
- "code": "/**\n * # Client Application\n *\n * A frontend that calls the User Service.\n *\n * **First:** Run the \"User Service\" example and save it as \"user-service\"\n * **Then:** Run this client to see full-stack in action\n *\n * This demonstrates:\n * - Importing local TJS modules\n * - Type-safe service calls\n * - Error handling\n */\n\n// Import from local module (saved in playground)\nimport { createUser, getUser, listUsers, searchUsers } from 'user-service'\n\n// Helper to display results\nfunction display(label: '', data: {}) {\n console.log(`\\\\n\\${label}:`)\n console.log(JSON.stringify(data, null, 2))\n}\n\n// Main app\nasync function main() {\n console.log('=== Client App ===')\n console.log('Connecting to user-service...\\\\n')\n\n // Create some users\n const user1 = createUser({ name: 'Dave', email: 'dave@startup.io' })\n display('Created user', user1)\n\n const user2 = createUser({ name: 'Eve', email: 'eve@startup.io' })\n display('Created user', user2)\n\n // Fetch a user\n const fetched = getUser({ id: user1.id })\n display('Fetched user', fetched)\n\n // List all\n const all = listUsers({ limit: 100, offset: 0 })\n display('All users', all)\n\n // Search\n const results = searchUsers({ query: 'eve' })\n display('Search results for \"eve\"', results)\n\n // Type error handling\n console.log('\\\\n--- Type Validation Demo ---')\n const badResult = createUser({ name: 999 })\n if (badResult.$error) {\n console.log('Caught type error:', badResult.message)\n }\n\n console.log('\\\\n=== Full-Stack Demo Complete ===')\n console.log('Everything ran in the browser. No server. No build step.')\n}\n\nmain()",
212
+ "code": "/**\n * # Client Application\n *\n * A frontend that calls the User Service.\n *\n * **First:** Run the \"User Service\" example and save it as \"user-service\"\n * **Then:** Run this client to see full-stack in action\n *\n * This demonstrates:\n * - Importing local TJS modules\n * - Type-safe service calls\n * - Error handling\n */\n\n// Import from local module (saved in playground)\nimport { createUser, getUser, listUsers, searchUsers } from 'user-service'\n\n// Helper to display results\nfunction display(label: '', data: {}) {\n console.log(`\\\\n\\${label}:`)\n console.log(JSON.stringify(data, null, 2))\n}\n\n// Main app\nasync function main() {\n console.log('=== Client App ===')\n console.log('Connecting to user-service...\\\\n')\n\n // Create some users\n const user1 = createUser({ name: 'Dave', email: 'dave@startup.io' })\n display('Created user', user1)\n\n const user2 = createUser({ name: 'Eve', email: 'eve@startup.io' })\n display('Created user', user2)\n\n // Fetch a user\n const fetched = getUser({ id: user1.id })\n display('Fetched user', fetched)\n\n // List all\n const all = listUsers({ limit: 100, offset: 0 })\n display('All users', all)\n\n // Search\n const results = searchUsers({ query: 'eve' })\n display('Search results for \"eve\"', results)\n\n // Type error handling\n console.log('\\\\n--- Type Validation Demo ---')\n const badResult = createUser({ name: 999 })\n if (badResult instanceof Error) {\n console.log('Caught type error:', badResult.message)\n }\n\n console.log('\\\\n=== Full-Stack Demo Complete ===')\n console.log('Everything ran in the browser. No server. No build step.')\n}\n\nmain()",
213
213
  "language": "tjs",
214
214
  "description": "Frontend that calls the User Service - run after saving user-service!"
215
215
  },
@@ -257,9 +257,9 @@
257
257
  "type": "example",
258
258
  "group": "patterns",
259
259
  "order": 7,
260
- "code": "/*#\n## Monadic Error Handling\n\nTJS uses the Result patternerrors are values, not exceptions.\nThis makes error handling explicit and type-safe.\n*/\ntest 'divide handles zero' {\n const result = divide(10, 0)\n expect(result.error).toBe('Division by zero')\n}\n\ntest 'divide works normally' {\n const result = divide(10, 2)\n expect(result.value).toBe(5)\n}\n\nfunction divide(a: 10, b: 2) -> { value: 0, error = '' } {\n if (b === 0) {\n return { value: NaN, error: 'Division by zero' }\n }\n return { value: a / b }\n}\n\nfunction safeParse(json: '{\"x\":1}') -! { data: null, error = '' } {\n try {\n return { data: JSON.parse(json) }\n } catch (e) {\n return { data: null, error: e.message }\n }\n}\n\n// Usage errors are values you can inspect\nconst result = divide(10, 0)\nif (!result.error) {\n console.log('Result:', result.value)\n} else {\n console.log('Error:', result.error)\n}",
260
+ "code": "/*#\n## Monadic Error Propagation\n\nType errors are values (MonadicError), not exceptions. They propagate\nautomatically through function chains if any function receives an\nerror as input, it short-circuits and returns the error immediately.\n\nNo try/catch needed. No manual error checking between calls.\n*/\n\n// --- Error propagation through a pipeline ---\n\nfunction validate(name: '') -> '' {\n return name.trim()\n}\n\nfunction greet(name: '') -> '' {\n return `Hello, ${name}!`\n}\n\nfunction shout(text: '') -> '' {\n return text.toUpperCase()\n}\n\ntest 'valid input flows through the pipeline' {\n // Each function's output feeds the next function's input\n const result = shout(greet(validate('alice')))\n expect(result).toBe('HELLO, ALICE!')\n}\n\ntest 'type error propagates through the entire chain' {\n // validate(42) returns a MonadicError (42 is not a string)\n // greet() receives the error, short-circuits, returns it\n // shout() receives the error, short-circuits, returns it\n const result = shout(greet(validate(42)))\n expect(result instanceof Error).toBe(true)\n expect(result.message.includes('string')).toBe(true)\n}\n\ntest 'error identity is preserved (same object, not a copy)' {\n const err = validate(42)\n expect(greet(err)).toBe(err)\n expect(shout(err)).toBe(err)\n}\n\n// --- Result pattern for domain errors ---\n\nfunction divide(a: 10, b: 2) -> { value: 0, error = '' } {\n if (b === 0) {\n return { value: NaN, error: 'Division by zero' }\n }\n return { value: a / b }\n}\n\ntest 'divide handles zero' {\n const result = divide(10, 0)\n expect(result.error).toBe('Division by zero')\n}\n\ntest 'divide works normally' {\n const result = divide(10, 2)\n expect(result.value).toBe(5)\n}\n\n// Errors propagate when passed as arguments to TJS functions.\n// If you use a potentially-error value in a JS expression (e.g. result.length),\n// check it first: if (result instanceof Error) return result\n// In debug mode, errors include a callStack showing the full call chain.",
261
261
  "language": "tjs",
262
- "description": "Type-safe error handling patterns"
262
+ "description": "Monadic error propagation and type-safe error patterns"
263
263
  },
264
264
  {
265
265
  "title": "Schema Validation",
@@ -269,9 +269,9 @@
269
269
  "type": "example",
270
270
  "group": "patterns",
271
271
  "order": 8,
272
- "code": "// TJS integrates with Schema for validation\nimport { Schema } from 'tosijs-schema'\n\n// Define a schema\nconst UserSchema = Schema({\n name: 'anonymous',\n email: 'user@example.com',\n age: 0\n})\n\n// Validate data\nfunction validateUser(data: { name: '', email: '', age: 0 }) -> { valid: true, errors: [''] } {\n const errors = []\n\n if (!UserSchema.validate(data)) {\n errors.push('Invalid user structure')\n }\n\n return {\n valid: errors.length === 0,\n errors\n }\n}\n\nvalidateUser({ name: 'Alice', email: 'alice@test.com', age: 30 })",
272
+ "code": "/*#\n## Runtime Types\n\nEvery TJS function carries `__tjs` metadata with full type information.\nThis enables runtime validation, auto-generated docs, and introspection\n all from the same type annotations you already write.\n*/\n\nfunction createUser(\n name: 'anonymous',\n email: 'user@example.com',\n age: +0\n) -> { name: '', email: '', age: 0 } {\n return { name, email, age }\n}\n\nfunction transfer(\n from: '',\n to: '',\n amount: 0.0\n) -> { from: '', to: '', amount: 0.0 } {\n return { from, to, amount }\n}\n\ntest 'functions validate at runtime' {\n const user = createUser('Alice', 'alice@test.com', 30)\n expect(user.name).toBe('Alice')\n\n // Wrong type — returns error, no exception\n const err = createUser('Alice', 'alice@test.com', -1)\n expect(err instanceof Error).toBe(true)\n}\n\ntest 'metadata is introspectable' {\n const meta = createUser.__tjs\n expect(meta.params.name.type.kind).toBe('string')\n expect(meta.params.age.type.kind).toBe('non-negative-integer')\n expect(meta.returns.type.kind).toBe('object')\n}\n\n// Inspect live metadata\nconsole.log('createUser params:', createUser.__tjs.params)\nconsole.log('transfer params:', transfer.__tjs.params)\nconsole.log('transfer returns:', transfer.__tjs.returns)",
273
273
  "language": "tjs",
274
- "description": "Using Schema for runtime type checking"
274
+ "description": "Types persist into runtime inspect, validate, and document at zero extra cost"
275
275
  },
276
276
  {
277
277
  "title": "Date Formatting (with import)",
@@ -147,11 +147,11 @@ export class ServiceHost {
147
147
  // Call with TJS validation (built into the transpiled code)
148
148
  const result = fn(args)
149
149
 
150
- // Check for monadic error
151
- if (result && result.$error) {
150
+ // Check for monadic error (MonadicError extends Error)
151
+ if (result instanceof Error) {
152
152
  return {
153
153
  success: false,
154
- error: { message: result.message, path: result.path },
154
+ error: { message: result.message, path: (result as any).path },
155
155
  fuel: 1, // minimal fuel for failed validation
156
156
  duration: performance.now() - start,
157
157
  }
@@ -163,8 +163,9 @@ console.log('10 / 0 =', divide(10, 0))
163
163
  const badResult = divide('ten' as any, 2)
164
164
  console.log('divide("ten", 2) =', badResult)
165
165
 
166
- if (badResult && badResult.$error) {
167
- console.log(' ^ This is a validation error, not a crash!')
166
+ if (badResult instanceof Error) {
167
+ console.log(' ^ This is a MonadicError, not a crash!')
168
+ console.log(' message:', badResult.message)
168
169
  }
169
170
  `,
170
171
  },
@@ -468,7 +468,7 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
468
468
 
469
469
  run = async () => {
470
470
  this.clearConsole()
471
- this.transpile()
471
+ await this.transpile()
472
472
 
473
473
  if (!this.lastJsCode) {
474
474
  this.log('Cannot run - transpilation failed')
@@ -548,7 +548,7 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
548
548
  }
549
549
 
550
550
  // Public method to set source code (auto-runs when examples are loaded)
551
- setSource(code: string, exampleName?: string) {
551
+ async setSource(code: string, exampleName?: string) {
552
552
  // Save current edits before switching
553
553
  if (this.currentExampleName) {
554
554
  this.editorCache.set(this.currentExampleName, this.parts.tsEditor.value)
@@ -565,9 +565,9 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
565
565
  // Update revert button visibility
566
566
  this.updateRevertButton()
567
567
 
568
- this.transpile()
568
+ await this.transpile()
569
569
  // Auto-run when source is loaded externally (e.g., from example selection)
570
- this.run()
570
+ await this.run()
571
571
  }
572
572
 
573
573
  // Revert to the original example code
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tjs-lang",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
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",
@@ -1602,6 +1602,211 @@ function test(x: 0) -! 0 {
1602
1602
  })
1603
1603
  })
1604
1604
 
1605
+ describe('stack management', () => {
1606
+ it('stack is empty after successful calls', () => {
1607
+ const { configure, resetRuntime } = require('./runtime')
1608
+ resetRuntime()
1609
+ configure({ debug: true })
1610
+
1611
+ try {
1612
+ const { code } = tjs(`
1613
+ function add(a: 0, b: 0) -! 0 {
1614
+ return a + b
1615
+ }
1616
+ `)
1617
+ const add = new Function(code + '; return add')()
1618
+
1619
+ add(1, 2)
1620
+ add(3, 4)
1621
+ add(5, 6)
1622
+
1623
+ // Stack must be empty — no stale entries from successful calls
1624
+ expect(globalThis.__tjs.getStack()).toEqual([])
1625
+ } finally {
1626
+ resetRuntime()
1627
+ }
1628
+ })
1629
+
1630
+ it('stack is clean after error propagates back up', () => {
1631
+ const { configure, resetRuntime } = require('./runtime')
1632
+ resetRuntime()
1633
+ configure({ debug: true })
1634
+
1635
+ try {
1636
+ const { code } = tjs(`/* tjs <- src/app.ts */
1637
+ /* line 1 */
1638
+ function outer(x: 0) -! 0 {
1639
+ return inner(x)
1640
+ }
1641
+ /* line 5 */
1642
+ function inner(x: '') -! '' {
1643
+ return x.toUpperCase()
1644
+ }
1645
+ `)
1646
+ const fns = new Function(code + '; return { outer, inner }')()
1647
+ const err = fns.outer(42)
1648
+
1649
+ expect(err).toBeInstanceOf(MonadicError)
1650
+ // Stack must be empty after error has propagated back
1651
+ expect(globalThis.__tjs.getStack()).toEqual([])
1652
+ } finally {
1653
+ resetRuntime()
1654
+ }
1655
+ })
1656
+
1657
+ it('no stale entries after mixed success and error calls', () => {
1658
+ const { configure, resetRuntime } = require('./runtime')
1659
+ resetRuntime()
1660
+ configure({ debug: true })
1661
+
1662
+ try {
1663
+ const { code } = tjs(`
1664
+ function greet(name: '') -! '' {
1665
+ return 'Hello, ' + name
1666
+ }
1667
+ `)
1668
+ const greet = new Function(code + '; return greet')()
1669
+
1670
+ // Successful calls
1671
+ greet('Alice')
1672
+ greet('Bob')
1673
+
1674
+ // Error call
1675
+ const err = greet(42)
1676
+ expect(err).toBeInstanceOf(MonadicError)
1677
+
1678
+ // More successful calls
1679
+ greet('Charlie')
1680
+
1681
+ // Stack must be empty throughout
1682
+ expect(globalThis.__tjs.getStack()).toEqual([])
1683
+ } finally {
1684
+ resetRuntime()
1685
+ }
1686
+ })
1687
+
1688
+ it('call stack reflects chain when error occurs at depth', () => {
1689
+ const { configure, resetRuntime } = require('./runtime')
1690
+ resetRuntime()
1691
+ configure({ debug: true })
1692
+
1693
+ try {
1694
+ const { code } = tjs(`/* tjs <- src/pipeline.ts */
1695
+ /* line 1 */
1696
+ function a(x: 0) -! 0 { return b(x) }
1697
+ /* line 3 */
1698
+ function b(x: 0) -! 0 { return c(x) }
1699
+ /* line 5 */
1700
+ function c(x: 0) -! 0 { return d(x) }
1701
+ /* line 7 */
1702
+ function d(x: '') -! '' { return x.toUpperCase() }
1703
+ `)
1704
+ const fns = new Function(code + '; return { a, b, c, d }')()
1705
+ const err = fns.a(99)
1706
+
1707
+ expect(err).toBeInstanceOf(MonadicError)
1708
+ expect(err.path).toBe('src/pipeline.ts:7:d.x')
1709
+
1710
+ // Call stack includes all functions in the chain
1711
+ expect(err.callStack).toEqual([
1712
+ 'src/pipeline.ts:1:a',
1713
+ 'src/pipeline.ts:3:b',
1714
+ 'src/pipeline.ts:5:c',
1715
+ 'src/pipeline.ts:7:d',
1716
+ ])
1717
+ } finally {
1718
+ resetRuntime()
1719
+ }
1720
+ })
1721
+ })
1722
+
1723
+ describe('input-side error propagation', () => {
1724
+ it('error from inner function caught by outer input check', () => {
1725
+ const { code } = tjs(`
1726
+ function step1(x: '') -! '' {
1727
+ return x.toUpperCase()
1728
+ }
1729
+
1730
+ function step2(x: '') -! '' {
1731
+ return x + '!'
1732
+ }
1733
+ `)
1734
+ const fns = new Function(code + '; return { step1, step2 }')()
1735
+
1736
+ // step1(42) returns MonadicError, step2 receives it as input
1737
+ const err = fns.step2(fns.step1(42))
1738
+
1739
+ expect(err).toBeInstanceOf(MonadicError)
1740
+ // Error originated in step1, not step2
1741
+ expect(err.path).toContain('step1.x')
1742
+ })
1743
+
1744
+ it('error identity preserved through chain', () => {
1745
+ const { code } = tjs(`
1746
+ function a(x: '') -! '' { return x }
1747
+ function b(x: '') -! '' { return x }
1748
+ function c(x: '') -! '' { return x }
1749
+ `)
1750
+ const fns = new Function(code + '; return { a, b, c }')()
1751
+
1752
+ // Create error at a, flow through b and c
1753
+ const originalError = fns.a(42)
1754
+ expect(originalError).toBeInstanceOf(MonadicError)
1755
+
1756
+ const throughB = fns.b(originalError)
1757
+ const throughC = fns.c(throughB)
1758
+
1759
+ // Same object reference all the way through
1760
+ expect(throughB).toBe(originalError)
1761
+ expect(throughC).toBe(originalError)
1762
+ })
1763
+
1764
+ it('multi-level nested call propagation', () => {
1765
+ const { code } = tjs(`
1766
+ function validate(x: '') -! '' { return x }
1767
+ function transform(x: '') -! '' { return x.toUpperCase() }
1768
+ function format(x: '') -! '' { return x + '!' }
1769
+ `)
1770
+ const fns = new Function(
1771
+ code + '; return { validate, transform, format }'
1772
+ )()
1773
+
1774
+ // format(transform(validate(42)))
1775
+ // validate(42) → error, transform(error) → pass-through, format(error) → pass-through
1776
+ const result = fns.format(fns.transform(fns.validate(42)))
1777
+
1778
+ expect(result).toBeInstanceOf(MonadicError)
1779
+ expect(result.path).toContain('validate.x')
1780
+ })
1781
+
1782
+ it('error short-circuits function body', () => {
1783
+ let bodyExecuted = false
1784
+
1785
+ const { code } = tjs(`
1786
+ function process(x: '') -! '' {
1787
+ globalThis.__test_body_ran = true
1788
+ return x.toUpperCase()
1789
+ }
1790
+ `)
1791
+ globalThis.__test_body_ran = false
1792
+ const process = new Function(code + '; return process')()
1793
+
1794
+ // Pass an error — body should NOT execute
1795
+ const inputError = new MonadicError(
1796
+ 'upstream',
1797
+ 'upstream.x',
1798
+ 'string',
1799
+ 'number'
1800
+ )
1801
+ const result = process(inputError)
1802
+
1803
+ expect(result).toBe(inputError)
1804
+ expect(globalThis.__test_body_ran).toBe(false)
1805
+
1806
+ delete globalThis.__test_body_ran
1807
+ })
1808
+ })
1809
+
1605
1810
  describe('return type default keys', () => {
1606
1811
  it('signature test passes when optional key is absent', () => {
1607
1812
  const result = tjs(`
@@ -623,7 +623,8 @@ export function runAllTests(
623
623
  // TJS stub setup/restore
624
624
  const tjsStub = `
625
625
  const __saved_tjs = globalThis.__tjs;
626
- const __stub_tjs = { version: '0.0.0', pushStack: () => {}, typeError: (path, expected, value) => new Error(\`Type error at \${path}: expected \${expected}\`), createRuntime: function() { return this; } };
626
+ class __MonadicError extends Error { constructor(m,p,e,a,c){super(m);this.name='MonadicError';this.path=p;this.expected=e;this.actual=a;this.callStack=c;} }
627
+ const __stub_tjs = { version: '0.0.0', MonadicError: __MonadicError, pushStack: () => {}, popStack: () => {}, getStack: () => [], typeError: (path, expected, value) => new __MonadicError(\`Type error at \${path}: expected \${expected}\`, path, expected, typeof value), createRuntime: function() { return this; } };
627
628
  globalThis.__tjs = __stub_tjs;
628
629
  `
629
630
  const tjsRestore = `globalThis.__tjs = __saved_tjs;`
@@ -832,7 +833,8 @@ function runTestBlocks(
832
833
  // Save and restore globalThis.__tjs to prevent pollution
833
834
  const tjsStub = `
834
835
  const __saved_tjs = globalThis.__tjs;
835
- const __stub_tjs = { version: '0.0.0', pushStack: () => {}, typeError: (path, expected, value) => new Error(\`Type error at \${path}: expected \${expected}\`), createRuntime: function() { return this; } };
836
+ class __MonadicError extends Error { constructor(m,p,e,a,c){super(m);this.name='MonadicError';this.path=p;this.expected=e;this.actual=a;this.callStack=c;} }
837
+ const __stub_tjs = { version: '0.0.0', MonadicError: __MonadicError, pushStack: () => {}, popStack: () => {}, getStack: () => [], typeError: (path, expected, value) => new __MonadicError(\`Type error at \${path}: expected \${expected}\`, path, expected, typeof value), createRuntime: function() { return this; } };
836
838
  globalThis.__tjs = __stub_tjs;
837
839
  `
838
840
  const tjsRestore = `globalThis.__tjs = __saved_tjs;`
@@ -1169,7 +1171,8 @@ function runSignatureTest(
1169
1171
  // Save and restore globalThis.__tjs to prevent pollution
1170
1172
  const tjsStub = `
1171
1173
  const __saved_tjs = globalThis.__tjs;
1172
- const __stub_tjs = { version: '0.0.0', pushStack: () => {}, typeError: (path, expected, value) => new Error(\`Type error at \${path}: expected \${expected}\`), createRuntime: function() { return this; } };
1174
+ class __MonadicError extends Error { constructor(m,p,e,a,c){super(m);this.name='MonadicError';this.path=p;this.expected=e;this.actual=a;this.callStack=c;} }
1175
+ const __stub_tjs = { version: '0.0.0', MonadicError: __MonadicError, pushStack: () => {}, popStack: () => {}, getStack: () => [], typeError: (path, expected, value) => new __MonadicError(\`Type error at \${path}: expected \${expected}\`, path, expected, typeof value), createRuntime: function() { return this; } };
1173
1176
  globalThis.__tjs = __stub_tjs;
1174
1177
  `
1175
1178
  const tjsRestore = `globalThis.__tjs = __saved_tjs;`
@@ -279,15 +279,12 @@ function generateInlineValidationCode(
279
279
  funcName: string,
280
280
  types: TJSTypeInfo,
281
281
  source?: string
282
- ): string | null {
282
+ ): { preamble: string; suffix: string } | null {
283
283
  const lines: string[] = []
284
284
  // Include source in path if available: "src/file.ts:42:funcName.param"
285
285
  const pathPrefix = source ? `${source}:` : ''
286
286
  const stackEntry = source ? `${source}:${funcName}` : funcName
287
287
 
288
- // Push onto call stack for debug mode (only runs if debug enabled)
289
- lines.push(`__tjs.pushStack('${stackEntry}');`)
290
-
291
288
  // Destructured params: validate each field of the input object
292
289
  if (types.isDestructuredParam && types.destructuredShape) {
293
290
  const shape = types.destructuredShape
@@ -321,7 +318,17 @@ function generateInlineValidationCode(
321
318
  }
322
319
  }
323
320
 
324
- return lines.length > 0 ? lines.join('\n ') : null
321
+ if (lines.length === 0) return null
322
+
323
+ // Push stack first, then validate — callStack includes current function
324
+ // finally block ensures popStack on all exit paths
325
+ lines.unshift(`__tjs.pushStack('${stackEntry}');`)
326
+ lines.unshift(`try {`)
327
+
328
+ return {
329
+ preamble: lines.join('\n '),
330
+ suffix: '} finally { __tjs.popStack(); }',
331
+ }
325
332
  }
326
333
 
327
334
  // Positional params: validate each param
@@ -352,7 +359,17 @@ function generateInlineValidationCode(
352
359
  }
353
360
  }
354
361
 
355
- return lines.length > 0 ? lines.join('\n ') : null
362
+ if (lines.length === 0) return null
363
+
364
+ // Push stack first, then validate — callStack includes current function
365
+ // finally block ensures popStack on all exit paths
366
+ lines.unshift(`__tjs.pushStack('${stackEntry}');`)
367
+ lines.unshift(`try {`)
368
+
369
+ return {
370
+ preamble: lines.join('\n '),
371
+ suffix: '} finally { __tjs.popStack(); }',
372
+ }
356
373
  }
357
374
 
358
375
  /**
@@ -612,16 +629,21 @@ export function transpileToJS(
612
629
  // Skip for unsafe functions and polymorphic dispatchers (they handle routing)
613
630
  if (!isUnsafe && !isPolymorphicDispatcher) {
614
631
  const sourceStr = `${funcLoc.file}:${funcLoc.line}`
615
- const validationCode = generateInlineValidationCode(
632
+ const validation = generateInlineValidationCode(
616
633
  funcName,
617
634
  types,
618
635
  sourceStr
619
636
  )
620
- if (validationCode && func.body && func.body.start !== undefined) {
621
- // Insert right after the opening brace
637
+ if (validation && func.body && func.body.start !== undefined) {
638
+ // Insert preamble right after the opening brace
622
639
  insertions.push({
623
640
  position: func.body.start + 1,
624
- text: `\n ${validationCode}\n`,
641
+ text: `\n ${validation.preamble}\n`,
642
+ })
643
+ // Insert suffix (popStack) right before the closing brace
644
+ insertions.push({
645
+ position: func.body.end - 1,
646
+ text: `\n ${validation.suffix}\n`,
625
647
  })
626
648
  }
627
649
  }
@@ -638,11 +660,12 @@ export function transpileToJS(
638
660
  // Add __tjs reference for monadic error handling and structural equality
639
661
  // Use createRuntime() for isolated state per-module
640
662
  const needsTypeError = code.includes('__tjs.typeError(')
663
+ const needsStack = code.includes('__tjs.pushStack(')
641
664
  const needsIs = code.includes('Is(')
642
665
  const needsIsNot = code.includes('IsNot(')
643
666
  const needsSafeEval = preprocessed.tjsModes.tjsSafeEval
644
667
 
645
- if (needsTypeError || needsIs || needsIsNot || needsSafeEval) {
668
+ if (needsTypeError || needsStack || needsIs || needsIsNot || needsSafeEval) {
646
669
  // Create isolated runtime instance for this module
647
670
  // Falls back to shared global if createRuntime not available
648
671
  let preamble =
@@ -906,7 +929,7 @@ function canUseInlineValidation(types: TJSTypeInfo): boolean {
906
929
  * if (typeof input !== 'object' || input === null ||
907
930
  * typeof input.x !== 'number' ||
908
931
  * typeof input.y !== 'number') {
909
- * return { $error: true, message: '...', path: 'funcName.input' }
932
+ * return __tjs.typeError('funcName.input', 'object', input)
910
933
  * }
911
934
  */
912
935
  export function generateInlineValidation(
@@ -942,7 +965,7 @@ export function generateInlineValidation(
942
965
  if (checks.length === 0) return ''
943
966
 
944
967
  return `if (${checks.join(' || ')}) {
945
- return { $error: true, message: 'Invalid ${paramName}', path: '${path}' }
968
+ return __tjs.typeError('${path}', 'object', ${paramName})
946
969
  }`
947
970
  }
948
971
 
@@ -997,7 +1020,7 @@ const generateTypeCheck = generateTypeCheckExpr
997
1020
  * const _original_funcName = funcName
998
1021
  * funcName = function(__input) {
999
1022
  * if (typeof __input !== 'object' || __input === null || ...) {
1000
- * return { $error: true, message: '...', path: '...' }
1023
+ * return __tjs.typeError('funcName.input', 'object', __input)
1001
1024
  * }
1002
1025
  * return _original_funcName.call(this, __input)
1003
1026
  * }
@@ -799,9 +799,9 @@ function parse(s: '') {
799
799
  }
800
800
  }
801
801
  `)
802
- // Should add a catch block that returns monadic error
802
+ // Should add a catch block that returns a MonadicError
803
803
  expect(result.source).toContain('catch (__try_err)')
804
- expect(result.source).toContain('$error: true')
804
+ expect(result.source).toContain('MonadicError')
805
805
  expect(result.source).toContain('__try_err?.message')
806
806
  })
807
807
 
@@ -851,7 +851,7 @@ function safeParse(s: '') {
851
851
  )
852
852
  // The transpiled code should have the monadic error catch
853
853
  expect(result.code).toContain('catch (__try_err)')
854
- expect(result.code).toContain('$error: true')
854
+ expect(result.code).toContain('MonadicError')
855
855
  })
856
856
 
857
857
  it('monadic error should have proper structure', () => {
@@ -861,11 +861,10 @@ function test() {
861
861
  try { throw new Error('oops') }
862
862
  }
863
863
  `)
864
- // Check error structure
865
- expect(result.source).toContain("op: 'try'")
866
- expect(result.source).toContain('cause: __try_err')
867
- // Should capture call stack for debugging
868
- expect(result.source).toContain('stack: globalThis.__tjs?.getStack?.()')
864
+ // Should return a MonadicError to maintain monadic flow
865
+ expect(result.source).toContain('MonadicError')
866
+ expect(result.source).toContain('__try_err?.message')
867
+ expect(result.source).toContain('return new')
869
868
  })
870
869
  })
871
870
 
@@ -51,10 +51,10 @@ export function transformTryWithoutCatch(source: string): string {
51
51
  result += source.slice(i, j)
52
52
  i = j
53
53
  } else {
54
- // No catch or finally - add monadic error handler with call stack
55
- // In debug mode, __tjs.getStack() returns the call stack for diagnostics
54
+ // No catch or finally - add monadic error handler
55
+ // Returns MonadicError to maintain monadic flow (propagates through function chains)
56
56
  const body = source.slice(bodyStart, j - 1)
57
- result += `try {${body}} catch (__try_err) { return { $error: true, message: __try_err?.message || String(__try_err), op: 'try', cause: __try_err, stack: globalThis.__tjs?.getStack?.() } }`
57
+ result += `try {${body}} catch (__try_err) { return new (__tjs?.MonadicError ?? Error)(__try_err?.message || String(__try_err), 'try', undefined, undefined, __tjs?.getStack?.()) }`
58
58
  i = j
59
59
  }
60
60
  } else {
@@ -366,7 +366,7 @@ describe('TJS Performance', () => {
366
366
  // Sanity check
367
367
  expect(tryThrow(42)).toBe(42)
368
368
  const err = tryThrow(-1)
369
- expect(err.$error).toBe(true)
369
+ expect(err).toBeInstanceOf(Error)
370
370
  })
371
371
  })
372
372
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { describe, test, expect, beforeAll } from 'bun:test'
14
14
  import { tjs } from './index'
15
- import { Is, IsNot } from './runtime'
15
+ import { Is, IsNot, MonadicError } from './runtime'
16
16
 
17
17
  // Set up the minimal runtime needed for transpiled code
18
18
  beforeAll(() => {
@@ -21,12 +21,15 @@ beforeAll(() => {
21
21
  IsNot,
22
22
  pushStack: () => {},
23
23
  popStack: () => {},
24
+ MonadicError,
24
25
  typeError: (path: string, expected: string, got: any) => {
25
- const err = new Error(
26
- `Type error at ${path}: expected ${expected}, got ${typeof got}`
26
+ const actual = got === null ? 'null' : typeof got
27
+ return new MonadicError(
28
+ `Expected ${expected} for '${path}', got ${actual}`,
29
+ path,
30
+ expected,
31
+ actual
27
32
  )
28
- ;(err as any).$error = true
29
- return err
30
33
  },
31
34
  createRuntime: () => (globalThis as any).__tjs,
32
35
  }