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 +8 -8
- package/demo/src/service-host.ts +3 -3
- package/demo/src/ts-examples.ts +3 -2
- package/demo/src/ts-playground.ts +4 -4
- package/package.json +1 -1
- package/src/lang/codegen.test.ts +205 -0
- package/src/lang/emitters/js-tests.ts +6 -3
- package/src/lang/emitters/js.ts +37 -14
- package/src/lang/features.test.ts +7 -8
- package/src/lang/parser-transforms.ts +3 -3
- package/src/lang/perf.test.ts +1 -1
- package/src/lang/roundtrip.test.ts +8 -5
package/demo/docs.json
CHANGED
|
@@ -67,9 +67,9 @@
|
|
|
67
67
|
"type": "example",
|
|
68
68
|
"group": "basics",
|
|
69
69
|
"order": 1,
|
|
70
|
-
"code": "/*#\
|
|
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": "
|
|
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
|
|
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
|
|
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
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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)",
|
package/demo/src/service-host.ts
CHANGED
|
@@ -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
|
|
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
|
}
|
package/demo/src/ts-examples.ts
CHANGED
|
@@ -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
|
|
167
|
-
console.log(' ^ This is a
|
|
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.
|
|
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",
|
package/src/lang/codegen.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;`
|
package/src/lang/emitters/js.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
632
|
+
const validation = generateInlineValidationCode(
|
|
616
633
|
funcName,
|
|
617
634
|
types,
|
|
618
635
|
sourceStr
|
|
619
636
|
)
|
|
620
|
-
if (
|
|
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 ${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
802
|
+
// Should add a catch block that returns a MonadicError
|
|
803
803
|
expect(result.source).toContain('catch (__try_err)')
|
|
804
|
-
expect(result.source).toContain('
|
|
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('
|
|
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
|
-
//
|
|
865
|
-
expect(result.source).toContain(
|
|
866
|
-
expect(result.source).toContain('
|
|
867
|
-
|
|
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
|
|
55
|
-
//
|
|
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
|
|
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 {
|
package/src/lang/perf.test.ts
CHANGED
|
@@ -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
|
|
26
|
-
|
|
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
|
}
|