kopscript 0.1.0 → 0.2.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/README.md +24 -0
- package/assets/logo.png +0 -0
- package/dist/checker.js +39 -4
- package/dist/codegen.js +7 -1
- package/dist/lexer.js +1 -0
- package/dist/modules.js +4 -2
- package/dist/parser.js +21 -0
- package/dist/tokens.js +1 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<img src="https://cdn.jsdelivr.net/npm/kopscript@latest/assets/logo.png" width="96" height="96" alt="KopScript logo">
|
|
2
|
+
|
|
1
3
|
# KopScript
|
|
2
4
|
|
|
3
5
|
KopScript is a small, strongly-typed, object-oriented programming language that transpiles to
|
|
@@ -299,6 +301,28 @@ describable is old-style Node **callback-based** async that doesn't return a Pro
|
|
|
299
301
|
that specific convention, even though the callback parameter itself is describable as an
|
|
300
302
|
ordinary function type.
|
|
301
303
|
|
|
304
|
+
### Compile-time file embedding (`raw`)
|
|
305
|
+
|
|
306
|
+
`raw string <Name> from "<path>";` reads the file at `<path>` (resolved relative to the
|
|
307
|
+
`.ks` file that declares it) at **compile time** and embeds its contents as a plain string
|
|
308
|
+
constant — no `fetch`, no runtime file access, nothing left over at all once compiled.
|
|
309
|
+
Top-level only, always `string`, exported by default like any other top-level declaration
|
|
310
|
+
(`private` keeps it file-scoped):
|
|
311
|
+
|
|
312
|
+
```ks
|
|
313
|
+
raw string CounterHtml from "./counter.html";
|
|
314
|
+
|
|
315
|
+
void Main() {
|
|
316
|
+
print(CounterHtml); // the file's exact contents, as a string
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Unlike `extern`, there's no real JS export being described here — the compiler fabricates
|
|
321
|
+
the value itself — so there's no `as "jsName"` clause. This exists so a component's static
|
|
322
|
+
markup can live in a real `.html` file instead of a giant `.innerHTML = "..."` string
|
|
323
|
+
literal, the same way Angular's `templateUrl` works — resolved once at build time, not
|
|
324
|
+
fetched at runtime, so `Component.Render()` stays synchronous either way.
|
|
325
|
+
|
|
302
326
|
### async/await, task<T>, and try/catch
|
|
303
327
|
|
|
304
328
|
`task` (a promise of nothing) and `task<T>` (a promise of a `T`) are the one hardcoded
|
package/assets/logo.png
ADDED
|
Binary file
|
package/dist/checker.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
1
3
|
import * as T from "./types.js";
|
|
2
4
|
export function emptyModuleExports() {
|
|
3
5
|
return { namedTypes: new Map(), classes: new Map(), interfaces: new Map(), enums: new Map(), functions: new Map(), externValues: new Map() };
|
|
@@ -28,10 +30,11 @@ class Scope {
|
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
export class Checker {
|
|
31
|
-
constructor(program, diagnostics, imports = emptyModuleExports()) {
|
|
33
|
+
constructor(program, diagnostics, imports = emptyModuleExports(), currentFilePath = "test.ks") {
|
|
32
34
|
this.program = program;
|
|
33
35
|
this.diagnostics = diagnostics;
|
|
34
36
|
this.imports = imports;
|
|
37
|
+
this.currentFilePath = currentFilePath;
|
|
35
38
|
this.classes = new Map();
|
|
36
39
|
this.interfaces = new Map();
|
|
37
40
|
this.enums = new Map();
|
|
@@ -39,6 +42,7 @@ export class Checker {
|
|
|
39
42
|
this.externValues = new Map();
|
|
40
43
|
this.namedTypes = new Map();
|
|
41
44
|
this.importedNames = new Set();
|
|
45
|
+
this.rawContents = new Map();
|
|
42
46
|
}
|
|
43
47
|
check() {
|
|
44
48
|
for (const [name, kind] of this.imports.namedTypes) {
|
|
@@ -66,7 +70,8 @@ export class Checker {
|
|
|
66
70
|
const externFunctionDecls = this.program.statements.filter((s) => s.kind === "ExternFunctionDecl");
|
|
67
71
|
const externClassDecls = this.program.statements.filter((s) => s.kind === "ExternClassDecl");
|
|
68
72
|
const externValueDecls = this.program.statements.filter((s) => s.kind === "ExternValueDecl");
|
|
69
|
-
this.
|
|
73
|
+
const rawStringDecls = this.program.statements.filter((s) => s.kind === "RawStringDecl");
|
|
74
|
+
this.checkNoDuplicateTopLevelNames(classDecls, interfaceDecls, enumDecls, functionDecls, externFunctionDecls, externClassDecls, externValueDecls, rawStringDecls);
|
|
70
75
|
for (const c of classDecls)
|
|
71
76
|
this.namedTypes.set(c.name, "class");
|
|
72
77
|
for (const i of interfaceDecls)
|
|
@@ -105,6 +110,8 @@ export class Checker {
|
|
|
105
110
|
this.registerExternFunction(f);
|
|
106
111
|
for (const v of externValueDecls)
|
|
107
112
|
this.registerExternValue(v);
|
|
113
|
+
for (const r of rawStringDecls)
|
|
114
|
+
this.registerRawStringDecl(r);
|
|
108
115
|
const globalScope = new Scope();
|
|
109
116
|
for (const stmt of this.program.statements) {
|
|
110
117
|
if (stmt.kind === "ClassDecl" ||
|
|
@@ -112,7 +119,8 @@ export class Checker {
|
|
|
112
119
|
stmt.kind === "EnumDecl" ||
|
|
113
120
|
stmt.kind === "ExternFunctionDecl" ||
|
|
114
121
|
stmt.kind === "ExternClassDecl" ||
|
|
115
|
-
stmt.kind === "ExternValueDecl"
|
|
122
|
+
stmt.kind === "ExternValueDecl" ||
|
|
123
|
+
stmt.kind === "RawStringDecl") {
|
|
116
124
|
continue;
|
|
117
125
|
}
|
|
118
126
|
this.checkTopLevelStatement(stmt, globalScope);
|
|
@@ -160,6 +168,25 @@ export class Checker {
|
|
|
160
168
|
registerExternValue(decl) {
|
|
161
169
|
this.externValues.set(decl.name, this.resolveType(decl.type, decl.line, decl.col));
|
|
162
170
|
}
|
|
171
|
+
// `raw string <Name> from "<path>";` — resolves <path> relative to this
|
|
172
|
+
// module's own file, reads it at compile time, and types <Name> as an
|
|
173
|
+
// ordinary string identifier via the same externValues table an ambient
|
|
174
|
+
// extern value uses (free same-file resolution, free export propagation).
|
|
175
|
+
registerRawStringDecl(decl) {
|
|
176
|
+
this.externValues.set(decl.name, T.STRING);
|
|
177
|
+
const resolvedPath = resolve(dirname(this.currentFilePath), decl.path);
|
|
178
|
+
if (!existsSync(resolvedPath)) {
|
|
179
|
+
this.diagnostics.error(`Cannot find file '${decl.path}' referenced by 'raw string ${decl.name}' (looked for '${resolvedPath}')`, decl.line, decl.col);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
this.rawContents.set(decl.name, readFileSync(resolvedPath, "utf-8"));
|
|
183
|
+
}
|
|
184
|
+
// The raw-declared string contents resolved by this module's own `raw`
|
|
185
|
+
// declarations (not inherited from imports). Call only after check() has
|
|
186
|
+
// run; codegen uses this to emit each declaring file's string constants.
|
|
187
|
+
getRawContents() {
|
|
188
|
+
return this.rawContents;
|
|
189
|
+
}
|
|
163
190
|
// The subset of this module's registered declarations visible to a file
|
|
164
191
|
// that `using`s it. Call only after check() has run.
|
|
165
192
|
getExports() {
|
|
@@ -190,10 +217,13 @@ export class Checker {
|
|
|
190
217
|
else if (stmt.kind === "ExternValueDecl" && stmt.isExported) {
|
|
191
218
|
exports.externValues.set(stmt.name, this.externValues.get(stmt.name));
|
|
192
219
|
}
|
|
220
|
+
else if (stmt.kind === "RawStringDecl" && stmt.isExported) {
|
|
221
|
+
exports.externValues.set(stmt.name, this.externValues.get(stmt.name));
|
|
222
|
+
}
|
|
193
223
|
}
|
|
194
224
|
return exports;
|
|
195
225
|
}
|
|
196
|
-
checkNoDuplicateTopLevelNames(classDecls, interfaceDecls, enumDecls, functionDecls, externFunctionDecls, externClassDecls, externValueDecls) {
|
|
226
|
+
checkNoDuplicateTopLevelNames(classDecls, interfaceDecls, enumDecls, functionDecls, externFunctionDecls, externClassDecls, externValueDecls, rawStringDecls) {
|
|
197
227
|
const seen = new Map();
|
|
198
228
|
const declare = (name, line, col) => {
|
|
199
229
|
if (this.importedNames.has(name)) {
|
|
@@ -218,6 +248,8 @@ export class Checker {
|
|
|
218
248
|
declare(c.name, c.line, c.col);
|
|
219
249
|
for (const v of externValueDecls)
|
|
220
250
|
declare(v.name, v.line, v.col);
|
|
251
|
+
for (const r of rawStringDecls)
|
|
252
|
+
declare(r.name, r.line, r.col);
|
|
221
253
|
}
|
|
222
254
|
// An exported class's superclass and directly-implemented interfaces must
|
|
223
255
|
// also be exported — otherwise a file that imports this class would have
|
|
@@ -802,6 +834,9 @@ export class Checker {
|
|
|
802
834
|
case "ExternValueDecl":
|
|
803
835
|
this.diagnostics.error(`'extern' declarations are only allowed at the top level of a file`, stmt.line, stmt.col);
|
|
804
836
|
return;
|
|
837
|
+
case "RawStringDecl":
|
|
838
|
+
this.diagnostics.error(`'raw' declarations are only allowed at the top level of a file`, stmt.line, stmt.col);
|
|
839
|
+
return;
|
|
805
840
|
}
|
|
806
841
|
}
|
|
807
842
|
expectType(actual, expected, line, col, context) {
|
package/dist/codegen.js
CHANGED
|
@@ -60,7 +60,7 @@ export class CodeGenerator {
|
|
|
60
60
|
// the list of names that module exports — resolved by the caller from that
|
|
61
61
|
// module's own checked Program, since a bare `using` brings everything
|
|
62
62
|
// `public` into scope without naming it explicitly.
|
|
63
|
-
generate(program, usingExports = new Map()) {
|
|
63
|
+
generate(program, usingExports = new Map(), rawContents = new Map()) {
|
|
64
64
|
this.interfaceNames = new Set(program.statements.filter((s) => s.kind === "InterfaceDecl").map((s) => s.name));
|
|
65
65
|
const importLines = program.usings
|
|
66
66
|
.map((u) => {
|
|
@@ -82,6 +82,11 @@ export class CodeGenerator {
|
|
|
82
82
|
lines.push(binding);
|
|
83
83
|
continue;
|
|
84
84
|
}
|
|
85
|
+
if (stmt.kind === "RawStringDecl") {
|
|
86
|
+
const content = rawContents.get(stmt.name) ?? "";
|
|
87
|
+
lines.push(`${stmt.isExported ? "export const" : "const"} ${stmt.name} = ${JSON.stringify(content)};`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
85
90
|
let code = this.genStatement(stmt, 0);
|
|
86
91
|
if ((stmt.kind === "ClassDecl" || stmt.kind === "EnumDecl" || stmt.kind === "FunctionDecl") && stmt.isExported) {
|
|
87
92
|
code = `export ${code}`;
|
|
@@ -126,6 +131,7 @@ export class CodeGenerator {
|
|
|
126
131
|
case "ExternFunctionDecl":
|
|
127
132
|
case "ExternClassDecl":
|
|
128
133
|
case "ExternValueDecl":
|
|
134
|
+
case "RawStringDecl":
|
|
129
135
|
return ""; // handled directly in generate(); unreachable here except via a nested-decl error path
|
|
130
136
|
case "EnumDecl": {
|
|
131
137
|
const entries = stmt.members.map((m, i) => `${m}: ${i}`).join(", ");
|
package/dist/lexer.js
CHANGED
package/dist/modules.js
CHANGED
|
@@ -66,6 +66,7 @@ export function compileGraph(entryAbsPath) {
|
|
|
66
66
|
return { success: false, entryMissing: true, modules, order, outputs: new Map() };
|
|
67
67
|
}
|
|
68
68
|
const exportsByModule = new Map();
|
|
69
|
+
const rawContentsByModule = new Map();
|
|
69
70
|
let hasErrors = false;
|
|
70
71
|
for (const absPath of order) {
|
|
71
72
|
const mod = modules.get(absPath);
|
|
@@ -104,11 +105,12 @@ export function compileGraph(entryAbsPath) {
|
|
|
104
105
|
for (const [name, info] of depExports.enums)
|
|
105
106
|
merged.enums.set(name, info);
|
|
106
107
|
}
|
|
107
|
-
const checker = new Checker(mod.program, mod.diagnostics, merged);
|
|
108
|
+
const checker = new Checker(mod.program, mod.diagnostics, merged, absPath);
|
|
108
109
|
checker.check();
|
|
109
110
|
if (mod.diagnostics.hasErrors)
|
|
110
111
|
hasErrors = true;
|
|
111
112
|
exportsByModule.set(absPath, checker.getExports());
|
|
113
|
+
rawContentsByModule.set(absPath, checker.getRawContents());
|
|
112
114
|
}
|
|
113
115
|
if (hasErrors) {
|
|
114
116
|
return { success: false, entryMissing: false, modules, order, outputs: new Map() };
|
|
@@ -127,7 +129,7 @@ export function compileGraph(entryAbsPath) {
|
|
|
127
129
|
usingExports.set(u.path, [...importableTypeNames, ...depExports.functions.keys(), ...depExports.externValues.keys()]);
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
|
-
outputs.set(absPath, new CodeGenerator().generate(mod.program, usingExports));
|
|
132
|
+
outputs.set(absPath, new CodeGenerator().generate(mod.program, usingExports, rawContentsByModule.get(absPath) ?? new Map()));
|
|
131
133
|
}
|
|
132
134
|
return { success: true, entryMissing: false, modules, order, outputs };
|
|
133
135
|
}
|
package/dist/parser.js
CHANGED
|
@@ -50,6 +50,8 @@ export class Parser {
|
|
|
50
50
|
return this.parseEnumDecl(true);
|
|
51
51
|
if (this.check(TokenKind.Extern))
|
|
52
52
|
return this.parseExternDecl(true);
|
|
53
|
+
if (this.check(TokenKind.Raw))
|
|
54
|
+
return this.parseRawStringDecl(true);
|
|
53
55
|
if (this.check(TokenKind.LBrace))
|
|
54
56
|
return this.parseBlock();
|
|
55
57
|
if (this.check(TokenKind.If))
|
|
@@ -102,6 +104,8 @@ export class Parser {
|
|
|
102
104
|
return this.parseEnumDecl(isExported);
|
|
103
105
|
if (this.check(TokenKind.Extern))
|
|
104
106
|
return this.parseExternDecl(isExported);
|
|
107
|
+
if (this.check(TokenKind.Raw))
|
|
108
|
+
return this.parseRawStringDecl(isExported);
|
|
105
109
|
if (this.isDeclStart()) {
|
|
106
110
|
const decl = this.parseDeclaration(isExported);
|
|
107
111
|
if (decl.kind === "VarDecl") {
|
|
@@ -405,6 +409,22 @@ export class Parser {
|
|
|
405
409
|
}
|
|
406
410
|
return { modulePath, jsName };
|
|
407
411
|
}
|
|
412
|
+
// `raw string <Name> from "<path>";` — embeds a real file's contents as a
|
|
413
|
+
// JS string constant at compile time. Type must be literally 'string' (no
|
|
414
|
+
// other type makes sense for file contents); no 'as' clause — there's no
|
|
415
|
+
// JS-side name to alias, unlike extern.
|
|
416
|
+
parseRawStringDecl(isExported) {
|
|
417
|
+
const start = this.advance(); // 'raw'
|
|
418
|
+
const typeTok = this.consume(TokenKind.Identifier, "Expected 'string' after 'raw'");
|
|
419
|
+
if (typeTok.lexeme !== "string") {
|
|
420
|
+
this.diagnostics.error(`'raw' declarations must be of type 'string', got '${typeTok.lexeme}'`, typeTok.line, typeTok.col);
|
|
421
|
+
}
|
|
422
|
+
const name = this.consume(TokenKind.Identifier, "Expected name").lexeme;
|
|
423
|
+
this.consume(TokenKind.From, "Expected 'from \"<path>\"' after 'raw string <Name>'");
|
|
424
|
+
const pathTok = this.consume(TokenKind.String, "Expected a file path string after 'from'");
|
|
425
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after 'raw' declaration");
|
|
426
|
+
return { kind: "RawStringDecl", isExported, name, path: pathTok.lexeme, line: start.line, col: start.col };
|
|
427
|
+
}
|
|
408
428
|
parseExternClassBody(start, isExported) {
|
|
409
429
|
const name = this.consume(TokenKind.Identifier, "Expected class name").lexeme;
|
|
410
430
|
this.consume(TokenKind.LBrace, "Expected '{' before extern class body");
|
|
@@ -976,6 +996,7 @@ export class Parser {
|
|
|
976
996
|
this.check(TokenKind.Interface) ||
|
|
977
997
|
this.check(TokenKind.Enum) ||
|
|
978
998
|
this.check(TokenKind.Extern) ||
|
|
999
|
+
this.check(TokenKind.Raw) ||
|
|
979
1000
|
this.check(TokenKind.Const) ||
|
|
980
1001
|
this.check(TokenKind.If) ||
|
|
981
1002
|
this.check(TokenKind.While) ||
|
package/dist/tokens.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kopscript",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "KopScript: a small OOP, strongly-typed language that transpiles to JavaScript",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"main": "./dist/modules.js",
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
22
|
-
"bin"
|
|
22
|
+
"bin",
|
|
23
|
+
"assets"
|
|
23
24
|
],
|
|
24
25
|
"bin": {
|
|
25
26
|
"ks": "bin/ks.js"
|