kopscript 0.7.2 → 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 +17 -8
- package/README.md +29 -3
- package/dist/checker.js +89 -89
- package/dist/cli.js +12 -2
- package/dist/codegen.js +162 -17
- package/dist/diagnostics.js +5 -5
- package/dist/lexer.js +4 -4
- package/dist/modules.js +21 -14
- package/dist/parser.js +22 -22
- package/dist/sourcemap.js +113 -0
- package/dist/template_compiler.js +1 -1
- package/dist/template_lexer.js +2 -2
- package/dist/template_parser.js +12 -12
- package/package.json +1 -1
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/diagnostics.js
CHANGED
|
@@ -2,11 +2,11 @@ export class DiagnosticBag {
|
|
|
2
2
|
constructor() {
|
|
3
3
|
this.diagnostics = [];
|
|
4
4
|
}
|
|
5
|
-
error(message, line, col) {
|
|
6
|
-
this.diagnostics.push({ severity: "error", message, line, col });
|
|
5
|
+
error(code, message, line, col) {
|
|
6
|
+
this.diagnostics.push({ code, severity: "error", message, line, col });
|
|
7
7
|
}
|
|
8
|
-
warning(message, line, col) {
|
|
9
|
-
this.diagnostics.push({ severity: "warning", message, line, col });
|
|
8
|
+
warning(code, message, line, col) {
|
|
9
|
+
this.diagnostics.push({ code, severity: "warning", message, line, col });
|
|
10
10
|
}
|
|
11
11
|
get hasErrors() {
|
|
12
12
|
return this.diagnostics.some((d) => d.severity === "error");
|
|
@@ -17,7 +17,7 @@ export class DiagnosticBag {
|
|
|
17
17
|
.map((d) => {
|
|
18
18
|
const sourceLine = lines[d.line - 1] ?? "";
|
|
19
19
|
const pointer = " ".repeat(Math.max(0, d.col - 1)) + "^";
|
|
20
|
-
return (`${fileName}:${d.line}:${d.col} - ${d.severity}: ${d.message}\n` +
|
|
20
|
+
return (`${fileName}:${d.line}:${d.col} - ${d.severity} ${d.code}: ${d.message}\n` +
|
|
21
21
|
` ${sourceLine}\n` +
|
|
22
22
|
` ${pointer}`);
|
|
23
23
|
})
|
package/dist/lexer.js
CHANGED
|
@@ -107,7 +107,7 @@ export class Lexer {
|
|
|
107
107
|
value += this.readStringChar();
|
|
108
108
|
}
|
|
109
109
|
if (this.isAtEnd()) {
|
|
110
|
-
this.diagnostics.error("Unterminated string literal", line, col);
|
|
110
|
+
this.diagnostics.error("KS1001", "Unterminated string literal", line, col);
|
|
111
111
|
}
|
|
112
112
|
else {
|
|
113
113
|
this.advance(); // closing quote
|
|
@@ -127,7 +127,7 @@ export class Lexer {
|
|
|
127
127
|
raw += this.advance();
|
|
128
128
|
}
|
|
129
129
|
if (this.isAtEnd()) {
|
|
130
|
-
this.diagnostics.error("Unterminated interpolated string literal", line, col);
|
|
130
|
+
this.diagnostics.error("KS1002", "Unterminated interpolated string literal", line, col);
|
|
131
131
|
}
|
|
132
132
|
else {
|
|
133
133
|
this.advance(); // closing quote
|
|
@@ -157,7 +157,7 @@ export class Lexer {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
if (this.isAtEnd()) {
|
|
160
|
-
this.diagnostics.error("Unterminated regex literal", line, col);
|
|
160
|
+
this.diagnostics.error("KS1003", "Unterminated regex literal", line, col);
|
|
161
161
|
}
|
|
162
162
|
else {
|
|
163
163
|
this.advance(); // closing quote
|
|
@@ -284,7 +284,7 @@ export class Lexer {
|
|
|
284
284
|
}
|
|
285
285
|
break;
|
|
286
286
|
}
|
|
287
|
-
this.diagnostics.error(`Unexpected character '${c}'`, line, col);
|
|
287
|
+
this.diagnostics.error("KS1004", `Unexpected character '${c}'`, line, col);
|
|
288
288
|
return this.next();
|
|
289
289
|
}
|
|
290
290
|
make(kind, lexeme, line, col) {
|
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";
|
|
@@ -25,21 +25,21 @@ function expandTemplates(program, absPath, diagnostics, fileOverrides) {
|
|
|
25
25
|
continue;
|
|
26
26
|
const templateRef = stmt.template;
|
|
27
27
|
if (stmt.methods.some((m) => m.name === "Render" && !m.isStatic)) {
|
|
28
|
-
diagnostics.error(`Class '${stmt.name}' has both a 'template' declaration and a hand-written 'Render()' method — remove one`, templateRef.line, templateRef.col);
|
|
28
|
+
diagnostics.error("KS3001", `Class '${stmt.name}' has both a 'template' declaration and a hand-written 'Render()' method — remove one`, templateRef.line, templateRef.col);
|
|
29
29
|
continue;
|
|
30
30
|
}
|
|
31
31
|
const templatePath = resolve(dirname(absPath), templateRef.path);
|
|
32
32
|
watchFiles.push(templatePath);
|
|
33
33
|
const exists = fileOverrides?.has(templatePath) || existsSync(templatePath);
|
|
34
34
|
if (!exists) {
|
|
35
|
-
diagnostics.error(`Cannot find template file '${templateRef.path}' (looked for '${displayPath(templatePath)}')`, templateRef.line, templateRef.col);
|
|
35
|
+
diagnostics.error("KS3002", `Cannot find template file '${templateRef.path}' (looked for '${displayPath(templatePath)}')`, templateRef.line, templateRef.col);
|
|
36
36
|
continue;
|
|
37
37
|
}
|
|
38
38
|
const templateSource = fileOverrides?.get(templatePath) ?? readFileSync(templatePath, "utf-8");
|
|
39
39
|
const templateDiagnostics = new DiagnosticBag();
|
|
40
40
|
const root = new TemplateParser(templateSource, templateDiagnostics).parseDocument();
|
|
41
41
|
for (const d of templateDiagnostics.diagnostics) {
|
|
42
|
-
diagnostics.error(`[${templateRef.path}:${d.line}:${d.col}] ${d.message}`, templateRef.line, templateRef.col);
|
|
42
|
+
diagnostics.error("KS3003", `[${templateRef.path}:${d.line}:${d.col}] ${d.message}`, templateRef.line, templateRef.col);
|
|
43
43
|
}
|
|
44
44
|
if (!root)
|
|
45
45
|
continue;
|
|
@@ -48,12 +48,12 @@ function expandTemplates(program, absPath, diagnostics, fileOverrides) {
|
|
|
48
48
|
const compileDiagnostics = new DiagnosticBag();
|
|
49
49
|
const { renderMethod, autoSubscribeFields } = new TemplateCompiler(memberNames, stateFieldNames, compileDiagnostics).compile(root, templateRef);
|
|
50
50
|
for (const d of compileDiagnostics.diagnostics) {
|
|
51
|
-
diagnostics.error(`[${templateRef.path}:${d.line}:${d.col}] ${d.message}`, templateRef.line, templateRef.col);
|
|
51
|
+
diagnostics.error("KS3004", `[${templateRef.path}:${d.line}:${d.col}] ${d.message}`, templateRef.line, templateRef.col);
|
|
52
52
|
}
|
|
53
53
|
stmt.methods.push(renderMethod);
|
|
54
54
|
if (autoSubscribeFields.length > 0) {
|
|
55
55
|
if (!stmt.constructor) {
|
|
56
|
-
diagnostics.error(`Class '${stmt.name}' has a template referencing state<T> field(s) (${autoSubscribeFields.join(", ")}) but no constructor to subscribe from`, templateRef.line, templateRef.col);
|
|
56
|
+
diagnostics.error("KS3005", `Class '${stmt.name}' has a template referencing state<T> field(s) (${autoSubscribeFields.join(", ")}) but no constructor to subscribe from`, templateRef.line, templateRef.col);
|
|
57
57
|
continue;
|
|
58
58
|
}
|
|
59
59
|
for (const fieldName of autoSubscribeFields) {
|
|
@@ -128,16 +128,16 @@ export function loadModuleGraph(entryAbsPath, fileOverrides) {
|
|
|
128
128
|
stack.push(absPath);
|
|
129
129
|
for (const u of program.usings) {
|
|
130
130
|
if (!u.path.startsWith("./") && !u.path.startsWith("../")) {
|
|
131
|
-
diagnostics.error(`'using' path '${u.path}' must be relative (start with './' or '../')`, u.line, u.col);
|
|
131
|
+
diagnostics.error("KS3006", `'using' path '${u.path}' must be relative (start with './' or '../')`, u.line, u.col);
|
|
132
132
|
continue;
|
|
133
133
|
}
|
|
134
134
|
const depPath = resolve(dirname(absPath), u.path) + ".ks";
|
|
135
135
|
if (!exists(depPath)) {
|
|
136
|
-
diagnostics.error(`Cannot find module '${u.path}' (looked for '${displayPath(depPath)}')`, u.line, u.col);
|
|
136
|
+
diagnostics.error("KS3007", `Cannot find module '${u.path}' (looked for '${displayPath(depPath)}')`, u.line, u.col);
|
|
137
137
|
continue;
|
|
138
138
|
}
|
|
139
139
|
if (stack.includes(depPath)) {
|
|
140
|
-
diagnostics.error(`Circular 'using' dependency: ${[...stack, depPath].map(displayPath).join(" -> ")}`, u.line, u.col);
|
|
140
|
+
diagnostics.error("KS3008", `Circular 'using' dependency: ${[...stack, depPath].map(displayPath).join(" -> ")}`, u.line, u.col);
|
|
141
141
|
continue;
|
|
142
142
|
}
|
|
143
143
|
record.dependencies.push(depPath);
|
|
@@ -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();
|
|
@@ -180,7 +180,7 @@ export function compileGraph(entryAbsPath, fileOverrides) {
|
|
|
180
180
|
const mergeOne = (name, apply) => {
|
|
181
181
|
const existingFrom = importedFrom.get(name);
|
|
182
182
|
if (existingFrom && existingFrom !== depPath) {
|
|
183
|
-
mod.diagnostics.error(`'${name}' is exported by both '${displayPath(existingFrom)}' and '${displayPath(depPath)}' — ambiguous 'using'`, u.line, u.col);
|
|
183
|
+
mod.diagnostics.error("KS3009", `'${name}' is exported by both '${displayPath(existingFrom)}' and '${displayPath(depPath)}' — ambiguous 'using'`, u.line, u.col);
|
|
184
184
|
return;
|
|
185
185
|
}
|
|
186
186
|
importedFrom.set(name, depPath);
|
|
@@ -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
|
}
|