tjs-lang 0.7.6 → 0.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CLAUDE.md +101 -26
  2. package/bin/docs.js +4 -1
  3. package/demo/docs.json +46 -12
  4. package/demo/src/examples.test.ts +1 -0
  5. package/demo/src/imports.test.ts +16 -4
  6. package/demo/src/imports.ts +60 -15
  7. package/demo/src/playground-shared.ts +9 -8
  8. package/demo/src/tfs-worker.js +205 -147
  9. package/demo/src/tjs-playground.ts +34 -10
  10. package/demo/src/ts-playground.ts +24 -8
  11. package/dist/index.js +140 -119
  12. package/dist/index.js.map +4 -4
  13. package/dist/src/lang/bool-coercion.d.ts +50 -0
  14. package/dist/src/lang/docs.d.ts +31 -6
  15. package/dist/src/lang/linter.d.ts +8 -0
  16. package/dist/src/lang/parser-transforms.d.ts +18 -0
  17. package/dist/src/lang/parser-types.d.ts +2 -0
  18. package/dist/src/lang/parser.d.ts +9 -0
  19. package/dist/src/lang/runtime.d.ts +34 -0
  20. package/dist/src/lang/types.d.ts +9 -1
  21. package/dist/src/rbac/index.d.ts +1 -1
  22. package/dist/src/vm/runtime.d.ts +1 -1
  23. package/dist/tjs-eval.js +44 -39
  24. package/dist/tjs-eval.js.map +4 -4
  25. package/dist/tjs-from-ts.js +20 -20
  26. package/dist/tjs-from-ts.js.map +3 -3
  27. package/dist/tjs-lang.js +86 -80
  28. package/dist/tjs-lang.js.map +4 -4
  29. package/dist/tjs-vm.js +50 -45
  30. package/dist/tjs-vm.js.map +4 -4
  31. package/llms.txt +79 -0
  32. package/package.json +3 -2
  33. package/src/cli/commands/convert.test.ts +16 -21
  34. package/src/lang/bool-coercion.test.ts +203 -0
  35. package/src/lang/bool-coercion.ts +314 -0
  36. package/src/lang/codegen.test.ts +177 -0
  37. package/src/lang/docs.test.ts +328 -1
  38. package/src/lang/docs.ts +424 -24
  39. package/src/lang/emitters/ast.ts +11 -12
  40. package/src/lang/emitters/dts.test.ts +41 -0
  41. package/src/lang/emitters/dts.ts +9 -0
  42. package/src/lang/emitters/js-tests.ts +16 -4
  43. package/src/lang/emitters/js.ts +208 -2
  44. package/src/lang/features.test.ts +22 -0
  45. package/src/lang/inference.ts +54 -0
  46. package/src/lang/linter.test.ts +104 -1
  47. package/src/lang/linter.ts +124 -1
  48. package/src/lang/parser-params.ts +31 -0
  49. package/src/lang/parser-transforms.ts +539 -6
  50. package/src/lang/parser-types.ts +2 -0
  51. package/src/lang/parser.test.ts +73 -1
  52. package/src/lang/parser.ts +85 -1
  53. package/src/lang/runtime.ts +98 -0
  54. package/src/lang/tests.ts +21 -8
  55. package/src/lang/types.ts +6 -0
  56. package/src/rbac/index.ts +2 -2
  57. package/src/rbac/rules.tjs.d.ts +9 -0
  58. package/src/vm/atoms/batteries.ts +2 -2
  59. package/src/vm/runtime.ts +10 -3
  60. package/dist/src/rbac/rules.d.ts +0 -184
  61. package/src/rbac/rules.js +0 -338
@@ -51,11 +51,58 @@ import {
51
51
  transformConstBang,
52
52
  transformBangAccess,
53
53
  transformExtensionCalls,
54
+ transformLetTypeAnnotations,
54
55
  } from './parser-transforms'
55
56
 
56
57
  // Re-export transformExtensionCalls for js.ts
57
58
  export { transformExtensionCalls } from './parser-transforms'
58
59
 
60
+ /**
61
+ * Strip single-line comments (//) from source.
62
+ * Replaces comment content with spaces to preserve character offsets.
63
+ * Skips // inside strings and block comments.
64
+ */
65
+ export function stripLineComments(source: string): string {
66
+ let result = ''
67
+ let i = 0
68
+ while (i < source.length) {
69
+ const ch = source[i]
70
+ // String literals — skip to closing quote
71
+ if (ch === "'" || ch === '"' || ch === '`') {
72
+ const quote = ch
73
+ result += ch
74
+ i++
75
+ while (i < source.length && source[i] !== quote) {
76
+ if (source[i] === '\\') {
77
+ result += source[i++]
78
+ }
79
+ if (i < source.length) result += source[i++]
80
+ }
81
+ if (i < source.length) result += source[i++] // closing quote
82
+ continue
83
+ }
84
+ // Block comment — pass through (may contain //)
85
+ if (ch === '/' && source[i + 1] === '*') {
86
+ const end = source.indexOf('*/', i + 2)
87
+ const slice = end === -1 ? source.slice(i) : source.slice(i, end + 2)
88
+ result += slice
89
+ i += slice.length
90
+ continue
91
+ }
92
+ // Line comment — replace with spaces to preserve offsets
93
+ if (ch === '/' && source[i + 1] === '/') {
94
+ const nl = source.indexOf('\n', i)
95
+ const end = nl === -1 ? source.length : nl
96
+ result += ' '.repeat(end - i)
97
+ i = end // leave \n for next iteration
98
+ continue
99
+ }
100
+ result += ch
101
+ i++
102
+ }
103
+ return result
104
+ }
105
+
59
106
  export function preprocess(
60
107
  source: string,
61
108
  options: PreprocessOptions = {}
@@ -74,6 +121,7 @@ export function preprocess(
74
121
  testErrors: string[]
75
122
  polymorphicNames: Set<string>
76
123
  extensions: Map<string, Set<string>>
124
+ letAnnotations: Map<string, string>
77
125
  } {
78
126
  const originalSource = source
79
127
  let moduleSafety: 'none' | 'inputs' | 'all' | undefined
@@ -97,6 +145,7 @@ export function preprocess(
97
145
  tjsStandard: false,
98
146
  tjsSafeEval: false,
99
147
  tjsNoVar: false,
148
+ tjsSafeAssign: false,
100
149
  }
101
150
  : {
102
151
  tjsEquals: true,
@@ -106,6 +155,7 @@ export function preprocess(
106
155
  tjsStandard: true,
107
156
  tjsSafeEval: false, // opt-in only (adds import)
108
157
  tjsNoVar: true,
158
+ tjsSafeAssign: true,
109
159
  }
110
160
 
111
161
  // Safety: native TJS defaults to 'inputs' (runtime default),
@@ -134,7 +184,7 @@ export function preprocess(
134
184
  // TjsCompat disables all TJS modes (useful for native TJS opting out)
135
185
  // Individual modes: TjsEquals, TjsClass, TjsDate, TjsNoeval, TjsStandard, TjsSafeEval
136
186
  const directivePattern =
137
- /^(\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*)\s*(TjsStrict|TjsCompat|TjsEquals|TjsClass|TjsDate|TjsNoeval|TjsNoVar|TjsStandard|TjsSafeEval)\b/
187
+ /^(\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*)\s*(TjsStrict|TjsCompat|TjsEquals|TjsClass|TjsDate|TjsNoeval|TjsNoVar|TjsStandard|TjsSafeEval|TjsSafeAssign)\b/
138
188
 
139
189
  let match
140
190
  while ((match = source.match(directivePattern))) {
@@ -148,6 +198,7 @@ export function preprocess(
148
198
  tjsModes.tjsNoeval = true
149
199
  tjsModes.tjsNoVar = true
150
200
  tjsModes.tjsStandard = true
201
+ tjsModes.tjsSafeAssign = true
151
202
  } else if (directive === 'TjsCompat') {
152
203
  // Disable all TJS modes (JS-compatible)
153
204
  tjsModes.tjsEquals = false
@@ -157,6 +208,7 @@ export function preprocess(
157
208
  tjsModes.tjsNoVar = false
158
209
  tjsModes.tjsStandard = false
159
210
  tjsModes.tjsSafeEval = false
211
+ tjsModes.tjsSafeAssign = false
160
212
  } else if (directive === 'TjsEquals') {
161
213
  tjsModes.tjsEquals = true
162
214
  } else if (directive === 'TjsClass') {
@@ -171,6 +223,8 @@ export function preprocess(
171
223
  tjsModes.tjsStandard = true
172
224
  } else if (directive === 'TjsSafeEval') {
173
225
  tjsModes.tjsSafeEval = true
226
+ } else if (directive === 'TjsSafeAssign') {
227
+ tjsModes.tjsSafeAssign = true
174
228
  }
175
229
 
176
230
  // Remove the directive from source
@@ -182,6 +236,11 @@ export function preprocess(
182
236
  )
183
237
  }
184
238
 
239
+ // Strip single-line comments early — they confuse brace matching,
240
+ // ASI protection, and test extraction (e.g. apostrophes in comments)
241
+ // Preserves line structure by keeping the newline
242
+ source = stripLineComments(source)
243
+
185
244
  // TjsStandard mode: insert semicolons to prevent ASI footguns
186
245
  // Must happen early before other transformations modify line structure
187
246
  if (tjsModes.tjsStandard) {
@@ -196,6 +255,13 @@ export function preprocess(
196
255
  // Must happen before acorn parsing since !. is not valid JS
197
256
  source = transformBangAccess(source)
198
257
 
258
+ // Transform `let x: <example>` declarations: strip annotation and record
259
+ // varName -> example. Must happen before paren transforms so the colon
260
+ // is not confused with TS-style annotations on params/returns.
261
+ const letAnnoResult = transformLetTypeAnnotations(source)
262
+ source = letAnnoResult.source
263
+ const letAnnotations = letAnnoResult.annotations
264
+
199
265
  // Transform Is/IsNot infix operators to function calls
200
266
  // a Is b -> Is(a, b)
201
267
  // a IsNot b -> IsNot(a, b)
@@ -320,6 +386,7 @@ export function preprocess(
320
386
  testErrors: testResult.errors,
321
387
  polymorphicNames: polyResult.polymorphicNames,
322
388
  extensions: extResult.extensions,
389
+ letAnnotations,
323
390
  }
324
391
  }
325
392
 
@@ -341,6 +408,8 @@ export function parse(
341
408
  wasmBlocks: WasmBlock[]
342
409
  tests: TestBlock[]
343
410
  testErrors: string[]
411
+ letAnnotations: Map<string, string>
412
+ tjsModes: TjsModes
344
413
  } {
345
414
  const {
346
415
  filename = '<source>',
@@ -361,6 +430,8 @@ export function parse(
361
430
  wasmBlocks,
362
431
  tests,
363
432
  testErrors,
433
+ letAnnotations,
434
+ tjsModes,
364
435
  } = colonShorthand
365
436
  ? preprocess(source, { vmTarget })
366
437
  : {
@@ -375,6 +446,17 @@ export function parse(
375
446
  wasmBlocks: [] as WasmBlock[],
376
447
  tests: [] as TestBlock[],
377
448
  testErrors: [] as string[],
449
+ letAnnotations: new Map<string, string>(),
450
+ tjsModes: {
451
+ tjsEquals: false,
452
+ tjsClass: false,
453
+ tjsDate: false,
454
+ tjsNoeval: false,
455
+ tjsStandard: false,
456
+ tjsSafeEval: false,
457
+ tjsNoVar: false,
458
+ tjsSafeAssign: false,
459
+ } as TjsModes,
378
460
  }
379
461
 
380
462
  try {
@@ -397,6 +479,8 @@ export function parse(
397
479
  wasmBlocks,
398
480
  tests,
399
481
  testErrors,
482
+ letAnnotations,
483
+ tjsModes,
400
484
  }
401
485
  } catch (e: any) {
402
486
  // Convert Acorn error to our error type
@@ -605,6 +605,26 @@ export function TypeOf(value: unknown): string {
605
605
  return typeof value
606
606
  }
607
607
 
608
+ /**
609
+ * Honest boolean coercion. Like `Boolean(x)` but unwraps boxed primitives
610
+ * first, fixing the JS footgun `Boolean(new Boolean(false)) === true`.
611
+ *
612
+ * Under TjsStandard, the source rewriter wraps every truthiness context
613
+ * (if/while/for/do-while conditions, `!`, `&&`, `||`, ternary, and
614
+ * top-level `Boolean(x)` calls) with this function so a boxed `false`
615
+ * actually behaves as `false`.
616
+ */
617
+ export function toBool(value: unknown): boolean {
618
+ if (
619
+ value instanceof Boolean ||
620
+ value instanceof Number ||
621
+ value instanceof String
622
+ ) {
623
+ return Boolean((value as any).valueOf())
624
+ }
625
+ return Boolean(value)
626
+ }
627
+
608
628
  export function Eq(a: unknown, b: unknown): boolean {
609
629
  // Unwrap boxed primitives
610
630
  if (a instanceof String || a instanceof Number || a instanceof Boolean) {
@@ -844,6 +864,78 @@ type TypeSpec =
844
864
  | string
845
865
  | { check: (v: unknown) => boolean | string; description: string }
846
866
 
867
+ /**
868
+ * Check that a passed-in function's declared shape matches the expected
869
+ * shape. Returns the function unchanged on a match, or a MonadicError on
870
+ * mismatch. Untyped functions (no `__tjs` metadata — anonymous arrows
871
+ * like `x => false`) pass through unchanged on the assumption that the
872
+ * caller knows what they're doing; they accept any args and return
873
+ * whatever they return.
874
+ *
875
+ * This is a ONE-SHOT check at pass time, NOT a per-call wrapper. The TJS
876
+ * design call: a wrong-shape callback is ONE error at the boundary, not
877
+ * N errors when the receiving function invokes the callback N times.
878
+ *
879
+ * Compatibility rules (deliberately permissive — strict subtyping is a
880
+ * separate, larger feature):
881
+ * - For each expected param: the actual function may declare fewer
882
+ * params (extras simply not used). If both declare a kind, they
883
+ * must match exactly. Either side being `any` always matches.
884
+ * - For the return type: same exact-match rule when both are known.
885
+ */
886
+ export function checkFnShape(
887
+ fn: unknown,
888
+ expectedParamKinds: string[],
889
+ expectedReturnKind: string,
890
+ path: string
891
+ ): unknown {
892
+ if (typeof fn !== 'function') return fn // outer "is callable" check already ran
893
+ const meta = (fn as any).__tjs
894
+ if (!meta || !meta.params) return fn // untyped — let it run
895
+
896
+ const actualEntries = Object.entries(meta.params) as Array<
897
+ [string, { type?: { kind?: string } }]
898
+ >
899
+ for (let i = 0; i < expectedParamKinds.length; i++) {
900
+ const expectedKind = expectedParamKinds[i]
901
+ if (expectedKind === 'any') continue
902
+ const actual = actualEntries[i]
903
+ if (!actual) continue // function takes fewer params, OK
904
+ const actualKind = actual[1]?.type?.kind
905
+ if (!actualKind || actualKind === 'any') continue
906
+ if (actualKind !== expectedKind) {
907
+ return new MonadicError(
908
+ `Expected (...arg${i}: ${expectedKind}, ...) for '${path}', ` +
909
+ `but callback declares arg${i} as ${actualKind}`,
910
+ `${path}(arg${i})`,
911
+ expectedKind,
912
+ actualKind
913
+ )
914
+ }
915
+ }
916
+
917
+ if (expectedReturnKind !== 'any' && meta.returns) {
918
+ // Metadata's `returns` is `{ type: TypeDescriptor, defaults?: ... }`,
919
+ // but defensively also accept a bare TypeDescriptor.
920
+ const actualReturnKind = meta.returns.type?.kind ?? meta.returns.kind
921
+ if (
922
+ actualReturnKind &&
923
+ actualReturnKind !== 'any' &&
924
+ actualReturnKind !== expectedReturnKind
925
+ ) {
926
+ return new MonadicError(
927
+ `Expected callback returning ${expectedReturnKind} for '${path}', ` +
928
+ `but callback returns ${actualReturnKind}`,
929
+ `${path}(return)`,
930
+ expectedReturnKind,
931
+ actualReturnKind
932
+ )
933
+ }
934
+ }
935
+
936
+ return fn
937
+ }
938
+
847
939
  /** Parameter metadata with optional location */
848
940
  interface ParamMeta {
849
941
  type: TypeSpec
@@ -1478,6 +1570,7 @@ export function createRuntime() {
1478
1570
  checkType,
1479
1571
  validateArgs,
1480
1572
  wrap,
1573
+ checkFnShape,
1481
1574
  wrapClass,
1482
1575
  compareVersions,
1483
1576
  versionsCompatible,
@@ -1528,6 +1621,8 @@ export function createRuntime() {
1528
1621
  NotEq,
1529
1622
  // Honest typeof (typeof with TjsEquals)
1530
1623
  TypeOf,
1624
+ // Honest truthiness (unwraps boxed primitives)
1625
+ toBool,
1531
1626
  tjsEquals,
1532
1627
  // Extensions
1533
1628
  registerExtension: instanceRegisterExtension,
@@ -1559,6 +1654,7 @@ export const runtime = {
1559
1654
  checkType,
1560
1655
  validateArgs,
1561
1656
  wrap,
1657
+ checkFnShape,
1562
1658
  wrapClass,
1563
1659
  compareVersions,
1564
1660
  versionsCompatible,
@@ -1612,6 +1708,8 @@ export const runtime = {
1612
1708
  NotEq,
1613
1709
  // Honest typeof (used by typeof with TjsEquals)
1614
1710
  TypeOf,
1711
+ // Honest truthiness (used in TjsStandard for boxed-primitive coercion)
1712
+ toBool,
1615
1713
  }
1616
1714
 
1617
1715
  /**
package/src/lang/tests.ts CHANGED
@@ -106,15 +106,17 @@ function extractEmbeddedTests(source: string): ExtractedTest[] {
106
106
  const tests: ExtractedTest[] = []
107
107
 
108
108
  // Match: /*test 'description' { ... }*/ or /*test { ... }*/
109
+ // Each quote type gets its own alternative so the description can contain
110
+ // the other quote types (e.g. `test 'typeof null is "null"' {`).
109
111
  const embeddedRegex =
110
- /\/\*test\s+(['"`])([^'"`]*)\1\s*\{([\s\S]*?)\}\s*\*\/|\/\*test\s*\{([\s\S]*?)\}\s*\*\//g
112
+ /\/\*test\s+'([^']*)'\s*\{([\s\S]*?)\}\s*\*\/|\/\*test\s+"([^"]*)"\s*\{([\s\S]*?)\}\s*\*\/|\/\*test\s+`([^`]*)`\s*\{([\s\S]*?)\}\s*\*\/|\/\*test\s*\{([\s\S]*?)\}\s*\*\//g
111
113
 
112
114
  let match
113
115
  while ((match = embeddedRegex.exec(source)) !== null) {
114
- // Group 2 = description for quoted, group 3 = body for quoted
115
- // Group 4 = body for anonymous
116
- const desc = match[2] || `embedded test ${tests.length + 1}`
117
- const body = (match[3] || match[4] || '').trim()
116
+ // Groups: 1/3/5 = description for ' " ` ; 2/4/6 = body for ' " ` ; 7 = body for anonymous
117
+ const desc =
118
+ match[1] || match[3] || match[5] || `embedded test ${tests.length + 1}`
119
+ const body = (match[2] || match[4] || match[6] || match[7] || '').trim()
118
120
 
119
121
  tests.push({
120
122
  description: desc,
@@ -145,8 +147,10 @@ export function extractTests(source: string): TestExtractionResult {
145
147
  // test { ... } (anonymous test)
146
148
  // test 'description' { ... } (canonical TJS)
147
149
  // test('description') { ... } (also valid - parenthesized string is still a string)
150
+ // Each quote type has its own alternative so the description can contain
151
+ // the other quote types (e.g. `test 'typeof null is "null"' {`).
148
152
  const testRegex =
149
- /test\s+(['"`])([^'"`]*)\1\s*\{|test\s*\(\s*(['"`])([^'"`]*)\3\s*\)\s*\{|test\s*\{/g
153
+ /test\s+'([^']*)'\s*\{|test\s+"([^"]*)"\s*\{|test\s+`([^`]*)`\s*\{|test\s*\(\s*'([^']*)'\s*\)\s*\{|test\s*\(\s*"([^"]*)"\s*\)\s*\{|test\s*\(\s*`([^`]*)`\s*\)\s*\{|test\s*\{/g
150
154
  const mockRegex = /mock\s*\{/g
151
155
 
152
156
  let cleanCode = source
@@ -164,8 +168,17 @@ export function extractTests(source: string): TestExtractionResult {
164
168
  continue
165
169
  }
166
170
 
167
- // Description is in group 2 for `test 'desc'`, group 4 for `test('desc')`, or undefined for `test {`
168
- const desc = match[2] || match[4] || `test ${tests.length + 1}`
171
+ // Groups 1/2/3 = `test 'desc'` / `test "desc"` / `test \`desc\``
172
+ // Groups 4/5/6 = parenthesized variants
173
+ // No group when description is omitted
174
+ const desc =
175
+ match[1] ||
176
+ match[2] ||
177
+ match[3] ||
178
+ match[4] ||
179
+ match[5] ||
180
+ match[6] ||
181
+ `test ${tests.length + 1}`
169
182
  const bodyStart = match.index + match[0].length
170
183
 
171
184
  // Find matching closing brace
package/src/lang/types.ts CHANGED
@@ -22,6 +22,7 @@ export interface TypeDescriptor {
22
22
  | 'array'
23
23
  | 'object'
24
24
  | 'union'
25
+ | 'function'
25
26
  | 'any'
26
27
  nullable?: boolean
27
28
  /** For arrays: the element type */
@@ -32,6 +33,11 @@ export interface TypeDescriptor {
32
33
  members?: TypeDescriptor[]
33
34
  /** For destructured parameters: full parameter descriptors */
34
35
  destructuredParams?: Record<string, ParameterDescriptor>
36
+ /** For functions: declared parameters with names and inferred types */
37
+ params?: Array<{ name: string; type: TypeDescriptor }>
38
+ /** For functions: inferred return type. Concise arrow bodies infer from
39
+ * the expression; block bodies and complex expressions stay `any`. */
40
+ returns?: TypeDescriptor
35
41
  }
36
42
 
37
43
  /** Describes a function parameter */
package/src/rbac/index.ts CHANGED
@@ -46,7 +46,7 @@ export {
46
46
  interpretRuleResult,
47
47
  hasRoleLevel,
48
48
  buildRuleContext,
49
- } from './rules.js'
49
+ } from './rules.tjs'
50
50
 
51
51
  /**
52
52
  * Security rule definition
@@ -125,7 +125,7 @@ import {
125
125
  validateSchema,
126
126
  buildRuleContext,
127
127
  interpretRuleResult,
128
- } from './rules.js'
128
+ } from './rules.tjs'
129
129
 
130
130
  /**
131
131
  * Create an RBAC instance with a store backend
@@ -0,0 +1,9 @@
1
+ // Ambient declarations for rules.tjs. The bun plugin (bunfig.toml)
2
+ // transpiles .tjs at runtime; tsc needs explicit names to resolve the
3
+ // `from './rules.tjs'` imports in index.ts.
4
+ export function evaluateAccessShortcut(accessRule: any, context: any): any
5
+ export function selectAccessRule(rule: any, context: any): any
6
+ export function validateSchema(schema: any, data: any): any
7
+ export function interpretRuleResult(result: any): any
8
+ export function hasRoleLevel(userRoles: any, requiredRole: any): any
9
+ export function buildRuleContext(options: any): any
@@ -25,7 +25,7 @@ interface StoreBattery {
25
25
  interface LLMBattery {
26
26
  predict(
27
27
  system: string,
28
- user: string,
28
+ user: string | any[], // string for single-turn, message array for multi-turn
29
29
  tools?: any[],
30
30
  responseFormat?: any
31
31
  ): Promise<any>
@@ -140,7 +140,7 @@ export const llmPredictBattery = defineAtom(
140
140
  'llmPredictBattery',
141
141
  s.object({
142
142
  system: s.string.optional,
143
- user: s.string,
143
+ user: s.union([s.string, s.array(s.any)]), // string or message array for multi-turn
144
144
  tools: s.array(s.any).optional,
145
145
  responseFormat: s.any.optional,
146
146
  }),
package/src/vm/runtime.ts CHANGED
@@ -451,7 +451,9 @@ export type ExprNode =
451
451
  | {
452
452
  $expr: 'member'
453
453
  object: ExprNode
454
- property: string
454
+ // string for static `obj.foo` and literal-indexed `arr[0]`;
455
+ // ExprNode for variable-indexed `arr[i]` (evaluated at runtime).
456
+ property: string | ExprNode
455
457
  computed?: boolean
456
458
  optional?: boolean
457
459
  }
@@ -1132,8 +1134,12 @@ export function evaluateExpr(node: ExprNode, ctx: RuntimeContext): any {
1132
1134
  return undefined
1133
1135
  }
1134
1136
 
1135
- const prop = node.property
1136
- assertSafeProperty(prop)
1137
+ // Property is either a static string or a computed expression node (e.g. arr[i])
1138
+ const prop =
1139
+ typeof node.property === 'object' && node.property !== null
1140
+ ? evaluateExpr(node.property, ctx)
1141
+ : node.property
1142
+ assertSafeProperty(String(prop))
1137
1143
 
1138
1144
  return obj?.[prop]
1139
1145
  }
@@ -1524,6 +1530,7 @@ export const whileLoop = defineAtom(
1524
1530
  if ((ctx.fuel.current -= 0.1) <= 0) throw new Error('Out of Fuel')
1525
1531
  await seq.exec({ op: 'seq', steps: step.body } as any, ctx)
1526
1532
  if (ctx.output !== undefined) return
1533
+ if (ctx.error) return // Propagate monadic errors out of the loop
1527
1534
  }
1528
1535
  },
1529
1536
  { docs: 'While Loop', timeoutMs: 0, cost: 0.1 }
@@ -1,184 +0,0 @@
1
- export function evaluateAccessShortcut(accessRule: any, context: any): {
2
- allowed: boolean;
3
- reason: string;
4
- } | {
5
- allowed: boolean;
6
- reason?: undefined;
7
- } | null;
8
- export namespace evaluateAccessShortcut {
9
- namespace __tjs {
10
- namespace params {
11
- namespace accessRule {
12
- namespace type {
13
- let kind: string;
14
- }
15
- let required: boolean;
16
- }
17
- namespace context {
18
- export namespace type_1 {
19
- let kind_1: string;
20
- export { kind_1 as kind };
21
- }
22
- export { type_1 as type };
23
- let required_1: boolean;
24
- export { required_1 as required };
25
- }
26
- }
27
- let unsafe: boolean;
28
- let source: string;
29
- }
30
- }
31
- export function selectAccessRule(rule: any, context: any): any;
32
- export namespace selectAccessRule {
33
- export namespace __tjs_1 {
34
- export namespace params_1 {
35
- export namespace rule {
36
- export namespace type_2 {
37
- let kind_2: string;
38
- export { kind_2 as kind };
39
- }
40
- export { type_2 as type };
41
- let required_2: boolean;
42
- export { required_2 as required };
43
- }
44
- export namespace context_1 {
45
- export namespace type_3 {
46
- let kind_3: string;
47
- export { kind_3 as kind };
48
- }
49
- export { type_3 as type };
50
- let required_3: boolean;
51
- export { required_3 as required };
52
- }
53
- export { context_1 as context };
54
- }
55
- export { params_1 as params };
56
- let unsafe_1: boolean;
57
- export { unsafe_1 as unsafe };
58
- let source_1: string;
59
- export { source_1 as source };
60
- }
61
- export { __tjs_1 as __tjs };
62
- }
63
- export function validateSchema(schema: any, data: any): {
64
- valid: boolean;
65
- errors: string[];
66
- };
67
- export namespace validateSchema {
68
- export namespace __tjs_2 {
69
- export namespace params_2 {
70
- namespace schema {
71
- export namespace type_4 {
72
- let kind_4: string;
73
- export { kind_4 as kind };
74
- }
75
- export { type_4 as type };
76
- let required_4: boolean;
77
- export { required_4 as required };
78
- }
79
- namespace data {
80
- export namespace type_5 {
81
- let kind_5: string;
82
- export { kind_5 as kind };
83
- }
84
- export { type_5 as type };
85
- let required_5: boolean;
86
- export { required_5 as required };
87
- }
88
- }
89
- export { params_2 as params };
90
- let unsafe_2: boolean;
91
- export { unsafe_2 as unsafe };
92
- let source_2: string;
93
- export { source_2 as source };
94
- }
95
- export { __tjs_2 as __tjs };
96
- }
97
- export function interpretRuleResult(result: any): {
98
- allowed: boolean;
99
- reason: any;
100
- };
101
- export namespace interpretRuleResult {
102
- export namespace __tjs_3 {
103
- export namespace params_3 {
104
- namespace result {
105
- export namespace type_6 {
106
- let kind_6: string;
107
- export { kind_6 as kind };
108
- }
109
- export { type_6 as type };
110
- let required_6: boolean;
111
- export { required_6 as required };
112
- }
113
- }
114
- export { params_3 as params };
115
- let unsafe_3: boolean;
116
- export { unsafe_3 as unsafe };
117
- let source_3: string;
118
- export { source_3 as source };
119
- }
120
- export { __tjs_3 as __tjs };
121
- }
122
- export function hasRoleLevel(userRoles: any, requiredRole: any): boolean;
123
- export namespace hasRoleLevel {
124
- export namespace __tjs_4 {
125
- export namespace params_4 {
126
- namespace userRoles {
127
- export namespace type_7 {
128
- let kind_7: string;
129
- export { kind_7 as kind };
130
- }
131
- export { type_7 as type };
132
- let required_7: boolean;
133
- export { required_7 as required };
134
- }
135
- namespace requiredRole {
136
- export namespace type_8 {
137
- let kind_8: string;
138
- export { kind_8 as kind };
139
- }
140
- export { type_8 as type };
141
- let required_8: boolean;
142
- export { required_8 as required };
143
- }
144
- }
145
- export { params_4 as params };
146
- let unsafe_4: boolean;
147
- export { unsafe_4 as unsafe };
148
- let source_4: string;
149
- export { source_4 as source };
150
- }
151
- export { __tjs_4 as __tjs };
152
- }
153
- export function buildRuleContext(options: any): {
154
- _uid: any;
155
- _roles: any;
156
- _isAdmin: any;
157
- _isAuthor: any;
158
- _method: any;
159
- _collection: any;
160
- _docId: any;
161
- doc: any;
162
- newData: any;
163
- };
164
- export namespace buildRuleContext {
165
- export namespace __tjs_5 {
166
- export namespace params_5 {
167
- namespace options {
168
- export namespace type_9 {
169
- let kind_9: string;
170
- export { kind_9 as kind };
171
- }
172
- export { type_9 as type };
173
- let required_9: boolean;
174
- export { required_9 as required };
175
- }
176
- }
177
- export { params_5 as params };
178
- let unsafe_5: boolean;
179
- export { unsafe_5 as unsafe };
180
- let source_5: string;
181
- export { source_5 as source };
182
- }
183
- export { __tjs_5 as __tjs };
184
- }