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/lexer.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { TokenKind } from "./tokens.js";
|
|
2
|
+
const KEYWORDS = {
|
|
3
|
+
using: TokenKind.Using,
|
|
4
|
+
extern: TokenKind.Extern,
|
|
5
|
+
from: TokenKind.From,
|
|
6
|
+
as: TokenKind.As,
|
|
7
|
+
const: TokenKind.Const,
|
|
8
|
+
class: TokenKind.Class,
|
|
9
|
+
interface: TokenKind.Interface,
|
|
10
|
+
enum: TokenKind.Enum,
|
|
11
|
+
constructor: TokenKind.Constructor,
|
|
12
|
+
public: TokenKind.Public,
|
|
13
|
+
private: TokenKind.Private,
|
|
14
|
+
protected: TokenKind.Protected,
|
|
15
|
+
static: TokenKind.Static,
|
|
16
|
+
virtual: TokenKind.Virtual,
|
|
17
|
+
override: TokenKind.Override,
|
|
18
|
+
get: TokenKind.Get,
|
|
19
|
+
set: TokenKind.Set,
|
|
20
|
+
return: TokenKind.Return,
|
|
21
|
+
if: TokenKind.If,
|
|
22
|
+
else: TokenKind.Else,
|
|
23
|
+
while: TokenKind.While,
|
|
24
|
+
for: TokenKind.For,
|
|
25
|
+
foreach: TokenKind.Foreach,
|
|
26
|
+
in: TokenKind.In,
|
|
27
|
+
break: TokenKind.Break,
|
|
28
|
+
continue: TokenKind.Continue,
|
|
29
|
+
match: TokenKind.Match,
|
|
30
|
+
this: TokenKind.This,
|
|
31
|
+
base: TokenKind.Base,
|
|
32
|
+
new: TokenKind.New,
|
|
33
|
+
void: TokenKind.Void,
|
|
34
|
+
true: TokenKind.True,
|
|
35
|
+
false: TokenKind.False,
|
|
36
|
+
task: TokenKind.Task,
|
|
37
|
+
state: TokenKind.State,
|
|
38
|
+
async: TokenKind.Async,
|
|
39
|
+
await: TokenKind.Await,
|
|
40
|
+
try: TokenKind.Try,
|
|
41
|
+
catch: TokenKind.Catch,
|
|
42
|
+
finally: TokenKind.Finally,
|
|
43
|
+
throw: TokenKind.Throw,
|
|
44
|
+
};
|
|
45
|
+
export class Lexer {
|
|
46
|
+
constructor(source, diagnostics) {
|
|
47
|
+
this.source = source;
|
|
48
|
+
this.diagnostics = diagnostics;
|
|
49
|
+
this.pos = 0;
|
|
50
|
+
this.line = 1;
|
|
51
|
+
this.col = 1;
|
|
52
|
+
}
|
|
53
|
+
tokenize() {
|
|
54
|
+
const tokens = [];
|
|
55
|
+
for (;;) {
|
|
56
|
+
const token = this.next();
|
|
57
|
+
tokens.push(token);
|
|
58
|
+
if (token.kind === TokenKind.EOF)
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
return tokens;
|
|
62
|
+
}
|
|
63
|
+
next() {
|
|
64
|
+
this.skipTrivia();
|
|
65
|
+
const line = this.line;
|
|
66
|
+
const col = this.col;
|
|
67
|
+
if (this.isAtEnd())
|
|
68
|
+
return this.make(TokenKind.EOF, "", line, col);
|
|
69
|
+
const c = this.peek();
|
|
70
|
+
if (c === '"')
|
|
71
|
+
return this.readString(line, col);
|
|
72
|
+
if (c === "$" && this.peek(1) === '"')
|
|
73
|
+
return this.readInterpolatedString(line, col);
|
|
74
|
+
if (c === "r" && this.peek(1) === '"')
|
|
75
|
+
return this.readRegexLiteral(line, col);
|
|
76
|
+
if (this.isDigit(c))
|
|
77
|
+
return this.readNumber(line, col);
|
|
78
|
+
if (this.isIdentStart(c))
|
|
79
|
+
return this.readIdentifier(line, col);
|
|
80
|
+
return this.readOperator(line, col);
|
|
81
|
+
}
|
|
82
|
+
skipTrivia() {
|
|
83
|
+
for (;;) {
|
|
84
|
+
const c = this.peek();
|
|
85
|
+
if (c === " " || c === "\t" || c === "\r") {
|
|
86
|
+
this.advance();
|
|
87
|
+
}
|
|
88
|
+
else if (c === "\n") {
|
|
89
|
+
this.advance();
|
|
90
|
+
}
|
|
91
|
+
else if (c === "/" && this.peek(1) === "/") {
|
|
92
|
+
while (!this.isAtEnd() && this.peek() !== "\n")
|
|
93
|
+
this.advance();
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
readString(line, col) {
|
|
101
|
+
this.advance(); // opening quote
|
|
102
|
+
let value = "";
|
|
103
|
+
while (!this.isAtEnd() && this.peek() !== '"') {
|
|
104
|
+
value += this.readStringChar();
|
|
105
|
+
}
|
|
106
|
+
if (this.isAtEnd()) {
|
|
107
|
+
this.diagnostics.error("Unterminated string literal", line, col);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
this.advance(); // closing quote
|
|
111
|
+
}
|
|
112
|
+
return this.make(TokenKind.String, value, line, col);
|
|
113
|
+
}
|
|
114
|
+
readInterpolatedString(line, col) {
|
|
115
|
+
this.advance(); // '$'
|
|
116
|
+
this.advance(); // opening quote
|
|
117
|
+
let raw = "";
|
|
118
|
+
let depth = 0;
|
|
119
|
+
while (!this.isAtEnd() && !(this.peek() === '"' && depth === 0)) {
|
|
120
|
+
if (this.peek() === "{")
|
|
121
|
+
depth++;
|
|
122
|
+
if (this.peek() === "}")
|
|
123
|
+
depth--;
|
|
124
|
+
raw += this.advance();
|
|
125
|
+
}
|
|
126
|
+
if (this.isAtEnd()) {
|
|
127
|
+
this.diagnostics.error("Unterminated interpolated string literal", line, col);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
this.advance(); // closing quote
|
|
131
|
+
}
|
|
132
|
+
return this.make(TokenKind.InterpolatedString, raw, line, col);
|
|
133
|
+
}
|
|
134
|
+
readRegexLiteral(line, col) {
|
|
135
|
+
this.advance(); // 'r'
|
|
136
|
+
this.advance(); // opening quote
|
|
137
|
+
let value = "";
|
|
138
|
+
while (!this.isAtEnd() && this.peek() !== '"') {
|
|
139
|
+
value += this.readStringChar();
|
|
140
|
+
}
|
|
141
|
+
if (this.isAtEnd()) {
|
|
142
|
+
this.diagnostics.error("Unterminated regex literal", line, col);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
this.advance(); // closing quote
|
|
146
|
+
}
|
|
147
|
+
return this.make(TokenKind.RegexLiteral, value, line, col);
|
|
148
|
+
}
|
|
149
|
+
readStringChar() {
|
|
150
|
+
const c = this.advance();
|
|
151
|
+
if (c !== "\\")
|
|
152
|
+
return c;
|
|
153
|
+
const esc = this.advance();
|
|
154
|
+
switch (esc) {
|
|
155
|
+
case "n":
|
|
156
|
+
return "\n";
|
|
157
|
+
case "t":
|
|
158
|
+
return "\t";
|
|
159
|
+
case "r":
|
|
160
|
+
return "\r";
|
|
161
|
+
case "\\":
|
|
162
|
+
return "\\";
|
|
163
|
+
case '"':
|
|
164
|
+
return '"';
|
|
165
|
+
case "{":
|
|
166
|
+
return "{";
|
|
167
|
+
case "}":
|
|
168
|
+
return "}";
|
|
169
|
+
default:
|
|
170
|
+
return esc;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
readNumber(line, col) {
|
|
174
|
+
let value = "";
|
|
175
|
+
while (this.isDigit(this.peek()))
|
|
176
|
+
value += this.advance();
|
|
177
|
+
if (this.peek() === "." && this.isDigit(this.peek(1))) {
|
|
178
|
+
value += this.advance();
|
|
179
|
+
while (this.isDigit(this.peek()))
|
|
180
|
+
value += this.advance();
|
|
181
|
+
}
|
|
182
|
+
return this.make(TokenKind.Number, value, line, col);
|
|
183
|
+
}
|
|
184
|
+
readIdentifier(line, col) {
|
|
185
|
+
let value = "";
|
|
186
|
+
while (this.isIdentPart(this.peek()))
|
|
187
|
+
value += this.advance();
|
|
188
|
+
if (value === "_")
|
|
189
|
+
return this.make(TokenKind.Underscore, value, line, col);
|
|
190
|
+
const kind = KEYWORDS[value] ?? TokenKind.Identifier;
|
|
191
|
+
return this.make(kind, value, line, col);
|
|
192
|
+
}
|
|
193
|
+
readOperator(line, col) {
|
|
194
|
+
const c = this.advance();
|
|
195
|
+
switch (c) {
|
|
196
|
+
case "(":
|
|
197
|
+
return this.make(TokenKind.LParen, c, line, col);
|
|
198
|
+
case ")":
|
|
199
|
+
return this.make(TokenKind.RParen, c, line, col);
|
|
200
|
+
case "{":
|
|
201
|
+
return this.make(TokenKind.LBrace, c, line, col);
|
|
202
|
+
case "}":
|
|
203
|
+
return this.make(TokenKind.RBrace, c, line, col);
|
|
204
|
+
case "[":
|
|
205
|
+
return this.make(TokenKind.LBracket, c, line, col);
|
|
206
|
+
case "]":
|
|
207
|
+
return this.make(TokenKind.RBracket, c, line, col);
|
|
208
|
+
case ",":
|
|
209
|
+
return this.make(TokenKind.Comma, c, line, col);
|
|
210
|
+
case ":":
|
|
211
|
+
return this.make(TokenKind.Colon, c, line, col);
|
|
212
|
+
case ";":
|
|
213
|
+
return this.make(TokenKind.Semicolon, c, line, col);
|
|
214
|
+
case ".":
|
|
215
|
+
return this.make(TokenKind.Dot, c, line, col);
|
|
216
|
+
case "+":
|
|
217
|
+
return this.make(TokenKind.Plus, c, line, col);
|
|
218
|
+
case "-":
|
|
219
|
+
return this.make(TokenKind.Minus, c, line, col);
|
|
220
|
+
case "*":
|
|
221
|
+
return this.make(TokenKind.Star, c, line, col);
|
|
222
|
+
case "/":
|
|
223
|
+
return this.make(TokenKind.Slash, c, line, col);
|
|
224
|
+
case "%":
|
|
225
|
+
return this.make(TokenKind.Percent, c, line, col);
|
|
226
|
+
case "!":
|
|
227
|
+
if (this.peek() === "=") {
|
|
228
|
+
this.advance();
|
|
229
|
+
return this.make(TokenKind.NotEq, "!=", line, col);
|
|
230
|
+
}
|
|
231
|
+
return this.make(TokenKind.Not, c, line, col);
|
|
232
|
+
case "=":
|
|
233
|
+
if (this.peek() === "=") {
|
|
234
|
+
this.advance();
|
|
235
|
+
return this.make(TokenKind.Eq, "==", line, col);
|
|
236
|
+
}
|
|
237
|
+
if (this.peek() === ">") {
|
|
238
|
+
this.advance();
|
|
239
|
+
return this.make(TokenKind.Arrow, "=>", line, col);
|
|
240
|
+
}
|
|
241
|
+
return this.make(TokenKind.Assign, c, line, col);
|
|
242
|
+
case "<":
|
|
243
|
+
if (this.peek() === "=") {
|
|
244
|
+
this.advance();
|
|
245
|
+
return this.make(TokenKind.LtEq, "<=", line, col);
|
|
246
|
+
}
|
|
247
|
+
return this.make(TokenKind.Lt, c, line, col);
|
|
248
|
+
case ">":
|
|
249
|
+
if (this.peek() === "=") {
|
|
250
|
+
this.advance();
|
|
251
|
+
return this.make(TokenKind.GtEq, ">=", line, col);
|
|
252
|
+
}
|
|
253
|
+
return this.make(TokenKind.Gt, c, line, col);
|
|
254
|
+
case "&":
|
|
255
|
+
if (this.peek() === "&") {
|
|
256
|
+
this.advance();
|
|
257
|
+
return this.make(TokenKind.AndAnd, "&&", line, col);
|
|
258
|
+
}
|
|
259
|
+
break;
|
|
260
|
+
case "|":
|
|
261
|
+
if (this.peek() === "|") {
|
|
262
|
+
this.advance();
|
|
263
|
+
return this.make(TokenKind.OrOr, "||", line, col);
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
this.diagnostics.error(`Unexpected character '${c}'`, line, col);
|
|
268
|
+
return this.next();
|
|
269
|
+
}
|
|
270
|
+
make(kind, lexeme, line, col) {
|
|
271
|
+
return { kind, lexeme, line, col };
|
|
272
|
+
}
|
|
273
|
+
isAtEnd() {
|
|
274
|
+
return this.pos >= this.source.length;
|
|
275
|
+
}
|
|
276
|
+
peek(offset = 0) {
|
|
277
|
+
const i = this.pos + offset;
|
|
278
|
+
return i < this.source.length ? this.source[i] : "\0";
|
|
279
|
+
}
|
|
280
|
+
advance() {
|
|
281
|
+
const c = this.source[this.pos];
|
|
282
|
+
this.pos++;
|
|
283
|
+
if (c === "\n") {
|
|
284
|
+
this.line++;
|
|
285
|
+
this.col = 1;
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
this.col++;
|
|
289
|
+
}
|
|
290
|
+
return c;
|
|
291
|
+
}
|
|
292
|
+
isDigit(c) {
|
|
293
|
+
return c >= "0" && c <= "9";
|
|
294
|
+
}
|
|
295
|
+
isIdentStart(c) {
|
|
296
|
+
return c === "_" || (c >= "a" && c <= "z") || (c >= "A" && c <= "Z");
|
|
297
|
+
}
|
|
298
|
+
isIdentPart(c) {
|
|
299
|
+
return this.isIdentStart(c) || this.isDigit(c);
|
|
300
|
+
}
|
|
301
|
+
}
|
package/dist/modules.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, relative, resolve } from "node:path";
|
|
3
|
+
import { Lexer } from "./lexer.js";
|
|
4
|
+
import { Parser } from "./parser.js";
|
|
5
|
+
import { Checker, emptyModuleExports } from "./checker.js";
|
|
6
|
+
import { CodeGenerator } from "./codegen.js";
|
|
7
|
+
import { DiagnosticBag } from "./diagnostics.js";
|
|
8
|
+
function displayPath(absPath) {
|
|
9
|
+
return relative(process.cwd(), absPath);
|
|
10
|
+
}
|
|
11
|
+
// Parses the entry file and every file it transitively `using`s, in
|
|
12
|
+
// dependency-first (topological) order. Each module's own DiagnosticBag
|
|
13
|
+
// carries any parse errors and any problems with its `using` directives
|
|
14
|
+
// (missing file, non-relative path, circular dependency) — a missing or
|
|
15
|
+
// cyclic dependency is reported on the referencing `using` statement and
|
|
16
|
+
// simply isn't added to that file's dependency list, so the rest of the
|
|
17
|
+
// graph can still be explored and reported on in one pass.
|
|
18
|
+
export function loadModuleGraph(entryAbsPath) {
|
|
19
|
+
const modules = new Map();
|
|
20
|
+
const order = [];
|
|
21
|
+
const stack = [];
|
|
22
|
+
if (!existsSync(entryAbsPath)) {
|
|
23
|
+
return { modules, order, entryMissing: true };
|
|
24
|
+
}
|
|
25
|
+
function visit(absPath) {
|
|
26
|
+
if (modules.has(absPath))
|
|
27
|
+
return;
|
|
28
|
+
const source = readFileSync(absPath, "utf-8");
|
|
29
|
+
const diagnostics = new DiagnosticBag();
|
|
30
|
+
const tokens = new Lexer(source, diagnostics).tokenize();
|
|
31
|
+
const program = new Parser(tokens, diagnostics).parseProgram();
|
|
32
|
+
const record = { absPath, source, program, diagnostics, dependencies: [] };
|
|
33
|
+
modules.set(absPath, record);
|
|
34
|
+
stack.push(absPath);
|
|
35
|
+
for (const u of program.usings) {
|
|
36
|
+
if (!u.path.startsWith("./") && !u.path.startsWith("../")) {
|
|
37
|
+
diagnostics.error(`'using' path '${u.path}' must be relative (start with './' or '../')`, u.line, u.col);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const depPath = resolve(dirname(absPath), u.path) + ".ks";
|
|
41
|
+
if (!existsSync(depPath)) {
|
|
42
|
+
diagnostics.error(`Cannot find module '${u.path}' (looked for '${displayPath(depPath)}')`, u.line, u.col);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (stack.includes(depPath)) {
|
|
46
|
+
diagnostics.error(`Circular 'using' dependency: ${[...stack, depPath].map(displayPath).join(" -> ")}`, u.line, u.col);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
record.dependencies.push(depPath);
|
|
50
|
+
visit(depPath);
|
|
51
|
+
}
|
|
52
|
+
stack.pop();
|
|
53
|
+
order.push(absPath);
|
|
54
|
+
}
|
|
55
|
+
visit(entryAbsPath);
|
|
56
|
+
return { modules, order, entryMissing: false };
|
|
57
|
+
}
|
|
58
|
+
// Compiles the whole module graph reachable from entryAbsPath: parses every
|
|
59
|
+
// file, type-checks each in dependency order (seeding each module's checker
|
|
60
|
+
// with its direct dependencies' merged exports), and — only if the entire
|
|
61
|
+
// graph is error-free — generates one JS file's contents per module, with
|
|
62
|
+
// real ES `import`/`export` statements wiring them together.
|
|
63
|
+
export function compileGraph(entryAbsPath) {
|
|
64
|
+
const { modules, order, entryMissing } = loadModuleGraph(entryAbsPath);
|
|
65
|
+
if (entryMissing) {
|
|
66
|
+
return { success: false, entryMissing: true, modules, order, outputs: new Map() };
|
|
67
|
+
}
|
|
68
|
+
const exportsByModule = new Map();
|
|
69
|
+
let hasErrors = false;
|
|
70
|
+
for (const absPath of order) {
|
|
71
|
+
const mod = modules.get(absPath);
|
|
72
|
+
if (mod.diagnostics.hasErrors) {
|
|
73
|
+
hasErrors = true;
|
|
74
|
+
continue; // parse/using errors already recorded; skip semantic checking
|
|
75
|
+
}
|
|
76
|
+
const merged = emptyModuleExports();
|
|
77
|
+
const importedFrom = new Map();
|
|
78
|
+
for (const u of mod.program.usings) {
|
|
79
|
+
if (!u.path.startsWith("./") && !u.path.startsWith("../"))
|
|
80
|
+
continue; // already reported
|
|
81
|
+
const depPath = resolve(dirname(absPath), u.path) + ".ks";
|
|
82
|
+
const depExports = exportsByModule.get(depPath);
|
|
83
|
+
if (!depExports)
|
|
84
|
+
continue; // dependency missing/cyclic/failed; already reported
|
|
85
|
+
const mergeOne = (name, apply) => {
|
|
86
|
+
const existingFrom = importedFrom.get(name);
|
|
87
|
+
if (existingFrom && existingFrom !== depPath) {
|
|
88
|
+
mod.diagnostics.error(`'${name}' is exported by both '${displayPath(existingFrom)}' and '${displayPath(depPath)}' — ambiguous 'using'`, u.line, u.col);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
importedFrom.set(name, depPath);
|
|
92
|
+
apply();
|
|
93
|
+
};
|
|
94
|
+
for (const [name, kind] of depExports.namedTypes)
|
|
95
|
+
mergeOne(name, () => merged.namedTypes.set(name, kind));
|
|
96
|
+
for (const [name, info] of depExports.functions)
|
|
97
|
+
mergeOne(name, () => merged.functions.set(name, info));
|
|
98
|
+
for (const [name, type] of depExports.externValues)
|
|
99
|
+
mergeOne(name, () => merged.externValues.set(name, type));
|
|
100
|
+
for (const [name, info] of depExports.classes)
|
|
101
|
+
merged.classes.set(name, info);
|
|
102
|
+
for (const [name, info] of depExports.interfaces)
|
|
103
|
+
merged.interfaces.set(name, info);
|
|
104
|
+
for (const [name, info] of depExports.enums)
|
|
105
|
+
merged.enums.set(name, info);
|
|
106
|
+
}
|
|
107
|
+
const checker = new Checker(mod.program, mod.diagnostics, merged);
|
|
108
|
+
checker.check();
|
|
109
|
+
if (mod.diagnostics.hasErrors)
|
|
110
|
+
hasErrors = true;
|
|
111
|
+
exportsByModule.set(absPath, checker.getExports());
|
|
112
|
+
}
|
|
113
|
+
if (hasErrors) {
|
|
114
|
+
return { success: false, entryMissing: false, modules, order, outputs: new Map() };
|
|
115
|
+
}
|
|
116
|
+
const outputs = new Map();
|
|
117
|
+
for (const absPath of order) {
|
|
118
|
+
const mod = modules.get(absPath);
|
|
119
|
+
const usingExports = new Map();
|
|
120
|
+
for (const u of mod.program.usings) {
|
|
121
|
+
const depPath = resolve(dirname(absPath), u.path) + ".ks";
|
|
122
|
+
const depExports = exportsByModule.get(depPath);
|
|
123
|
+
if (depExports) {
|
|
124
|
+
// Interfaces are compile-time only and never produce a JS `export` —
|
|
125
|
+
// they'd make an invalid import specifier if listed here.
|
|
126
|
+
const importableTypeNames = [...depExports.namedTypes.entries()].filter(([, kind]) => kind !== "interface").map(([name]) => name);
|
|
127
|
+
usingExports.set(u.path, [...importableTypeNames, ...depExports.functions.keys(), ...depExports.externValues.keys()]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
outputs.set(absPath, new CodeGenerator().generate(mod.program, usingExports));
|
|
131
|
+
}
|
|
132
|
+
return { success: true, entryMissing: false, modules, order, outputs };
|
|
133
|
+
}
|