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 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&lt;T&gt;, and try/catch
303
327
 
304
328
  `task` (a promise of nothing) and `task<T>` (a promise of a `T`) are the one hardcoded
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.checkNoDuplicateTopLevelNames(classDecls, interfaceDecls, enumDecls, functionDecls, externFunctionDecls, externClassDecls, externValueDecls);
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
@@ -2,6 +2,7 @@ import { TokenKind } from "./tokens.js";
2
2
  const KEYWORDS = {
3
3
  using: TokenKind.Using,
4
4
  extern: TokenKind.Extern,
5
+ raw: TokenKind.Raw,
5
6
  from: TokenKind.From,
6
7
  as: TokenKind.As,
7
8
  const: TokenKind.Const,
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
@@ -11,6 +11,7 @@ export var TokenKind;
11
11
  // Keywords
12
12
  TokenKind["Using"] = "Using";
13
13
  TokenKind["Extern"] = "Extern";
14
+ TokenKind["Raw"] = "Raw";
14
15
  TokenKind["From"] = "From";
15
16
  TokenKind["As"] = "As";
16
17
  TokenKind["Const"] = "Const";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kopscript",
3
- "version": "0.1.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"