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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joe Koppin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,588 @@
1
+ # KopScript
2
+
3
+ KopScript is a small, strongly-typed, object-oriented programming language that transpiles to
4
+ readable JavaScript. It's a hobby/learning project: a from-scratch lexer, parser, type
5
+ checker, and code generator, written in TypeScript — leaning deliberately toward C#
6
+ paradigms (type-first declarations, interfaces, explicit virtual/override dispatch,
7
+ auto-properties, enums) rather than TypeScript ones.
8
+
9
+ Because KopScript compiles to plain JavaScript and runs on Node, it runs identically on Windows,
10
+ macOS, and Linux — there's no native toolchain to maintain.
11
+
12
+ ## Highlights
13
+
14
+ - **Object-oriented, C#-flavored**: type-first declarations (`string Name;`, not `name: string`),
15
+ classes with auto-properties, single inheritance plus interfaces via `class Dog : Animal, IPet`,
16
+ explicit `virtual`/`override` dispatch (methods are sealed unless marked `virtual`), and a full
17
+ `public`/`protected`/`private` access model enforced at compile time.
18
+ - **Strongly typed**: every declaration is explicitly typed and checked at compile time.
19
+ - **Built-in string pattern matching**: a `match` expression over strings supporting literal,
20
+ wildcard, and regex patterns — no `if`/`else` chains required.
21
+ - **Real multi-file programs**: `using "./shapes";` compiles a whole dependency graph
22
+ together, checking `public`/`private` visibility across files and emitting genuine ES
23
+ `import`/`export` statements — so a compiled KopScript module is also a normal JS module that
24
+ a plain Node/TS project can `import` directly.
25
+ - **First-class functions and real closures**: lambdas (`(number x) => x * 2`), function
26
+ types (`(number, number) => number`), and calling any function-valued expression — not
27
+ just named functions — compiling straight to native JS arrow functions, so closures work
28
+ exactly like they do in JS.
29
+ - **JS/npm interop**: `extern` declarations describe the shape of an existing JS function,
30
+ class, or global value — ambient (`Math`, `document`, no import) or imported from a
31
+ module specifier — so KopScript code can call real JS APIs, including a `virtual`
32
+ method on an `extern class` that a real KopScript class can `override`, which is what
33
+ lets a class subclass something from another package/repo entirely.
34
+ - **`async`/`await`**: `task<T>` (a promise of a `T`) compiles to a real JS `Promise`,
35
+ `async`/`await` to the real thing, and `try`/`catch`/`finally`/`throw` give a rejected
36
+ task something to catch.
37
+ - **Reactive state without RxJS**: `state<T>` is a small reactive box (`.Value` get/set,
38
+ `.Subscribe((T) => void)`) for holding state and reacting to it changing — no
39
+ Observables, no operators, no manual unsubscribe bookkeeping.
40
+ - **A companion framework, [Kopular](https://dev.azure.com/koppinator/Koppindependence/_git/Kopular)**:
41
+ components, constructor-injected services (no DI container), and hash-based routing (no
42
+ config DSL) — built entirely on the features above, in a separate repo consumed as a
43
+ real package. [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo)
44
+ is a real app built on both.
45
+
46
+ ## Getting started
47
+
48
+ ```bash
49
+ npm install
50
+ npm run ks -- run examples/animals.ks
51
+ npm run ks -- run examples/classify.ks
52
+ npm run ks -- run examples/features.ks
53
+ npm run ks -- run examples/shapes.ks
54
+ npm run ks -- run examples/modules/main.ks
55
+ npm run ks -- run examples/closures.ks
56
+ npm run ks -- run examples/async.ks
57
+ npm run ks -- run examples/arrays.ks
58
+ ```
59
+
60
+ These are all plain console scripts, runnable with Node directly. A browser-facing
61
+ KopScript project (DOM `extern` bindings, `ks watch` + a static file server for a live
62
+ reload-free dev loop) is a different setup — see
63
+ [Kopular](https://dev.azure.com/koppinator/Koppindependence/_git/Kopular) and
64
+ [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo) for a
65
+ real example of that workflow.
66
+
67
+ Or build to a `.js` file without running it:
68
+
69
+ ```bash
70
+ npm run ks -- build examples/animals.ks
71
+ ```
72
+
73
+ Run the test suite:
74
+
75
+ ```bash
76
+ npm test
77
+ ```
78
+
79
+ ## Language tour
80
+
81
+ ### Variables
82
+
83
+ Declarations are type-first, C# style — the type comes before the name, with no `let`:
84
+
85
+ ```ks
86
+ number x = 5;
87
+ const string name = "Joe";
88
+ ```
89
+
90
+ ### Functions
91
+
92
+ Free functions are also type-first, with the return type before the name (no `function`
93
+ keyword — the same shape as a class method, just outside a class):
94
+
95
+ ```ks
96
+ number Add(number a, number b) {
97
+ return a + b;
98
+ }
99
+
100
+ void Announce(string message) {
101
+ print(message);
102
+ }
103
+ ```
104
+
105
+ ### Functions as values, and closures
106
+
107
+ A function type is written `(ParamType, ...) => ReturnType`. Lambdas always have explicit
108
+ parameter types (no inference in v1) and never write their own return type — it's checked
109
+ against whatever function type the lambda is used where, which KopScript always knows concretely
110
+ since every declaration is explicitly typed:
111
+
112
+ ```ks
113
+ number Apply((number) => number f, number x) {
114
+ return f(x);
115
+ }
116
+
117
+ number Square(number x) { return x * x; }
118
+
119
+ print(Apply((number x) => x * 2, 5)); // 10 — an inline lambda
120
+ print(Apply(Square, 4)); // 16 — a named function used as a value
121
+ ```
122
+
123
+ A lambda body can be an expression (`(number x) => x * 2`) or a block
124
+ (`(number x) => { return x * 2; }`). Lambdas are real closures — they capture their
125
+ enclosing scope by reference, compiling directly to JS arrow functions, so mutating a
126
+ captured variable from inside a lambda is visible to the code that captured it:
127
+
128
+ ```ks
129
+ number counter = 0;
130
+ () => number next = () => { counter = counter + 1; return counter; };
131
+ print(next()); // 1
132
+ print(next()); // 2
133
+ ```
134
+
135
+ Calling any expression whose type is a function type works — a variable, a field, a call
136
+ that returns a function, not just a named function or method.
137
+
138
+ ### Classes, auto-properties, and inheritance
139
+
140
+ ```ks
141
+ class Animal {
142
+ public string Name { get; set; }
143
+ constructor(string name) { this.Name = name; }
144
+ public virtual string Speak() { return this.Name + " makes a sound"; }
145
+ }
146
+
147
+ class Dog : Animal {
148
+ public override string Speak() { return this.Name + " barks"; }
149
+ }
150
+ ```
151
+
152
+ - `public string Name { get; set; }` is a read-write auto-property; `public string Name { get; }`
153
+ is get-only — it can only be assigned within its own class's constructor (like a C#
154
+ read-only auto-property), and is rejected anywhere else, including other methods of the
155
+ same class.
156
+ - `public` / `protected` / `private` are enforced at compile time: `private` members are
157
+ only reachable from within the declaring class (any instance, not just `this`);
158
+ `protected` members are reachable from the declaring class and its subclasses; `public`
159
+ is unrestricted. Fields default to `public` if no modifier is written.
160
+ - Methods are sealed by default, like C#. A subclass may only redefine a method that the
161
+ base class marked `virtual`, and must mark its own version `override`; the compiler
162
+ rejects a redefinition that omits `override`, an `override` with no matching `virtual`
163
+ base method, or a signature mismatch between the two.
164
+ - `static` fields and methods belong to the class itself, accessed as `ClassName.Member`
165
+ (never through an instance or `this`) and shared across every instance. A static field
166
+ requires an inline initializer, since there's no constructor to assign it in:
167
+ `private static number Count = 0;`. `static` cannot be combined with `virtual`/`override`,
168
+ and static auto-properties aren't supported in v1 — use a static field instead.
169
+ - A subclass that doesn't declare its own constructor inherits the superclass's (the same
170
+ way JavaScript's `extends` works). Declaring a constructor in a subclass that extends
171
+ another class is not supported in v1.
172
+
173
+ ### Interfaces
174
+
175
+ `class Foo : Base, IBar, IBaz` mixes at most one base class with any number of interfaces
176
+ in a single colon-separated list — the compiler figures out which name is which. A class
177
+ implementing an interface must provide every method the interface declares, with a
178
+ matching signature. Interfaces can themselves extend other interfaces
179
+ (`interface INamedShape : IShape { ... }`), which pulls in the parent's method
180
+ requirements too; implementing classes must satisfy the whole chain. Interfaces only
181
+ declare method signatures in v1 — no properties.
182
+
183
+ ```ks
184
+ interface ISpeaker {
185
+ string Speak();
186
+ }
187
+
188
+ class Dog : ISpeaker {
189
+ public string Speak() { return "Woof"; }
190
+ }
191
+
192
+ void Announce(ISpeaker s) {
193
+ print(s.Speak());
194
+ }
195
+ ```
196
+
197
+ ### Enums
198
+
199
+ ```ks
200
+ enum Color { Red, Green, Blue }
201
+
202
+ Color c = Color.Green;
203
+ if (c == Color.Green) { print("It's green"); }
204
+ ```
205
+
206
+ Members are numbered from `0` in declaration order, compiling to a frozen JS object.
207
+
208
+ ### Modules
209
+
210
+ `using "./shapes";` brings every `public` top-level declaration from that file (resolved
211
+ relative to the current file, no `.ks` extension) into unqualified scope — there's no
212
+ namespace prefix and no picking individual names, the same way a C# `using` directive
213
+ brings a whole namespace into scope. `using` directives must be a contiguous block at the
214
+ very top of the file.
215
+
216
+ ```ks
217
+ // shapes.ks
218
+ interface IShape {
219
+ number Area();
220
+ }
221
+
222
+ class Circle : IShape {
223
+ public number Radius;
224
+ constructor(number radius) { this.Radius = radius; }
225
+ public number Area() { return this.Radius * this.Radius * 3; }
226
+ }
227
+ ```
228
+
229
+ ```ks
230
+ // main.ks
231
+ using "./shapes";
232
+
233
+ Circle c = new Circle(2);
234
+ print(c.Area());
235
+ ```
236
+
237
+ Every top-level class, interface, enum, and function is `public` (exported, visible to
238
+ files that `using` this one) by default — mark it `private` to keep it file-scoped:
239
+
240
+ ```ks
241
+ private number Helper() { return 42; } // not visible outside this file
242
+ class Public { } // visible by default
243
+ ```
244
+
245
+ Top-level `let`/`const` variables are never exportable — only types and functions cross
246
+ file boundaries. There's also a structural rule worth knowing: if a class or interface is
247
+ exported, everything in its public surface must be exported too (an exported class's base
248
+ class and implemented interfaces, an exported interface's base interfaces) — the compiler
249
+ rejects `public class Derived : Base` if `Base` isn't also `public`, since otherwise a
250
+ file importing `Derived` would have no way to make sense of its own base type.
251
+
252
+ `ks build`/`ks run` compile the entry file plus everything it transitively `using`s,
253
+ each to its own `.js` file with real ES `import`/`export` statements — so `node
254
+ main.js` (or a plain JS/TS project importing the compiled output directly) just works,
255
+ with Node's own module resolution doing the wiring.
256
+
257
+ ### Interop with JS/npm (`extern`)
258
+
259
+ `extern` declarations describe the shape of something that already exists in JS, without
260
+ providing a KopScript implementation — KopScript trusts the declared types (same trust model as a
261
+ TypeScript `.d.ts` file). There are three forms, and each can be either **ambient** (no
262
+ `from` — an already-existing global like `Math` or `document`, nothing to import) or
263
+ **imported** (`from "<module>"` — a named export from an npm/Node module):
264
+
265
+ ```ks
266
+ extern string ReadFileSync(string path) from "node:fs" as "readFileSync"; // function
267
+ extern Document document; // value (ambient)
268
+
269
+ extern class Element { // class — a type shape, describing an existing class
270
+ constructor();
271
+ string textContent { get; set; }
272
+ void addEventListener(string eventType, (Event) => void handler);
273
+ static number InstanceCount { get; }
274
+ } from "some-dom-lib";
275
+ ```
276
+
277
+ `as "<jsName>"` maps a KopScript-facing name to the real JS-side identifier (defaults to the
278
+ declared name if omitted) — needed constantly in practice, since JS naming (`camelCase`,
279
+ `readFileSync`) rarely matches KopScript's (`PascalCase`, `ReadFileSync`). Extern **class
280
+ members** have no separate rename mechanism, though — write them with the exact real JS
281
+ name (`addEventListener`, not `AddEventListener`), since that's what actually exists at
282
+ runtime. Calling an extern method or accessing an extern property compiles exactly like
283
+ any other member access — no special codegen, since it isn't reimplementing the class,
284
+ just describing one that's already there.
285
+
286
+ A real, working example: [Kopular](https://dev.azure.com/koppinator/Koppindependence/_git/Kopular)'s
287
+ `dom.ks` declares ambient bindings for `document`/`Element`/`Event`/`window`/`location`,
288
+ which its `Component`/`Router` are built on — and
289
+ [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo)
290
+ consumes `Component`/`Router` themselves via `extern class ... from "kopular/...";`,
291
+ proving `extern` works as a real cross-*package* boundary, not just for describing DOM
292
+ globals within a single project.
293
+
294
+ Now that KopScript has `async`/`await` and `task<T>` (see below), a Promise-based JS API is
295
+ describable too — `extern task<string> Fetch(...) from "..." as "fetch";` is legitimate,
296
+ and `await`ing it works exactly like awaiting any other KopScript task. What's still not cleanly
297
+ describable is old-style Node **callback-based** async that doesn't return a Promise at all
298
+ (`fs.readFile(path, callback)` with an error-first callback) — nothing in KopScript understands
299
+ that specific convention, even though the callback parameter itself is describable as an
300
+ ordinary function type.
301
+
302
+ ### async/await, task&lt;T&gt;, and try/catch
303
+
304
+ `task` (a promise of nothing) and `task<T>` (a promise of a `T`) are the one hardcoded
305
+ parametrized type in v1 — not general generics, just enough to give `async`/`await` a
306
+ return type. `async` requires (and is required by) a `task`/`task<T>` return type; `return`
307
+ statements inside an async body are checked against the unwrapped result type, exactly
308
+ like real C#/JS async functions — you write `return 5;`, not `return SomeTaskOf(5);`:
309
+
310
+ ```ks
311
+ async task<number> Double(number x) {
312
+ return x * 2;
313
+ }
314
+
315
+ async task<number> Chain(number x) {
316
+ number a = await Double(x);
317
+ number b = await Double(a);
318
+ return b;
319
+ }
320
+ ```
321
+
322
+ Both compile directly to their real JS equivalents (`async function`, `await`), so an
323
+ `async` KopScript function returns a genuine `Promise` and composes with plain JS/TS code
324
+ without any wrapping. `await` also works at the top level of a file, matching real
325
+ top-level await in an ES module.
326
+
327
+ Exception handling exists specifically so a rejected task has something to catch:
328
+
329
+ ```ks
330
+ try {
331
+ number result = await MightFail(x);
332
+ print(result);
333
+ } catch (string message) {
334
+ print("caught: " + message);
335
+ } finally {
336
+ print("done");
337
+ }
338
+ ```
339
+
340
+ `throw` accepts any type — there's no base exception/error type to require conformance
341
+ to, matching JS's own looseness — and a `catch (Type name)` parameter's type is trusted,
342
+ not verified (the same trust model as `extern`: KopScript has no way to know what a given `throw`
343
+ site might actually produce). At least one of `catch`/`finally` is required; a bare `try {}`
344
+ alone is rejected. `examples/async.ks` and `test/codegen.test.ts` exercise a full chain of
345
+ async calls, a caught error, and a `finally` that runs on both the success and failure path.
346
+
347
+ Known v1 limitations: no async lambdas (`await` is never valid inside a lambda body, even
348
+ inside an async function — only free functions and methods can be `async`), and no way to
349
+ construct a `task` value directly outside of an `async` function body.
350
+
351
+ ### Reactive state: state&lt;T&gt;
352
+
353
+ `state<T>` is the other hardcoded parametrized type in v1, alongside `task<T>` — a reactive
354
+ box holding a `T`. Construct one with `state(initial)` (no explicit type argument; `T` is
355
+ inferred from `initial`), read/write it through `.Value`, and register a listener with
356
+ `.Subscribe((T) => void)` that fires every time `.Value` is assigned:
357
+
358
+ ```ks
359
+ state<number> count = state(0);
360
+ count.Subscribe((number v) => print("now " + v));
361
+ count.Value = count.Value + 1; // prints "now 1"
362
+ ```
363
+
364
+ This exists to give Kopular components a way to re-render on state change without pulling in
365
+ anything like RxJS — no Observables, no operators, no manual unsubscribe bookkeeping. A
366
+ component subscribes once, in its constructor, to call its own `Update()`:
367
+
368
+ ```ks
369
+ class Counter : Component {
370
+ private state<number> Count;
371
+
372
+ constructor() : base() {
373
+ this.Count = state(0);
374
+ this.Count.Subscribe((number v) => this.Update());
375
+ }
376
+
377
+ public override Element Render() {
378
+ Element button = document.createElement("button");
379
+ button.textContent = "Count: " + this.Count.Value;
380
+ button.addEventListener("click", (Event e) => {
381
+ this.Count.Value = this.Count.Value + 1; // Update() fires automatically
382
+ });
383
+ return button;
384
+ }
385
+ }
386
+ ```
387
+
388
+ Compare this to the pre-`state<T>` version of the same component, which had to call
389
+ `this.Update();` by hand inside every single event handler that touched state — one
390
+ `Subscribe` call at construction now does that job everywhere.
391
+ [Kopular](https://dev.azure.com/koppinator/Koppindependence/_git/Kopular)'s `Component`
392
+ is built around exactly this pattern, and its own README covers the rest of the
393
+ "Angular's separation, none of the bloat" story — constructor-injected services instead
394
+ of a DI container, and hash-based routing instead of a config DSL — both built entirely
395
+ on ordinary KopScript, no further compiler features required beyond `state<T>` and the
396
+ `extern`/`virtual` support above.
397
+
398
+ ### String interpolation
399
+
400
+ ```ks
401
+ string name = "Joe";
402
+ string msg = $"Hello, {name}!";
403
+ ```
404
+
405
+ ### Pattern matching over strings
406
+
407
+ The headline feature: a `match` expression with literal, comma-separated, and regex patterns.
408
+ A `match` must end with a `_` wildcard arm.
409
+
410
+ ```ks
411
+ string Classify(string input) {
412
+ return match input {
413
+ "cat", "dog" => "animal",
414
+ r"^[0-9]+$" => "number",
415
+ _ => "unknown"
416
+ };
417
+ }
418
+ ```
419
+
420
+ ### String stdlib
421
+
422
+ KopScript's `string` type exposes PascalCase members that map directly onto
423
+ `String.prototype`:
424
+
425
+ | KopScript | JavaScript |
426
+ | ----------------------- | -------------------------- |
427
+ | `s.Contains(x)` | `s.includes(x)` |
428
+ | `s.StartsWith(x)` | `s.startsWith(x)` |
429
+ | `s.EndsWith(x)` | `s.endsWith(x)` |
430
+ | `s.Replace(a, b)` | `s.replaceAll(a, b)` |
431
+ | `s.Split(x)` | `s.split(x)` |
432
+ | `s.Trim()` | `s.trim()` |
433
+ | `s.ToUpper()` | `s.toUpperCase()` |
434
+ | `s.ToLower()` | `s.toLowerCase()` |
435
+ | `s.Length` | `s.length` |
436
+
437
+ ### Array stdlib
438
+
439
+ Arrays expose `.Length`, plus `Map`/`Filter`/`ForEach`/`Push`, using lambdas or any other
440
+ function-valued expression (a named function, a variable holding one, ...):
441
+
442
+ ```ks
443
+ number[] xs = [1, 2, 3, 4];
444
+ string[] labels = xs.Map((number x) => "n" + x); // ["n1", "n2", "n3", "n4"]
445
+ number[] evens = xs.Filter((number x) => x % 2 == 0); // [2, 4]
446
+ xs.ForEach((number x) => print(x));
447
+ number[] grown = xs.Push(5); // [1, 2, 3, 4, 5]; xs itself is untouched
448
+ ```
449
+
450
+ `Map`/`Filter`/`ForEach` compile straight to their real `Array.prototype` equivalents.
451
+ `Push` is the one departure from JS: it's **non-mutating** (returns a new array; `xs`
452
+ itself is unchanged), unlike JS's own `Array.prototype.push` — chosen for consistency with
453
+ `Map`/`Filter` (already non-mutating) and because nothing else in KopScript's type system models
454
+ aliasing/mutable-reference semantics, so a silently-mutating `Push` would be a surprising
455
+ outlier. It compiles to a plain spread (`[...xs, 5]`), not a `.push()` call.
456
+
457
+ `Map`'s result type is the one genuinely polymorphic piece of the whole language — the
458
+ result element type is whatever the callback actually returns, not a fixed signature.
459
+ A plain function reference (`xs.Map(SomeFunction)`) already carries a fully-known type, so
460
+ that case is exact; an inline expression-bodied lambda (`xs.Map((number x) => ...)`) has
461
+ its return type inferred from the body. A block-bodied lambda passed to `Map` is a known
462
+ v1 gap — its result type can't be inferred that way yet.
463
+
464
+ ### Control flow
465
+
466
+ ```ks
467
+ if (x > 0) {
468
+ print("positive");
469
+ } else {
470
+ print("non-positive");
471
+ }
472
+
473
+ while (x < 10) {
474
+ x = x + 1;
475
+ }
476
+
477
+ for (number i = 0; i < 10; i = i + 1) {
478
+ print(i);
479
+ }
480
+
481
+ foreach (number item in items) {
482
+ print(item);
483
+ }
484
+ ```
485
+
486
+ `foreach` requires an explicit element type (there's no `var`/type inference in v1).
487
+ `break` and `continue` work inside `while`, `for`, and `foreach` loops.
488
+
489
+ ### Types (v1 scope)
490
+
491
+ `number`, `string`, `bool`, `void`, `T[]` (arrays), class types, interface types, enum
492
+ types, and function types (`(T, ...) => R`). No generics or nullable types yet — a
493
+ possible future extension.
494
+
495
+ ### Built-ins
496
+
497
+ `print(...)` compiles to `console.log(...)`.
498
+
499
+ ## Architecture
500
+
501
+ ```
502
+ src/
503
+ lexer.ts tokenizer: source -> Token[]
504
+ tokens.ts token kind enum + Token type
505
+ ast.ts AST node type definitions
506
+ parser.ts recursive-descent parser: Token[] -> AST (Program)
507
+ diagnostics.ts error/warning collection with line/col + source snippets
508
+ types.ts type system: Type representations + compatibility rules
509
+ checker.ts semantic analysis: scopes, symbol table, type checking over the AST
510
+ codegen.ts AST -> JavaScript source string (readable ES2020 output)
511
+ modules.ts multi-file orchestration: resolves the `using` graph, checks
512
+ modules in dependency order, drives codegen across all of them
513
+ cli.ts `ks build <file>` / `ks run <file>` entry point
514
+ examples/ sample .ks programs (examples/modules/ is a multi-file one)
515
+ test/ vitest unit and end-to-end tests
516
+ ```
517
+
518
+ Pipeline, per file: source -> lexer -> parser (AST) -> checker (validates the AST,
519
+ collects diagnostics) -> codegen (emits JS). `modules.ts` sits above this: it parses the
520
+ entry file and everything it transitively `using`s, topologically sorts them
521
+ (dependencies first), and checks each one with its direct dependencies' exports seeded in
522
+ — so a class from another file resolves exactly like a local one once the checker starts.
523
+ Codegen then runs per file, turning each `using` into a real `import` statement and each
524
+ exported top-level declaration into a real `export`. The CLI aborts before writing
525
+ anything if *any* file in the graph has errors, and otherwise writes one `.js` file next
526
+ to each `.ks` source and (for `run`) executes the entry file's output with `node`.
527
+
528
+ Interfaces and the virtual/override discipline are purely compile-time: JS methods are
529
+ always dynamically dispatched, so codegen doesn't need to do anything special for either
530
+ one — the checker just validates the contract before code is ever emitted. Enums compile
531
+ to a small frozen object (`Object.freeze({ Red: 0, Green: 1, ... })`); interfaces have no
532
+ runtime representation at all and are dropped from the emitted JS entirely.
533
+
534
+ ## CLI
535
+
536
+ ```
537
+ ks build <file.ks> # type-check and emit <file>.js next to the source
538
+ ks run <file.ks> # build, then execute the emitted JS with node
539
+ ks watch <file.ks> # build, then rebuild on every change to any file in the graph
540
+ ```
541
+
542
+ During development, use `npm run ks -- <build|run|watch> <file.ks>` (backed by `tsx`),
543
+ or run `npm run build` to compile the TypeScript compiler itself to `dist/` and use `node
544
+ dist/cli.js` directly.
545
+
546
+ `watch` rebuilds on a save to *any* `.ks` file it reached while compiling — the entry and
547
+ everything it (transitively, non-transitively per-file) `using`s — not just the entry
548
+ file, and re-establishes its watch list after every rebuild since the dependency set
549
+ itself can change (a `using` added or removed). This is the piece that makes a
550
+ browser-facing dev loop bearable: run `ks watch` in one terminal, a static file server in
551
+ another, and refreshing the browser after a save is the only manual step left — there's
552
+ no watch-triggered auto-refresh, since KopScript has no dev-server integration to push
553
+ that to the page. See [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo)
554
+ for this workflow in practice.
555
+
556
+ ## Editor support
557
+
558
+ `editors/vscode/` is a local-install-only VS Code extension providing `.ks` syntax
559
+ highlighting (a TextMate grammar — comments, all three string forms including highlighted
560
+ interpolation expressions, keywords, types, function calls) and bracket/comment
561
+ configuration. No language server, no Marketplace listing — see `editors/vscode/README.md`
562
+ for installing it locally (`Developer: Install Extension from Location...`, or symlink it
563
+ into your extensions folder).
564
+
565
+ ## Status
566
+
567
+ This is a v1 / hobby-project scope. Not yet supported: generics, nullable types, static
568
+ auto-properties, interface properties (methods only), and nested functions/classes.
569
+ Generics and nullable types in particular are a substantially bigger undertaking than
570
+ everything else here — they touch the type system's core (type parameters, variance,
571
+ constraint checking) rather than being additive features, so they're deliberately left
572
+ for a dedicated future pass rather than bolted on.
573
+
574
+ `async`/`await`, `task<T>`, and `try`/`catch`/`finally`/`throw` are now in place (see the
575
+ language tour above) — the ceiling that's left is what's *inside* those: no async lambdas,
576
+ and no way to build a `task` value by hand outside an `async` function body.
577
+
578
+ ### Toward a frontend framework: Kopular
579
+
580
+ The language shape — modules, closures, DOM interop, `state<T>`, and `extern`/`virtual`
581
+ subclassing across a package boundary — exists to support real UI components, not just
582
+ scripts. That framework itself, [Kopular](https://dev.azure.com/koppinator/Koppindependence/_git/Kopular),
583
+ lives in its own repo now rather than in `examples/` here, consumed as a real package
584
+ (currently via a `file:` dependency, pending an npm publish of both). A real app built on
585
+ both lives in [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo) —
586
+ components, `state<T>`-driven reactivity, a constructor-injected service, and
587
+ hash-based routing between two pages, verified against a real DOM via jsdom and in an
588
+ actual browser.
package/bin/ks.js ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname, join } from "node:path";
5
+
6
+ // Runs the compiled CLI directly — a published/installed package has no
7
+ // `src/` or `tsx` (a devDependency of this repo, not shipped), only
8
+ // `dist/`. Local development uses `npm run ks`, which still runs `tsx`
9
+ // against `src/cli.ts` directly for a fast edit-run loop.
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const cliEntry = join(__dirname, "..", "dist", "cli.js");
12
+
13
+ const result = spawnSync(process.execPath, [cliEntry, ...process.argv.slice(2)], {
14
+ stdio: "inherit",
15
+ });
16
+
17
+ process.exit(result.status ?? 0);
package/dist/ast.js ADDED
@@ -0,0 +1,2 @@
1
+ // ---------- Type annotations (syntactic, as written in source) ----------
2
+ export {};