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/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
+ }
@@ -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
+ }