kopscript 0.1.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/LICENSE +21 -0
- package/README.md +588 -0
- package/bin/ks.js +17 -0
- package/dist/ast.js +2 -0
- package/dist/checker.js +1334 -0
- package/dist/cli.js +100 -0
- package/dist/codegen.js +339 -0
- package/dist/diagnostics.js +26 -0
- package/dist/lexer.js +301 -0
- package/dist/modules.js +133 -0
- package/dist/parser.js +995 -0
- package/dist/tokens.js +81 -0
- package/dist/types.js +108 -0
- package/package.json +43 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { writeFileSync, watch as fsWatch } from "node:fs";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { resolve, basename, join } from "node:path";
|
|
5
|
+
import { compileGraph } from "./modules.js";
|
|
6
|
+
function outputPathFor(filePath) {
|
|
7
|
+
const name = basename(filePath, ".ks");
|
|
8
|
+
return join(resolve(filePath, ".."), `${name}.js`);
|
|
9
|
+
}
|
|
10
|
+
// Compiles the whole module graph reachable from `filePath`, writing one
|
|
11
|
+
// .js file next to each .ks source.
|
|
12
|
+
function build(filePath) {
|
|
13
|
+
const result = compileGraph(filePath);
|
|
14
|
+
if (result.entryMissing) {
|
|
15
|
+
console.error(`ks: cannot find file '${filePath}'`);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
return { outPath: null, watchFiles: [] };
|
|
18
|
+
}
|
|
19
|
+
if (!result.success) {
|
|
20
|
+
for (const absPath of result.order) {
|
|
21
|
+
const mod = result.modules.get(absPath);
|
|
22
|
+
if (mod.diagnostics.hasErrors) {
|
|
23
|
+
console.error(mod.diagnostics.format(mod.source, absPath));
|
|
24
|
+
console.error("");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
return { outPath: null, watchFiles: result.order };
|
|
29
|
+
}
|
|
30
|
+
for (const absPath of result.order) {
|
|
31
|
+
writeFileSync(outputPathFor(absPath), result.outputs.get(absPath), "utf-8");
|
|
32
|
+
}
|
|
33
|
+
return { outPath: outputPathFor(filePath), watchFiles: result.order };
|
|
34
|
+
}
|
|
35
|
+
function run(filePath) {
|
|
36
|
+
const { outPath } = build(filePath);
|
|
37
|
+
if (!outPath)
|
|
38
|
+
return;
|
|
39
|
+
const result = spawnSync(process.execPath, [outPath], { stdio: "inherit" });
|
|
40
|
+
process.exitCode = result.status ?? 0;
|
|
41
|
+
}
|
|
42
|
+
// Rebuilds on every save of any file in the graph (not just the entry —
|
|
43
|
+
// editing a `using`'d dependency triggers a rebuild too). Watchers are torn
|
|
44
|
+
// down and re-established after each rebuild, since the dependency set
|
|
45
|
+
// itself can change (a `using` added or removed). Multiple change events
|
|
46
|
+
// from one save (common with editors that write-then-rename) are
|
|
47
|
+
// debounced into a single rebuild.
|
|
48
|
+
function watchCommand(filePath) {
|
|
49
|
+
let watchers = [];
|
|
50
|
+
let debounceTimer = null;
|
|
51
|
+
const clearWatchers = () => {
|
|
52
|
+
for (const w of watchers)
|
|
53
|
+
w.close();
|
|
54
|
+
watchers = [];
|
|
55
|
+
};
|
|
56
|
+
const rebuild = () => {
|
|
57
|
+
clearWatchers();
|
|
58
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
59
|
+
process.exitCode = 0; // each rebuild gets a clean slate; a fixed error shouldn't leave the process pre-marked failed
|
|
60
|
+
const { outPath, watchFiles } = build(filePath);
|
|
61
|
+
console.log(outPath ? `[${timestamp}] Wrote ${outPath}` : `[${timestamp}] Build failed`);
|
|
62
|
+
const filesToWatch = watchFiles.length > 0 ? watchFiles : [filePath];
|
|
63
|
+
for (const f of filesToWatch) {
|
|
64
|
+
try {
|
|
65
|
+
watchers.push(fsWatch(f, () => scheduleRebuild()));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Missing file (e.g. a typo'd entry path) — nothing to watch until it exists.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const scheduleRebuild = () => {
|
|
73
|
+
if (debounceTimer)
|
|
74
|
+
clearTimeout(debounceTimer);
|
|
75
|
+
debounceTimer = setTimeout(rebuild, 80);
|
|
76
|
+
};
|
|
77
|
+
console.log(`[watch] ${filePath} — watching for changes. Press Ctrl+C to stop.`);
|
|
78
|
+
rebuild();
|
|
79
|
+
}
|
|
80
|
+
function main() {
|
|
81
|
+
const [, , command, file] = process.argv;
|
|
82
|
+
if (!command || !file || (command !== "build" && command !== "run" && command !== "watch")) {
|
|
83
|
+
console.error("Usage: ks <build|run|watch> <file.ks>");
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const filePath = resolve(file);
|
|
88
|
+
if (command === "build") {
|
|
89
|
+
const { outPath } = build(filePath);
|
|
90
|
+
if (outPath)
|
|
91
|
+
console.log(`Wrote ${outPath}`);
|
|
92
|
+
}
|
|
93
|
+
else if (command === "watch") {
|
|
94
|
+
watchCommand(filePath);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
run(filePath);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
main();
|
package/dist/codegen.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// KopScript's PascalCase stdlib member names, mapped to their real JS names — a
|
|
2
|
+
// blind syntactic rename (codegen has no type information, so this fires on
|
|
3
|
+
// any member access with a matching name, string/array/user-class alike;
|
|
4
|
+
// same pre-existing risk as any of the entries below, going back to the
|
|
5
|
+
// original string stdlib). Map/Filter/ForEach need nothing beyond the
|
|
6
|
+
// rename — the callback and everything else already codegens generically.
|
|
7
|
+
// Push is handled separately in genCall, since it's non-mutating in KopScript
|
|
8
|
+
// (unlike JS's own Array.push) and needs a different call shape entirely.
|
|
9
|
+
const MEMBER_METHOD_MAP = {
|
|
10
|
+
Contains: "includes",
|
|
11
|
+
StartsWith: "startsWith",
|
|
12
|
+
EndsWith: "endsWith",
|
|
13
|
+
Replace: "replaceAll",
|
|
14
|
+
Split: "split",
|
|
15
|
+
Trim: "trim",
|
|
16
|
+
ToUpper: "toUpperCase",
|
|
17
|
+
ToLower: "toLowerCase",
|
|
18
|
+
Map: "map",
|
|
19
|
+
Filter: "filter",
|
|
20
|
+
ForEach: "forEach",
|
|
21
|
+
};
|
|
22
|
+
const BINARY_OP_MAP = {
|
|
23
|
+
"+": "+",
|
|
24
|
+
"-": "-",
|
|
25
|
+
"*": "*",
|
|
26
|
+
"/": "/",
|
|
27
|
+
"%": "%",
|
|
28
|
+
"==": "===",
|
|
29
|
+
"!=": "!==",
|
|
30
|
+
"<": "<",
|
|
31
|
+
">": ">",
|
|
32
|
+
"<=": "<=",
|
|
33
|
+
">=": ">=",
|
|
34
|
+
};
|
|
35
|
+
// Runtime backing for `state<T>`: a value plus its change listeners. Emitted
|
|
36
|
+
// inline (once per file) rather than pulled from a shared runtime package —
|
|
37
|
+
// consistent with the rest of codegen producing self-contained JS with no
|
|
38
|
+
// external dependency of its own.
|
|
39
|
+
const STATE_PRELUDE = `class __KopState {
|
|
40
|
+
constructor(value) {
|
|
41
|
+
this._value = value;
|
|
42
|
+
this._listeners = [];
|
|
43
|
+
}
|
|
44
|
+
get Value() { return this._value; }
|
|
45
|
+
set Value(v) {
|
|
46
|
+
this._value = v;
|
|
47
|
+
for (const listener of this._listeners) listener(v);
|
|
48
|
+
}
|
|
49
|
+
Subscribe(listener) {
|
|
50
|
+
this._listeners.push(listener);
|
|
51
|
+
}
|
|
52
|
+
}`;
|
|
53
|
+
export class CodeGenerator {
|
|
54
|
+
constructor() {
|
|
55
|
+
this.matchCounter = 0;
|
|
56
|
+
this.interfaceNames = new Set();
|
|
57
|
+
this.usesState = false;
|
|
58
|
+
}
|
|
59
|
+
// `usingExports` maps each `using "<path>"` path (as written in source) to
|
|
60
|
+
// the list of names that module exports — resolved by the caller from that
|
|
61
|
+
// 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()) {
|
|
64
|
+
this.interfaceNames = new Set(program.statements.filter((s) => s.kind === "InterfaceDecl").map((s) => s.name));
|
|
65
|
+
const importLines = program.usings
|
|
66
|
+
.map((u) => {
|
|
67
|
+
const names = usingExports.get(u.path) ?? [];
|
|
68
|
+
return names.length > 0 ? `import { ${names.join(", ")} } from "${u.path}.js";` : null;
|
|
69
|
+
})
|
|
70
|
+
.filter((line) => line !== null);
|
|
71
|
+
const lines = [];
|
|
72
|
+
for (const stmt of program.statements) {
|
|
73
|
+
if (stmt.kind === "InterfaceDecl")
|
|
74
|
+
continue; // compile-time only, no runtime representation
|
|
75
|
+
if (stmt.kind === "ExternFunctionDecl" || stmt.kind === "ExternClassDecl" || stmt.kind === "ExternValueDecl") {
|
|
76
|
+
// Extern class members need no codegen of their own — a call like
|
|
77
|
+
// `el.addEventListener(...)` is just an ordinary member call against
|
|
78
|
+
// whatever the class's own binding resolves to at runtime; only the
|
|
79
|
+
// binding itself (import / ambient alias / re-export) needs emitting.
|
|
80
|
+
const binding = this.genExternBinding(stmt.name, stmt.jsName, stmt.modulePath, stmt.isExported);
|
|
81
|
+
if (binding)
|
|
82
|
+
lines.push(binding);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
let code = this.genStatement(stmt, 0);
|
|
86
|
+
if ((stmt.kind === "ClassDecl" || stmt.kind === "EnumDecl" || stmt.kind === "FunctionDecl") && stmt.isExported) {
|
|
87
|
+
code = `export ${code}`;
|
|
88
|
+
}
|
|
89
|
+
lines.push(code);
|
|
90
|
+
}
|
|
91
|
+
const body = lines.join("\n") + "\n";
|
|
92
|
+
const sections = [importLines.join("\n"), this.usesState ? STATE_PRELUDE : "", body].filter((s) => s.length > 0);
|
|
93
|
+
return sections.join("\n\n");
|
|
94
|
+
}
|
|
95
|
+
// Builds the code (if any) that creates the local `name` binding for an
|
|
96
|
+
// extern declaration, wiring in `export` when needed:
|
|
97
|
+
// - imported (modulePath set): a real `import`, plus a plain re-export if exported
|
|
98
|
+
// - ambient + no rename: nothing at all — the bare global just resolves at runtime
|
|
99
|
+
// - ambient + renamed, or ambient + exported: an explicit `const` alias off
|
|
100
|
+
// `globalThis` (needed either way: to rebind the name, or because a bare
|
|
101
|
+
// ambient reference has no local binding to `export` in the first place)
|
|
102
|
+
genExternBinding(name, jsName, modulePath, isExported) {
|
|
103
|
+
if (modulePath) {
|
|
104
|
+
const importClause = jsName === name ? name : `${jsName} as ${name}`;
|
|
105
|
+
const importLine = `import { ${importClause} } from "${modulePath}";`;
|
|
106
|
+
return isExported ? `${importLine}\nexport { ${name} };` : importLine;
|
|
107
|
+
}
|
|
108
|
+
if (jsName === name && !isExported)
|
|
109
|
+
return null;
|
|
110
|
+
return `${isExported ? "export const" : "const"} ${name} = globalThis.${jsName};`;
|
|
111
|
+
}
|
|
112
|
+
// ---------- statements ----------
|
|
113
|
+
indentStr(indent) {
|
|
114
|
+
return " ".repeat(indent);
|
|
115
|
+
}
|
|
116
|
+
genStatement(stmt, indent) {
|
|
117
|
+
const pad = this.indentStr(indent);
|
|
118
|
+
switch (stmt.kind) {
|
|
119
|
+
case "VarDecl":
|
|
120
|
+
return `${pad}${stmt.isConst ? "const" : "let"} ${stmt.name} = ${this.genExpr(stmt.init)};`;
|
|
121
|
+
case "FunctionDecl":
|
|
122
|
+
return this.genFunction(stmt, indent);
|
|
123
|
+
case "ClassDecl":
|
|
124
|
+
return this.genClass(stmt, indent);
|
|
125
|
+
case "InterfaceDecl":
|
|
126
|
+
case "ExternFunctionDecl":
|
|
127
|
+
case "ExternClassDecl":
|
|
128
|
+
case "ExternValueDecl":
|
|
129
|
+
return ""; // handled directly in generate(); unreachable here except via a nested-decl error path
|
|
130
|
+
case "EnumDecl": {
|
|
131
|
+
const entries = stmt.members.map((m, i) => `${m}: ${i}`).join(", ");
|
|
132
|
+
return `${pad}const ${stmt.name} = Object.freeze({ ${entries} });`;
|
|
133
|
+
}
|
|
134
|
+
case "Block":
|
|
135
|
+
return this.genBlock(stmt, indent);
|
|
136
|
+
case "IfStatement":
|
|
137
|
+
return this.genIf(stmt, indent);
|
|
138
|
+
case "WhileStatement":
|
|
139
|
+
return `${pad}while (${this.genExpr(stmt.condition)}) ${this.genBlock(stmt.body, indent).trimStart()}`;
|
|
140
|
+
case "ForStatement": {
|
|
141
|
+
const init = stmt.init
|
|
142
|
+
? stmt.init.kind === "VarDecl"
|
|
143
|
+
? `${stmt.init.isConst ? "const" : "let"} ${stmt.init.name} = ${this.genExpr(stmt.init.init)}`
|
|
144
|
+
: this.genExpr(stmt.init.expression)
|
|
145
|
+
: "";
|
|
146
|
+
const cond = stmt.condition ? this.genExpr(stmt.condition) : "";
|
|
147
|
+
const update = stmt.update ? this.genExpr(stmt.update) : "";
|
|
148
|
+
return `${pad}for (${init}; ${cond}; ${update}) ${this.genBlock(stmt.body, indent).trimStart()}`;
|
|
149
|
+
}
|
|
150
|
+
case "ForInStatement":
|
|
151
|
+
return `${pad}for (const ${stmt.varName} of ${this.genExpr(stmt.iterable)}) ${this.genBlock(stmt.body, indent).trimStart()}`;
|
|
152
|
+
case "ReturnStatement":
|
|
153
|
+
return stmt.value ? `${pad}return ${this.genExpr(stmt.value)};` : `${pad}return;`;
|
|
154
|
+
case "BreakStatement":
|
|
155
|
+
return `${pad}break;`;
|
|
156
|
+
case "ContinueStatement":
|
|
157
|
+
return `${pad}continue;`;
|
|
158
|
+
case "ExpressionStatement":
|
|
159
|
+
return `${pad}${this.genExpr(stmt.expression)};`;
|
|
160
|
+
case "TryStatement":
|
|
161
|
+
return this.genTry(stmt, indent);
|
|
162
|
+
case "ThrowStatement":
|
|
163
|
+
return `${pad}throw ${this.genExpr(stmt.expression)};`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
genTry(stmt, indent) {
|
|
167
|
+
let code = `${this.indentStr(indent)}try ${this.genBlock(stmt.tryBlock, indent).trimStart()}`;
|
|
168
|
+
if (stmt.catchBlock && stmt.catchParam) {
|
|
169
|
+
code += ` catch (${stmt.catchParam.name}) ${this.genBlock(stmt.catchBlock, indent).trimStart()}`;
|
|
170
|
+
}
|
|
171
|
+
if (stmt.finallyBlock) {
|
|
172
|
+
code += ` finally ${this.genBlock(stmt.finallyBlock, indent).trimStart()}`;
|
|
173
|
+
}
|
|
174
|
+
return code;
|
|
175
|
+
}
|
|
176
|
+
genBlock(block, indent) {
|
|
177
|
+
const pad = this.indentStr(indent);
|
|
178
|
+
if (block.statements.length === 0)
|
|
179
|
+
return `{\n${pad}}`;
|
|
180
|
+
const body = block.statements.map((s) => this.genStatement(s, indent + 1)).join("\n");
|
|
181
|
+
return `{\n${body}\n${pad}}`;
|
|
182
|
+
}
|
|
183
|
+
genIf(stmt, indent) {
|
|
184
|
+
const pad = this.indentStr(indent);
|
|
185
|
+
let code = `${pad}if (${this.genExpr(stmt.condition)}) ${this.genBlock(stmt.thenBranch, indent).trimStart()}`;
|
|
186
|
+
if (stmt.elseBranch) {
|
|
187
|
+
if (stmt.elseBranch.kind === "IfStatement") {
|
|
188
|
+
code += ` else ${this.genIf(stmt.elseBranch, indent).trimStart()}`;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
code += ` else ${this.genBlock(stmt.elseBranch, indent).trimStart()}`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return code;
|
|
195
|
+
}
|
|
196
|
+
genFunction(decl, indent) {
|
|
197
|
+
const pad = this.indentStr(indent);
|
|
198
|
+
const params = decl.params.map((p) => p.name).join(", ");
|
|
199
|
+
const prefix = decl.isAsync ? "async function" : "function";
|
|
200
|
+
return `${pad}${prefix} ${decl.name}(${params}) ${this.genBlock(decl.body, indent).trimStart()}`;
|
|
201
|
+
}
|
|
202
|
+
genClass(decl, indent) {
|
|
203
|
+
const pad = this.indentStr(indent);
|
|
204
|
+
// The base list mixes an optional superclass with interface names (checker-validated);
|
|
205
|
+
// only the non-interface entry, if any, becomes a JS `extends` clause.
|
|
206
|
+
const superclass = decl.baseList.find((n) => !this.interfaceNames.has(n)) ?? null;
|
|
207
|
+
const header = superclass ? `class ${decl.name} extends ${superclass} {` : `class ${decl.name} {`;
|
|
208
|
+
const parts = [];
|
|
209
|
+
const memberPad = this.indentStr(indent + 1);
|
|
210
|
+
for (const field of decl.fields) {
|
|
211
|
+
if (field.isStatic)
|
|
212
|
+
parts.push(`${memberPad}static ${field.name} = ${this.genExpr(field.initializer)};`);
|
|
213
|
+
}
|
|
214
|
+
if (decl.constructor) {
|
|
215
|
+
parts.push(this.genConstructor(decl.constructor, indent + 1));
|
|
216
|
+
}
|
|
217
|
+
for (const method of decl.methods) {
|
|
218
|
+
const params = method.params.map((p) => p.name).join(", ");
|
|
219
|
+
const prefix = `${method.isStatic ? "static " : ""}${method.isAsync ? "async " : ""}`;
|
|
220
|
+
parts.push(`${memberPad}${prefix}${method.name}(${params}) ${this.genBlock(method.body, indent + 1).trimStart()}`);
|
|
221
|
+
}
|
|
222
|
+
const body = parts.join("\n\n");
|
|
223
|
+
return `${pad}${header}\n${body}\n${pad}}`;
|
|
224
|
+
}
|
|
225
|
+
// `: base(...)` compiles to `super(...)` as the literal first statement in
|
|
226
|
+
// the constructor body — required by real JS semantics (super() must run
|
|
227
|
+
// before `this` is touched), which is exactly the rule the checker already
|
|
228
|
+
// enforces, so this can't fail here.
|
|
229
|
+
genConstructor(ctor, indent) {
|
|
230
|
+
const pad = this.indentStr(indent);
|
|
231
|
+
const bodyPad = this.indentStr(indent + 1);
|
|
232
|
+
const params = ctor.params.map((p) => p.name).join(", ");
|
|
233
|
+
const lines = [];
|
|
234
|
+
if (ctor.baseArgs) {
|
|
235
|
+
lines.push(`${bodyPad}super(${ctor.baseArgs.map((a) => this.genExpr(a)).join(", ")});`);
|
|
236
|
+
}
|
|
237
|
+
lines.push(...ctor.body.statements.map((s) => this.genStatement(s, indent + 1)));
|
|
238
|
+
const bodyStr = lines.length > 0 ? `{\n${lines.join("\n")}\n${pad}}` : `{\n${pad}}`;
|
|
239
|
+
return `${pad}constructor(${params}) ${bodyStr}`;
|
|
240
|
+
}
|
|
241
|
+
// ---------- expressions ----------
|
|
242
|
+
genExpr(expr) {
|
|
243
|
+
switch (expr.kind) {
|
|
244
|
+
case "NumberLiteral":
|
|
245
|
+
return String(expr.value);
|
|
246
|
+
case "StringLiteral":
|
|
247
|
+
return JSON.stringify(expr.value);
|
|
248
|
+
case "BoolLiteral":
|
|
249
|
+
return String(expr.value);
|
|
250
|
+
case "InterpolatedStringLiteral":
|
|
251
|
+
return this.genInterpolatedString(expr);
|
|
252
|
+
case "ArrayLiteral":
|
|
253
|
+
return `[${expr.elements.map((e) => this.genExpr(e)).join(", ")}]`;
|
|
254
|
+
case "Identifier":
|
|
255
|
+
return expr.name;
|
|
256
|
+
case "ThisExpr":
|
|
257
|
+
return "this";
|
|
258
|
+
case "UnaryExpr":
|
|
259
|
+
return `${expr.op}${this.genExpr(expr.operand)}`;
|
|
260
|
+
case "BinaryExpr":
|
|
261
|
+
return `(${this.genExpr(expr.left)} ${BINARY_OP_MAP[expr.op]} ${this.genExpr(expr.right)})`;
|
|
262
|
+
case "LogicalExpr":
|
|
263
|
+
return `(${this.genExpr(expr.left)} ${expr.op} ${this.genExpr(expr.right)})`;
|
|
264
|
+
case "AssignExpr":
|
|
265
|
+
return `(${this.genExpr(expr.target)} = ${this.genExpr(expr.value)})`;
|
|
266
|
+
case "CallExpr":
|
|
267
|
+
return this.genCall(expr);
|
|
268
|
+
case "NewExpr":
|
|
269
|
+
return `new ${expr.className}(${expr.args.map((a) => this.genExpr(a)).join(", ")})`;
|
|
270
|
+
case "MemberExpr":
|
|
271
|
+
return this.genMember(expr);
|
|
272
|
+
case "IndexExpr":
|
|
273
|
+
return `${this.genExpr(expr.object)}[${this.genExpr(expr.index)}]`;
|
|
274
|
+
case "MatchExpr":
|
|
275
|
+
return this.genMatch(expr);
|
|
276
|
+
case "LambdaExpr":
|
|
277
|
+
return this.genLambda(expr);
|
|
278
|
+
case "AwaitExpr":
|
|
279
|
+
return `await ${this.genExpr(expr.operand)}`;
|
|
280
|
+
case "StateExpr":
|
|
281
|
+
this.usesState = true;
|
|
282
|
+
return `new __KopState(${this.genExpr(expr.initializer)})`;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
genLambda(expr) {
|
|
286
|
+
const params = expr.params.map((p) => p.name).join(", ");
|
|
287
|
+
if (expr.body.kind === "Block") {
|
|
288
|
+
return `(${params}) => ${this.genBlock(expr.body, 0).trimStart()}`;
|
|
289
|
+
}
|
|
290
|
+
return `(${params}) => (${this.genExpr(expr.body)})`;
|
|
291
|
+
}
|
|
292
|
+
genInterpolatedString(expr) {
|
|
293
|
+
const escapeText = (text) => text.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
294
|
+
const body = expr.parts
|
|
295
|
+
.map((part) => (part.kind === "Text" ? escapeText(part.text) : `\${${this.genExpr(part.expression)}}`))
|
|
296
|
+
.join("");
|
|
297
|
+
return `\`${body}\``;
|
|
298
|
+
}
|
|
299
|
+
genCall(expr) {
|
|
300
|
+
if (expr.callee.kind === "Identifier" && expr.callee.name === "print") {
|
|
301
|
+
return `console.log(${expr.args.map((a) => this.genExpr(a)).join(", ")})`;
|
|
302
|
+
}
|
|
303
|
+
// Array.Push is non-mutating in KopScript, unlike JS's own (mutating,
|
|
304
|
+
// length-returning) Array.prototype.push — so it can't just be a member
|
|
305
|
+
// rename like Map/Filter/ForEach; it needs an entirely different call
|
|
306
|
+
// shape (a spread into a fresh array).
|
|
307
|
+
if (expr.callee.kind === "MemberExpr" && expr.callee.property === "Push" && expr.args.length === 1) {
|
|
308
|
+
return `[...${this.genExpr(expr.callee.object)}, ${this.genExpr(expr.args[0])}]`;
|
|
309
|
+
}
|
|
310
|
+
return `${this.genExpr(expr.callee)}(${expr.args.map((a) => this.genExpr(a)).join(", ")})`;
|
|
311
|
+
}
|
|
312
|
+
genMember(expr) {
|
|
313
|
+
const jsProperty = expr.property === "Length" ? "length" : MEMBER_METHOD_MAP[expr.property] ?? expr.property;
|
|
314
|
+
return `${this.genExpr(expr.object)}.${jsProperty}`;
|
|
315
|
+
}
|
|
316
|
+
genMatch(expr) {
|
|
317
|
+
const subjectVar = `__subject${this.matchCounter++}`;
|
|
318
|
+
const lines = [`(() => {`, ` const ${subjectVar} = ${this.genExpr(expr.subject)};`];
|
|
319
|
+
let emittedIf = false;
|
|
320
|
+
expr.arms.forEach((arm) => {
|
|
321
|
+
if (arm.pattern.kind === "WildcardPattern") {
|
|
322
|
+
lines.push(emittedIf ? ` else {` : ` if (true) {`);
|
|
323
|
+
lines.push(` return ${this.genExpr(arm.result)};`);
|
|
324
|
+
lines.push(` }`);
|
|
325
|
+
emittedIf = true;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const condition = arm.pattern.kind === "RegexPattern"
|
|
329
|
+
? `new RegExp(${JSON.stringify(arm.pattern.source)}).test(${subjectVar})`
|
|
330
|
+
: arm.pattern.values.map((v) => `${subjectVar} === ${this.genExpr(v)}`).join(" || ");
|
|
331
|
+
lines.push(` ${emittedIf ? "else if" : "if"} (${condition}) {`);
|
|
332
|
+
lines.push(` return ${this.genExpr(arm.result)};`);
|
|
333
|
+
lines.push(` }`);
|
|
334
|
+
emittedIf = true;
|
|
335
|
+
});
|
|
336
|
+
lines.push(`})()`);
|
|
337
|
+
return lines.join("\n");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class DiagnosticBag {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.diagnostics = [];
|
|
4
|
+
}
|
|
5
|
+
error(message, line, col) {
|
|
6
|
+
this.diagnostics.push({ severity: "error", message, line, col });
|
|
7
|
+
}
|
|
8
|
+
warning(message, line, col) {
|
|
9
|
+
this.diagnostics.push({ severity: "warning", message, line, col });
|
|
10
|
+
}
|
|
11
|
+
get hasErrors() {
|
|
12
|
+
return this.diagnostics.some((d) => d.severity === "error");
|
|
13
|
+
}
|
|
14
|
+
format(source, fileName) {
|
|
15
|
+
const lines = source.split("\n");
|
|
16
|
+
return this.diagnostics
|
|
17
|
+
.map((d) => {
|
|
18
|
+
const sourceLine = lines[d.line - 1] ?? "";
|
|
19
|
+
const pointer = " ".repeat(Math.max(0, d.col - 1)) + "^";
|
|
20
|
+
return (`${fileName}:${d.line}:${d.col} - ${d.severity}: ${d.message}\n` +
|
|
21
|
+
` ${sourceLine}\n` +
|
|
22
|
+
` ${pointer}`);
|
|
23
|
+
})
|
|
24
|
+
.join("\n\n");
|
|
25
|
+
}
|
|
26
|
+
}
|