kopscript 0.8.0 → 0.9.0
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/LLM.md +8 -3
- package/README.md +19 -2
- package/dist/cli.js +12 -2
- package/dist/codegen.js +162 -17
- package/dist/modules.js +12 -5
- package/dist/sourcemap.js +113 -0
- package/package.json +1 -1
package/LLM.md
CHANGED
|
@@ -11,8 +11,8 @@ Compiles to plain ES modules.
|
|
|
11
11
|
## CLI
|
|
12
12
|
|
|
13
13
|
```
|
|
14
|
-
ks build <file.ks> # type-check + emit <file>.js next to the source
|
|
15
|
-
ks run <file.ks> # build, then execute with node
|
|
14
|
+
ks build <file.ks> # type-check + emit <file>.js and <file>.js.map next to the source
|
|
15
|
+
ks run <file.ks> # build, then execute with node --enable-source-maps
|
|
16
16
|
ks watch <file.ks> # build, then rebuild on every change to any file in the graph
|
|
17
17
|
ks check <file.ks> # type-check only, no output files
|
|
18
18
|
ks build|check <file.ks> --json # single JSON object on stdout instead of human text
|
|
@@ -25,9 +25,14 @@ ks build|check <file.ks> --json # single JSON object on stdout instead of huma
|
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
`written` is the absolute paths actually written (`build` only, and only on success — a
|
|
28
|
-
failed `build` writes nothing
|
|
28
|
+
failed `build` writes nothing; `.js.map` paths aren't listed separately, one is written next
|
|
29
|
+
to each `.js` path in `written`). Exit code is `0` iff `success` is `true`. A missing entry
|
|
29
30
|
file yields `{ success: false, diagnostics: [], written: [], error: "cannot find file '...'" }`.
|
|
30
31
|
|
|
32
|
+
Source maps are statement-level, not column-level (see `src/sourcemap.ts`, hand-rolled VLQ,
|
|
33
|
+
zero added dependencies) — a thrown/uncaught error resolves to the right `.ks` line, but a
|
|
34
|
+
specific sub-expression within one line isn't separately mapped.
|
|
35
|
+
|
|
31
36
|
## File shape
|
|
32
37
|
|
|
33
38
|
```ks
|
package/README.md
CHANGED
|
@@ -54,6 +54,10 @@ LLM's context, as opposed to this README's narrative explanation.
|
|
|
54
54
|
file — interpolation, event/property bindings, `*if`/`*for` — down to the exact same
|
|
55
55
|
AST a hand-written `Render()` would produce, with auto-`Subscribe` wiring for state
|
|
56
56
|
referenced directly in the markup. See [Templates](#templates).
|
|
57
|
+
- **Real debugging, not just readable output**: `ks build` emits a real source map next to
|
|
58
|
+
every `.js` file (original `.ks` embedded, no separate file to ship), and `ks run` enables
|
|
59
|
+
it automatically — an uncaught exception names the real `.ks` file and line, not the
|
|
60
|
+
generated JS. Hand-rolled VLQ encoder, zero added dependencies. See "Source maps" below.
|
|
57
61
|
- **A companion framework, [Kopular](https://dev.azure.com/koppinator/Koppindependence/_git/Kopular)**:
|
|
58
62
|
components, constructor-injected services via a composition root (no DI container), and
|
|
59
63
|
real-URL routing (no config DSL) — built entirely on the features above, in a separate
|
|
@@ -741,8 +745,8 @@ runtime representation at all and are dropped from the emitted JS entirely.
|
|
|
741
745
|
## CLI
|
|
742
746
|
|
|
743
747
|
```
|
|
744
|
-
ks build <file.ks> # type-check and emit <file>.js next to the source
|
|
745
|
-
ks run <file.ks> # build, then execute the emitted JS with node
|
|
748
|
+
ks build <file.ks> # type-check and emit <file>.js (+ <file>.js.map) next to the source
|
|
749
|
+
ks run <file.ks> # build, then execute the emitted JS with node --enable-source-maps
|
|
746
750
|
ks watch <file.ks> # build, then rebuild on every change to any file in the graph
|
|
747
751
|
ks check <file.ks> # type-check only — no output files written
|
|
748
752
|
```
|
|
@@ -776,6 +780,19 @@ here), `KS5xxx` templates. There's no generated reference doc mapping every code
|
|
|
776
780
|
explanation yet — for now, `message` is still the primary explanation; `code` is for
|
|
777
781
|
matching, not (yet) for looking up docs.
|
|
778
782
|
|
|
783
|
+
**Source maps**: `build` writes a real source-map v3 `<file>.js.map` alongside every
|
|
784
|
+
`<file>.js`, with the original `.ks` source embedded (`sourcesContent`) so a deployed app
|
|
785
|
+
doesn't need to ship its `.ks` files for devtools/stack traces to show real source. `run`
|
|
786
|
+
passes Node's own `--enable-source-maps` flag automatically, so an uncaught exception
|
|
787
|
+
during `ks run` names the real `.ks` file and line, not the generated `.js`. Hand-rolled
|
|
788
|
+
(base64 VLQ), not the `source-map` npm package — kopscript has zero runtime dependencies
|
|
789
|
+
and this keeps it that way (see `src/sourcemap.ts`). **Scope**: statement-level, not full
|
|
790
|
+
expression/column-level — every statement (a class member, a block statement, a top-level
|
|
791
|
+
declaration) gets its own mapping, but a specific sub-expression *within* one line doesn't.
|
|
792
|
+
This is a deliberate v1 cut, the same spirit as generics/nullable types/templates: real,
|
|
793
|
+
useful debugging (correct stack-trace lines, working breakpoints) without a full rewrite of
|
|
794
|
+
codegen's string-concatenation architecture into a position-tracking writer.
|
|
795
|
+
|
|
779
796
|
During development, use `npm run ks -- <build|run|watch|check> <file.ks>` (backed by
|
|
780
797
|
`tsx`), or run `npm run build` to compile the TypeScript compiler itself to `dist/` and
|
|
781
798
|
use `node dist/cli.js` directly.
|
package/dist/cli.js
CHANGED
|
@@ -66,7 +66,14 @@ function build(filePath, jsonMode = false) {
|
|
|
66
66
|
return { outPath: null, watchFiles: allWatchFiles(result) };
|
|
67
67
|
}
|
|
68
68
|
for (const absPath of result.order) {
|
|
69
|
-
|
|
69
|
+
const outPath = outputPathFor(absPath);
|
|
70
|
+
writeFileSync(outPath, result.outputs.get(absPath), "utf-8");
|
|
71
|
+
const map = result.sourceMaps.get(absPath);
|
|
72
|
+
// Only a file with real code got a `sourceMappingURL` comment appended
|
|
73
|
+
// (see codegen.ts) — skip writing an orphaned .map for one that didn't.
|
|
74
|
+
if (result.outputs.get(absPath).includes("//# sourceMappingURL=")) {
|
|
75
|
+
writeFileSync(`${outPath}.map`, map, "utf-8");
|
|
76
|
+
}
|
|
70
77
|
}
|
|
71
78
|
if (jsonMode)
|
|
72
79
|
printJson(result, result.order.map(outputPathFor));
|
|
@@ -102,7 +109,10 @@ function run(filePath) {
|
|
|
102
109
|
const { outPath } = build(filePath);
|
|
103
110
|
if (!outPath)
|
|
104
111
|
return;
|
|
105
|
-
|
|
112
|
+
// Node has resolved `//# sourceMappingURL` comments natively since v12.12
|
|
113
|
+
// — this is what makes an uncaught exception during `ks run` show the
|
|
114
|
+
// real .ks file/line instead of the generated .js, with no extra setup.
|
|
115
|
+
const result = spawnSync(process.execPath, ["--enable-source-maps", outPath], { stdio: "inherit" });
|
|
106
116
|
process.exitCode = result.status ?? 0;
|
|
107
117
|
}
|
|
108
118
|
// Rebuilds on every save of any file in the graph (not just the entry —
|
package/dist/codegen.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import { SourceMapBuilder } from "./sourcemap.js";
|
|
2
|
+
function countNewlines(text) {
|
|
3
|
+
let n = 0;
|
|
4
|
+
for (let i = 0; i < text.length; i++)
|
|
5
|
+
if (text[i] === "\n")
|
|
6
|
+
n++;
|
|
7
|
+
return n;
|
|
8
|
+
}
|
|
1
9
|
// KopScript's PascalCase stdlib member names, mapped to their real JS names — a
|
|
2
10
|
// blind syntactic rename (codegen has no type information, so this fires on
|
|
3
11
|
// any member access with a matching name, string/array/user-class alike;
|
|
@@ -55,19 +63,45 @@ export class CodeGenerator {
|
|
|
55
63
|
this.matchCounter = 0;
|
|
56
64
|
this.interfaceNames = new Set();
|
|
57
65
|
this.usesState = false;
|
|
66
|
+
// The current 1-indexed *generated* output line — kept in sync with the
|
|
67
|
+
// text being built by every method that can introduce a literal `\n`
|
|
68
|
+
// (genBlock, genClass, genConstructor, genMatch, genInterpolatedString,
|
|
69
|
+
// and this method's own top-level loop). Nothing else needs to touch it:
|
|
70
|
+
// a "simple" single-line gen* case just leaves it untouched, and by the
|
|
71
|
+
// time control returns to whichever loop called it, `outputLine` already
|
|
72
|
+
// correctly reflects how many lines that call's output actually took —
|
|
73
|
+
// including a lambda/match-expression buried inside it, since those
|
|
74
|
+
// recurse back into the same bookkeeping. See sourcemap.ts for the actual
|
|
75
|
+
// encoder; see README.md's "Source maps" section for the v1 scope this
|
|
76
|
+
// is deliberately limited to (statement-level, not per-expression/column).
|
|
77
|
+
this.outputLine = 1;
|
|
78
|
+
}
|
|
79
|
+
mark(node, generatedColumn) {
|
|
80
|
+
this.sourceMap.addMapping(this.outputLine, generatedColumn, node.line, node.col);
|
|
58
81
|
}
|
|
59
82
|
// `usingExports` maps each `using "<path>"` path (as written in source) to
|
|
60
83
|
// the list of names that module exports — resolved by the caller from that
|
|
61
84
|
// module's own checked Program, since a bare `using` brings everything
|
|
62
|
-
// `public` into scope without naming it explicitly.
|
|
63
|
-
|
|
85
|
+
// `public` into scope without naming it explicitly. `sourceFileName`/
|
|
86
|
+
// `sourceText` are for the emitted source map only (both optional so every
|
|
87
|
+
// existing test call site keeps compiling unchanged).
|
|
88
|
+
generate(program, usingExports = new Map(), rawContents = new Map(), sourceFileName = "source.ks", sourceText = "") {
|
|
64
89
|
this.interfaceNames = new Set(program.statements.filter((s) => s.kind === "InterfaceDecl").map((s) => s.name));
|
|
90
|
+
this.outputLine = 1;
|
|
91
|
+
const outFileName = sourceFileName.replace(/\.ks$/, ".js");
|
|
92
|
+
this.sourceMap = new SourceMapBuilder(outFileName, sourceFileName, sourceText);
|
|
65
93
|
const importLines = program.usings
|
|
66
94
|
.map((u) => {
|
|
67
95
|
const names = usingExports.get(u.path) ?? [];
|
|
68
96
|
return names.length > 0 ? `import { ${names.join(", ")} } from "${u.path}.js";` : null;
|
|
69
97
|
})
|
|
70
98
|
.filter((line) => line !== null);
|
|
99
|
+
// Body statements are generated (and marked) starting from *relative*
|
|
100
|
+
// line 1 — whether the import/prelude sections actually precede them in
|
|
101
|
+
// the final output depends on `this.usesState`, which genExpr only
|
|
102
|
+
// finishes deciding partway through this very loop (StateExpr flips it
|
|
103
|
+
// lazily). Every mark recorded below gets shifted by the real prefix's
|
|
104
|
+
// line count right after, once that's known for certain.
|
|
71
105
|
const lines = [];
|
|
72
106
|
for (const stmt of program.statements) {
|
|
73
107
|
if (stmt.kind === "InterfaceDecl")
|
|
@@ -78,24 +112,41 @@ export class CodeGenerator {
|
|
|
78
112
|
// whatever the class's own binding resolves to at runtime; only the
|
|
79
113
|
// binding itself (import / ambient alias / re-export) needs emitting.
|
|
80
114
|
const binding = this.genExternBinding(stmt.name, stmt.jsName, stmt.modulePath, stmt.isExported);
|
|
81
|
-
if (binding)
|
|
115
|
+
if (binding) {
|
|
116
|
+
this.mark(stmt, 0);
|
|
82
117
|
lines.push(binding);
|
|
118
|
+
this.outputLine += countNewlines(binding) + 1;
|
|
119
|
+
}
|
|
83
120
|
continue;
|
|
84
121
|
}
|
|
85
122
|
if (stmt.kind === "RawStringDecl") {
|
|
86
123
|
const content = rawContents.get(stmt.name) ?? "";
|
|
87
|
-
|
|
124
|
+
const code = `${stmt.isExported ? "export const" : "const"} ${stmt.name} = ${JSON.stringify(content)};`;
|
|
125
|
+
this.mark(stmt, 0);
|
|
126
|
+
lines.push(code);
|
|
127
|
+
this.outputLine += countNewlines(code) + 1;
|
|
88
128
|
continue;
|
|
89
129
|
}
|
|
130
|
+
this.mark(stmt, 0);
|
|
90
131
|
let code = this.genStatement(stmt, 0);
|
|
91
132
|
if ((stmt.kind === "ClassDecl" || stmt.kind === "EnumDecl" || stmt.kind === "FunctionDecl") && stmt.isExported) {
|
|
92
133
|
code = `export ${code}`;
|
|
93
134
|
}
|
|
94
135
|
lines.push(code);
|
|
136
|
+
this.outputLine += 1; // move past this statement's own last line, to the "\n" join separator
|
|
95
137
|
}
|
|
96
138
|
const body = lines.join("\n") + "\n";
|
|
97
139
|
const sections = [importLines.join("\n"), this.usesState ? STATE_PRELUDE : "", body].filter((s) => s.length > 0);
|
|
98
|
-
|
|
140
|
+
const prefixSections = sections.slice(0, -1); // body is always last (see filter above) and always non-empty
|
|
141
|
+
const prefixText = prefixSections.length > 0 ? prefixSections.join("\n\n") + "\n\n" : "";
|
|
142
|
+
this.sourceMap.shiftLines(countNewlines(prefixText));
|
|
143
|
+
const rawCode = sections.join("\n\n");
|
|
144
|
+
// A file whose only statements were private/unexported/ambient-with-no-
|
|
145
|
+
// rename externs (nothing else) legitimately generates zero real code —
|
|
146
|
+
// leave it exactly empty rather than tacking on a comment pointing at a
|
|
147
|
+
// map with nothing worth mapping.
|
|
148
|
+
const code = rawCode.trim().length > 0 ? rawCode + `\n//# sourceMappingURL=${outFileName.split("/").pop()}.map\n` : rawCode;
|
|
149
|
+
return { code, map: this.sourceMap.toJSON() };
|
|
99
150
|
}
|
|
100
151
|
// Builds the code (if any) that creates the local `name` binding for an
|
|
101
152
|
// extern declaration, wiring in `export` when needed:
|
|
@@ -179,11 +230,30 @@ export class CodeGenerator {
|
|
|
179
230
|
}
|
|
180
231
|
return code;
|
|
181
232
|
}
|
|
233
|
+
// Entry: `outputLine` is the line the caller's own `{` is being appended
|
|
234
|
+
// to (a block never starts its own fresh line — see e.g. genIf's ` else `
|
|
235
|
+
// continuing straight on from the previous block's own closing `}`).
|
|
236
|
+
// Exit: `outputLine` is left AT this block's own closing-`}` line (not one
|
|
237
|
+
// past it), so a caller that continues on the same line (`} else {`,
|
|
238
|
+
// `} catch (e) {`) doesn't need to undo anything, and one that doesn't
|
|
239
|
+
// (the statement/member loops below) just adds one more line itself.
|
|
182
240
|
genBlock(block, indent) {
|
|
183
241
|
const pad = this.indentStr(indent);
|
|
184
|
-
if (block.statements.length === 0)
|
|
242
|
+
if (block.statements.length === 0) {
|
|
243
|
+
this.outputLine += 1; // the "\n" between "{" and the closing pad}
|
|
185
244
|
return `{\n${pad}}`;
|
|
186
|
-
|
|
245
|
+
}
|
|
246
|
+
const memberCol = this.indentStr(indent + 1).length;
|
|
247
|
+
this.outputLine += 1; // "\n" after "{", moving to the first statement's line
|
|
248
|
+
const pieces = block.statements.map((s, i) => {
|
|
249
|
+
this.mark(s, memberCol);
|
|
250
|
+
const text = this.genStatement(s, indent + 1);
|
|
251
|
+
if (i < block.statements.length - 1)
|
|
252
|
+
this.outputLine += 1; // "\n" before the next statement
|
|
253
|
+
return text;
|
|
254
|
+
});
|
|
255
|
+
this.outputLine += 1; // "\n" before the closing pad}
|
|
256
|
+
const body = pieces.join("\n");
|
|
187
257
|
return `{\n${body}\n${pad}}`;
|
|
188
258
|
}
|
|
189
259
|
genIf(stmt, indent) {
|
|
@@ -205,26 +275,48 @@ export class CodeGenerator {
|
|
|
205
275
|
const prefix = decl.isAsync ? "async function" : "function";
|
|
206
276
|
return `${pad}${prefix} ${decl.name}(${params}) ${this.genBlock(decl.body, indent).trimStart()}`;
|
|
207
277
|
}
|
|
278
|
+
// Same entry/exit `outputLine` convention as genBlock. The one difference:
|
|
279
|
+
// members are separated by a full blank line (`.join("\n\n")`), so each
|
|
280
|
+
// non-last member advances by 2, not 1. Marking has to happen immediately
|
|
281
|
+
// before each member's *own* generation call (a constructor/method body
|
|
282
|
+
// can itself contain nested blocks that advance `outputLine` further), so
|
|
283
|
+
// member sources are built as `{ node, build }` pairs and only invoked
|
|
284
|
+
// inside the loop below, not gathered as plain strings up front.
|
|
208
285
|
genClass(decl, indent) {
|
|
209
286
|
const pad = this.indentStr(indent);
|
|
210
287
|
// The base list mixes an optional superclass with interface names (checker-validated);
|
|
211
288
|
// only the non-interface entry, if any, becomes a JS `extends` clause.
|
|
212
289
|
const superclass = decl.baseList.find((n) => !this.interfaceNames.has(n)) ?? null;
|
|
213
290
|
const header = superclass ? `class ${decl.name} extends ${superclass} {` : `class ${decl.name} {`;
|
|
214
|
-
const parts = [];
|
|
215
291
|
const memberPad = this.indentStr(indent + 1);
|
|
292
|
+
const memberCol = memberPad.length;
|
|
293
|
+
const sources = [];
|
|
216
294
|
for (const field of decl.fields) {
|
|
217
295
|
if (field.isStatic)
|
|
218
|
-
|
|
296
|
+
sources.push({ node: field, build: () => `${memberPad}static ${field.name} = ${this.genExpr(field.initializer)};` });
|
|
219
297
|
}
|
|
220
298
|
if (decl.constructor) {
|
|
221
|
-
|
|
299
|
+
const ctor = decl.constructor;
|
|
300
|
+
sources.push({ node: ctor, build: () => this.genConstructor(ctor, indent + 1) });
|
|
222
301
|
}
|
|
223
302
|
for (const method of decl.methods) {
|
|
224
303
|
const params = method.params.map((p) => p.name).join(", ");
|
|
225
304
|
const prefix = `${method.isStatic ? "static " : ""}${method.isAsync ? "async " : ""}`;
|
|
226
|
-
|
|
305
|
+
sources.push({ node: method, build: () => `${memberPad}${prefix}${method.name}(${params}) ${this.genBlock(method.body, indent + 1).trimStart()}` });
|
|
306
|
+
}
|
|
307
|
+
if (sources.length === 0) {
|
|
308
|
+
this.outputLine += 2; // "\n" after header, "\n" before the closing pad}
|
|
309
|
+
return `${pad}${header}\n\n${pad}}`;
|
|
227
310
|
}
|
|
311
|
+
this.outputLine += 1; // "\n" after header, moving to the first member's line
|
|
312
|
+
const parts = sources.map((m, i) => {
|
|
313
|
+
this.mark(m.node, memberCol);
|
|
314
|
+
const text = m.build();
|
|
315
|
+
if (i < sources.length - 1)
|
|
316
|
+
this.outputLine += 2; // blank line before the next member
|
|
317
|
+
return text;
|
|
318
|
+
});
|
|
319
|
+
this.outputLine += 1; // "\n" before the closing pad}
|
|
228
320
|
const body = parts.join("\n\n");
|
|
229
321
|
return `${pad}${header}\n${body}\n${pad}}`;
|
|
230
322
|
}
|
|
@@ -235,14 +327,36 @@ export class CodeGenerator {
|
|
|
235
327
|
genConstructor(ctor, indent) {
|
|
236
328
|
const pad = this.indentStr(indent);
|
|
237
329
|
const bodyPad = this.indentStr(indent + 1);
|
|
330
|
+
const bodyCol = bodyPad.length;
|
|
238
331
|
const params = ctor.params.map((p) => p.name).join(", ");
|
|
332
|
+
// `super(...)` (if any) has no AST statement node of its own to mark —
|
|
333
|
+
// it's synthesized from `ctor.baseArgs`, a plain expression list, not a
|
|
334
|
+
// Statement — so it's the one line in a constructor body that goes
|
|
335
|
+
// unmapped; every real statement after it still gets its own mark.
|
|
336
|
+
const hasSuper = ctor.baseArgs !== null;
|
|
337
|
+
const totalLines = (hasSuper ? 1 : 0) + ctor.body.statements.length;
|
|
338
|
+
if (totalLines === 0) {
|
|
339
|
+
this.outputLine += 1; // "\n" before the closing pad}
|
|
340
|
+
return `${pad}constructor(${params}) {\n${pad}}`;
|
|
341
|
+
}
|
|
342
|
+
this.outputLine += 1; // "\n" after "{", moving to the first line
|
|
239
343
|
const lines = [];
|
|
240
|
-
|
|
344
|
+
let emitted = 0;
|
|
345
|
+
if (hasSuper) {
|
|
241
346
|
lines.push(`${bodyPad}super(${ctor.baseArgs.map((a) => this.genExpr(a)).join(", ")});`);
|
|
347
|
+
emitted++;
|
|
348
|
+
if (emitted < totalLines)
|
|
349
|
+
this.outputLine += 1;
|
|
350
|
+
}
|
|
351
|
+
for (const s of ctor.body.statements) {
|
|
352
|
+
this.mark(s, bodyCol);
|
|
353
|
+
lines.push(this.genStatement(s, indent + 1));
|
|
354
|
+
emitted++;
|
|
355
|
+
if (emitted < totalLines)
|
|
356
|
+
this.outputLine += 1;
|
|
242
357
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
return `${pad}constructor(${params}) ${bodyStr}`;
|
|
358
|
+
this.outputLine += 1; // "\n" before the closing pad}
|
|
359
|
+
return `${pad}constructor(${params}) {\n${lines.join("\n")}\n${pad}}`;
|
|
246
360
|
}
|
|
247
361
|
// ---------- expressions ----------
|
|
248
362
|
// genExpr wraps an assignment in parens unconditionally (`(x = y)`), since
|
|
@@ -316,7 +430,19 @@ export class CodeGenerator {
|
|
|
316
430
|
genInterpolatedString(expr) {
|
|
317
431
|
const escapeText = (text) => text.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
318
432
|
const body = expr.parts
|
|
319
|
-
.map((part) =>
|
|
433
|
+
.map((part) => {
|
|
434
|
+
if (part.kind === "Text") {
|
|
435
|
+
// A `\n` escape in the source literal is already a real newline
|
|
436
|
+
// character in `part.text` by the time it reaches codegen (the
|
|
437
|
+
// lexer resolves it) — it lands verbatim in the JS template
|
|
438
|
+
// literal too (backticks support real embedded newlines), so
|
|
439
|
+
// `outputLine` has to account for it or every mark recorded after
|
|
440
|
+
// this point in the file would be off.
|
|
441
|
+
this.outputLine += countNewlines(part.text);
|
|
442
|
+
return escapeText(part.text);
|
|
443
|
+
}
|
|
444
|
+
return `\${${this.genExpr(part.expression)}}`;
|
|
445
|
+
})
|
|
320
446
|
.join("");
|
|
321
447
|
return `\`${body}\``;
|
|
322
448
|
}
|
|
@@ -348,15 +474,31 @@ export class CodeGenerator {
|
|
|
348
474
|
const jsProperty = expr.property === "Length" ? "length" : MEMBER_METHOD_MAP[expr.property] ?? expr.property;
|
|
349
475
|
return `${this.genExpr(expr.object)}.${jsProperty}`;
|
|
350
476
|
}
|
|
477
|
+
// Each arm gets its own mark, at the line its own `if`/`else` opens on —
|
|
478
|
+
// MatchArm carries `line`/`col` even though its pattern variants don't
|
|
479
|
+
// (see ast.ts). One known v1 imprecision, same spirit as the rest of this
|
|
480
|
+
// file's statement-level (not full-expression) granularity: if an arm's
|
|
481
|
+
// *result* expression itself contains a nested multi-line construct (a
|
|
482
|
+
// lambda with a block body, or another `match`), the fixed "+1 line" this
|
|
483
|
+
// method advances by per template line undercounts, and marks recorded
|
|
484
|
+
// after that arm land one or more lines early. Rare in practice (a match
|
|
485
|
+
// arm's result is usually a short expression), not attempted here.
|
|
351
486
|
genMatch(expr) {
|
|
352
487
|
const subjectVar = `__subject${this.matchCounter++}`;
|
|
353
|
-
|
|
488
|
+
this.outputLine += 1; // "\n" after "(() => {", moving to the "const subjectN = ..." line
|
|
489
|
+
const subjectText = this.genExpr(expr.subject);
|
|
490
|
+
const lines = [`(() => {`, ` const ${subjectVar} = ${subjectText};`];
|
|
491
|
+
this.outputLine += 1; // "\n" before the first arm's own line
|
|
354
492
|
let emittedIf = false;
|
|
355
493
|
expr.arms.forEach((arm) => {
|
|
494
|
+
this.mark(arm, 2);
|
|
356
495
|
if (arm.pattern.kind === "WildcardPattern") {
|
|
357
496
|
lines.push(emittedIf ? ` else {` : ` if (true) {`);
|
|
497
|
+
this.outputLine += 1;
|
|
358
498
|
lines.push(` return ${this.genExpr(arm.result)};`);
|
|
499
|
+
this.outputLine += 1;
|
|
359
500
|
lines.push(` }`);
|
|
501
|
+
this.outputLine += 1;
|
|
360
502
|
emittedIf = true;
|
|
361
503
|
return;
|
|
362
504
|
}
|
|
@@ -364,8 +506,11 @@ export class CodeGenerator {
|
|
|
364
506
|
? `new RegExp(${JSON.stringify(arm.pattern.source)}).test(${subjectVar})`
|
|
365
507
|
: arm.pattern.values.map((v) => `${subjectVar} === ${this.genExpr(v)}`).join(" || ");
|
|
366
508
|
lines.push(` ${emittedIf ? "else if" : "if"} (${condition}) {`);
|
|
509
|
+
this.outputLine += 1;
|
|
367
510
|
lines.push(` return ${this.genExpr(arm.result)};`);
|
|
511
|
+
this.outputLine += 1;
|
|
368
512
|
lines.push(` }`);
|
|
513
|
+
this.outputLine += 1;
|
|
369
514
|
emittedIf = true;
|
|
370
515
|
});
|
|
371
516
|
lines.push(`})()`);
|
package/dist/modules.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, relative, resolve } from "node:path";
|
|
2
|
+
import { basename, dirname, relative, resolve } from "node:path";
|
|
3
3
|
import { Lexer } from "./lexer.js";
|
|
4
4
|
import { Parser } from "./parser.js";
|
|
5
5
|
import { Checker, emptyModuleExports } from "./checker.js";
|
|
@@ -157,7 +157,7 @@ export function loadModuleGraph(entryAbsPath, fileOverrides) {
|
|
|
157
157
|
export function compileGraph(entryAbsPath, fileOverrides) {
|
|
158
158
|
const { modules, order, entryMissing } = loadModuleGraph(entryAbsPath, fileOverrides);
|
|
159
159
|
if (entryMissing) {
|
|
160
|
-
return { success: false, entryMissing: true, modules, order, outputs: new Map() };
|
|
160
|
+
return { success: false, entryMissing: true, modules, order, outputs: new Map(), sourceMaps: new Map() };
|
|
161
161
|
}
|
|
162
162
|
const exportsByModule = new Map();
|
|
163
163
|
const rawContentsByModule = new Map();
|
|
@@ -219,9 +219,10 @@ export function compileGraph(entryAbsPath, fileOverrides) {
|
|
|
219
219
|
rawContentsByModule.set(absPath, checker.getRawContents());
|
|
220
220
|
}
|
|
221
221
|
if (hasErrors) {
|
|
222
|
-
return { success: false, entryMissing: false, modules, order, outputs: new Map() };
|
|
222
|
+
return { success: false, entryMissing: false, modules, order, outputs: new Map(), sourceMaps: new Map() };
|
|
223
223
|
}
|
|
224
224
|
const outputs = new Map();
|
|
225
|
+
const sourceMaps = new Map();
|
|
225
226
|
for (const absPath of order) {
|
|
226
227
|
const mod = modules.get(absPath);
|
|
227
228
|
const usingExports = new Map();
|
|
@@ -235,7 +236,13 @@ export function compileGraph(entryAbsPath, fileOverrides) {
|
|
|
235
236
|
usingExports.set(u.path, [...importableTypeNames, ...depExports.functions.keys(), ...depExports.externValues.keys()]);
|
|
236
237
|
}
|
|
237
238
|
}
|
|
238
|
-
|
|
239
|
+
// Just the basename, not a cwd-relative path — the .js.map file always
|
|
240
|
+
// lands right next to its .ks/.js siblings (ks build writes output in
|
|
241
|
+
// place), so "sources" has to resolve relative to *that* directory, not
|
|
242
|
+
// wherever the compiler happened to be invoked from.
|
|
243
|
+
const { code, map } = new CodeGenerator().generate(mod.program, usingExports, rawContentsByModule.get(absPath) ?? new Map(), basename(absPath), mod.source);
|
|
244
|
+
outputs.set(absPath, code);
|
|
245
|
+
sourceMaps.set(absPath, map);
|
|
239
246
|
}
|
|
240
|
-
return { success: true, entryMissing: false, modules, order, outputs };
|
|
247
|
+
return { success: true, entryMissing: false, modules, order, outputs, sourceMaps };
|
|
241
248
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// A minimal, self-contained source-map v3 (base64 VLQ) encoder — hand-rolled
|
|
2
|
+
// rather than pulling in the `source-map` package, to keep kopscript at zero
|
|
3
|
+
// runtime dependencies (see README.md/package.json). No imports beyond this
|
|
4
|
+
// file's own code; nothing here reads or writes the filesystem.
|
|
5
|
+
//
|
|
6
|
+
// Spec: https://sourcemaps.info/spec.html — the encoding this file
|
|
7
|
+
// implements (VLQ digit format, per-line column reset) is described there.
|
|
8
|
+
const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
9
|
+
const VLQ_BASE_SHIFT = 5;
|
|
10
|
+
const VLQ_BASE = 1 << VLQ_BASE_SHIFT; // 32 — 5 data bits per base64 digit
|
|
11
|
+
const VLQ_BASE_MASK = VLQ_BASE - 1; // 0x1F
|
|
12
|
+
const VLQ_CONTINUATION_BIT = VLQ_BASE; // 0x20 — set on every digit but the last
|
|
13
|
+
// Sign-and-magnitude, not two's complement: the sign lives in the low bit of
|
|
14
|
+
// the shifted value, not as a separate leading digit — this is what makes a
|
|
15
|
+
// small negative delta (the common case for a line's *second* segment
|
|
16
|
+
// resetting a column back down) encode just as compactly as a small
|
|
17
|
+
// positive one.
|
|
18
|
+
function toSignedVLQ(value) {
|
|
19
|
+
return value < 0 ? (-value << 1) + 1 : value << 1;
|
|
20
|
+
}
|
|
21
|
+
function encodeVLQValue(value) {
|
|
22
|
+
let vlq = toSignedVLQ(value);
|
|
23
|
+
let result = "";
|
|
24
|
+
do {
|
|
25
|
+
let digit = vlq & VLQ_BASE_MASK;
|
|
26
|
+
vlq >>>= VLQ_BASE_SHIFT;
|
|
27
|
+
if (vlq > 0)
|
|
28
|
+
digit |= VLQ_CONTINUATION_BIT;
|
|
29
|
+
result += BASE64_CHARS[digit];
|
|
30
|
+
} while (vlq > 0);
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
// Encodes one mapping segment's fields (already converted to deltas by the
|
|
34
|
+
// caller) as a contiguous run of VLQ digit-groups — e.g. `[0, 0, 0, 0]` (the
|
|
35
|
+
// "no change from the previous segment" case) always encodes to `"AAAA"`.
|
|
36
|
+
export function encodeVLQ(values) {
|
|
37
|
+
return values.map(encodeVLQValue).join("");
|
|
38
|
+
}
|
|
39
|
+
// Builds one file's source-map v3 JSON incrementally, one `addMapping` call
|
|
40
|
+
// per source position worth recording (see codegen.ts for how call sites are
|
|
41
|
+
// chosen — statement-level, not full expression/column granularity). Only
|
|
42
|
+
// ever describes a single source file per generated file, matching
|
|
43
|
+
// kopscript's one-.ks-file-in, one-.js-file-out compilation model.
|
|
44
|
+
export class SourceMapBuilder {
|
|
45
|
+
constructor(fileName, sourceFileName, sourceContent) {
|
|
46
|
+
// `mappings` is one comma-joined segment-string per GENERATED line,
|
|
47
|
+
// joined with `;` at the end — an empty array entry for a generated line
|
|
48
|
+
// with no recorded mapping produces the required-but-empty group between
|
|
49
|
+
// two semicolons, not a skipped line (that would shift every later line's
|
|
50
|
+
// meaning, since position in the `;`-separated list *is* the line number).
|
|
51
|
+
this.lineGroups = [];
|
|
52
|
+
// Per the spec, `generatedColumn` deltas reset to being measured from 0
|
|
53
|
+
// at the start of every generated line, but `sourceLine`/`sourceColumn`
|
|
54
|
+
// deltas accumulate across the *entire* file, never resetting — tracking
|
|
55
|
+
// both correctly is the one genuinely easy-to-get-wrong part of this.
|
|
56
|
+
this.prevGeneratedColumn = 0;
|
|
57
|
+
this.prevSourceLine = 0;
|
|
58
|
+
this.prevSourceColumn = 0;
|
|
59
|
+
this.fileName = fileName;
|
|
60
|
+
this.sourceFileName = sourceFileName;
|
|
61
|
+
this.sourceContent = sourceContent;
|
|
62
|
+
}
|
|
63
|
+
lineGroup(generatedLine) {
|
|
64
|
+
const index = generatedLine - 1;
|
|
65
|
+
while (this.lineGroups.length <= index)
|
|
66
|
+
this.lineGroups.push([]);
|
|
67
|
+
return this.lineGroups[index];
|
|
68
|
+
}
|
|
69
|
+
// `generatedLine`/`sourceLine`/`sourceColumn` are 1-indexed (kopscript's
|
|
70
|
+
// own convention throughout the lexer/parser/AST — see lexer.ts). Per the
|
|
71
|
+
// source-map spec itself, everything is 0-indexed in the encoded output;
|
|
72
|
+
// the `- 1`s below are that conversion, done in exactly one place.
|
|
73
|
+
// `generatedColumn` is the real (already 0-indexed) character offset from
|
|
74
|
+
// the start of its generated line — there's no separate convention to
|
|
75
|
+
// convert there since "0 spaces of indent" already means column 0.
|
|
76
|
+
addMapping(generatedLine, generatedColumn, sourceLine, sourceColumn) {
|
|
77
|
+
const group = this.lineGroup(generatedLine);
|
|
78
|
+
if (group.length === 0)
|
|
79
|
+
this.prevGeneratedColumn = 0; // new line: column deltas restart at 0
|
|
80
|
+
const zSourceLine = sourceLine - 1;
|
|
81
|
+
const zSourceColumn = sourceColumn - 1;
|
|
82
|
+
group.push(encodeVLQ([
|
|
83
|
+
generatedColumn - this.prevGeneratedColumn,
|
|
84
|
+
0, // sourceIndex delta — always 0, exactly one source file per map
|
|
85
|
+
zSourceLine - this.prevSourceLine,
|
|
86
|
+
zSourceColumn - this.prevSourceColumn,
|
|
87
|
+
]));
|
|
88
|
+
this.prevGeneratedColumn = generatedColumn;
|
|
89
|
+
this.prevSourceLine = zSourceLine;
|
|
90
|
+
this.prevSourceColumn = zSourceColumn;
|
|
91
|
+
}
|
|
92
|
+
// Prepends `count` empty line-groups — for when marks were recorded
|
|
93
|
+
// relative to a chunk's own start (line 1) before it was known how many
|
|
94
|
+
// lines of *other* content would end up preceding that chunk in the final
|
|
95
|
+
// output. See codegen.ts's `generate()`: body statements are marked
|
|
96
|
+
// before it's known whether the `state<T>` prelude will precede them.
|
|
97
|
+
shiftLines(count) {
|
|
98
|
+
if (count <= 0)
|
|
99
|
+
return;
|
|
100
|
+
this.lineGroups.unshift(...Array.from({ length: count }, () => []));
|
|
101
|
+
}
|
|
102
|
+
toJSON() {
|
|
103
|
+
const mappings = this.lineGroups.map((segments) => segments.join(",")).join(";");
|
|
104
|
+
return JSON.stringify({
|
|
105
|
+
version: 3,
|
|
106
|
+
file: this.fileName,
|
|
107
|
+
sources: [this.sourceFileName],
|
|
108
|
+
sourcesContent: [this.sourceContent],
|
|
109
|
+
names: [],
|
|
110
|
+
mappings,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
package/package.json
CHANGED