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
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Boolean coercion rewriter.
3
+ *
4
+ * Fixes the JS footgun `Boolean(new Boolean(false)) === true` (and friends)
5
+ * by rewriting every truthiness context to call `__tjs.toBool(x)`, which
6
+ * unwraps boxed primitives before coercing.
7
+ *
8
+ * Contexts rewritten:
9
+ * if (cond) → if (__tjs.toBool(cond))
10
+ * while (cond) → while (__tjs.toBool(cond))
11
+ * do {} while (cond) → do {} while (__tjs.toBool(cond))
12
+ * for (_; cond; _) → for (_; __tjs.toBool(cond); _)
13
+ * !x → !__tjs.toBool(x)
14
+ * a && b → ((__tjs__t)=>__tjs.toBool(__tjs__t)?(b):__tjs__t)(a)
15
+ * a || b → ((__tjs__t)=>__tjs.toBool(__tjs__t)?__tjs__t:(b))(a)
16
+ * a ? b : c → __tjs.toBool(a)?(b):(c)
17
+ * Boolean(x) → __tjs.toBool(x) (call form, not `new`)
18
+ *
19
+ * `??` (nullish coalescing) is intentionally NOT rewritten — its semantics
20
+ * are about null/undefined specifically, not truthiness, so boxed primitives
21
+ * behave correctly already.
22
+ *
23
+ * `===` / `!==` (identity) are also not touched — that's a separate
24
+ * footgun handled by the `Is` / `Eq` operators under TjsEquals.
25
+ *
26
+ * Always-on under TjsStandard.
27
+ */
28
+ import type { Program } from 'acorn';
29
+ export interface BoolCoercionPatch {
30
+ start: number;
31
+ end: number;
32
+ newText: string;
33
+ }
34
+ /**
35
+ * Walk the AST and emit replacement patches for every truthiness context.
36
+ * Patches are pre-deduped: nested coercions inside an outer patch are
37
+ * folded into the outer patch's newText, so returned patches don't overlap.
38
+ */
39
+ export declare function rewriteBoolCoercion(ast: Program, source: string): BoolCoercionPatch[];
40
+ /**
41
+ * Source-text wrapper: parse, rewrite, re-emit. Used for code that's
42
+ * extracted before the main parse (test/mock bodies) but still needs the
43
+ * coercion rewrite to behave consistently with the rest of the module.
44
+ *
45
+ * Wraps the body in a function so top-level statements like `expect(...)`
46
+ * parse as a Program. Returns the original source unchanged if parsing
47
+ * fails (rather than throwing — a bad test body would already have been
48
+ * caught by the main parse).
49
+ */
50
+ export declare function rewriteBoolCoercionInSource(source: string): string;
@@ -4,10 +4,13 @@
4
4
  * Dead simple: walk source in order, emit what you find.
5
5
  * - Doc blocks render as markdown
6
6
  * - Function signatures render as code blocks
7
+ * - Inline `test 'name' { ... }` blocks render as "Test Cases" section
8
+ * with `expect(...).toBe(...)` style assertions translated to comments
7
9
  *
8
10
  * No magic pairing. No attachment logic. The signature IS the docs.
9
11
  * Doc blocks are just editorial commentary when you need it.
10
12
  */
13
+ import type { TypeDescriptor } from './types';
11
14
  export interface DocResult {
12
15
  /** Items in document order */
13
16
  items: DocItem[];
@@ -21,6 +24,11 @@ export type DocItem = {
21
24
  type: 'function';
22
25
  name: string;
23
26
  signature: string;
27
+ } | {
28
+ type: 'class';
29
+ name: string;
30
+ extendsName?: string;
31
+ members: string[];
24
32
  };
25
33
  /**
26
34
  * Generate documentation from TJS source
@@ -33,9 +41,7 @@ export declare function generateDocs(source: string): DocResult;
33
41
  * Type metadata for a function parameter
34
42
  */
35
43
  export interface ParamTypeInfo {
36
- type?: {
37
- kind: string;
38
- };
44
+ type?: TypeDescriptor;
39
45
  required?: boolean;
40
46
  example?: any;
41
47
  }
@@ -44,9 +50,7 @@ export interface ParamTypeInfo {
44
50
  */
45
51
  export interface FunctionTypeInfo {
46
52
  params?: Record<string, ParamTypeInfo>;
47
- returns?: {
48
- kind: string;
49
- };
53
+ returns?: TypeDescriptor;
50
54
  }
51
55
  /**
52
56
  * Generate markdown documentation with type metadata
@@ -67,3 +71,24 @@ export interface FunctionTypeInfo {
67
71
  * ```
68
72
  */
69
73
  export declare function generateDocsMarkdown(source: string, types?: Record<string, FunctionTypeInfo>): string;
74
+ /**
75
+ * Translate `expect(actual).matcher(expected)` calls into inline comments
76
+ * for documentation rendering. Other lines (setup, console.log, etc.) are
77
+ * preserved as-is.
78
+ *
79
+ * expect(x).toBe(y) → x // → y
80
+ * expect(x).toEqual(y) → x // ≡ y (deep equality)
81
+ * expect(x).toBeTruthy() → x // → truthy
82
+ * expect(x).toBeFalsy() → x // → falsy
83
+ * expect(x).toBeNull() → x // → null
84
+ * expect(x).toBeUndefined() → x // → undefined
85
+ * expect(x).toContain(y) → x // → contains y
86
+ * expect(x).toThrow() → x // → throws
87
+ * expect(x).toBeGreaterThan(n) → x // → > n
88
+ * expect(x).toBeLessThan(n) → x // → < n
89
+ * expect(x).toBeNaN() → x // → NaN
90
+ *
91
+ * Uses balanced-paren scanning so nested calls (`expect(f(a, b)).toBe(c)`)
92
+ * work correctly.
93
+ */
94
+ export declare function prettifyTestBody(body: string): string;
@@ -29,8 +29,16 @@ export interface LintOptions {
29
29
  unreachableCode?: boolean;
30
30
  /** Warn about explicit `new` keyword usage (TJS makes classes callable without new) */
31
31
  noExplicitNew?: boolean;
32
+ /**
33
+ * Check `let` declarations for missing type information and forbid literal
34
+ * undefined/null assignments to typed lets. If undefined, the parser's
35
+ * `TjsSafeAssign` mode controls whether the rule runs.
36
+ */
37
+ safeAssign?: boolean;
32
38
  /** Filename for error messages */
33
39
  filename?: string;
40
+ /** Treat safeAssign violations as errors instead of warnings (TjsStrict semantics) */
41
+ strict?: boolean;
34
42
  }
35
43
  /**
36
44
  * Lint TJS source code
@@ -309,3 +309,21 @@ export declare function validateNoEval(source: string): string;
309
309
  * Otherwise, performs a bare property access (throws as usual).
310
310
  */
311
311
  export declare function transformBangAccess(source: string): string;
312
+ /**
313
+ * Transform `let x: <example>` and `let x: <example> = value` declarations.
314
+ *
315
+ * Strips the `: <example>` annotation so Acorn can parse, and records the
316
+ * variable name + example text so the linter and (later) type inference can
317
+ * use the annotation. Acorn rejects the colon since it is not valid JS.
318
+ *
319
+ * let x: '' → let x (annotation: x → '')
320
+ * let x: 0 = 5 → let x = 5 (annotation: x → 0)
321
+ * let result: { ok: false } = ... (annotation: result → { ok: false })
322
+ *
323
+ * Only `let` is processed. `const` always has an initializer, so the type
324
+ * is always inferable. `var` is rejected by TjsNoVar mode.
325
+ */
326
+ export declare function transformLetTypeAnnotations(source: string): {
327
+ source: string;
328
+ annotations: Map<string, string>;
329
+ };
@@ -124,6 +124,8 @@ export interface TjsModes {
124
124
  tjsSafeEval: boolean;
125
125
  /** TjsNoVar: var declarations are syntax errors */
126
126
  tjsNoVar: boolean;
127
+ /** TjsSafeAssign: let declarations need an initializer or `: example` annotation; literal undefined/null/void 0 assigned to typed lets is flagged */
128
+ tjsSafeAssign: boolean;
127
129
  }
128
130
  /**
129
131
  * Extension info for a single extend block
@@ -8,6 +8,12 @@ import type { Program, FunctionDeclaration } from 'acorn';
8
8
  export type { ParseOptions, WasmBlock, TestBlock, PreprocessOptions, TjsModes, } from './parser-types';
9
9
  import type { ParseOptions, WasmBlock, TestBlock, PreprocessOptions, TjsModes } from './parser-types';
10
10
  export { transformExtensionCalls } from './parser-transforms';
11
+ /**
12
+ * Strip single-line comments (//) from source.
13
+ * Replaces comment content with spaces to preserve character offsets.
14
+ * Skips // inside strings and block comments.
15
+ */
16
+ export declare function stripLineComments(source: string): string;
11
17
  export declare function preprocess(source: string, options?: PreprocessOptions): {
12
18
  source: string;
13
19
  returnType?: string;
@@ -23,6 +29,7 @@ export declare function preprocess(source: string, options?: PreprocessOptions):
23
29
  testErrors: string[];
24
30
  polymorphicNames: Set<string>;
25
31
  extensions: Map<string, Set<string>>;
32
+ letAnnotations: Map<string, string>;
26
33
  };
27
34
  /**
28
35
  * Parse source code into an Acorn AST
@@ -39,6 +46,8 @@ export declare function parse(source: string, options?: ParseOptions): {
39
46
  wasmBlocks: WasmBlock[];
40
47
  tests: TestBlock[];
41
48
  testErrors: string[];
49
+ letAnnotations: Map<string, string>;
50
+ tjsModes: TjsModes;
42
51
  };
43
52
  /**
44
53
  * Validate that the source contains exactly one function declaration
@@ -226,6 +226,16 @@ export declare function IsNot(a: unknown, b: unknown): boolean;
226
226
  * Usage: `typeof x` with TjsEquals transforms to `TypeOf(x)`
227
227
  */
228
228
  export declare function TypeOf(value: unknown): string;
229
+ /**
230
+ * Honest boolean coercion. Like `Boolean(x)` but unwraps boxed primitives
231
+ * first, fixing the JS footgun `Boolean(new Boolean(false)) === true`.
232
+ *
233
+ * Under TjsStandard, the source rewriter wraps every truthiness context
234
+ * (if/while/for/do-while conditions, `!`, `&&`, `||`, ternary, and
235
+ * top-level `Boolean(x)` calls) with this function so a boxed `false`
236
+ * actually behaves as `false`.
237
+ */
238
+ export declare function toBool(value: unknown): boolean;
229
239
  export declare function Eq(a: unknown, b: unknown): boolean;
230
240
  /**
231
241
  * Honest inequality — what != should have been.
@@ -291,6 +301,26 @@ type TypeSpec = string | {
291
301
  check: (v: unknown) => boolean | string;
292
302
  description: string;
293
303
  };
304
+ /**
305
+ * Check that a passed-in function's declared shape matches the expected
306
+ * shape. Returns the function unchanged on a match, or a MonadicError on
307
+ * mismatch. Untyped functions (no `__tjs` metadata — anonymous arrows
308
+ * like `x => false`) pass through unchanged on the assumption that the
309
+ * caller knows what they're doing; they accept any args and return
310
+ * whatever they return.
311
+ *
312
+ * This is a ONE-SHOT check at pass time, NOT a per-call wrapper. The TJS
313
+ * design call: a wrong-shape callback is ONE error at the boundary, not
314
+ * N errors when the receiving function invokes the callback N times.
315
+ *
316
+ * Compatibility rules (deliberately permissive — strict subtyping is a
317
+ * separate, larger feature):
318
+ * - For each expected param: the actual function may declare fewer
319
+ * params (extras simply not used). If both declare a kind, they
320
+ * must match exactly. Either side being `any` always matches.
321
+ * - For the return type: same exact-match rule when both are known.
322
+ */
323
+ export declare function checkFnShape(fn: unknown, expectedParamKinds: string[], expectedReturnKind: string, path: string): unknown;
294
324
  /** Parameter metadata with optional location */
295
325
  interface ParamMeta {
296
326
  type: TypeSpec;
@@ -376,6 +406,7 @@ export declare function createRuntime(): {
376
406
  checkType: typeof checkType;
377
407
  validateArgs: typeof validateArgs;
378
408
  wrap: typeof wrap;
409
+ checkFnShape: typeof checkFnShape;
379
410
  wrapClass: typeof wrapClass;
380
411
  compareVersions: typeof compareVersions;
381
412
  versionsCompatible: typeof versionsCompatible;
@@ -419,6 +450,7 @@ export declare function createRuntime(): {
419
450
  Eq: typeof Eq;
420
451
  NotEq: typeof NotEq;
421
452
  TypeOf: typeof TypeOf;
453
+ toBool: typeof toBool;
422
454
  tjsEquals: symbol;
423
455
  registerExtension: (typeName: string, methodName: string, fn: (...args: any[]) => any) => void;
424
456
  resolveExtension: (value: unknown, methodName: string) => ((...args: any[]) => any) | undefined;
@@ -444,6 +476,7 @@ export declare const runtime: {
444
476
  checkType: typeof checkType;
445
477
  validateArgs: typeof validateArgs;
446
478
  wrap: typeof wrap;
479
+ checkFnShape: typeof checkFnShape;
447
480
  wrapClass: typeof wrapClass;
448
481
  compareVersions: typeof compareVersions;
449
482
  versionsCompatible: typeof versionsCompatible;
@@ -489,6 +522,7 @@ export declare const runtime: {
489
522
  Eq: typeof Eq;
490
523
  NotEq: typeof NotEq;
491
524
  TypeOf: typeof TypeOf;
525
+ toBool: typeof toBool;
492
526
  };
493
527
  /**
494
528
  * Install runtime globally (idempotent, version-checked)
@@ -5,7 +5,7 @@ import type { Node } from 'acorn';
5
5
  import type { SeqNode } from '../builder';
6
6
  /** Represents a type extracted from value patterns */
7
7
  export interface TypeDescriptor {
8
- kind: 'string' | 'number' | 'integer' | 'non-negative-integer' | 'boolean' | 'null' | 'undefined' | 'array' | 'object' | 'union' | 'any';
8
+ kind: 'string' | 'number' | 'integer' | 'non-negative-integer' | 'boolean' | 'null' | 'undefined' | 'array' | 'object' | 'union' | 'function' | 'any';
9
9
  nullable?: boolean;
10
10
  /** For arrays: the element type */
11
11
  items?: TypeDescriptor;
@@ -15,6 +15,14 @@ export interface TypeDescriptor {
15
15
  members?: TypeDescriptor[];
16
16
  /** For destructured parameters: full parameter descriptors */
17
17
  destructuredParams?: Record<string, ParameterDescriptor>;
18
+ /** For functions: declared parameters with names and inferred types */
19
+ params?: Array<{
20
+ name: string;
21
+ type: TypeDescriptor;
22
+ }>;
23
+ /** For functions: inferred return type. Concise arrow bodies infer from
24
+ * the expression; block bodies and complex expressions stay `any`. */
25
+ returns?: TypeDescriptor;
18
26
  }
19
27
  /** Describes a function parameter */
20
28
  export interface ParameterDescriptor {
@@ -1,5 +1,5 @@
1
1
  import type { Store } from '../store/interface';
2
- export { evaluateAccessShortcut, selectAccessRule, validateSchema, interpretRuleResult, hasRoleLevel, buildRuleContext, } from './rules.js';
2
+ export { evaluateAccessShortcut, selectAccessRule, validateSchema, interpretRuleResult, hasRoleLevel, buildRuleContext, } from './rules.tjs';
3
3
  /**
4
4
  * Security rule definition
5
5
  */
@@ -149,7 +149,7 @@ export type ExprNode = {
149
149
  } | {
150
150
  $expr: 'member';
151
151
  object: ExprNode;
152
- property: string;
152
+ property: string | ExprNode;
153
153
  computed?: boolean;
154
154
  optional?: boolean;
155
155
  } | {