tjs-lang 0.8.0 → 0.8.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/CLAUDE.md +15 -5
- package/CONTEXT.md +4 -0
- package/demo/docs.json +12 -690
- package/demo/examples.test.ts +6 -2
- package/demo/src/playground-shared.ts +48 -16
- package/demo/src/playground-test-results.test.ts +112 -0
- package/demo/src/tjs-playground.ts +11 -5
- package/dist/eslint.config.d.ts +2 -0
- package/dist/index.js +146 -141
- package/dist/index.js.map +4 -4
- package/dist/src/lang/core.d.ts +1 -1
- package/dist/src/lang/dialect.d.ts +35 -0
- package/dist/src/lang/emitters/ast.d.ts +1 -1
- package/dist/src/lang/emitters/js-tests.d.ts +7 -0
- package/dist/src/lang/emitters/js-wasm.d.ts +5 -1
- package/dist/src/lang/emitters/js.d.ts +21 -0
- package/dist/src/lang/index.d.ts +3 -1
- package/dist/src/lang/module-loader.d.ts +125 -0
- package/dist/src/lang/parser-transforms.d.ts +79 -0
- package/dist/src/lang/parser-types.d.ts +50 -0
- package/dist/src/lang/parser.d.ts +18 -0
- package/dist/src/lang/transpiler.d.ts +1 -0
- package/dist/src/lang/types.d.ts +18 -0
- package/dist/src/lang/wasm.d.ts +67 -1
- package/dist/src/vm/runtime.d.ts +18 -0
- package/dist/src/vm/vm.d.ts +15 -1
- package/dist/tjs-batteries.js +2 -2
- package/dist/tjs-batteries.js.map +2 -2
- package/dist/tjs-eval.js +43 -41
- package/dist/tjs-eval.js.map +3 -3
- package/dist/tjs-from-ts.js +2 -2
- package/dist/tjs-from-ts.js.map +2 -2
- package/dist/tjs-lang.js +107 -104
- package/dist/tjs-lang.js.map +4 -4
- package/dist/tjs-vm.js +53 -51
- package/dist/tjs-vm.js.map +3 -3
- package/docs/README.md +2 -0
- package/docs/lm-studio-setup.md +143 -0
- package/docs/universal-endpoint.md +122 -0
- package/llms.txt +9 -2
- package/package.json +24 -4
- package/src/batteries/audit.ts +6 -5
- package/src/batteries/llm.ts +8 -3
- package/src/builder.ts +0 -3
- package/src/cli/commands/check.ts +3 -2
- package/src/cli/commands/emit.ts +4 -2
- package/src/cli/commands/run.ts +6 -2
- package/src/cli/commands/test.ts +1 -1
- package/src/cli/commands/types.ts +2 -2
- package/src/lang/codegen.test.ts +4 -1
- package/src/lang/core.ts +6 -4
- package/src/lang/dialect.test.ts +63 -0
- package/src/lang/dialect.ts +50 -0
- package/src/lang/docs.test.ts +0 -1
- package/src/lang/emitters/ast.ts +145 -2
- package/src/lang/emitters/from-ts.ts +1 -1
- package/src/lang/emitters/js-tests.ts +46 -37
- package/src/lang/emitters/js.ts +19 -3
- package/src/lang/features.test.ts +6 -5
- package/src/lang/index.ts +18 -5
- package/src/lang/linter.ts +1 -1
- package/src/lang/module-loader.test.ts +9 -5
- package/src/lang/module-loader.ts +4 -5
- package/src/lang/parser-params.ts +1 -1
- package/src/lang/parser-transforms.ts +12 -18
- package/src/lang/parser-types.ts +17 -0
- package/src/lang/parser.test.ts +12 -6
- package/src/lang/parser.ts +113 -3
- package/src/lang/perf.test.ts +10 -4
- package/src/lang/runtime.ts +0 -1
- package/src/lang/subset-invariant.test.ts +90 -0
- package/src/lang/transpiler.ts +8 -0
- package/src/lang/types.ts +18 -0
- package/src/lang/wasm.test.ts +22 -16
- package/src/linalg/linalg.test.ts +16 -4
- package/src/linalg/vector-search.bench.test.ts +31 -10
- package/src/types/Type.ts +6 -6
- package/src/use-cases/asymmetric-client-server.test.ts +0 -2
- package/src/use-cases/batteries.test.ts +4 -0
- package/src/use-cases/client-server.test.ts +1 -1
- package/src/use-cases/local-helpers.test.ts +219 -0
- package/src/use-cases/timeout-overrides.test.ts +169 -0
- package/src/use-cases/unbundled-imports.test.ts +0 -1
- package/src/vm/atoms/batteries.ts +17 -3
- package/src/vm/runtime.ts +92 -7
- package/src/vm/vm.ts +50 -10
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
|
+
import { tjs } from './index'
|
|
3
|
+
import { dialectForFilename, sourceKindForFilename } from './dialect'
|
|
4
|
+
|
|
5
|
+
describe('source dialect', () => {
|
|
6
|
+
// A line of vanilla JS whose meaning TJS would otherwise change: structural
|
|
7
|
+
// `==` (→ Eq), honest truthiness (→ toBool), and `typeof` (→ TypeOf).
|
|
8
|
+
const JS = `function f(a, b) { if (a == b) { return typeof a } return null }`
|
|
9
|
+
|
|
10
|
+
const isMolested = (code: string) =>
|
|
11
|
+
code.includes('Eq(') || code.includes('toBool') || code.includes('TypeOf')
|
|
12
|
+
|
|
13
|
+
describe('dialect option', () => {
|
|
14
|
+
it("dialect: 'js' preserves plain-JS semantics (no rewrites)", () => {
|
|
15
|
+
expect(isMolested(tjs(JS, { dialect: 'js' }).code)).toBe(false)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("dialect: 'tjs' applies native footgun-removal modes", () => {
|
|
19
|
+
expect(isMolested(tjs(JS, { dialect: 'tjs' }).code)).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('a bare string still defaults to native TJS (backward compatible)', () => {
|
|
23
|
+
expect(isMolested(tjs(JS).code)).toBe(true)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("dialect: 'js' is equivalent to the TjsCompat directive", () => {
|
|
27
|
+
const viaOption = tjs(JS, { dialect: 'js' }).code
|
|
28
|
+
const viaDirective = tjs(`TjsCompat\n${JS}`).code
|
|
29
|
+
// Both leave the comparison/typeof untouched.
|
|
30
|
+
expect(isMolested(viaOption)).toBe(false)
|
|
31
|
+
expect(isMolested(viaDirective)).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('dialectForFilename', () => {
|
|
36
|
+
it('maps JS extensions to the js dialect', () => {
|
|
37
|
+
for (const f of ['a.js', 'a.mjs', 'a.cjs', 'deep/b.JS']) {
|
|
38
|
+
expect(dialectForFilename(f)).toBe('js')
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('maps .tjs and unknown/TS extensions to the tjs dialect', () => {
|
|
43
|
+
// .ts is not a tjs() dialect — it routes through fromTS — so the dialect
|
|
44
|
+
// helper reports 'tjs'; use sourceKindForFilename to tell them apart.
|
|
45
|
+
for (const f of ['a.tjs', 'a.ts', 'a.mts', 'a.d.ts', 'noext']) {
|
|
46
|
+
expect(dialectForFilename(f)).toBe('tjs')
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('sourceKindForFilename', () => {
|
|
52
|
+
it('classifies js / ts / tjs by extension', () => {
|
|
53
|
+
expect(sourceKindForFilename('x.js')).toBe('js')
|
|
54
|
+
expect(sourceKindForFilename('x.mjs')).toBe('js')
|
|
55
|
+
expect(sourceKindForFilename('x.cjs')).toBe('js')
|
|
56
|
+
expect(sourceKindForFilename('x.ts')).toBe('ts')
|
|
57
|
+
expect(sourceKindForFilename('x.mts')).toBe('ts')
|
|
58
|
+
expect(sourceKindForFilename('x.d.ts')).toBe('ts')
|
|
59
|
+
expect(sourceKindForFilename('x.tjs')).toBe('tjs')
|
|
60
|
+
expect(sourceKindForFilename('x')).toBe('tjs')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dialect resolution for file-based tooling.
|
|
3
|
+
*
|
|
4
|
+
* The subset invariant (see PRINCIPLES.md) says plain JavaScript must transpile
|
|
5
|
+
* through TJS without changing meaning, while native `.tjs` gets the full
|
|
6
|
+
* footgun-removal feature set. The unit of opt-in is the *dialect* — and for a
|
|
7
|
+
* file, the dialect is its extension. This is the one canonical mapping that
|
|
8
|
+
* CLIs, bundler plugins, module loaders, and doc systems should all share so
|
|
9
|
+
* `.js` is never silently "improved" into different semantics.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Source dialect understood by `tjs(src, { dialect })`. */
|
|
13
|
+
export type Dialect = 'js' | 'tjs'
|
|
14
|
+
|
|
15
|
+
/** How a file should be transpiled, based on its extension. */
|
|
16
|
+
export type SourceKind = 'js' | 'ts' | 'tjs'
|
|
17
|
+
|
|
18
|
+
const JS_EXT = /\.[mc]?js$/i // .js .mjs .cjs
|
|
19
|
+
const TS_EXT = /\.[mc]?ts$/i // .ts .mts .cts (excludes .d.ts — handled below)
|
|
20
|
+
const TJS_EXT = /\.tjs$/i
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Classify a file by extension into the source kind that determines its
|
|
24
|
+
* transpile path:
|
|
25
|
+
* - `'tjs'` → native TJS: `tjs(src)` (all modes ON — the better language)
|
|
26
|
+
* - `'js'` → plain JS: `tjs(src, { dialect: 'js' })` (semantics preserved)
|
|
27
|
+
* - `'ts'` → TypeScript: route through `fromTS` (TS → TJS → JS)
|
|
28
|
+
*
|
|
29
|
+
* Unknown extensions default to `'tjs'` (the native language). `.d.ts` is
|
|
30
|
+
* treated as TypeScript.
|
|
31
|
+
*/
|
|
32
|
+
export function sourceKindForFilename(filename: string): SourceKind {
|
|
33
|
+
if (TJS_EXT.test(filename)) return 'tjs'
|
|
34
|
+
if (JS_EXT.test(filename)) return 'js'
|
|
35
|
+
if (TS_EXT.test(filename)) return 'ts'
|
|
36
|
+
return 'tjs'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Map a filename to the `dialect` option for `tjs()`. `.js`/`.mjs`/`.cjs` ⇒
|
|
41
|
+
* `'js'` (plain JavaScript, semantics preserved); everything else ⇒ `'tjs'`
|
|
42
|
+
* (native TJS).
|
|
43
|
+
*
|
|
44
|
+
* NOTE: TypeScript files are NOT a `tjs()` dialect — transpile them with
|
|
45
|
+
* `fromTS` first. Use {@link sourceKindForFilename} when you need to tell
|
|
46
|
+
* `.ts` apart from `.js`/`.tjs`.
|
|
47
|
+
*/
|
|
48
|
+
export function dialectForFilename(filename: string): Dialect {
|
|
49
|
+
return sourceKindForFilename(filename) === 'js' ? 'js' : 'tjs'
|
|
50
|
+
}
|
package/src/lang/docs.test.ts
CHANGED
|
@@ -723,7 +723,6 @@ function outer() {
|
|
|
723
723
|
describe('function param rendering in docs', () => {
|
|
724
724
|
// We need transpiled type metadata for the param-table renderer.
|
|
725
725
|
// Use the lang index's `tjs()` to get types.
|
|
726
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
727
726
|
const { tjs } = require('./index')
|
|
728
727
|
|
|
729
728
|
it('renders an arrow-default param as `(x: any) => any` (not `function`)', () => {
|
package/src/lang/emitters/ast.ts
CHANGED
|
@@ -124,7 +124,8 @@ export function transformFunction(
|
|
|
124
124
|
source: string,
|
|
125
125
|
returnTypeAnnotation: string | undefined,
|
|
126
126
|
options: TranspileOptions = {},
|
|
127
|
-
requiredParamsFromPreprocess?: Set<string
|
|
127
|
+
requiredParamsFromPreprocess?: Set<string>,
|
|
128
|
+
helpers?: Map<string, FunctionDeclaration>
|
|
128
129
|
): {
|
|
129
130
|
ast: BaseNode
|
|
130
131
|
signature: FunctionSignature
|
|
@@ -175,6 +176,9 @@ export function transformFunction(
|
|
|
175
176
|
source,
|
|
176
177
|
filename: options.filename || '<source>',
|
|
177
178
|
options,
|
|
179
|
+
helpers,
|
|
180
|
+
helperSteps: helpers ? new Map() : undefined,
|
|
181
|
+
helperTransforming: helpers ? new Set() : undefined,
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
// Transform function body
|
|
@@ -244,8 +248,21 @@ export function transformFunction(
|
|
|
244
248
|
// Generate input schema for runtime validation
|
|
245
249
|
const inputSchema = parametersToJsonSchema(signatureParams)
|
|
246
250
|
|
|
251
|
+
// Collect transitively-used helper bodies (transformed once) onto the root
|
|
252
|
+
// node. The VM installs these into ctx.helpers so callLocal can dispatch by
|
|
253
|
+
// name — call sites stay tiny and recursion is a runtime loop.
|
|
254
|
+
const helperBodies =
|
|
255
|
+
ctx.helperSteps && ctx.helperSteps.size > 0
|
|
256
|
+
? Object.fromEntries(ctx.helperSteps)
|
|
257
|
+
: undefined
|
|
258
|
+
|
|
247
259
|
return {
|
|
248
|
-
ast: {
|
|
260
|
+
ast: {
|
|
261
|
+
op: 'seq',
|
|
262
|
+
steps,
|
|
263
|
+
inputSchema,
|
|
264
|
+
...(helperBodies && { helpers: helperBodies }),
|
|
265
|
+
},
|
|
249
266
|
signature,
|
|
250
267
|
warnings: ctx.warnings,
|
|
251
268
|
}
|
|
@@ -1079,6 +1096,34 @@ function transformCallExpression(
|
|
|
1079
1096
|
// This would be caught above, but just in case
|
|
1080
1097
|
}
|
|
1081
1098
|
|
|
1099
|
+
// Helper call? Emit callLocal referencing the helper by name. The body is
|
|
1100
|
+
// transformed once and stored in ctx.helperSteps (collected onto the seq
|
|
1101
|
+
// node), so call sites stay tiny and recursion is just a runtime loop.
|
|
1102
|
+
if (ctx.helpers?.has(funcName)) {
|
|
1103
|
+
const paramNames = ensureHelperTransformed(funcName, ctx, expr)
|
|
1104
|
+
const argExprs = expr.arguments.map((arg) =>
|
|
1105
|
+
expressionToValue(arg as Expression, ctx)
|
|
1106
|
+
)
|
|
1107
|
+
if (argExprs.length !== paramNames.length) {
|
|
1108
|
+
throw new TranspileError(
|
|
1109
|
+
`Helper '${funcName}' expects ${paramNames.length} argument(s), got ${argExprs.length}`,
|
|
1110
|
+
getLocation(expr),
|
|
1111
|
+
ctx.source,
|
|
1112
|
+
ctx.filename
|
|
1113
|
+
)
|
|
1114
|
+
}
|
|
1115
|
+
return {
|
|
1116
|
+
step: {
|
|
1117
|
+
op: 'callLocal',
|
|
1118
|
+
name: funcName,
|
|
1119
|
+
args: argExprs,
|
|
1120
|
+
...(resultVar && { result: resultVar }),
|
|
1121
|
+
...(resultVar && isConst && { resultConst: true }),
|
|
1122
|
+
},
|
|
1123
|
+
resultVar,
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1082
1127
|
// Check if it's a known atom
|
|
1083
1128
|
// For now, we assume any function call is an atom call
|
|
1084
1129
|
// The VM will validate at runtime
|
|
@@ -1097,6 +1142,89 @@ function transformCallExpression(
|
|
|
1097
1142
|
}
|
|
1098
1143
|
}
|
|
1099
1144
|
|
|
1145
|
+
/**
|
|
1146
|
+
* Ensure a helper's body has been transformed once into `ctx.helperSteps`,
|
|
1147
|
+
* returning its parameter names (for arity checking at the call site).
|
|
1148
|
+
*
|
|
1149
|
+
* Bodies are stored by name and called by reference at runtime, so recursion
|
|
1150
|
+
* is fine: when a helper references itself (or a mutually-recursive sibling)
|
|
1151
|
+
* the call site only needs the param names — the steps are filled in when the
|
|
1152
|
+
* in-progress transform completes. The `helperTransforming` set just prevents
|
|
1153
|
+
* re-entering a transform that's already underway.
|
|
1154
|
+
*/
|
|
1155
|
+
function ensureHelperTransformed(
|
|
1156
|
+
name: string,
|
|
1157
|
+
ctx: TransformContext,
|
|
1158
|
+
callSite: CallExpression
|
|
1159
|
+
): string[] {
|
|
1160
|
+
const fn = ctx.helpers!.get(name)!
|
|
1161
|
+
// Accept plain identifiers (`x`) and example/default params (`x: 0` → `x = 0`
|
|
1162
|
+
// after colon-shorthand desugaring). Destructuring isn't supported in v1.
|
|
1163
|
+
// Helpers are arity-checked and called with explicit args, so the example/
|
|
1164
|
+
// default is never applied — we only need the parameter name and position.
|
|
1165
|
+
const paramNames: string[] = []
|
|
1166
|
+
for (const param of fn.params) {
|
|
1167
|
+
let id: Identifier | undefined
|
|
1168
|
+
if (param.type === 'Identifier') {
|
|
1169
|
+
id = param as Identifier
|
|
1170
|
+
} else if (
|
|
1171
|
+
param.type === 'AssignmentPattern' &&
|
|
1172
|
+
(param as any).left?.type === 'Identifier'
|
|
1173
|
+
) {
|
|
1174
|
+
id = (param as any).left as Identifier
|
|
1175
|
+
}
|
|
1176
|
+
if (!id) {
|
|
1177
|
+
throw new TranspileError(
|
|
1178
|
+
`Helper '${name}' parameters must be plain identifiers (optionally with an example value); destructuring is not supported`,
|
|
1179
|
+
param.loc?.start ?? getLocation(callSite),
|
|
1180
|
+
ctx.source,
|
|
1181
|
+
ctx.filename
|
|
1182
|
+
)
|
|
1183
|
+
}
|
|
1184
|
+
paramNames.push(id.name)
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Already transformed, or transform already underway (recursive reference):
|
|
1188
|
+
// the call site only needs paramNames; the body is/will be in helperSteps.
|
|
1189
|
+
if (ctx.helperSteps!.has(name) || ctx.helperTransforming!.has(name)) {
|
|
1190
|
+
return paramNames
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
ctx.helperTransforming!.add(name)
|
|
1194
|
+
try {
|
|
1195
|
+
// Transform the helper body in a fresh inner context — isolated locals,
|
|
1196
|
+
// params as the only known identifiers; helpers map is shared so it can
|
|
1197
|
+
// call (and recurse into) other helpers.
|
|
1198
|
+
const helperCtx: TransformContext = {
|
|
1199
|
+
depth: 0,
|
|
1200
|
+
locals: new Map(),
|
|
1201
|
+
parameters: new Map(
|
|
1202
|
+
paramNames.map((p) => [
|
|
1203
|
+
p,
|
|
1204
|
+
{
|
|
1205
|
+
name: p,
|
|
1206
|
+
type: { kind: 'any' as const },
|
|
1207
|
+
required: true,
|
|
1208
|
+
} as ParameterDescriptor,
|
|
1209
|
+
])
|
|
1210
|
+
),
|
|
1211
|
+
atoms: ctx.atoms,
|
|
1212
|
+
warnings: ctx.warnings,
|
|
1213
|
+
source: ctx.source,
|
|
1214
|
+
filename: ctx.filename,
|
|
1215
|
+
options: ctx.options,
|
|
1216
|
+
helpers: ctx.helpers,
|
|
1217
|
+
helperSteps: ctx.helperSteps,
|
|
1218
|
+
helperTransforming: ctx.helperTransforming,
|
|
1219
|
+
}
|
|
1220
|
+
const bodySteps = transformBlock(fn.body, helperCtx)
|
|
1221
|
+
ctx.helperSteps!.set(name, { steps: bodySteps, paramNames })
|
|
1222
|
+
} finally {
|
|
1223
|
+
ctx.helperTransforming!.delete(name)
|
|
1224
|
+
}
|
|
1225
|
+
return paramNames
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1100
1228
|
/**
|
|
1101
1229
|
* Handle method calls like arr.map(), str.slice(), etc.
|
|
1102
1230
|
*/
|
|
@@ -1544,6 +1672,21 @@ function expressionToExprNode(
|
|
|
1544
1672
|
// Handle global function calls (e.g., parseInt(x), parseFloat(x))
|
|
1545
1673
|
if (call.callee.type === 'Identifier') {
|
|
1546
1674
|
const funcName = (call.callee as Identifier).name
|
|
1675
|
+
|
|
1676
|
+
// Helper calls cannot be nested inside expressions — like template
|
|
1677
|
+
// literals and complex calls, they must live at statement level so the
|
|
1678
|
+
// expression sandbox stays call-free. Lift to a temp first.
|
|
1679
|
+
if (ctx.helpers?.has(funcName)) {
|
|
1680
|
+
throw new TranspileError(
|
|
1681
|
+
`Helper '${funcName}' cannot be called inside an expression. ` +
|
|
1682
|
+
`Assign its result to a variable first: ` +
|
|
1683
|
+
`const result = ${funcName}(...); then use result.`,
|
|
1684
|
+
getLocation(expr),
|
|
1685
|
+
ctx.source,
|
|
1686
|
+
ctx.filename
|
|
1687
|
+
)
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1547
1690
|
return {
|
|
1548
1691
|
$expr: 'call',
|
|
1549
1692
|
callee: funcName,
|
|
@@ -1525,7 +1525,7 @@ function transformFunctionToTJS(
|
|
|
1525
1525
|
}
|
|
1526
1526
|
|
|
1527
1527
|
// Get function body and strip TypeScript syntax using ts.transpileModule
|
|
1528
|
-
let body
|
|
1528
|
+
let body: string
|
|
1529
1529
|
if (node.body) {
|
|
1530
1530
|
const bodyText = ts.isBlock(node.body)
|
|
1531
1531
|
? node.body.getText(sourceFile)
|
|
@@ -18,6 +18,13 @@ export interface TestResult {
|
|
|
18
18
|
error?: string
|
|
19
19
|
/** Whether this was an implicit signature test */
|
|
20
20
|
isSignatureTest?: boolean
|
|
21
|
+
/**
|
|
22
|
+
* The test could not be *run* (e.g. it references a name TJS can't resolve at
|
|
23
|
+
* build time — like an AJS atom — or the harness couldn't execute the module).
|
|
24
|
+
* Inconclusive ≠ failed: it never blocks transpilation. Surfaced as a warning
|
|
25
|
+
* (e.g. in the playground). See PRINCIPLES.md ("more, never illegal").
|
|
26
|
+
*/
|
|
27
|
+
inconclusive?: boolean
|
|
21
28
|
/** Source line number (1-indexed) where the test or error occurred */
|
|
22
29
|
line?: number
|
|
23
30
|
/** Source column number (1-indexed) */
|
|
@@ -678,7 +685,7 @@ export function runAllTests(
|
|
|
678
685
|
${body}
|
|
679
686
|
__testResults.push({ idx: ${i}, passed: true });
|
|
680
687
|
} catch (e) {
|
|
681
|
-
__testResults.push({ idx: ${i}, passed: false, error: e.message || String(e) });
|
|
688
|
+
__testResults.push({ idx: ${i}, passed: false, isRef: e instanceof ReferenceError, error: e.message || String(e) });
|
|
682
689
|
}
|
|
683
690
|
`
|
|
684
691
|
})
|
|
@@ -721,7 +728,10 @@ export function runAllTests(
|
|
|
721
728
|
__sigTestResults.push({ idx: ${i}, passed: false, error: 'Expected ' + __format(__expected) + ' at \\'${testLabel}\\', got ' + __format(__actual) });
|
|
722
729
|
}
|
|
723
730
|
} catch (e) {
|
|
724
|
-
|
|
731
|
+
// The function threw on its example inputs — the example couldn't be
|
|
732
|
+
// *evaluated* (e.g. it calls an atom that doesn't exist at build time).
|
|
733
|
+
// That's inconclusive, not a mismatch. Never block transpilation on it.
|
|
734
|
+
__sigTestResults.push({ idx: ${i}, passed: false, threw: true, error: e.message || String(e) });
|
|
725
735
|
}
|
|
726
736
|
`
|
|
727
737
|
})
|
|
@@ -834,62 +844,62 @@ export function runAllTests(
|
|
|
834
844
|
typeMatches
|
|
835
845
|
)
|
|
836
846
|
|
|
837
|
-
// Map block test results
|
|
847
|
+
// Map block test results. A block test that threw a ReferenceError
|
|
848
|
+
// couldn't *run* (it names something TJS can't resolve at build time, e.g.
|
|
849
|
+
// an AJS atom) → inconclusive, not failed. An assertion failure (plain
|
|
850
|
+
// Error from expect()) is a genuine failure.
|
|
838
851
|
for (const r of blockResults) {
|
|
839
852
|
const test = tests[r.idx]
|
|
840
|
-
|
|
841
|
-
const isImportError =
|
|
842
|
-
hasUnresolvedImports &&
|
|
853
|
+
const inconclusive =
|
|
843
854
|
!r.passed &&
|
|
844
|
-
r.
|
|
845
|
-
|
|
855
|
+
(r.isRef ||
|
|
856
|
+
(hasUnresolvedImports && r.error && /is not defined$/.test(r.error)))
|
|
846
857
|
results.push({
|
|
847
858
|
description: test.description,
|
|
848
|
-
passed:
|
|
849
|
-
|
|
859
|
+
passed: r.passed,
|
|
860
|
+
inconclusive: inconclusive || undefined,
|
|
861
|
+
error: r.error,
|
|
850
862
|
line: test.line,
|
|
851
863
|
})
|
|
852
864
|
}
|
|
853
865
|
|
|
854
|
-
// Map signature test results
|
|
866
|
+
// Map signature test results. A test that *threw* (couldn't evaluate the
|
|
867
|
+
// example) is inconclusive; only a clean value-mismatch is a failure.
|
|
855
868
|
for (const r of sigTestResults) {
|
|
856
869
|
const info = syncSigTestInfos[r.idx]
|
|
857
|
-
|
|
858
|
-
const isImportError =
|
|
859
|
-
hasUnresolvedImports &&
|
|
870
|
+
const inconclusive =
|
|
860
871
|
!r.passed &&
|
|
861
|
-
r.
|
|
862
|
-
|
|
872
|
+
(r.threw ||
|
|
873
|
+
(hasUnresolvedImports && r.error && /is not defined$/.test(r.error)))
|
|
863
874
|
const label = info.className
|
|
864
875
|
? `${info.className}.${info.funcName}`
|
|
865
876
|
: info.funcName
|
|
866
877
|
results.push({
|
|
867
878
|
description: `${label} signature example`,
|
|
868
|
-
passed:
|
|
869
|
-
|
|
879
|
+
passed: r.passed,
|
|
880
|
+
inconclusive: inconclusive || undefined,
|
|
881
|
+
error: r.error,
|
|
870
882
|
isSignatureTest: true,
|
|
871
883
|
line: info.line,
|
|
872
884
|
})
|
|
873
885
|
}
|
|
874
886
|
} catch (e: any) {
|
|
875
|
-
//
|
|
876
|
-
//
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
//
|
|
882
|
-
//
|
|
883
|
-
//
|
|
884
|
-
|
|
885
|
-
// through the runtime console.
|
|
887
|
+
// The whole module failed to execute, so NO test could run — every result
|
|
888
|
+
// here is inconclusive, never a failure. Making transpilation fail because
|
|
889
|
+
// an auto-generated test harness couldn't execute the module would turn
|
|
890
|
+
// legal code illegal (e.g. a module referencing AJS atoms, or — an edge —
|
|
891
|
+
// two top-level functions on one line). See PRINCIPLES.md.
|
|
892
|
+
//
|
|
893
|
+
// No line is attributed: the error came from module-level execution, not a
|
|
894
|
+
// specific function, and mis-attributing it would point the editor at the
|
|
895
|
+
// wrong site. The message is preserved as a warning on each result.
|
|
896
|
+
const note = `Module could not be executed for testing: ${e.message}`
|
|
886
897
|
for (const test of tests) {
|
|
887
898
|
results.push({
|
|
888
899
|
description: test.description,
|
|
889
|
-
passed:
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
: `Module execution failed: ${e.message}`,
|
|
900
|
+
passed: false,
|
|
901
|
+
inconclusive: true,
|
|
902
|
+
error: note,
|
|
893
903
|
})
|
|
894
904
|
}
|
|
895
905
|
for (const info of syncSigTestInfos) {
|
|
@@ -898,10 +908,9 @@ export function runAllTests(
|
|
|
898
908
|
: info.funcName
|
|
899
909
|
results.push({
|
|
900
910
|
description: `${label} signature example`,
|
|
901
|
-
passed:
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
: `Module execution failed: ${e.message}`,
|
|
911
|
+
passed: false,
|
|
912
|
+
inconclusive: true,
|
|
913
|
+
error: note,
|
|
905
914
|
isSignatureTest: true,
|
|
906
915
|
})
|
|
907
916
|
}
|
package/src/lang/emitters/js.ts
CHANGED
|
@@ -81,6 +81,18 @@ export interface TJSTranspileOptions {
|
|
|
81
81
|
sourceMap?: boolean
|
|
82
82
|
/** Mode: 'dev' | 'strict' | 'production' */
|
|
83
83
|
mode?: 'dev' | 'strict' | 'production'
|
|
84
|
+
/**
|
|
85
|
+
* Source dialect — what kind of source this string is:
|
|
86
|
+
* - `'tjs'` (default for a bare string): native TJS, footgun-removal modes ON
|
|
87
|
+
* (structural `==`, `TjsStandard`, etc.).
|
|
88
|
+
* - `'js'`: plain JavaScript — modes OFF, `safety: 'none'`; the source's own
|
|
89
|
+
* semantics are preserved (no `==`→`Eq`, no truthiness rewrite, no input
|
|
90
|
+
* validation). Use this when feeding tjs() a vanilla `.js` string so it
|
|
91
|
+
* transpiles without changing meaning. See PRINCIPLES.md (TJS ⊇ JS).
|
|
92
|
+
*
|
|
93
|
+
* For TypeScript, use the `fromTS` entry point (TS → TJS → JS).
|
|
94
|
+
*/
|
|
95
|
+
dialect?: 'js' | 'tjs'
|
|
84
96
|
/**
|
|
85
97
|
* Test execution mode:
|
|
86
98
|
* - true (default): run tests at transpile time, throw on failure
|
|
@@ -640,6 +652,7 @@ export function transpileToJS(
|
|
|
640
652
|
} = parse(cleanSource, {
|
|
641
653
|
filename,
|
|
642
654
|
colonShorthand: true,
|
|
655
|
+
dialect: options.dialect,
|
|
643
656
|
moduleLoader: options.moduleLoader,
|
|
644
657
|
})
|
|
645
658
|
|
|
@@ -650,6 +663,7 @@ export function transpileToJS(
|
|
|
650
663
|
// Pass through the moduleLoader so Phase 3 cross-file wasm composition
|
|
651
664
|
// sees imported `wasm function` declarations.
|
|
652
665
|
const preprocessed = preprocess(cleanSource, {
|
|
666
|
+
dialect: options.dialect,
|
|
653
667
|
moduleLoader: options.moduleLoader,
|
|
654
668
|
filename,
|
|
655
669
|
})
|
|
@@ -1119,8 +1133,11 @@ export function transpileToJS(
|
|
|
1119
1133
|
)
|
|
1120
1134
|
|
|
1121
1135
|
// Check for failures and throw only if runTests === true (strict mode)
|
|
1122
|
-
// 'only' and 'report' modes return results without throwing
|
|
1123
|
-
|
|
1136
|
+
// 'only' and 'report' modes return results without throwing.
|
|
1137
|
+
// Inconclusive results (a test that couldn't *run* — undefined refs like AJS
|
|
1138
|
+
// atoms, or a module the harness can't execute) never block transpilation:
|
|
1139
|
+
// that would turn subset-legal code illegal. See PRINCIPLES.md.
|
|
1140
|
+
const failures = testResults.filter((r) => !r.passed && !r.inconclusive)
|
|
1124
1141
|
if (failures.length > 0 && runTests === true) {
|
|
1125
1142
|
const errorLines = failures.map((f) => {
|
|
1126
1143
|
if (f.isSignatureTest) {
|
|
@@ -1149,7 +1166,6 @@ export function transpileToJS(
|
|
|
1149
1166
|
| { id: string; success: boolean; error?: string; byteLength?: number }[]
|
|
1150
1167
|
| undefined
|
|
1151
1168
|
if (preprocessed.wasmBlocks.length > 0) {
|
|
1152
|
-
wasmCompiled = []
|
|
1153
1169
|
const wasmBootstrap = generateWasmBootstrap(preprocessed.wasmBlocks)
|
|
1154
1170
|
if (wasmBootstrap.code) {
|
|
1155
1171
|
code = wasmBootstrap.code + '\n' + code
|
|
@@ -1097,14 +1097,15 @@ describe('signature tests (transpile-time)', () => {
|
|
|
1097
1097
|
`,
|
|
1098
1098
|
{ runTests: 'report' }
|
|
1099
1099
|
)
|
|
1100
|
-
//
|
|
1100
|
+
// The test can't run (unresolved import) → inconclusive, not failed.
|
|
1101
|
+
// Inconclusive never blocks transpilation (see PRINCIPLES.md).
|
|
1101
1102
|
expect(result.testResults).toBeDefined()
|
|
1102
1103
|
const sigTest = result.testResults!.find((t: any) =>
|
|
1103
1104
|
t.description.includes('validateUser')
|
|
1104
1105
|
)
|
|
1105
1106
|
expect(sigTest).toBeDefined()
|
|
1106
|
-
expect(sigTest?.passed).toBe(
|
|
1107
|
-
expect(sigTest?.
|
|
1107
|
+
expect(sigTest?.passed).toBe(false)
|
|
1108
|
+
expect(sigTest?.inconclusive).toBe(true)
|
|
1108
1109
|
})
|
|
1109
1110
|
|
|
1110
1111
|
it('should skip tests gracefully when imports are unresolved (function-level)', () => {
|
|
@@ -1124,8 +1125,8 @@ describe('signature tests (transpile-time)', () => {
|
|
|
1124
1125
|
t.description.includes('formatDate')
|
|
1125
1126
|
)
|
|
1126
1127
|
expect(sigTest).toBeDefined()
|
|
1127
|
-
expect(sigTest?.passed).toBe(
|
|
1128
|
-
expect(sigTest?.
|
|
1128
|
+
expect(sigTest?.passed).toBe(false)
|
|
1129
|
+
expect(sigTest?.inconclusive).toBe(true)
|
|
1129
1130
|
})
|
|
1130
1131
|
|
|
1131
1132
|
it('should test sync functions alongside async functions', () => {
|
package/src/lang/index.ts
CHANGED
|
@@ -27,11 +27,23 @@ import type {
|
|
|
27
27
|
TranspileResult,
|
|
28
28
|
FunctionSignature,
|
|
29
29
|
} from './types'
|
|
30
|
-
import { parse,
|
|
30
|
+
import { parse, extractFunctions } from './parser'
|
|
31
31
|
import { transformFunction } from './emitters/ast'
|
|
32
32
|
|
|
33
33
|
export * from './types'
|
|
34
|
-
export {
|
|
34
|
+
export {
|
|
35
|
+
parse,
|
|
36
|
+
preprocess,
|
|
37
|
+
extractTDoc,
|
|
38
|
+
validateSingleFunction,
|
|
39
|
+
extractFunctions,
|
|
40
|
+
} from './parser'
|
|
41
|
+
export {
|
|
42
|
+
dialectForFilename,
|
|
43
|
+
sourceKindForFilename,
|
|
44
|
+
type Dialect,
|
|
45
|
+
type SourceKind,
|
|
46
|
+
} from './dialect'
|
|
35
47
|
export { transformFunction } from './emitters/ast'
|
|
36
48
|
export {
|
|
37
49
|
transpileToJS,
|
|
@@ -177,15 +189,16 @@ export function transpile(
|
|
|
177
189
|
})
|
|
178
190
|
|
|
179
191
|
// Validate structure
|
|
180
|
-
const
|
|
192
|
+
const { entry, helpers } = extractFunctions(program, options.filename)
|
|
181
193
|
|
|
182
194
|
// Transform to Agent99 AST
|
|
183
195
|
const { ast, signature, warnings } = transformFunction(
|
|
184
|
-
|
|
196
|
+
entry,
|
|
185
197
|
originalSource,
|
|
186
198
|
returnType,
|
|
187
199
|
options,
|
|
188
|
-
requiredParams
|
|
200
|
+
requiredParams,
|
|
201
|
+
helpers.size > 0 ? helpers : undefined
|
|
189
202
|
)
|
|
190
203
|
|
|
191
204
|
return {
|
package/src/lang/linter.ts
CHANGED
|
@@ -72,7 +72,7 @@ export function lint(source: string, options: LintOptions = {}): LintResult {
|
|
|
72
72
|
// Parse the source
|
|
73
73
|
let program: Program
|
|
74
74
|
let letAnnotations: Map<string, string> = new Map()
|
|
75
|
-
let safeAssignMode
|
|
75
|
+
let safeAssignMode: boolean
|
|
76
76
|
try {
|
|
77
77
|
const result = parse(source, {
|
|
78
78
|
filename: opts.filename,
|
|
@@ -39,7 +39,9 @@ describe('ModuleLoader.resolve', () => {
|
|
|
39
39
|
'/proj/app.tjs': 'import { x } from "./math.tjs"',
|
|
40
40
|
'/proj/math.tjs': 'export const x = 1',
|
|
41
41
|
})
|
|
42
|
-
expect(loader.resolve('./math.tjs', p`/proj/app.tjs`)).toBe(
|
|
42
|
+
expect(loader.resolve('./math.tjs', p`/proj/app.tjs`)).toBe(
|
|
43
|
+
p`/proj/math.tjs`
|
|
44
|
+
)
|
|
43
45
|
})
|
|
44
46
|
|
|
45
47
|
it('resolves relative paths against baseDir when no importer is given', () => {
|
|
@@ -71,7 +73,9 @@ describe('ModuleLoader.resolve', () => {
|
|
|
71
73
|
const loader = loaderWith({
|
|
72
74
|
'/proj/legacy.ts': 'export const a = 1',
|
|
73
75
|
})
|
|
74
|
-
expect(loader.resolve('./legacy', p`/proj/app.tjs`)).toBe(
|
|
76
|
+
expect(loader.resolve('./legacy', p`/proj/app.tjs`)).toBe(
|
|
77
|
+
p`/proj/legacy.ts`
|
|
78
|
+
)
|
|
75
79
|
})
|
|
76
80
|
|
|
77
81
|
it('prefers .tjs when multiple extensions exist', () => {
|
|
@@ -97,9 +101,9 @@ describe('ModuleLoader.resolve', () => {
|
|
|
97
101
|
'/proj/node_modules/tjs-lang/linalg/index.tjs': 'export const dot = 1',
|
|
98
102
|
'/proj/src/inner/app.tjs': 'import { dot } from "tjs-lang/linalg"',
|
|
99
103
|
})
|
|
100
|
-
expect(
|
|
101
|
-
|
|
102
|
-
)
|
|
104
|
+
expect(loader.resolve('tjs-lang/linalg', p`/proj/src/inner/app.tjs`)).toBe(
|
|
105
|
+
p`/proj/node_modules/tjs-lang/linalg/index.tjs`
|
|
106
|
+
)
|
|
103
107
|
})
|
|
104
108
|
|
|
105
109
|
it('checks bareSpecifierRoots before walking node_modules', () => {
|
|
@@ -380,9 +380,9 @@ function collectExports(ast: any): ExportEntry[] {
|
|
|
380
380
|
decl?.type === 'ArrowFunctionExpression'
|
|
381
381
|
? 'function'
|
|
382
382
|
: decl?.type === 'ClassDeclaration' ||
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
383
|
+
decl?.type === 'ClassExpression'
|
|
384
|
+
? 'class'
|
|
385
|
+
: 'unknown'
|
|
386
386
|
exports.push({ name: 'default', kind })
|
|
387
387
|
} else if (node.type === 'ExportAllDeclaration') {
|
|
388
388
|
// export * from './other'
|
|
@@ -407,8 +407,7 @@ function collectExports(ast: any): ExportEntry[] {
|
|
|
407
407
|
export function inMemoryFileSystem(
|
|
408
408
|
files: Map<string, string> | Record<string, string>
|
|
409
409
|
): FileSystem {
|
|
410
|
-
const map =
|
|
411
|
-
files instanceof Map ? files : new Map(Object.entries(files))
|
|
410
|
+
const map = files instanceof Map ? files : new Map(Object.entries(files))
|
|
412
411
|
// Normalize separators so path.resolve()'s output matches map keys
|
|
413
412
|
const normalize = (p: string) => p.split('/').join(sep)
|
|
414
413
|
const lookup = (p: string) => map.get(p) ?? map.get(normalize(p)) ?? null
|