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/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
- writeFileSync(outputPathFor(absPath), result.outputs.get(absPath), "utf-8");
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
- const result = spawnSync(process.execPath, [outPath], { stdio: "inherit" });
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
- generate(program, usingExports = new Map(), rawContents = new Map()) {
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
- lines.push(`${stmt.isExported ? "export const" : "const"} ${stmt.name} = ${JSON.stringify(content)};`);
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
- return sections.join("\n\n");
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
- const body = block.statements.map((s) => this.genStatement(s, indent + 1)).join("\n");
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
- parts.push(`${memberPad}static ${field.name} = ${this.genExpr(field.initializer)};`);
296
+ sources.push({ node: field, build: () => `${memberPad}static ${field.name} = ${this.genExpr(field.initializer)};` });
219
297
  }
220
298
  if (decl.constructor) {
221
- parts.push(this.genConstructor(decl.constructor, indent + 1));
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
- parts.push(`${memberPad}${prefix}${method.name}(${params}) ${this.genBlock(method.body, indent + 1).trimStart()}`);
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
- if (ctor.baseArgs) {
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
- lines.push(...ctor.body.statements.map((s) => this.genStatement(s, indent + 1)));
244
- const bodyStr = lines.length > 0 ? `{\n${lines.join("\n")}\n${pad}}` : `{\n${pad}}`;
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) => (part.kind === "Text" ? escapeText(part.text) : `\${${this.genExpr(part.expression)}}`))
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
- const lines = [`(() => {`, ` const ${subjectVar} = ${this.genExpr(expr.subject)};`];
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(`})()`);
@@ -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
- outputs.set(absPath, new CodeGenerator().generate(mod.program, usingExports, rawContentsByModule.get(absPath) ?? new Map()));
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
  }